المعاملات (parameters) في جافا – لغة جافا

[*]

إذا كان البرنامج الفرعي (subroutine) عبارة عن صندوق أسود، فإن المُعامِلات (parameter) هي ببساطة طريقة لتمرير بعض المعلومات إليه من العالم الخارجي، لذا فإنها تُعدّ جزءًا من واجهة (interface) البرنامج الفرعي، وتَسمَح لك بتَخْصيص طريقة عمل البرنامج الفرعي للتَكيُف مع موقف محدد.

على سبيل المثال، إذا نظرنا لمنظم الحرارة كصندوق أسود، تتلخَّص مُهِمّته في الحفاظ على درجة حرارة معينة، فإن ذلك المنظم لديه مُعامِل، هو القرص، والذي يُستخدَم لتَخْصيص درجة الحرارة المطلوبة. سيُنفِّذ منظم الحرارة دائمًا نفس المُهِمّة، أيّ الحفاظ على درجة حرارة ثابتة، لكن قيمة درجة الحرارة تلك يُمكِن تَخْصيصها من خلال القرص.

استخدام المعاملات

سنتَطَرَّق مجددًا لمسألة متتالية الأعداد “3N+1″، والتي قد تَعرَّضنا لها بالقسم الفرعي ٣.٢.٢. تُحسَّب قيم مُتتالية الأعداد “3N+1” وفقًا للقاعدة التالية:

“إذا كان N عَدَدًا فرديًا، اِحسب حاصل ضربه في العَدَد ٣، ثُمَّ أَزِد قيمة حاصل الضرب بمقدار ١، أمَا إذا كان زوجيًا، اِحسب حاصل قِسمته على العَدَد ٢، بحيث تستمر بحِسَاب قيم عناصر مُتتالية الأعداد بنفس الطريقة حتى تُصبِح قيمة N مُساوِية للعَدَد ١. على سبيل المثال، إذا كانت قيمة N المبدئية تُساوِي ٣، سنَحصُل على مُتتالية الأعداد التالية: ٣، ١٠، ٥، ١٦، ٨، ٤، ٢، ١.”

اُكْتُب برنامجًا فرعيًا (subroutine) يَطبَع قيم مُتتالية الأعداد تلك. لاحِظ أن دور البرنامج هو طباعة متتالية الأعداد “3N+1” في العموم، أي أن القيم ذاتها التي سيَطبَعها لابُدّ وأن تعتمد على قيمة N المبدئية، ولهذا سنُمرِّر تلك القيمة كمُعامِل (parameter) للبرنامج الفرعي. يُمكِن كتابة البرنامج الفرعي كالتالي:

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) {
        // ‫إذا كان N عدد فردي
        if (N % 2 == 1)     
            N = 3 * N + 1;
        else
            N = N / 2;
        count++;   // أزد عدد عناصر المتتالية بمقدار الواحد
        System.out.println(N);  // اطبع العنصر
    }

    System.out.println();
    System.out.println("There were " + count + " terms in the sequence.");

}  // ‫نهاية print3NSequence

تَتكوَّن قائمة المعاملات (parameter list) -بتعريف البرنامج الفرعي بالأعلى- من المُعامِل int startingValue، مما يَعنِي أن ذلك البرنامج الفرعي سيَستقبِل مُعامِلًا (parameter) وحيدًا من النوع العَدَدَي الصحيح int، وسيَتَمكَّنْ من اِستخدَام اسم ذلك المُعامِل بداخل المَتْن، بنفس الطريقة التي يَستخدِم بها اسم أيّ مُتَغيِّر آخر. لاحِظ أننا لم نُسنِد أي قيمة للمُعامِل أثناء عملية التعريف ذاتها، فهو يَحصُل على قيمته المبدئية من مصدر خارجي أثناء عملية الاستدعاء، لذا لابُدّ للمُستدعِي أن يُمرِّر قيمة معينة لذلك المُعامِل ضِمْن تَعْليمَة الاستدعاء (subroutine call statement)، وعندها فقط ستُسنَد إلى startingValue قبل تَّنْفيذ المَتْن. مثلًا، عندما يُنفِّذ الحاسوب تَعْليمَة استدعاء البرنامج الفرعي print3NSequence(17);‎، فإنه يُسنِد أولًا القيمة ١٧ إلى startingValue، ثم يُنفِّذ بَعْدها التَعْليمَات الموجودة بمَتْن ذلك البرنامج، والتي تَطبَع قيم مُتتالية الأعداد “3N+1” بداية من العدد ١٧. وبصورة أعم، إذا كان K مُتَغيِّرًا من النوع العَدََدَي الصحيح int، يُمكِن اِستخدَام التَعْليمَة print3NSequence(K);‎ بهدف اِستدعاء البرنامج الفرعي، مما سيؤدي إلى إِسْناد قيمة المُتَغيِّر K إلى startingValue، ومِنْ ثَمَّ تَّنْفيذ مَتْن البرنامج الفرعي.

يُمكِن اِستدعاء البرنامج الفرعي print3NSequence بواسطة أيّ برنامج فرعي -البرنامج main()‎ أو غيره- مُعرَّف ضِمْن نفس الصَنْف المُتَضمِّن لتعريف البرنامج الفرعي print3NSequence. فمثلًا، يَطبَع البرنامج main()‎ -بالمثال التالي- قيم عناصر المُتتالية “3N+1” أكثر من مرة وبقيم مبدئية مختلفة، والتي تُخصَّص من قِبَل المُستخدِم:

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.getInt();  // اقرأ عنصر المتتالية الأول من المستخدم
        if (K > 0)   // اطبع المتتالية
            print3NSequence(K);
    } while (K > 0);   // ‫إستمر إذا كانت قيمة k أكبر من الصفر
} // نهاية main

