يُطلَق على البرامج الفرعية (subroutine) التي تُعيد قيمة اسم "الدوال (function)"، والتي يُمكِنها أن تُعيد قيمة من نوع واحد مُحدَّد ضِمْن التعريف ويُسمى النوع المُعاد (return type) من الدالة. بإِمكَانك استدعاء الدوال (function call) في العموم بأيّ مكان يَتوقَّع فيه الحاسوب قيمة (value)، مثل الجانب الأيمن من تَعْليمَات الإِسْناد (assignment statement)، أو كمُعامِل فعليّ (actual parameter) بتَعْليمَات استدعاء البرامج الفرعية، أو كجُزء من تعبير (expression) أكبر. يُمكِن حتى اِستخدَام الدوال من النوع boolean
، أي التي تُعيد قيمة من ذلك النوع، مَحل الشَّرْط (condition) ضِمْن تَعْليمَات مثل if
و while
و for
و do..while
.
يُمكِن أيضًا اِستخدَام تَعْليمَة استدعاء دالة (function call)، كما لو كانت برنامجًا فرعيًا، أيّ كما لو أنها لا تُعيد قيمة. في تلك الحالة، يَتجاهَل الحاسوب القيمة المُعادة من البرنامج الفرعي، وهو ما يَكُون منطقيًا في بعض الأحيان. على سبيل المثال، تَقرأ الدالة TextIO.getln()
سَطْرًا مُدْخَلًا مِنْ قِبَل المُستخدِم وتُعيده كقيمة من النوع String
. عادة، يُسنَد السَطْر المُعاد من تلك الدالة إلى مُتَغيِّر؛ لكي يُستخدَم لاحقًا، كما في التَعْليمَة name = TextIO.getln();
. لكن، يُمكِن اِستخدَام تلك الدالة ضِمْن تَعْليمَة استدعاء برنامج فرعي على الصورة TextIO.getln();
، والتي، كالعادة، ستَستمِر بقراءة مُدْخَلات المُستخدِم إلى أن تَصِل إلى محرف العودة إلى بداية السطر (carriage return). لمّا لم تُسنَد القيمة المُعادة من تلك الدالة إلى مُتَغيِّر، أو تُستخدَم ضِمْن تعبير (expression)، فإنها تُستبعَد ببساطة، لذا يَكُون تأثير استدعاء البرنامج الفرعي في تلك الحالة هو مجرد قراءة بعض المُدْخَلات واِستبعَادها، وهو الشيء الذي قد تَرغَب به في بعض الأحيان.
تعليمة return
بوصولك إلى هنا، فأنت قد تَعلَّمت طريقة استدعاء الدوال (functions) -التي تُعيد قيم فعليّة- مثل Math.sqrt()
و TextIO.getInt()
، لكنك لَمْ تَكتُب أيّ دالة خاصة بك بَعْد. في الواقع، ستبدو كتابة الدوال أمرًا مألوفًا بالنسبة لك؛ فهي بالنهاية مُشابهة تمامًا لصيغة كتابة أيّ برنامج فرعي (subroutine) عادي باستثناء ضرورة اِستخدَام التَعْليمَة return
؛ لتَخْصِيص القيمة التي سيُعيدها البرنامج الفرعي (return value). تُكتَب تَعْليمَة return
بالصيغة (syntax) التالية:
return <expression>;
يُمكِن اِستخدَام تَعْليمَة return
بالأعلى داخل تعريف الدوال فقط (function definition)، ولابُدّ لنوع التعبير
عندما يُنفِّذ الحاسوب تَعْليمَة return
، فإنه يَحسِب قيمة التعبير المُخصَّص، ثم يُنهِي تَّنْفيذ الدالة، وأخيرًا، يُعيد قيمة التعبير لتُصبِح القيمة المُعادة (return value) من الدالة. اُنظر تعريف الدالة التالي على سبيل المثال:
static double pythagoras(double x, double y) { return Math.sqrt( x*x + y*y ); }
بفَرْض تَّنْفيذ الحاسوب لتَعْليمَة إِسْناد تَتضمَّن تَعْليمَة استدعاء الدالة بالأعلى، مثل totalLength = 17 + pythagoras(12,5);
، فإنه سيُحاوِل تَحْصِيل قيمة تَعْليمَة الاستدعاء pythagoras(12,5)
، لذلك سيبدأ بإِسْناد قيم المُعامِلات الفعليّة (actual parameters) المُمرَّرة، أيّ ١٢ و ٥، إلى المُعامِلات الصُّوريّة (formal parameters) x
و y
بتعريف الدالة، ثم سينتقل إلى تَّنْفيذ مَتْن الدالة (function body)، وبالتالي سيضطرّ إلى تَحْصِيل قيمة التعبير Math.sqrt(12.0*12.0 + 5.0*5.0)
والتي تؤول إلى القيمة 13.0
، ثم سيُعيد هذه القيمة لتَكُون القيمة المُعادة (return value) من الدالة، وعليه تُستبدَل القيمة 13.0
بتَعْليمَة استدعاء الدالة، فتصبح تَعْليمَة الإِسْناد وكأنها مَكْتُوبة على الصورة totalLength = 17+13.0
. أخيرًا، تُضاف القيمة المُعادة (return value) إلى العدد 17
ثم تُخْزَن النتيجة 30.0
بالمُتَغيِّر totalLength
.
لاحظ أنه وبينما تُنفَّذ الدالة، فإنك قد تَتَمكَّن من تَحْصِيل القيمة المُفْترَض إعادتها منها، وفي تلك الحالة، تستطيع اِستخدَام تَعْليمَة return
؛ لإعادة تلك القيمة مباشرة، ومِنْ ثَمَّ تَخَطِّي أية تَعْليمَات آخرى بمَتْن الدالة، دون الحاجة إلى الانتظار إلى نهاية المَتْن، أيّ أنه ليس من الضروري أن تَكُون تَعْليمَة return
هي آخر تَعْليمَة بمَتْن الدالة، بل يُمكِن اِستخدَامها بأي مكان ضِمْن المَتْن. ولكن، لابُدّ دائمًا من إعادة قيمة ما من الدالة وذلك بجميع مسارات التَّنْفيذ المُحتمَلة التي قد تَتَّخذها الدالة.
كما ذَكَرَنا مُسْبَّقًا، لابُدّ دائمًا من كتابة تَعْليمَة return
مَتبوعة بتعبير (expression) ضِمْن تعريف أي دالة (function)؛ لأن الدوال دائمًا ما تُعيد قيمة. في المقابل، لا يُعدّ ذلك ضروريًا مع البرامج الفرعية من غير الدوال (non-function)، أي تلك التي تَستخدِم void
ضِمْن تعريفها للتَّصْريح عن نوعها المُعاد، فهي في النهاية لا تُعيد قيمة، ومع ذلك يَظِلّ اِستخدَام تَعْليمَة return
ضِمْن هذا السِّياق مُمكنًا، بحيث يَقْتصِر دورها على إنهاء تَّنْفيذ البرنامج الفرعي بمكان ما وسط المَتْن وإعادة التَحكُّم (return control) مرة آخرى إلى سَطْر الشيفرة المسئول عن استدعاء البرنامج الفرعي. تُكتَب تَعْليمَة return
في تلك الحالة على الصورة return;
بدون تعبير (expression).
بالإضافة إلى ما سبق، قد تُستخدَم تَعْليمَة return
أيضًا داخل حَلْقة تَكْرار (loop) لإنهاء كُلًا من الحَلْقة (loop) والبرنامج الفرعي الحاضن لها. بالمثل، قد تُستخدَم بتَعْليمَة switch
، للخروج من كُلًا من تَعْليمَة switch
والبرنامج الفرعي الحاضن لها، أيّ ستَجِدْ أحيانًا تَعْليمَة return
مُستخدَمة ضِمْن سِّياقات اعْتَدْت فيها استخدام تَعْليمَة break
.
أمثلة على الدوال (functions)
تَعرَّضنا لمسألة حِسَاب قيم عناصر مُتتالية الأعداد"3N+1" عدة مرات بما في ذلك القسم السابق. يُمكِننا تعريف دالة (function) بسيطة جدًا، تَستقبِل قيمة العنصر الحالي بالمُتتالية، ثم تُعيد قيمة عنصر المتتالية التالي. اُنظر الشيفرة التالية:
static int nextN(int currentN) { if (currentN % 2 == 1) // إذا كان currentN فرديًا return 3*currentN + 1; // أعد تلك القيمة else return currentN / 2; // إذا لم يكن، أعد تلك القيمة }
نُلاحِظ أن تعريف الدالة بالأعلى يَتضمَّن تَعْليمَتي return
. ستُنفَّذ دائمًا تَعْليمَة return
واحدة فقط بِغَضّ النظر عن عدد تلك التَعْليمَات المُستخدَمة ضِمْن التعريف. يُفضِّل بعض المبرمجين كتابة تَعْليمَة return
وحيدة بنهاية الدالة، ويحاولوا القيام بذلك قدر الإمكان؛ خاصة وأن ذلك يُسهِل من العثور على تَعْليمَة return
ضِمْن التعريف. فمثلًا، يُمكِننا إعادة كتابة الدالة nextN()
-المُعرَّفة بالأعلى- مرة آخرى على النحو التالي:
static int nextN(int currentN) { int answer; // متغير يحمل القيمة المعادة من الدالة if (currentN % 2 == 1) // إذا كان currentN فرديًا answer = 3*currentN+1; // أسند القيمة إلى المتغير else answer = currentN / 2; // إذا لم يكن، أسند القيمة إلى المتغير return answer; // لا تنسى أن تعيد المتغير }
دَعْنَا الآن نَكتُب برنامجًا فرعيًا (subroutine) لحِسَاب قيم عناصر المُتتالية، لكن بالاعتماد على الدالة nextN
هذه المرة. نظرًا لكَوْن الدالة nextN
قصيرة نوعًا ما، فإن التَحْسِينات المُجراة على البرنامج الفرعي ليست ذا شأن بالموازنة مع نسخة البرنامج بالقسم ٤.٣. في المقابل، إذا كانت الدالة nextN()
طويلة لكَوْنها مثلًا تَحسِب عملية معقدة، فعندها سيَكُون من المنطقي إخفاء ذلك التعقيد بداخل دالة منفصلة:
static void print3NSequence(int startingValue) { int N; // أحد عناصر المتتالية int count; // عدد عناصر المتتالية N = startingValue; // أول قيمة بالمتتالية count = 1; System.out.println("The 3N+1 sequence starting from " + N); System.out.println(); System.out.println(N); while (N > 1) { // احسب قيمة عنصر المتتالية التالي باستدعاء الدالة nextN N = nextN( N ); count++; // أزد عدد عناصر المتتالية بمقدار الواحد System.out.println(N); // اطبع قيمة العنصر } System.out.println(); System.out.println("There were " + count + " terms in the sequence."); }
اُنظر المثال التالي للدالة letterGrade
، والتي تَستقبِل درجة عددية (numerical grade)، وتُحوِّلها إلى نظيرتها المحرفية (letter grade) بالاستعانة بمقياس درجات (grading scale) تقليدي:
/** * أعد الدرجة المحرفية المناظرة للدرجة العددية الممررة للدالة كمعامل */ static char letterGrade(int numGrade) { if (numGrade >= 90) return 'A'; // 90 or above gets an A else if (numGrade >= 80) return 'B'; // 80 to 89 gets a B else if (numGrade >= 65) return 'C'; // 65 to 79 gets a C else if (numGrade >= 50) return 'D'; // 50 to 64 gets a D else return 'F'; } // نهاية الدالة letterGrade
تستطيع الدوال أن تُعيد قيم من أي نوع، فمثلًا، نوع القيمة المُعادة (return type) من الدالة letterGrade()
-بالأعلى- هو النوع char
، أما الدالة المُعرَّفة بالمثال التالي، فإنها تُعيد قيمة (return value) من النوع boolean
. اُنظر الشيفرة التالية، واحْرِصْ على قراءة التعليقات (comments)؛ لأنها تُوضِح بعض النقاط المثيرة للاهتمام:
/** * تعيد هذه الدالة القيمة المنطقية true إذا كان N عدد أولي. * العدد الأولي هو عدد صحيح أكبر من العدد 1 * بشرط ألا يكون قابلًا للقسمة إلا على نفسه وعلى الواحد. * Nإذا كان لدى N أي قاسم D يتراوح بين الواحد والعدد * فسيكون لديه قاسم يتراوح بين العدد 2 والجذر التربيعي للعدد N * لذلك سوف نختبر قيم القواسم المحتملة بداية من العدد 2 * وحتى الجذر التربيعي للعدد N */ static boolean isPrime(int N) { int divisor; // عدد يحتمل أن يكون قاسما للعدد if (N <= 1) return false; // العدد الأولي لابد أن يكون أكبر من الواحد int maxToTry; // أكبر قيمة قاسم محتمل ينبغي اختبارها // ينبغي إجراء عملية التحويل بين الأنواع لأن Math.sqrt // تعيد قيمة من النوع double maxToTry = (int)Math.sqrt(N); // سنحاول قسمة N على الأعداد من 2 ووصولا الى قيمة maxToTry // إذا لم يكن N قابل للقسمة على أي عدد من تلك الأعداد // سيكون N عددا اوليا for (divisor = 2; divisor <= maxToTry; divisor++) { if ( N % divisor == 0 ) // إذا تمكن divisor من تقسم N return false; // إذًا N لا يمكن أن يكون عددًا أوليًا // لا حاجة للاستمرار } // إذا وصلنا إلى هنا، فإن N حتما هو عدد أولي // لأنه إذا لم يكن أوليًا، ستكون الدالة بالفعل قد انتهت // باستخدام تعليمة return الموجودة بالحلقة السابقة return true; // نعم N هو عدد أولي } // نهاية الدالة isPrime
نَسْتَعْرِض بالشيفرة التالية دالة آخرى، والتي تَستقبِل مُعامِل (parameter) من النوع String
، وتُعيد قيمة (return value) من نفس النوع String
. تَعْكِس تلك الدالة ترتيب محارف السِلسِلة النصية المُمرَّرة، وتُعيد نسخة مَعْكوسة منها، فمثلًا، النسخة المَعْكوسة من السِلسِلة النصية "Hello World" هي "dlroW olleH". يُمكِن توصيف الخوارزمية (algorithm) المطلوبة لعَكْسَ سِلسِلة نصية str
على النحو التالي: ابدأ بسِلسِلة نصية فارغة (empty string)، ثم أَضِف إلى نهايتها كل محرف (character) موجود بالسِلسِلة النصية str
، بحيث تبدأ بالمحرف الأخير من السِلسِلة str
، وتعود للوراء حتى تَصِل إلى المحرف الأول. اُنظر الشيفرة التالية:
static String reverse(String str) { String copy; // النسخة المعكوسة // الموضع الحالي من السلسلة النصية الممرة // يأخذ القيم بداية من طول السلسلة وحتى صفر int i; copy = ""; // ابدأ بسلسلة نصية فارغة for ( i = str.length() - 1; i >= 0; i-- ) { // أضف المحرف الحالي إلي نهاية النسخة المعكوسة copy = copy + str.charAt(i); } return copy; }
لاحِظ أن بعض السَلاسِل النصية لا تتغَيَّر سواء قُرأت طردًا (forwards) أو عكسًا (backwards)، أي سواء قرأت من اليسار إلى اليمين أو العَكْس. يُعرَف ذلك باسم السِّياق المُتَناظِر أو القلب المستو (palindrome)، والذي يُمكِنك اختبار حُدُوثه على سِلسِلة نصية معينة word
بالاستعانة بالدالة المُعرَّفة بالأعلى وباِستخدَام الاختبار if (word.equals(reverse(word)))
تحديدًا.
يقع كثير من المبتدئين، أثناء كتابتهم للدوال، بخطأ طباعة الإجابة بدلًا من إعادتها، وهو ما يُمثِل سوء فهم تام لماهية الدوال. إن مُهِمّة الدالة (function) ببساطة هو مجرد حِسَاب قيمة ما، ثم العودة مرة آخرى إلى سَطْر الشيفرة المسئول عن اِستدعاء الدالة، حيث ينبغي اِستخدَام القيمة المُعادة. قد يَطبَع ذلك السَطْر من الشيفرة تلك القيمة أو يُسنِدها إلى مُتَغيِّر أو يَستخدِمها ضِمْن تعبير (expression)، أو أي شيء آخر قد يَتطلَّبه البرنامج، المهم هنا هو أن تُدرِك أن أيًا كانت الطريقة التي سيُوظِّف بها المُستدعِي تلك القيمة المُعادة فهي بالنهاية أمر لا يَخُصّ الدالة.
مسألة متتالية "3N+1"
سنُحاوِل بالمثال الأخير من هذا القسم اِستخدَام الدالة nextN()
التي عَرَفناها بالأعلى لتَعْديل البرنامج المسئول عن حِسَاب قيم عناصر المتتالية "3N+1". أُضيفَت بعض التَعْديلات الآخرى أيضًا، مثل طباعة قيم عناصر المتتالية بأعمدة، بحيث يَحتوِي كل سَطْر على ٥ عناصر فقط، مما سيُحسِن من هيئة الخَرْج الناتج عن البرنامج. تستطيع القيام بذلك ببساطة عن طريق الاحتفاظ بعدد العناصر المَطبوعة بالسَطْر الحالي، وبدء سَطْر خَرْج جديد عند وصول قيمة ذلك العدد إلى القيمة ٥. نُسِق الخَرْج بحيث تُصطَفَّ العناصر بأعمدة مُرتَّبة:
input textio.TextIO; /** * برنامج يحسب قيم عناصر المتتالية 3N+1 ويطبعها. تُحدد قيمة * أول عنصر بالمتتالية من قبل المستخدم، ثم تُطبع القيم بأعمدة * بحيث يحتوي كل سطر على 5 عناصر فقط. * كما تطبع عدد عناصر المتتالية بالنهاية */ public class ThreeN2 { public static void main(String[] args) { System.out.println("This program will print out 3N+1 sequences"); System.out.println("for starting values that you specify."); System.out.println(); int K; // اقرأ مدخل المستخدم do { System.out.println("Enter a starting value;"); System.out.print("To end the program, enter 0: "); K = TextIO.getlnInt(); // اقرأ قيمة أول عنصر بالمتتالية if (K > 0) print3NSequence(K); // اعد تكرار الحلقة إذا كان k أكبر من الصفر } while (K > 0); } // نهاية main /** * يطبع print3NSequence قيم المتتالية 3N+1 إلى الخرج القياسي، * بحيث يكون أول عنصر بالمتتالية هو قيمة المعامل الممررة * كما يطبع أيضًا عدد عناصر المتتالية * قيمة المعامل startingValue الممررة ينبغي أن تكون عددًا صحيحا موجبا */ static void print3NSequence(int startingValue) { int N; // أحد عناصر المتتالية int count; // عدد العناصر حتى الآن int onLine; // عدد العناصر المطبوعة بالسطر الحالي N = startingValue; // ابدأ المتتالية بقيمة المعامل الممررة count = 1; // لدينا عنصر واحد بالمتتالية System.out.println("The 3N+1 sequence starting from " + N); System.out.println(); System.out.printf("%8d", N); onLine = 1; // هناك عدد مطبوع بالسطر الآن while (N > 1) { N = nextN(N); // احسب قيمة العنصر التالي count++; // أزد عناصر المتتالية بمقدار الواحد if (onLine == 5) { System.out.println(); // اطبع محرف العودة الى بداية السطر onLine = 0; // أعد ضبط عدد العناصر المطبوعة بالسطر } System.out.printf("%8d", N); // اطبع قيمة العنصر الحالي onLine++; // أزد قيمة عدد العنصر بالسطر بمقدار الواحد } System.out.println(); // انهي السطر الحالي من الخرج System.out.println(); // اطبع سطر فارغ System.out.println("There were " + count + " terms in the sequence."); } // نهاية print3NSequence /** * تحسب الدالة nextN قيمة عنصر المتتالية التالي وتعيده * بحيث تستقبل قيمة عنصر المتتالية الحالي كمعامل currentN */ static int nextN(int currentN) { if (currentN % 2 == 1) return 3 * currentN + 1; else return currentN / 2; } // نهاية الدالة nextN } // نهاية الصنف ThreeN2
ينبغي أن تَقْرأ هذا البرنامج بتأنِّي وأن تُحاوِل اِستيعاب طريقة عَمَله.
ترجمة -بتصرّف- للقسم Section 4: Return Values من فصل Chapter 4: Programming in the Large I: Subroutines من كتاب Introduction to Programming Using Java.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.