المعاملات والوسائط وقيم الإرجاع
في الدرس السابق تعرّفت على كيفية تعريف الدوالّ واستدعائها. الآن حان وقت جعل الدوالّ مفيدة فعلًا: ستتعلّم كيف ترسل بيانات إلى الدالّة عبر المعاملات (Parameters)، وكيف تستقبل نتيجةً منها عبر قيمة الإرجاع (Return Value)، ولماذا تُعدّ قاعدة التمرير بالقيمة (Pass-by-Value) في Java أمرًا بالغ الأهمية في كل مرة تُمرّر فيها متغيّرًا إلى دالّة.
المعاملات والوسائط: ما الفرق؟
يُستخدَم هذان المصطلحان أحيانًا بالتبادل في الحديث، غير أنّهما يعنيان شيئين مختلفين:
- المعامل (Parameter) — المتغيّر المُعلَن في توقيع الدالّة (العنصر النائب).
- الوسيط (Argument) — القيمة الفعلية التي تُمرّرها عند استدعاء الدالّة.
// "radius" هو المعامل — عنصر نائب مُعلَن في التوقيع
static double circleArea(double radius) {
return Math.PI * radius * radius;
}
public static void main(String[] args) {
// 5.0 هو الوسيط — القيمة الحقيقية التي تُمرَّر عند الاستدعاء
double area = circleArea(5.0);
System.out.println(area); // 78.53981633974483
}
معامل واحد، وسيط واحد. تُطابق Java الوسائط مع المعاملات بدقّة وفق الترتيب والنوع. إذا أعلنت الدالّة int a, int b، يجب تمرير قيمتين متوافقتين مع int بالترتيب ذاته.
قيم الإرجاع
يمكن للدالّة إرسال نتيجة إلى المُستدعي باستخدام كلمة return. يُخبر نوع الإرجاع المُعلَن في التوقيع المُصرِّفَ بنوع القيمة المتوقّعة. إذا لم تُنتِج الدالّة أي نتيجة فنوع إرجاعها هو void.
// تُرجع int — مجموع المعاملَين
static int add(int a, int b) {
return a + b; // ينتهي التنفيذ هنا وتنتقل النتيجة إلى المُستدعي
}
// لا تُرجع شيئًا — تنتج أثرًا جانبيًا فقط (الطباعة)
static void greet(String name) {
System.out.println("Hello, " + name + "!");
// لا حاجة لـ return في الدوالّ من نوع void
}
public static void main(String[] args) {
int result = add(3, 7); // result == 10
greet("Layla"); // تطبع: Hello, Layla!
}
فضّل إرجاع القيم على الطباعة داخل الدوالّ. الدالّة التي تُرجع نتيجة يمكن إعادة استخدامها في أي مكان — تخزينها في متغيّر، أو تمريرها إلى دالّة أخرى، أو استخدامها في تعبير. أمّا الدالّة التي تطبع فقط، فلا تستطيع تغذية مخرجاتها في منطق آخر.
Java تعمل بالتمرير بالقيمة — دائمًا
هذه إحدى أهم القواعد في Java: كل وسيط يُمرَّر كنسخة من قيمته. تستقبل الدالّة نسخة مستقلة خاصة بها؛ التغييرات على المعامل داخل الدالّة لا تؤثّر في المتغيّر الأصلي لدى المُستدعي.
static void tryToDouble(int number) {
number = number * 2; // تتغيّر النسخة المحلية فقط
System.out.println("Inside method: " + number); // 20
}
public static void main(String[] args) {
int x = 10;
tryToDouble(x);
System.out.println("After call: " + x); // لا تزال 10 — لم يتغيّر x
}
يبقى المتغيّر x يساوي 10 لأن tryToDouble استقبلت نسخة من 10، لا مرجعًا إلى x نفسه.
ماذا عن الكائنات؟
عند تمرير كائن، تُمرّر Java بالقيمة أيضًا — لكن القيمة المنسوخة هي المرجع (عنوان الكائن في الذاكرة). هذا يعني أن الدالّة تستطيع تعديل الحالة الداخلية للكائن عبر ذلك المرجع، لكنّها لا تستطيع جعل متغيّر المُستدعي يشير إلى كائن مختلف.
import java.util.ArrayList;
static void addItem(ArrayList<String> list, String item) {
list.add(item); // يُعدّل الكائن ذاته في الذاكرة
list = new ArrayList<>(); // إعادة توجيه النسخة المحلية — لا أثر على المُستدعي
}
public static void main(String[] args) {
ArrayList<String> fruits = new ArrayList<>();
fruits.add("apple");
addItem(fruits, "banana");
System.out.println(fruits); // [apple, banana] — نجح add()
// لكن إعادة التعيين داخل addItem لم تؤثّر في fruits هنا
}
لبس شائع: يظنّ المبتدئون أحيانًا أن Java تعمل بـ "التمرير بالمرجع" للكائنات. هذا غير صحيح. المرجع نفسه يُنسَخ. إعادة تعيين المعامل (list = new ArrayList<>()) لا أثر لها على متغيّر المُستدعي. فقط التعديلات التي تجري عبر المرجع تكون مرئية من الخارج.
النطاق (Scope) داخل الدوالّ
النطاق يحدّد أين يمكن الوصول إلى متغيّر ما. في Java، كل متغيّر يوجد فقط داخل الكتلة — زوج الأقواس المعقوصة { } — التي أُعلن فيها.
static int compute(int x) {
int result = x * x; // "result" موجود داخل هذه الدالّة فقط
return result;
}
public static void main(String[] args) {
int answer = compute(6);
System.out.println(answer); // 36
// System.out.println(result); // خطأ في الترجمة — "result" خارج النطاق هنا
}
المعامل x يتصرّف كمتغيّر محلي: يُنشأ عند استدعاء الدالّة ويُدمَّر عند انتهائها. يمكن لدالّتين مختلفتين أن تحتويا على متغيّر محلي يُسمّى x دون أي تعارض — فكلٌّ منهما يعيش في نطاق مستقل تمامًا.
لماذا يهمّ النطاق: يمنع النطاقُ التفاعلاتِ غير المقصودة بين الدوالّ. كل دالّة وحدة مستقلة بذاتها. لا تحتاج إلى القلق من أن متغيّرًا أسميته i داخل دالّة سيتعارض مع i في دالّة أخرى.
نضع كل شيء معًا
إليك مثالًا صغيرًا وعمليًا يجمع المعاملات وقيمة الإرجاع ويوضّح النطاق بجلاء:
static double celsiusToFahrenheit(double celsius) {
double fahrenheit = (celsius * 9.0 / 5.0) + 32.0; // محلية لهذه الدالّة
return fahrenheit;
}
public static void main(String[] args) {
double bodyTemp = celsiusToFahrenheit(37.0);
System.out.printf("37 C = %.1f F%n", bodyTemp); // 37 C = 98.6 F
double boiling = celsiusToFahrenheit(100.0);
System.out.printf("100 C = %.1f F%n", boiling); // 100 C = 212.0 F
}
الدالّة قابلة لإعادة الاستخدام، يمكن اختبارها بمعزل عن غيرها، والمتغيّر الداخلي fahrenheit غير مرئي من main — وهذا بالضبط ما تبدو عليه الدوالّ المُصمَّمة بشكل جيد.
الخلاصة
- المعاملات هي العناصر النائبة في توقيع الدالّة؛ الوسائط هي القيم التي تمرّرها عند الاستدعاء.
- كلمة
return ترسل قيمة إلى المُستدعي؛ نوع الإرجاع في التوقيع يُعلن عن نوعها.
- Java تمرّر بالقيمة دائمًا: الأنواع الأوّلية (primitives) تُنسَخ بالكامل؛ للكائنات يُنسَخ المرجع (فالتعديلات الداخلية مرئية، لكن إعادة التعيين لا أثر لها).
- المتغيّرات المُعلَنة داخل الدالّة محلية — توجد فقط في نطاق تلك الدالّة وغير مرئية في أي مكان آخر.