تنبيه: لابُدّ أن يُعرَّف كُلًا من البرنامجين main و print3NSequence ضِمْن نفس الصَنْف؛ كي تتمكَّن من تَشْغِيل البرنامج بالأعلى.

المعاملات الصورية (formal) والفعلية (actual)

في الواقع، تُستخدَم كلمة “مُعامِل (parameter)” للإشارة إلى مفهومين، مُرتبطين نوعًا ما، لكنهما بالنهاية مختلفان. أولهما هو ذلك الذي نُشير إليه ضِمْن تعريفات البرامج الفرعية (definitions of subroutines)، مثل startingValue بالأعلى. والآخر هو ما نُشير إليه ضِمْن تَعْليمَات استدعاء البرامج الفرعية (subroutine call statements)، بحيث يُمرَّر إليها، مثل K بالتَعْليمَة print3NSequence(K);‎. يُطلَق على “المُعامِلات” من النوع الأول اسم المُعامِلات الصُّوريّة (formal parameters) أو المُعامِلات الوهمية (dummy parameters)، بينما يُطلَق على “المُعامِلات” من النوع الثاني اسم المُعامِلات الفعليّة (actual parameters) أو الوُسَطاء (arguments). عندما تَستدعِي برنامجًا فرعيًا، يُحصِّل الحاسوب قيم المُعامِلات الفعليّة بتَعْليمَة الاستدعاء، ثم يُسنِد تلك القيم إلى المُعامِلات الصُّوريّة المُصرَّح عنها بتعريف ذلك البرنامج الفرعي، وأخيرًا، يُنفِّذ مَتْن البرنامج الفرعي.

المُعامِل الصُّوريّ (formal parameter) عبارة عن “اسم”، أو بتعبير آخر، مُعرِّف بسيط (simple identifier)، فهو في الواقع أشبه ما يَكُون بالمُتَغيِّر، وكأي مُتَغيِّر، لابُدّ أن يكون له نوع مثل int أو boolean أو String أو double[]‎. في المقابل، المُعامِل الفعلي (actual parameter) هو مجرد “قيمة” يُفْترَض اِسْنادها لمُتَغيِّر المُعامِل الصُّوريّ المُناظِر عند الاستدعاء الفعليّ للبرنامج الفرعي، لذا يُمكِن اِستخدَام أي تعبير (expression) طالما كان يَؤول إلى قيمة هي من نوع يمكن إِسْناده -بواسطة تَعْليمَة إِسْناد (assignment)- إلى نوع المُعامِل الصُّوريّ المُناظِر. فمثلًا، إذا كان لدينا مُعامِل صُّوريّ من النوع double، ونظرًا لإمكانية إِسْناد قيمة من النوع int إلى المُتَغيِّرات من النوع double، فإن قيمة المُعامِل الفعليّ المُمرَّرة لذلك المُعامِل الصُّوريّ يُمكِن أن تَكُون من النوع int. لاحِظ أنه من الضروري تَمرير مُعامِل فعليّ لكل مُعامِل صُّوريّ أثناء الاستدعاء الفعليّ للبرنامج الفرعي. اُنظر البرنامج الفرعي التالي كمثال:

static void doTask(int N, double x, boolean test) {
    // تعليمات تنفيذ المهمة
}

تستطيع اِستدعاء البرنامج الفرعي بالأعلى باِستخدَام تَعْليمَة الاِستدعاء التالية:

doTask(17, Math.sqrt(z+1), z >= 10);

يُمكِن تَوصِيف ما يُنفِّذه الحاسوب -أثناء تَّنْفيذه لتَعْليمَة الاستدعاء بالأعلى- إلى تَعْليمَة الكُتلة (block statement) التالية:

{
    int N;       // خصص مساحة بالذاكرة للمعاملات الصورية
    double x;
    boolean test;
    // اسند القيمة 17 للمعامل الصوري الأول 
    N = 17;              
    // احسب قيمة التعبير واسندها للمعامل الصوري الثاني
    x = Math.sqrt(z+1);  
    // احسب قيمة التعبير المنطقي واسندها للمعامل الصوري الثالث
    test = (z >= 10);    
    // تعليمات تنفيذ المهمة
}

إلى جانب الاختلاف الجَلِيّ بحجم الشيفرة التي تَتَطلَّبها كِلا الطريقتين، هناك أيضًا اختلافات آخرى مُتعلقة بنِطاق (scope) المُتَغيِّرات، وكيفية التَعامُل في حالة وجود عدة مُتَغيِّرات أو مُعامِلات بنفس الاسم.

قد تَكُون فكرة اِستخدَام مُعامِل (parameter) لتمرير بعض المعلومات إلى برنامج فرعي (subroutine) معين بديهية نوعًا ما، ولذلك فإن اِستدعاء التوابع الفرعية المُعرَّفة مُسْبَّقًا ليس بمشكلة، وهو ما يَختلِف تمامًا عن كتابة تعريف البرنامج الفرعي ذاته (subroutine definition)، والذي عادة ما يُرْبِك دَارسِي البرمجة المبتدئين، بالأخص ما يتعلق منها بكيفية عَمَل المُعامِلات. في الواقع، يَقع الكثيرون منهم في خطأ إِسْناد قيم إلى المُعامِلات الصُّوريّة (formal parameters) ببداية مَتْن البرنامج الفرعي، وهو ما يُمثِل سوء فهم تام لماهية مُعامِلات البرامج الفرعية؛ فعندما يُنفِّذ الحاسوب تَعْليمَة اِستدعاء برنامج فرعي معين، فإنه يُسنِد قيم المُعامِلات الفعليّة (actual parameters) المُمرَّرة بتَعْليمَة الاستدعاء إلى المُعامِلات الصُّوريّة، وذلك قَبْل البدء بتَّنْفيذ مَتْن البرنامج الفرعي، أيّ أنه حينما يبدأ الحاسوب بتَّنْفيذ ذلك المَتْن، تَكُون المُعامِلات الصُّوريّة قد هُيئت بالفعل بقيمها المبدئية المُمرَّرة، ولذلك لا معنى من بدء المَتْن بإِسْناد قيم إلى تلك المُعامِلات. تَذكَّر أن البرنامج الفرعي ليس مُستقلًا؛ فبالنهاية هو يُستدعَى بواسطة برنامج آخر (routine)، ولذا تَقع مسئولية تَوْفِير قيم مُعامِلاته على تَعْليمَة الاستدعاء، ومِنْ ثَمَّ على ذلك البرنامج المُستدعِي.

التحميل الزائد (overloading)

يَتَطلَّب استدعاء برنامج فرعي معين مَعرِفة بعض المعلومات عنه، والتي يُطلَق عليها اسم “بَصْمَة البرنامج الفرعي (subroutine’s signature)”. تَتَكوَّن تلك البَصْمَة من كُلًا من اسم البرنامج الفرعي، وعدد المُعامِلات الصُّوريّة (formal parameters) المُعرَّفة ضِمْن قائمة مُعامِلاته، بالإضافة إلى نوعها. على سبيل المثال، يُمكِنك كتابة بَصْمَة البرنامج الفرعي doTask المذكور بالأعلى كالتالي: doTask(int,double,boolean)‎. لاحِظ أن بَصْمَة البرنامج الفرعي لا تَتضمَّن أسماء المُعامِلات، وهو في الواقع أمر يَسهُل تبريره؛ فليس هناك أيّ نَفْع من مَعرِفة أسماء المُعامِلات الصُّوريّة إذا كان كل غرضك هو مجرد اِستخدَام ذلك البرنامج الفرعي، ولذلك لا تُعدّ أسماء المُعامِلات جزءًا من واجهة البرنامج الفرعي (subroutine’s interface).

تَسمَح لغة الجافا باِستخدَام نفس الاسم لأكثر من برنامج فرعي داخل نفس الصَنْف، بشَّرْط اختلاف البَصْمَة (signatures) الخاصة بهم، فيما يُعرَف باسم “التحميل الزائد (overloading)” لاسم البرنامج الفرعي؛ حيث أصبح ذلك الاسم يَمتلك عدة معاني مختلفة. لا يَخلِط الحاسوب بين تلك البرامج الفرعية التي تَحمِل نفس الاسم، ويَستطيع تَّمييز أي منها تَرغَب باستدعائه، وذلك بالاعتماد على عدد المُعامِلات الفعليّة المُمرَّرة ضِمْن تَعْليمَة الاستدعاء، وأنواع تلك المُعامِلات. في الواقع، يَستخدِم الكائن (object)‏ System.out التحميل الزائد (overloading)؛ حيث تَتَوفَّر له العديد من التوابع (methods) المختلفة، والتي تَحمِل جميعها الاسم println مع اختلاف بَصْمَاتها (signatures) بالتأكيد. اُنظر على سبيل المثال:

println(int)                   println(double)
println(char)                  println(boolean)
println()

يَعتمِد الحاسوب على نوع المُعامِل الفعليّ (actual parameter) المُمرَّر بتَعْليمَة الاستدعاء لمَعرِفة أي بَصْمَة من تلك البرامج الفرعية ترغب بتَّنْفيذها. فمثلًا، عند اِستخدَام التَعْليمَة System.out.println(17)‎، فإنه يَستدعِي البرنامج الفرعي ذو البَصْمَة println(int)‎، أما في حالة اِستخدَام التَعْليمَة System.out.println('A')‎، فإنه يَستدعِي البرنامج الفرعي ذو البَصْمَة println(char)‎. تُعدّ جميع البرامج الفرعية بالأعلى مرتبطة نوعًا ما من الناحية الدلالية، فجميعها يقوم بالطباعة، وهو ما يُبرِّر تسميتها جميعًا بنفس الاسم، ولكن فيما يتعلق بالحاسوب، فإن طباعة عدد صحيح من النوع int تختلف تمامًا عن طباعة محرف من النوع char، والتي بدورها تختلف عن طباعة قيمة منطقية من النوع boolean، وهو ما يُبرِّر تعريف برنامج فرعي منفصل لكُلًا من تلك العمليات المختلفة.

لا يُعدّ نوع القيمة المُعادة من البرنامج الفرعي (return type) جزءًا من البَصْمَة، ولهذا لا يُسمَح بتعريف برنامجين فرعيين بصَنْف معين إذا كان لهما نفس الاسم والبَصْمَة (signature)، حتى مع اختلاف نوع القيمة المُعادة منهما. فمثلًا، سيُؤدي تعريف البرنامجين الفرعيين التاليين بنفس الصَنْف إلى حُدوث خطأ في بناء الجملة (syntax error):

int    getln() { ... }
double getln() { ... }

وفي الواقع، هذا هو السبب وراء عدم تسمية جميع البرامج الفرعية المُعرَّفة ضِمْن الصَنْف TextIO، والمُستخدَمة لقراءة الأنواع المختلفة، بنفس الاسم getln()‎، وإنما يُستخدَم اسم مختلف لكل نوع مثل getlnInt()‎ و getlnDouble()‎؛ لأن الصَنْف TextIO لا يُمكِن أن يَتَضمَّن أكثر من برنامج فرعي دون مُعامِلات، ويَحمِل نفس الاسم getln.

أمثلة لبرامج فرعية

تَتكوَّن البرمجة باستخدام البرامج الفرعية من شقّين، الأول هو تصميم البرنامج (program design)، أيّ تَقسِّيم مُهِمّة البرنامج (program) إلى مَهَامّ فرعية (subtasks) صغيرة بحيث يُسنَد كل منها إلى برنامج فرعي (subroutine)، أما الشقّ الآخر، فيَتعلَّق بعملية كتابة تعريف تلك البرامج الفرعية الصغيرة، والتي هي مسئولة عن تَّنْفيذ المَهَامّ الفرعية. سنقوم الآن بكتابة عدة أمثلة لبعض من تلك البرامج الفرعية الصغيرة، بينما سنُعود إلى مناقشة موضوع تصميم البرامج (program design) بالقسم ٤.٧.

أول مثال هو كتابة برنامج فرعي يَستقبِل عددًا صحيحًا موجبًا كمُعامِل، ثم يَحسِب جميع قواسم (divisors) هذا العدد، ويَطبَعها. يُكتَب أيّ برنامج فرعي بالصيغة (syntax) التالية:

<modifiers>  <return-type>  <subroutine-name>  ( <parameter-list> ) {
    <statements>
}

تَتلخَّص كتابة أي برنامج فرعي (subroutine) في مَلْئ هذه الصيغة، لذا دَعْنَا نقوم بذلك لمسألة حِسَاب القواسم. أولًا، تَنُصّ المسألة على اِستقبال البرنامج الفرعي لمُعامِل (parameter) وحيد من النوع int، كما تُبيِّن المُهِمّة المطلوب تَّنْفيذها بواسطة التَعْليمَات المَكًتوبة بمَتْن ذلك البرنامج. ثانيًا، لمّا كان حديثنا في الوقت الراهن مقصورًا على البرامج الفرعية الساكنة (static subroutines)، فإننا سنَستخدِم المُبدِّل static بالتعريف. ثالثًا، قد نُضيف مُبدِّل وصول (access modifier) بالتعريف، مثل public أو private، ولكن نظرًا لعدم النَصّ بذلك صراحة ضِمْن نَّصّ المسألة، فلن نَستخدِم أيًا منهما. رابعًا، لم تَنُصّ المسألة على وجود أي قيمة مُعادة، ولذلك سيَكُون النوع المُعاد (return type) هو void. أخيرًا، لمّا لم تُحدِّد المسألة كُلًا من اسم البرنامج الفرعي (subroutine)، وأسماء المُعامِلات الصُّوريّة (formal parameter) صراحةً، فإننا سنَضَطرّ لاختيار تلك الأسماء بأنفسنا، ولذلك سيُستخدَم N كاسم للمُعامِل، و printDivisors كاسم للبرنامج الفرعي. اُنظر تعريف البرنامج الفرعي:

static void printDivisors( int N ) {
    <statements>
}

ينبغي لنا الآن كتابة مجموعة التَعْليمَات التي ستُكوِّن مَتْن البرنامج (routine body)، وهو ليس أمرًا صعبًا. تَذكَّر فقط أن المُعامِل N سيَكُون لديه قيمة بالفعل ببداية تَّنْفيذ المَتْن. يُمكِن كتابة تَوصِيف الخوارزمية كالتالي:

“لكل عدد يُحتمَل أن يَكُون قَاسِمًا D، أيّ بدايةً من الواحد ووصولًا للعَدَد N، إذا تَمكَّن العدد D من تَقسِّيم العَدَد N تَقسِّيمًا مُتعادلًا، اِطبَع قيمة D.”

تُصبح الخوارزمية كالتالي بعد ترجمتها إلى لغة الجافا:

/**
 * اطبع قواسم‫ N
 * ‫بفرض أن N هو عدد صحيح موجب
 */
static void printDivisors( int N ) {
    int D;   
    System.out.println("The divisors of " + N + " are:");
    for ( D = 1; D <= N; D++ ) {
        if ( N % D == 0 )  // ‫إذا نجح D في تقسيم N تقسيما متعادلا
            System.out.println(D);
    }
}

يُمثِل التعليق (comment)، المُضاف قَبْل تعريف البرنامج الفرعي (subroutine definition)، ما يُعرَف باسم المواصفة الاصطلاحية للبرنامج (subroutine contract)، والمُكوَّنة من: بَيان هدف البرنامج الفرعي، بالإضافة إلى التَّصْريح عن أيّ اِفتراضات ينبغي مَعرفِتها قَبْل اِستخدَام ذلك البرنامج. مثلًا، في المثال بالأعلى، ذُكِر أن N ينبغي أن يَكُون عددًا صحيحًا موجبًا، ولذلك ينبغي لمُستدعِي البرنامج الفرعي التَأكُّد من تَوْفِية ذلك الفَرْض.

لنَفْحَص مثالًا آخر، تَنُصّ المسألة على التالي:

“اُكتب برنامجًا فرعيًا خاصًا private اسمه printRow، بحيث يَستقبِل ذلك البرنامج كُلًا من المُعامِلين: ch من النوع char، و N من النوع int. يَتلخَّص دور ذلك البرنامج بطباعة المحرف ch عدد N من المرات وذلك بسَطْر نصي مُنفصِل.”

بخلاف المسألة السابقة، نَصَّت هذه المسألة صراحةً على كُلًا من اسم البرنامج الفرعي وأسماء المُعامِلات، كما نَصَّت على ضرورة كَوْنه خاصًا، أيّ ينبغي اِستخدَام مُبدِّل الوصول private، ولذلك نحن مقيدين نوعًا ما فيما يتعلق بالسطر الأول من تعريف البرنامج الفرعي (subroutine definition). أخيرًا، مُهِمّة البرنامج بسيطة جدًا، ولذا يَسهُل كتابة مَتْن البرنامج الفرعي. اُنظر شيفرة البرنامج بالكامل:

/**
 * ‫ اطبع المحرف `ch` عدد `N` من المرات وذلك بسَطْر نصي مُنفصِل
 * ‫إذا كان N أقل من أو يساوي الصفر، اطبع سطرًا فارغًا 
 */
private static void printRow( char ch, int N ) {
    int i;
    for ( i = 1; i <= N; i++ ) {
        System.out.print( ch );
    }
    System.out.println();
}

لَمْ تُصَرِّح المواصفة الاصطلاحية (contract) للبرنامج بالأعلى عن وجود أية افتراضات فيما يَخُص المُعامِل N، ولكنها في المقابل أوْضَحَت طريقة استجابة البرنامج الفرعي لجميع الحالات المُمكِنة، بما فيها الحالة غَيْر المُتوقَّعة من كَوْن N <= 0.

لنَفْحَص مثالًا آخر، ولكن في هذه المرة، سنُوضِح كيف يُمكِن لبرنامجين فرعيين أن يتفاعلا. تحديدًا، سنَكتُب برنامجًا فرعيًا يَستقبِل مُعامِلًا من النوع String، بحيث يَطبَع كل محرف بالسِلسِلة النصية (string) المُمرَّرة ٢٥ مرة وبسَطْر مُنفصِل. ينبغي أن تَستعين بالبرنامج الفرعي printRow()‎ الذي كتبناه للتو لطباعة ذلك الخَرْج.

في هذا المثال، لمّا لم تُحدِّد المسألة كُلًا من اسم البرنامج الفرعي (subroutine)، وأسماء المُعامِلات الصُّوريّة (formal parameter) صراحةً، فإننا سنضطر لاختيار تلك الأسماء، ولذلك سيُستخدَم str كاسم للمُعامِل، وprintRowsFromString كاسم للبرنامج الفرعي. يُمكِن كتابة تَوصِيف الخوارزمية كالتالي:

“لكل مَوْضِع i بالسِلسِلة النصية str، اِستدعِي البرنامج الفرعي printRow(str.charAt(i),25)‎؛ لطباعة سَطْر الخَرْج.”

تُصبِح الخوارزمية كالتالي بَعْد ترجمتها إلى لغة الجافا:

/**
 * لكل محرف بالسلسلة النصية، اطبع سطر مكون من 25 نسخة من ذلك المحرف
 */
private static void printRowsFromString( String str ) {
    int i;  
    for ( i = 0; i < str.length(); i++ ) {
        printRow( str.charAt(i), 25 );
    }
}

نستطيع الآن استدعاء البرنامج الفرعي printRowsFromString المُعرَّف بالأعلى داخل البرنامج main()‎ كالتالي:

public static void main(String[] args) {
    String inputLine;  // السطر النصي المدخل من قبل المستخدم
    System.out.print("Enter a line of text: ");
    inputLine = TextIO.getln();
    System.out.println();
    printRowsFromString( inputLine );
}

لاحِظ أنه من الضروري تَضْمِين تعريف البرامج الثلاثة main()‎ و printRowsFromString()‎ و printRow()‎ بنفس الصَنْف. على الرغم من كَوْن البرنامج بالأعلى عديم الفائدة نوعًا ما، لكنه على الأقل يُبيِّن طريقة اِستخدَام البرامج الفرعية. يُمكِنك الإطلاع على البرنامج كاملًا بالملف RowsOfChars.java.

المصفوفات كمعامل

بالإضافة إلى إمكانية تمرير المُعامِلات من الأنواع البسيطة (primitive types) إلى البرامج الفرعية، يُسمَح أيضًا بتمرير المُعامِلات من أنواع المصفوفة (array types)، ما يَعنِي تمرير مصفوفة كاملة من القيم (المُتَغيِّرات إذا شِئنا الدقة) من خلال مُعامِل وحيد. لنَكتُب، على سبيل المثال، برنامجًا فرعيًا يَستقبِل مُعامِلًا من نوع المصفوفة int[]‎؛ ثم يَطبَع جميع الأعداد الصحيحة الموجودة بها، بحيث يُفصَل بين كل عدد والذي يَليه فاصلة (comma)، وبحيث تُحاط جميع تلك الأعداد بزوج من الأقواس []، اُنظر تعريف البرنامج الفرعي:

static void printValuesInList( int[] list ) {
    System.out.print('[');
    int i;
    for ( i = 0; i < list.length; i++ ) {
        if ( i > 0 )
            System.out.print(','); 
        System.out.print(list[i]);
    }
    System.out.println(']');
}

لكي نَستدعِي البرنامج الفرعي بالأعلى، سنحتاج إلى مصفوفة فعليّة (actual). تُنشِئ الشيفرة التالية مصفوفة أعداد صحيحة (array of ints)، وتُمرِّرها كوسيط (argument) للبرنامج الفرعي:

int[] numbers;
numbers = new int[3];
numbers[0] = 42;
numbers[1] = 17;
numbers[2] = 256;
printValuesInList( numbers );

سنَحصُل على الخَرْج [42,17,256].

وسطاء سطر الأوامر (command-line arguments)

لمّا كان البرنامج (routine)‏ main يَستقبِل مُعامِل مصفوفة من النوع String[]‎، كان لزامًا على مُستدعِيه -أيّ النظام (system) في هذه الحالة- أن يُمرِّر مصفوفة سَلاسِل نصية (array of String) فعليّة (actual) كقيمة لذلك المُعامِل الصُّوريّ (formal parameter). لذا لابُدّ أن يَحصُل النظام على قيم السَلاسِل النصية بتلك المصفوفة بطريقة ما، فما الذي يعنيه ذلك؟ وكيف سيَتحصَّل النظام على تلك القيم؟ في الواقع، تَتَكوّن تلك القيم من وُسَطاء سطر الأوامر (command-line arguments)[*] المُمرَّرين إلى الأمر المُستخدَم لتَشْغِيل البرنامج، وبالتالي يُسنِد النظام قيم هؤلاء الوُسَطاء إلى مصفوفة سَلاسِل نصية (array of strings)، ثم يُمرِّرها إلى البرنامج main()‎.

[*] يستطيع المُستخدِم عمومًا تَشْغِيل أحد البرامج من خلال كتابة أمر (command) معين بواجهة سَطْر الأوامر (command-line interface). يَتَكوّن الأمر عمومًا من اسم البرنامج المطلوب تَشْغِيله، ولكن يُمكِن أيضًا كتابة مُدْخَلات إضافية ضِمْن الأمر. تُسمَى تلك المُدْخَلات الإضافية باسم وُسَطاء سَطْر الأوامر (command-line arguments). فمثلًا، إذا كان اسم برنامج معين هو myProg، تستطيع تَشْغِيله باِستخدَام الأمر التالي:

java myProg

ولكن، في هذه الحالة، لا يوجد أي وُسَطاء (arguments)، في المقابل، تستطيع تمرير وسيط واحد أو أكثر باِستخدَام الأمر التالي:

java myProg one two three

في المثال بالأعلى، تُمثِل السَلاسِل النصية “one”، و “two”، و “three” قيم الوُسَطاء، ولذلك سيُسنِد النظام هذه السَلاسِل النصية إلى مصفوفة من النوع String[]‎، ثم يُمرِّرها كمُعامِل إلى البرنامج main()‎. على سبيل المثال، تَطبَع الشيفرة التالية قيمة أي وسيط (argument) أَدْخَله المُستخدِم:

public class CLDemo {

    public static void main(String[] args) {
        System.out.println("You entered " + args.length
                           + " command-line arguments");
        if (args.length > 0) {
            System.out.println("They were:");
            int i;
            for ( i = 0; i < args.length; i++ )
                System.out.println("   " + args[i]);
        }
    } // ‫نهاية main

} // ‫نهاية الصنف CLDemo

إذا لم يُخصِّص المُستخدِم أي وسيط (arguments) بأمر تَشْغِيل البرنامج، فإن المُعامِل args سيَكُون عبارة عن مصفوفة فارغة، طولها (length) يُساوي الصفر.

عمليًا، يُستخدَم وُسَطاء سَطْر الأوامر عادةً بهدف تمرير أسماء بعض الملفات إلى البرنامج. فمثلًا، يَنسَخ البرنامج التالي محتويات ملف نصي معين إلى ملف نصي آخر، ولذلك فإنه يَستخدِم الصَنْف TextIO ضِمْن حَلْقة تَكْرار، بحيث يَقْرأ سَطْرًا واحدًا من الملف الأصلي في كل مرة، ثم يَنسَخُه إلى الملف الآخر. يَستمِر في القيام بذلك حتى يَصِل إلى نهاية الملف، والتي يُستدَلّ عليها من القيمة المنطقية المُعادة من الدالة (function)‏ TextIO.eof()‎.

input textio.TextIO;
/** 
 * يتطلب ذلك الأمر وسيطين، هما أسماء الملفات
 * الأول ينبغي أن يكون اسم ملف موجود
 * والثاني ينبغي أن يكون اسم الملف الذي سينسخ البرنامج إليه بيانات الملف الأول
 * سيقوم البرنامج بنسخ محتويات الملف الأول إلى الملف الثاني
 * تحذير: إذا كان الملف الثاني موجود عند تشغيل البرنامج، ستتم الكتابة على البيانات السابقة
 * يعمل هذا البرنامج مع الملفات النصية فقط
 */
public class CopyTextFile {

    public static void main( String[] args ) {
        if (args.length < 2 ) {
            System.out.println("Two command-line arguments are required!");
            System.exit(1);
        }
        TextIO.readFile( args[0] );   // افتح الملف الأصلي بهدف القراءة
        TextIO.writeFile( args[1] );  // افتح الملف المراد النسخ إليه
        int lineCount;  // عدد الأسطر المنسوخة
        lineCount = 0;
        while ( TextIO.eof() == false ) {
            // اقرأ سطر من الملف الأصلي وانسخه للملف الآخر
            String line;
            line = TextIO.getln();
            TextIO.putln(line);
            lineCount++;
        }
        System.out.printf( "%d lines copied from %s to %s%n",
                          lineCount, args[0], args[1] );
    }

}

تُنفَّذ معظم البرامج حاليًا من خلال واجهة مُستخدِم رسومية (GUI environment)، ولذلك لم يَعُدْ وُسَطاء سَطْر الأمر (command-line arguments) بنفس ذات الأهمية في الوقت الحالي. مع ذلك ما يزال البرنامج بالأعلى مثالًا جيدًا لبيان كيفية اِستخدَام مُعامِلات المصفوفة (array parameters) على الأقل.

التبليغ عن الاعتراضات (exceptions)

ذَكَرَنا، منذ قليل، مصطلح المواصفة الاصطلاحية للبرنامج (subroutine contract)، والذي يُوضِح وظيفة البرنامج الفرعي في حالة تمرير قيم صالحة -وفقًا لما هو مُوضَّح بنفس المواصفة الاصطلاحية- لجميع المُعامِلات الخاصة به. تَتَبقَّى الحاجة إلى مَعرِفة كيفية تَصرُّف البرنامج الفرعي في حالة تمرير قيم غَيْر صالحة للمُعامِلات.

تَعرَّضنا بالفعل لعدة أمثلة بالقسم ٣.٧، والتي تُبلِّغ فيها البرامج الفرعية عن اِعتراضات (throwing exceptions) في حالة تمرير قيم غَيْر صالحة، فمثلًا، تَنُصّ المواصفة الاصطلاحية للبرنامج الفرعي المَبنِى مُسْبَّقًا (built-in)‏ Double.parseDouble على ضرورة أن يَكُون المُعامِل المُمرَّر إليها عبارة عن تمثيل نصي (string representation) لعَدََدَ من النوع double. إذا كانت قيمة المُعامِل المُمرَّرة مُتماشية مع ذلك الشَّرْط، سيُحوِّل البرنامج الفرعي السِلسِلة النصية (string) المُمرَّرة إلى قيمتها العَدَدية المكافئة، أما إذا لم تَكُن مُتماشية معها، سيُبلِّغ البرنامج الفرعي عن اِعتراض (exception) من النوع NumberFormatException.

تُبلِّغ الكثير من البرامج الفرعية عن اعتراض من النوع IllegalArgumentExceptions في حالة تمرير قيم غَيْر صالحة لمُعامِلاتها، وهو ما قد تَرغب في اتباعه بالبرامج الفرعية الخاصة بك. أيّ اعتراض (exception) هو بالنهاية عبارة عن كائن (object)، ستَضطرّ إلى إنشائه قَبْل التَبْليغ عنه باِستخدَام التَعْليمَة throw. سنتناول كيفية القيام بذلك تفصيليًا بالفصل الخامس، ولكن، إلى ذلك الحين، تستطيع اِستخدَام الصيغة (syntax) التالية لتَعْليمَة throw؛ للتَبْليغ عن اِعتراض من النوع IllegalArgumentException تحديدًا:

throw new  IllegalArgumentException( <error-message> );

تَتَكوّن رسالة الخطأ بالأعلى من سِلسِلة نصية (string) تَشرَح الخطأ المُكْتشَف بينما تُستخدَم new لإنشاء كائن الاعتراض (exception object). كل ما عليك القيام به هو فَحْص قيم المُعامِلات المُمرَّرة؛ لمَعرِفة ما إذا كانت صالحة أم لا، ثم التَبْليغ عن اعتراض باِستخدَام التَعْليمَة بالأعلى إذا لم تَكُن صالحة. على سبيل المثال، لابُدّ أن تَكُون قيمة المُعامِل المُمرَّرة إلى البرنامج الفرعي print3NSequence -المُعرَّف ببداية هذا القسم- عددًا صحيحًا موجبًا، لذلك يُمكِننا، في حالة انتهاك هذا الشَّرْط، تَعْدِيل تعريف البرنامج الفرعي (subroutine definition) بالصورة التالية؛ وذلك لكي نَتَمَكَّن من التَبْليغ عن الاِعتراض:

static void print3NSequence(int startingValue) {

    if (startingValue <= 0)  // إذا حدث انتهاك للمواصفة الاصطلاحية
        throw new IllegalArgumentException( "Starting value must be positive." );

    // بقية البرنامج الفرعي مثلما بالأعلى

إذا كانت القيمة المُمرَّرة إلى المُعامِل startingValue غَيْر صالحة، فستُنهِي تَعْليمَة throw بالأعلى البرنامج الفرعي فورًا بدون تَّنْفيذ بقية المَتْن، كما قد ينهار (crash) البرنامج بالكامل في حالة عدم اِلتقاط ذلك الاِعتراض (catching exception) بمكان آخر ضِمْن البرنامج باِستخدَام التَعْليمَة try..catch بحيث يُستدعَى البرنامج الفرعي print3NSequence داخل الجزء try، كما ناقشنا بالقسم ٣.٧.

المتغيرات العامة (global) والمحلية (local)

كملاحظة أخيرة بهذا القسم، يُمكِننا القول أننا نستطيع الآن اِستخدَام ثلاثة أنواع مختلفة من المُتَغيِّرات داخل أيّ برنامج فرعي، هي كالتالي: أولًا، المُتَغيِّرات المحليّة (local variables) والتي يُصرَّح عنها داخل البرنامج الفرعي. ثانيًا، أسماء المُعامِلات الصُّوريّة (formal parameter) ضِمْن تعريف البرنامج الفرعي. ثالثًا، المُتَغيِّرات الأعضاء الساكنة (static member variables)، والتي يُصرَّح عنها خارج البرنامج الفرعي.

تُعدّ المُتَغيِّرات المحليّة (local variables) من صميم أعمال البرنامج الفرعي الداخلية، وليس لها أيّ اتصال مع ما هو خارج البرنامج الفرعي.

تُمرِّر المُعامِلات الصُّوريّة (formal parameter) قيم خارجية إلى البرنامج الفرعي أثناء الاستدعاء، ولكنها مع ذلك تَتَصرَّف بطريقة مشابهة للمُتَغيِّرات المحليّة بمجرد بدء تَّنْفيذ البرنامج الفرعي. يَعنِي ذلك أنه في حالة حُدوث تَغْيِير بقيمة أحد المُعاملات الصُّوريّة (من النوع الأوَّليّ [primitive type] تحديدًا) أثناء تَّنْفيذ البرنامج الفرعي، فإن ذلك التَغْيِير لا يُؤثِر نهائيًا على بقية البرنامج. لاحِظ أنه في حالة كان المُعامِل الصُّوريّ عبارة عن مصفوفة أو كائن (object)، فإن الأمور تُصبِح أكثر تعقيدًا كما سنرى لاحقًا.

أخيرًا، بخلاف المُتَغيِّرات المحليّة (local variables) المُعرَّفة داخل البرامج الفرعية، فإن المُتَغيِّرات العامة (global variables) تُعرَّف خارج أي برنامج فرعي، ولذا فإنها تُعدّ مستقلة عنها، كما أنها قابلة للاِستخدَام ضِمْن أجزاء عديدة من البرنامج (program). في الواقع، يُمكِن اِستخدَام المُتَغيِّر العام (global variable) بأيّ مكان داخل الصَنْف المُُعرَّف بداخله، بل يُمكِن استخدامه بأي صنف آخر طالما لم يُصرَّح عنه باِستخدَام المُبدِّل private. لاحظ أنه إذا حَدَثَ تغيير على مُتَغيِّر عام (global variable) داخل برنامج فرعي معين، فإن تأثير ذلك التغيير يمتد إلى ما هو أبعد من مجرد ذلك البرنامج الفرعي الذي أَحَدَث التغيير، وهو ما قد تعرضت له بالفعل بالمثال الأخير بالقسم السابق؛ حيث عَدَّلت إحدى البرامج الفرعية قيم المُتَغيِّرات العامة gamesPlayed و gamesWon، ثم طُبعت بواسطة برنامج فرعي آخر هو main()‎.

إذا اِستخدَمت مُتَغيِّرًا عامًا (global variable) ضِمْن برنامج فرعي معين، فإن ذلك البرنامج الفرعي يُصبِح قادرًا على الاتصال مع بقية البرنامج (program) فيما يُشبه الاتصال السري (back-door communication)، ولهذا نستطيع القول أن المُتَغيِّرات العامة قد أصبحت بذلك جزءًا من واجهة البرنامج الفرعي (subroutine’s interface). يُعدّ الاتصال السري عمومًا أقل وضوحًا وصراحةً من ذلك المُقام باستخدام المُعامِلات، ولهذا فإنه قد يَتَسبَّب بكسر القاعدة التي تَنُصّ على ضرورة كَوْن واجهة (interface) “الصندوق الاسود (black box)” صريحة وسهلة الفهم. قد يَدْفَعك ذلك إلى الظّنّ بأن اِستخدَام المُتَغيِّرات العامة داخل البرامج الفرعية أمر سيئ عمومًا، ولكن هذا غَيْر صحيح، فهناك على الأقل سببًا واحدًا جيدًا قد يَدْفَعك للقيام بذلك: مثلًا إذا فكرت بالصَنْف (class) بكَوْنه نوعًا من “الصندوق الاسود (black box)”، يُصبِح من المعقول تمامًا السماح للبرامج الفرعية الواقعة ضِمْن ذلك الصندوق بالاتصال سريًا مع بعضها البعض (back-door communication) خاصة إذا ساهم ذلك بتبسيط الصَنْف ككل من الخارج. لهذا يَنصَح الكاتب بعدم اتخاذ أيّ موقف عدائي مُطْلَق من اِستخدَام المُتَغيِّرات العامة (global variables) داخل البرامج الفرعية، فقط عليك أن تُفكِر مَلِيًّا قَبْل اِستخدَام مُتَغيِّر عام (global variable) داخل برنامج فرعي للتَأكُّد مما إذا كان ذلك ضروريًا.

ترجمة -بتصرّف- للقسم Section 3: Parameters من فصل Chapter 4: Programming in the Large I: Subroutines من كتاب Introduction to Programming Using Java.

[*] [*]Source link

اظهر المزيد
زر الذهاب إلى الأعلى

أنت تستخدم إضافة Adblock

الاعلانات هي مصدرنا الوحيد لدفع التكلفة التشغيلية لهذا المشروع الريادي يرجى الغاء تفعيل حاجب الأعلانات