اذهب إلى المحتوى

البرامج الفرعية والمتغيرات الساكنة في جافا


رضوى العربي

تَسمَح بعض اللغات البرمجية -بل غالبيتها في الواقع- بتعريف البرامج الفرعية (subroutines) بصورة مستقلة خارج أي صَنْف، لكن تختلف لغة الجافا عنهم في تلك النقطة، حيث لابُدّ أن يُعرَّف أيّ برنامج فرعي (subroutine) بلغة الجافا ضِمْن صَنْف (class). الهدف من الصَنْف عمومًا هو تجميع البرامج الفرعية والمُتَغيِّرات المُرتبطة معًا ضِمْن وحدة واحدة. نظرًا لأن البرامج المكتوبة بلغة الجافا عادة ما تَستخدِم عددًا ضخمًا من البرامج الفرعية المَكْتوبة بواسطة مبرمجين مختلفين، فإن احتمالية الخلط بين أسمائها يَكُون كبيرًا، لذا حَرَص مُصمِّمي الجافا على وَضع تقييد حازم على الطرق التي يُمكِن بها تسمية الأشياء، ومن هنا كانت ضرورة تعريف البرامج الفرعية وتجميعها ضِمْن أصناف (classes) لها اسم وبالتبعية تجميع تلك الأصناف ضِمْن حزم (packages) لها اسم أيضًا كما سنرى لاحقًا.

تُميز لغة الجافا بشكل واضح بين البرامج الفرعية الساكنة (static) وغَيْر الساكنة (non-static). يُمكِن لأي صَنْف أن يَتضمَّن كِلا النوعين، ولكن كيفية اِستخدَامهما مختلفة تمامًا. تُعدّ البرامج الفرعية الساكنة (static subroutine) عمومًا أسهل في الفهم، فببساطة أي برنامج فرعي ساكن هو عضو (member) ينتمي للصنف ذاته المُعرَّف بداخله. في المقابل، تعريف البرامج الفرعية غَيْر الساكنة (non-static subroutine) موجود فقط ضِمْن الصَنْف حتى تَتَمكَّن كائنات (objects) ذلك الصنف -عند إنشائها- من اِستخدَام تلك البرامج الفرعية، وتُصبِح عندها تلك البرامج أعضاء بتلك الكائنات (objects). يُمكِن تطبيق ذلك الاختلاف على المُتَغيِّرات (variables) الساكنة وغَيْر الساكنة بنفس الكيفية، بل وعلى أي شيء آخر قد يَقع ضمن تعريف الأصناف (class definition). سنتناول في هذا الفصل كُلًا من البرامج الفرعية الساكنة والمُتَغيِّرات الساكنة فقط، وسننتقل بالفصل التالي إلى البرمجة كائنية التوجه (object-oriented programming) وعندها سنستطيع مناقشة الأعضاء غَيْر الساكنة.

عادة ما يُطلَق مصطلح "التابع (method)" على البرامج الفرعية (subroutines) المُعرَّفة ضِمْن صَنْف، وهو الاسم الذي يُفضِّله غالبية مبرمجي الجافا مع أن المصطلحان عمومًا مترادفان خاصة فيما يَتعلَّق بلغة الجافا. ستَجِدْ أيضًا البعض يَستخدِم مصطلحات آخرى للإشارة إلى البرامج الفرعية مثل "الإجراء (procedure)" أو "الدالة (function)". يُفضِّل الكاتب الاستمرار باِستخدَام المصطلح الأعم "البرنامج الفرعي (subroutine)" ضِمْن هذا الفصل للإشارة إلى البرامج الفرعية الساكنة على الأقل، ولكنه سيبدأ في اِستخدَام مصطلح "التابع (method)" من حين لآخر، كما أنه سيَستخدِم مصطلح "الدالة (function)" فقط للإشارة إلى البرامج الفرعية التي تَحسِب قيمة وتُعيدها مع أن بعض اللغات البرمجية الآخرى تَستخدِم ذلك المصطلح للإشارة إلى البرامج الفرعية (subroutines) بمفهومها الأعم.

تعريف البرامج الفرعية

ينبغي أن يُعرَّف (define) أيّ برنامج فرعي (subroutine)، تَنوِي اِستخدَامه، بمكان ما بالبرنامج، بحيث يَتضمَّن ذلك التعريف (subroutine definition) كُلًا من الآتي: اسم البرنامج الفرعي (subroutine name)، والمعلومات اللازمة لاستدعائه (subroutine call)، وأخيرًا، الشيفرة الفعليّة المطلوب تَّنْفيذها مع كل عملية استدعاء. يُكتَب تعريف البرنامج الفرعي (subroutine definition) بالجافا بالصياغة التالية:

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

قد تكون الصيغة بالأعلى مألوفة بالنسبة لك؛ فقد تَعرَّضنا بالفعل خلال الفصول السابقة لبعض البرامج الفرعية، مثل البرنامج main()‎ المُعرَّف بأيّ برنامج، والبرنامج drawFrame()‎ ببرامج التحريكة (animation) بالقسم ٣.٩. ومع ذلك، لاِستيعاب ما تَعنيه تلك الصيغة تفصيليًا، سنحتاج إلى مُناقشتها باستفاضة، وهو ما سنفعله بغالبية هذا الفصل.

أولًا، تُكوِّن التَعْليمَات -الواقعة بين القوسين المعقوصين {}- مَتْن البرنامج الفرعي (subroutine body)، وتُمثِل الجزء التَّنْفيذي (implementation) أو الداخلي "للصندوق الأسود (black box)" كما ذَكَرنا بالقسم السابق. يُنفِّذ الحاسوب تلك التَعْليمَات عند إجراء عملية اِستدعاء لذلك البرنامج (أو التابع [method]). لاحظ أن تلك التَعْليمَات قد تَتكوَّن من أي تَعْليمَة قد تَعرَّضنا لها خلال الفصلين الثاني والثالث.

ثانيًا، تُكتَب المُبدِّلات ببداية التعريف، وهي عبارة عن كلمات تُستخدَم لضَبْط بعض خاصيات البرنامج الفرعي، مثل ما إذا كان ساكنًا (static) أم غير ساكن (non-static). تُوفِّر لغة الجافا عمومًا ستة مُبدِّلات (modifiers) يُمكنك اِستخدَامها، ولقد تَعرَّضنا لاثنين منها فقط حتى الآن، وهي المُبدِّل الساكن static، والمُبدِّل العام public.

ثالثًا، يُستخدَم النوع المُعاد لتَخْصِيص نوع القيمة المُعادة من البرنامج الفرعي. تحديدًا، إذا كان البرنامج الفرعي بالأساس هو عبارة عن دالة (function) تَحسِب قيمة معينة، عندها قد تَستخدِم أي نوع، مثل String أو int أو حتى أنواع المصفوفة مثل double[]‎. أما إذا كان البرنامج الفرعي ليس بدالة، أيّ لا يُعيد قيمة، فعندها تُستخدَم القيمة الخاصة void، والتي تُشير إلى عدم وجود قيمة مُعادة، أيّ أنها إِما أن تَكُون فارغة (empty) أو غَيْر موجودة. سنتناول الدوال (functions)، والأنواع المُعادة (return types) تفصيليًا بالقسم ٤.٤.

وأخيرًا، قائمة المُعامِلات الخاصة بالتابع (method). تُعدّ المُعامِلات جزءًا من وَاجهة البرنامج الفرعي (subroutine interface)، وتُمثِل المعلومات المُمرَّرة (passed) إليه من الخارج، والتي قد يَستخدِمها لحِسَاب بعض العمليات الداخلية الخاصة به. فمثلًا، بفَرْض وجود الصَنْف Television، والذي يَتضمَّن التابع changeChannel()‎. في تلك الحالة، يُفْترَض أن يُطرَح سؤالًا تلقائيًا عن رقم القناة التي تُريد من البرنامج الفرعي الانتقال إليها، وهنا يأتي دور المُعامِلات (parameter)؛ حيث يُمكِن اِستخدَام مُعامِل للإجابة على مثل هذا السؤال، ولمّا كان رقم القناة هو عدد صحيح، فسيَكُون نوع ذلك المُعامِل (parameter type) هو int، ولهذا يُمكِنك التَّصْريح (declaration) عن التابع changeChannel()‎ كالتالي:

public void changeChannel(int channelNum) { ... }

يُشير التَّصْريح بالأعلى إلى أن التابع changeChannel()‎ لديه مُعامِل يَحمِل اسم channelNum من النوع العددي الصحيح int. لاحظ أن قيمة المُعامِل channelNum غير مُتوفِّرة حتى الآن، وإنما تَتوفَّر عند عملية الاستدعاء الفعليّ للتابع الفرعي، على سبيل المثال: changeChannel(17);‎.

قد تتكوَّن قائمة المُعامِلات (parameter list) -بتعريف البرنامج الفرعي- من تَّصْريح عن مُعامِل (parameter declaration) واحد أو أكثر، وذلك على الصورة ، بحيث يَتكوَّن كل تَّصْريح من مُعامِل وحيد، كما يُفصَل بين كل تَّصْريح والذي يليه باستخدام فاصلة (comma). على سبيل المثال، في حالة أردت التَّصْريح عن مُعامِلين من النوع double، فستضطر إلى كتابة double x, double y وليس double x, y. لاحِظ أن قائمة المُعامِلات قد تَكُون فارغة أيضًا.

سنتناول المُعامِلات (parameters) تفصيليًا بالقسم التالي.

ها هي التعريفات الخاصة ببعض البرامج الفرعية (subroutine definitions)، ولكن بدون الجزء التَّنْفيذي (implementation) منها، أي بدون التَعْليمَات الفعليّة التي تُعرِّف ما تُنفِّذه تلك البرامج الفرعية:

public static void playGame() {
    // المبدلات هي‫ public و static
    // ‫النوع المعاد هو void
    // ‫اسم البرنامج الفرعي هو playGame
    // قائمة المعاملات فارغةً
}

int getNextN(int N) {
    // لا يوجد مبدلات
    // ‫النوع المعاد هو int
    // ‫اسم البرنامج الفرعي هو getNextN
    // ‫قائمة المعاملات تتضمن معامل اسمه N من النوع int
}

static boolean lessThan(double x, double y) {
    // ‫المبدلات هي static
    // ‫النوع المعاد هو boolean
    // ‫اسم البرنامج الفرعي هو lessThan
    // ‫قائمة المعاملات تتضمن معاملين x و y كلاهما من النوع double
}

بالمثال الثاني بالأعلى، لمّا كان تعريف التابع getNextN لا يَتضمَّن المُبدل static، فإنه يُعدّ بصورة افتراضية تابعًا غَيْر ساكن (non-static method)، ولذلك لن نَفْحصه بهذا الفصل! اُستخدِم المُبدل public بالمثال الأول، والذي يُشير إلى إِمكانية استدعاء التابع (method) من أي مكان بالبرنامج، حتى من خارج الصَنْف (class) الذي عُرِّف فيه هذا التابع. في المقابل، يَتوفَّر المُبدل private، والذي يُشير إلى إمكانية استدعاء التابع من داخل نفس الصَنْف فقط. يُطلَق على كُلًا من المُبدلين public و private مُحدِّدات الوصول أو مُبدِّلات الوصول (access specifiers/access modifier). يَتوفَّر مُبدل وصول (access modifier) أخير، هو protected، والذي ستتضح أهميته عندما ننتقل إلى البرمجة كائنية التَوجه (object-oriented programming) بالفصل الخامس. في حالة عدم تخصيص مُحدِّد وصول لتابع معين، فإنه، وبصورة افتراضية، يُصبِح قابلًا للاستدعاء من أيّ مكان بالحزمة (package) التي تَتضمَّن صَنْفه، ولكن ليس من خارج تلك الحزمة. سنُناقش الحزم (packages) خلال هذا الفصل، تحديدًا بالقسم ٤.٦.

يَتَّبِع البرنامج main()‎، المُعرَّف بأي برنامج، نفس قواعد الصيغة (syntax rules) المُعتادة لأي برنامج فرعي:

public static void main(String[] args) { ... }

بفَحْص التعريف بالأعلى، تَجد أن المُبدلات المُستخدَمة هي public و static، أما النوع المُعاد فهو void، أما اسم البرنامج الفرعي فهو main، وأخيرًا، قائمة المعاملات هي String[] args، أيّ أن نوع المُعامِل المُمرَّر هو نوع المصفوفة String[]‎.

إذا كنت قد قرأت الفصول السابقة، فلديك بالفعل الخبرة الكافية لكتابة الجزء التَّنْفيذي من البرنامج الفرعي (implementation of a subroutine). في هذا الفصل، سنتعلَّم كتابة التَعرِيف بالكامل بما في ذلك جزء الواجهة (interface).

استدعاء البرامج الفرعية

يُعدّ تعريف البرنامج الفرعي (subroutine definition) بمثابة إعلام للحاسوب بوجود ذلك البرنامج الفرعي وبالوظيفة التي يُؤديها، لكن يُرجئ التَّنْفيذ الفعليّ للبرنامج الفرعي إلى حين استدعائه (call). يَنطبِق ذلك حتى على البرنامج (routine)‏ main()‎، والذي يَستدعيه النظام (system) -لا أنت- عند تَّنْفيذه للبرنامج (program) ككل. تُستخدَم، مثلًا، تَعْليمَة استدعاء البرنامج الفرعي (subroutine call) التالية؛ لاستدعاء التابع playGame()‎ المذكور بالأعلى:

playGame();

يُمكن عمومًا كتابة تَعْليمَة الاستدعاء بالأعلى بأيّ مكان داخل نفس الصَنْف (class) الذي عَرَّف التابع playGame()‎، سواء كان ذلك بالتابع‏ main()‎، أو بأيّ برنامج فرعي آخر. علاوة على ذلك، لمّا كان التابع playGame()‎ مُعرَّف باِستخدَام المُبدل public، وهو ما يَعنِي كَوْنه تابعًا عامًا، فإنه من الممكن لأيّ صَنْف آخر استدعائه أيضًا، ولكن ينبغي أن تُعلِم الحاسوب، في تلك الحالة، باسم التابع كاملًا أثناء الاستدعاء، أيّ بذكر الصَنْف الذي ينتمي إليه ذلك التابع. ولأن التابع playGame()‎ مُعرَّف باِستخدَام المُبدل static، وهو ما يَعنِي كَوْنه تابعًا ساكنًا، فإن اسمه الكامل يَتضمَّن اسم الصنف ذاته المُعرَّف بداخله. على سبيل المثال، إذا كان التابع playGame()‎ مُعرَّف بالصَنْف Poker، تُستخدَم التَعْليمَة التالية لاستدعائه من خارج هذا الصَنْف:

Poker.playGame();

يُعلِم اسم الصَنْف -بالأعلى- الحاسوب بأيّ صَنْف ينبغي له أن يَجِد التابع. بالإضافة إلى ذلك، يُساعدنا وجود اسم الصَنْف أثناء الاستدعاء على التمييز بين التابع Poker.playGame()‎ وأي توابع اخرى لها نفس الاسم ومُعرَّفة بأصناف آخرى، مثل Roulette.playGame()‎ أو Blackjack.playGame()‎.

تُكتَب عمومًا تَعْليمَة استدعاء أي برنامج فرعي ساكن static ‏(static subroutine call) بالجافا بالصيغة التالية إذا كان البرنامج الفرعي المُستدعَى مُعرَّفًا بنفس الصَنْف (class):

<subroutine-name>(parameters);

أو كالتالي إذا كان البرنامج الفرعي مُعرَّفًا بصَنْف آخر:

<class-name>.<subroutine-name>(parameters);

يَختلف ذلك عن التوابع غَيْر الساكنة (non-static methods) -سنتناولها لاحقًا-، والتي تنتمي إلى كائنات (objects) وليس أصناف (classes)، ولهذا يُستدعَى ذلك النوع من التوابع من خلال الكائنات (objects) ذاتها لا من خلال أسماء الأصناف.

لاحظ أنه في حين يُمكِن لقائمة المُعامِلات (parameter list) أن تَكُون فارغة (empty)، كما هو الحال بالمثال playGame()‎، فإن كتابة الأقواس (parentheses) بتَعْليمَة الاستدعاء ما تزال ضرورية حتى مع كَوْن ما بينها فارغًا. أما في حالة تخصيص مُعامِل (parameter) أو أكثر بقائمة المُعامِلات (parameter list) بالتعريف الخاص ببرنامج فرعي ما (subroutine definition)، فينبغي عمومًا أن يَتطابَق عدد المُعامِلات المُمرَّرة أثناء استدعاء ذلك البرنامج الفرعي مع العَدَدَ المُخصَّص بذلك التعريف، كما لابُدّ بطبيعة الحال أن تَتطابَق أنواع تلك المُعامِلات المُمرَّرة بتَعْليمَة الاستدعاء مع نوعها المُناظِر بنفس ذلك التعريف.

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

سنُعطي الآن مثالًا عما قد يبدو عليه البرنامج (program) عند تَضمُّنه لبرنامج فرعي آخر غَيْر البرنامج main()‎. سنَكتُب تحديدًا برنامجًا للعبة تخمين، يَختار فيه الحاسوب عددًا عشوائيًا بين العددين ١ و ١٠٠، ثُمَّ يُحاول المُستخدِم تخمين ذلك العدد، ليُخبره الحاسوب بَعْدها عما إذا كان تخمينه أكبر أو أقل أو يُساوِي العَدَد الصحيح، وبحيث يَفوز المُستخدِم بالمباراة إذا تَمكَّن من تخمين العدد الصحيح خلال ٦ تخمينات كحد أقصى. أخيرًا، يَستطيع المُستخدِم اختيار الاستمرار بلعب مباراة إضافية بنهاية كل مباراة.

لمّا كانت كل مباراة هي بالنهاية مُهِمّة مُترابطة مُفردة، كان بديهيًا كتابة برنامج فرعي playGame()‎ بهدف لعب مباراة تخمين واحدة مع المُستخدِم. سيَستخدِم البرنامج main()‎ حَلْقة تَكْرار (loop)، والتي ستَستدعِي ذلك البرنامج الفرعي مع كل مرة يختار فيها المُستخدِم الاستمرار بلعب مباراة إضافية. نحتاج الآن إلى تصميم البرنامج الفرعي playGame()‎ وكتابته، وفي الواقع، يُصمَّم أي برنامج فرعي بنفس طريقة تَصْميم البرنامج main()‎، أيّ نبدأ بكتابة توصيف للخوارزمية (algorithm)، ثم نُطبِق التَصْميم المُتدرج (stepwise refinement). اُنظر الخوارزمية التالية لبرنامج لعبة التخمين مكتوبًا بالشيفرة الوهمية:

// اختر عددًا عشوائيًا 
Pick a random number
// طالما لم تنته المباراة
while the game is not over:
    // اقرأ تخمين المستخدم
    Get the user's guess
    // اخبر المستخدم عما إذا كان تخمينه صحيحًا أم أكبر أم أقل
    Tell the user whether the guess is high, low, or correct.

يُعدّ الاختبار "طالما لم تنته المباراة" معقدًا نوعًا ما؛ وذلك لأن المباراة قد تنتهي لسببين: إما لأن تخمين المُستخدِم كان صحيحًا أو لوصوله للحد الأقصى من عدد التخمينات المُمكنة، أيّ ٦. أحد أسهل الطرائق للقيام بذلك هو اِستخدَام حَلْقة تَكْرار لا نهائية while (true)‎ تحتوي على تَعْليمَة break؛ بهدف إِنهاء الحَلْقة في الوقت المناسب. لاحِظ أننا سنحتاج إلى الاحتفاظ بعَدَد تخمينات المُستخدِم؛ حتى نَتمكَّن من إِنهاء المباراة في حالة وصول المُستخدِم للحد الأقصى من التخمينات. تُصبِح الخوارزمية كالتالي بعد إِجراء التعديلات:

// أسند قيمة عشوائية بين 1 و 100 إلى المتغير computersNumber
Let computersNumber be a random number between 1 and 100
// اضبط قيمة العداد المستخدم لعدّ عدد تخمينات المستخدم
Let guessCount = 0
// استمر بتنفيذ الآتي
while (true):
    // اقرأ تخمين المستخدم
    Get the user's guess
    // أزد قيمة العداد بمقدار الواحد
    Count the guess by adding 1 to guess count
    // إذا كان تخمين المستخدم صحيحًا
    if the user's guess equals computersNumber:
        // بلغ المستخدم بفوزه بالمباراة
        Tell the user he won
        // اخرج من الحلقة
        break out of the loop
    // إذا وصل العداد للحد الأقصى 6
    if the number of guesses is 6:
        // بلغ المستخدم بخسارته للمباراة
        Tell the user he lost
        // اخرج من الحلقة
        break out of the loop
    // إذا كان كان تخمين المستخدم أقل من العدد
    if the user's guess is less than computersNumber:
        // بلغ المستخدم بكَوْن التخمين أقل من العدد
        Tell the user the guess was low
    // إذا كان كان تخمين المستخدم أعلى من العدد
    else if the user's guess is higher than computersNumber:
        // بلغ المستخدم بكَوْن التخمين أكبر من العدد
        Tell the user the guess was high

يُستخدَم التعبير ‎(int)(100 * Math.random()) + 1 لاختيار عدد عشوائي يقع بين العددين ١ و ١٠٠. اُنظر الشيفرة التالية بلغة الجافا، والتي تَتضمَّن تعريف البرنامج playGame()‎ بعد التَّصْريح عن المُتَغيِّرات (variable declarations):

static void playGame() {
    int computersNumber; // العدد العشوائي
    int usersGuess;      // إحدى تخمينات المستخدم
    int guessCount;      // عدد تخمينات المستخدم
    computersNumber = (int)(100 * Math.random()) + 1;

    guessCount = 0;
    System.out.println();
    System.out.print("What is your first guess? ");
    while (true) {
        usersGuess = TextIO.getInt();  // اقرأ تخمين المستخدم
        guessCount++;
        if (usersGuess == computersNumber) {
            System.out.println("You got it in " + guessCount
                               + " guesses!  My number was " + computersNumber);
            break;  // انتهت المباراة بفوز المستخدم
        }
        if (guessCount == 6) {
            System.out.println("You didn't get the number in 6 guesses.");
            System.out.println("You lose.  My number was " + computersNumber);
            break;  // انتهت المباراة بخسارة المستخدم
        }
        // بلغ المستخدم عما إذا كان تخمينه أكبر أم أصغر من العدد
        if (usersGuess < computersNumber)
            System.out.print("That's too low.  Try again: ");
        else if (usersGuess > computersNumber)
            System.out.print("That's too high.  Try again: ");
    }
    System.out.println();
} // end of playGame()

الآن، وبعد انتهائنا من كتابة تعريف البرنامج الفرعي بالأعلى، فإننا سنحتاج إلى معرفة المكان الذي يُفْترَض أن نضع به هذا التعريف؟ ينبغي أن نضعه عمومًا ضِمْن نفس الصَنْف المُتضمِّن للبرنامج main()‎، ولكن ليس داخل البرنامج main ذاته؛ فمن غَيْر المسموح كتابة برنامج فرعي ضِمْن آخر (nested). لمّا كانت لغة الجافا لا تَشترِط أيّ ترتيب معين للبرامج الفرعية المُعرَّفة ضِمْن نفس الصَنْف، فيُمكِنك وضع تعريف playGame()‎ قَبْل البرنامج main()‎ أو بَعْده. لاحِظ أن البرنامج main()‎ سيَستدعِي البرنامج الفرعي playGame()‎، وهو ما يَعنِي مُجرد تَضمُّنه لتَعْليمَة استدعاء (call statement) لذلك البرنامج الفرعي، لا تَضمُّنه لتعريفها الكامل (definition). يتبقَّى لنا الآن كتابة البرنامج main، وهو ما قد تراه أمرًا بغاية السهولة خاصة مع رؤيتنا لكثير من الأمثلة المشابهة مُسْبَّقًا.

سيبدو البرنامج كاملًا كالتالي مع مُراعاة إِمكانية إضافة المزيد من التعليقات (comments):

import textio.TextIO;

public class GuessingGame {

    public static void main(String[] args) {
        System.out.println("Let's play a game.  I'll pick a number between");
        System.out.println("1 and 100, and you try to guess it.");
        boolean playAgain;
        do {
            playGame();  // اِستدعي البرنامج الفرعي لإجراء مباراة    
            System.out.print("Would you like to play again? ");
            playAgain = TextIO.getlnBoolean();
        } while (playAgain);
        System.out.println("Thanks for playing.  Goodbye.");
    } // ‫نهاية main 

    static void playGame() {
        int computersNumber; // العدد العشوائي
        int usersGuess;      // إحدى تخمينات المستخدم
        int guessCount;      // عدد تخمينات المستخدم

        computersNumber = (int)(100 * Math.random()) + 1;

        guessCount = 0;
        System.out.println();
        System.out.print("What is your first guess? ");
        while (true) {
            usersGuess = TextIO.getInt();  // اقرأ تخمين المستخدم
            guessCount++;
            if (usersGuess == computersNumber) {
                System.out.println("You got it in " + guessCount
                                   + " guesses!  My number was " + computersNumber);
                break;  // انتهت المباراة بفوز المستخدم
            }
            if (guessCount == 6) {
                System.out.println("You didn't get the number in 6 guesses.");
                System.out.println("You lose.  My number was " + computersNumber);
                break;  // انتهت المباراة بخسارة المستخدم
            }
            // بلغ المستخدم عما إذا كان تخمينه أكبر أم أصغر من العدد
            if (usersGuess < computersNumber)
                System.out.print("That's too low.  Try again: ");
            else if (usersGuess > computersNumber)
                System.out.print("That's too high.  Try again: ");
        }
        System.out.println();
    } // ‫نهاية البرنامج الفرعي playGame

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

اِستغرِق الوقت الكافي لقراءة شيفرة البرنامج بالأعلى، وحَاول اِستيعاب طريقة عملها. رُبما تَكُون قد لاحَظت أن تَقسيم البرنامج إلى تابعين (methods) قد جَعل البرنامج عمومًا أسهل في القراءة، وربما حتى في الكتابة حتى مع كَوْنه بسيطًا، أما إذا لم تَلحَظ ذلك، فحاول إقناع نفسك به في الوقت الحالي.

المتغيرات الأعضاء (Member Variables)

قد تَتضمَّن الأصناف (classes) أعضاء (members) آخرى، غير البرامج الفرعية، كالمُتَغيِّرات (variables)، فيُمكِن لأي صَنْف التَّصْريح عن مُتَغيِّر ما (variable declaration)، ولا يُقصَد بذلك تلك المُتَغيِّرات المُعرَّفة داخل برنامج فرعي معين، والمَعروفة باسم المُتَغيِّرات المحليّة (local variable)، وإنما تلك المُعرَّفة بمكان يقع خارج أيّ برنامج فرعي ضِمْن الصَنْف. تُعرَف تلك المُتَغيِّرات باسم المُتَغيِّرات العامة (global variable) أو المُتَغيِّرات الأعضاء (member variables)؛ وذلك لكَوْنهم أعضاء (members) ضِمْن الصنف (class).

مثلما هو الحال مع البرامج الفرعية، يُمكِن لأي مُتَغيِّر عضو (member variable) أن يُعرَّف بعدّه عضوًا ساكنًا (static) أو عضوًا غير ساكن (non-static). سنقتصر في هذا الفصل على الساكن منها. بدايةً، ينتمي أي مُتَغيِّر عضو، مُعرَّف باِستخدَام المُبدل static، إلى الصَنْف ذاته المُعرَّف بداخله -لا إلى كائنات ذلك الصَنْف-، فعندما يُحَمِّل مُفسر الجافا (Java interpreter) صَنْف معين، فإنه يُخصِّص مساحة بالذاكرة لكل مُتَغيِّر عضو ساكن ضِمْن ذلك الصَنْف. تُعدِّل أي تَعْليمَة إِسْناد (assignment) إلى واحد من تلك المُتَغيِّرات، وبغض النظر عن مكانها بالبرنامج، من محتوى نفس المساحة بالذاكرة، وكذلك يَلج (access) أي تعبير (expression) يَتضمَّن واحدًا من تلك المُتَغيِّرات، وبغض النظر عن مكانه بالبرنامج، إلى نفس المساحة بالذاكرة ويُعيد نفس القيمة، أيّ تتشارك البرامج الفرعية الساكنة المُعرَّفة بصَنْف ما قيم المُتَغيِّرات الأعضاء الساكنة المُعرَّفة ضِمْن نفس ذلك الصَنْف، فيُمكِن لبرنامج فرعي مُعين ضَبْط قيمة مُتَغيِّر عضو ساكن ما، بحيث يَستخدِمها برنامج فرعي آخر، وهو ما يَختلِف عن المُتَغيِّرات المحليّة المُعرَّفة (local variable) داخل أحد البرامج الفرعية؛ حيث تَتوفَّر فقط بينما يُنفَّذ ذلك البرنامج الفرعي، ثم لا يَعُدْ بالإمكان الوُلوج إليها (inaccessible) من خارج ذلك البرنامج الفرعي.

تتشابه تَعْليمَة التَّصْريح (declaration) عن مُتَغيِّر عضو (member variable) مع تلك المَسئولة عن التَّصْريح عن أي مُتَغيِّر محليّ تقليدي باستثناء شيئين. الأول هو وقوع التَّصْريح عن المُتَغيِّر العضو بمكان خارج أي برنامج فرعي (مع ذلك ما يزال التَّصْريح ضِمْن الصَنْف نفسه)، والثاني هو إمكانية اِستخدَام المُبدلات (modifiers) مثل static و public و private ضِمْن التَّصْريح. يقتصر هذا الفصل على الأعضاء الساكنة فقط، ولهذا ستَتضمَّن أي تَعْليمَة تَّصْريح عن مُتَغيِّر عضو (member variable) المُبدل static، وربما قد يُستخدَم أيًا من المُبدلين public أو private. على سبيل المثال، انظر التالي:

static String usersName;
public static int numberOfPlayers;
private static double velocity, time;

إذا لم تَستخدِم المُبدل private ضِمْن تَعْليمَة التَّصْريح عن مُتَغيِّر عضو ساكن معين، فإنه يُعامَل افتراضيًا كعضو عام public، أي يُمكن الوُلوج إليه بأي مكان سواء من داخل الصَنْف المُعرَّف به أو من خارج ذلك الصنف. ولكن لاحِظ أنك ستحتاج إلى اِستخدَام مُعرِّف (identifier) مُركَّب على الصورة . عند محاولة الإشارة إليه من خارج الصَنْف. فمثلًا، يحتوي الصَنْف System على مُتَغيِّر عضو ساكن عام اسمه هو out، لذلك تستطيع الإشارة إلى ذلك المُتَغيِّر باِستخدَام System.out بأيّ صَنْف خاص بك. مثال آخر هو المُتَغيِّر العضو الساكن العام Math.PI بالصَنْف Math. مثال أخير، وبفَرْض أن لدينا الصَنْف Poker، والذي يُصَرِّح عن مُتَغيِّر عضو ساكن عام، وليَكُن numberOfPlayers، فإنه يُمكِن الإشارة إلى ذلك المُتَغيِّر داخل الصَنْف Poker ببساطة باِستخدَام numberOfPlayers، في المقابل، يُمكِن الإشارة إليه باِستخدَام Poker.numberOfPlayers من خارج الصَنْف.

والآن، لنضيف عدة مُتَغيِّرات أعضاء ساكنة إلى الصَنْف GuessingGame الذي كتبناه مُسْبَّقًا بهذا القسم: أولًا، المُتَغيِّر العضو gamesPlayed بهدف الاحتفاظ بعَدَدَ المباريات التي لعبها المُستخدِم إجمالًا. ثانيًا، المُتَغيِّر العضو gamesWon بهدف الاحتفاظ بعَدَدَ المباريات التي كَسَبها المُستخدِم. يُصَرَّح عن تلك المُتغيرات كالتالي:

static int gamesPlayed;
static int gamesWon;

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

تُهيَئ (initialized) المُتَغيِّرات الأعضاء تلقائيًا بقيم افتراضية بعد التَّصْريح عنها، وهو ما يَختلِف عن المُتَغيِّرات المحليّة ضِمْن البرامج الفرعية، والتي لابُدّ من إِسْناد قيمة لها صراحةً قَبْل اِستخدَامها. تلك القيم الافتراضية هي ذاتها القيم الافتراضية لعناصر المصفوفات، فتُسنَد القيمة صفر افتراضيًا إلى المُتَغيِّرات العددية، بينما تُسنَد القيمة false للمُتَغيِّرات من النوع boolean، في حين يُسنَد المحرف المقابل لقيمة ترميز اليونيكود (Unicode code)‏ ‎\u0000 للمُتَغيِّرات من النوع المحرفي char، أما القيمة الافتراضية المبدئية للكائنات (objects)، كالسَلاسِل النصية من النوع String، فهي القيمة الفارغة null.

لمّا كان المُتَغيِّرين gamesPlayed و gamesWon من النوع int، فإنهما يُهيَئا أتوماتيكيًا إلى القيمة المبدئية صفر، وهو ما تَصادَف أن يَكُون القيمة المبدئية المناسبة لمُتَغيِّر يُنوَى اِستخدَامه كعَدَّاد (counter). مع ذلك، إذا لم تُناسبك القيمة المبدئية الافتراضية أو حتى إذا كنت تُريد مُجرد التَّصْريح عن نفس القيمة المبدئية لكن بصورة أكثر وضوحًا، فما يزال بإمكانك إِجراء عملية إِسْناد إلى أي من تلك المُتَغيِّرات ببداية البرنامج main()‎.

اُنظر نسخة البرنامج GuessingGame.java بَعْد التعديل:

import textio.TextIO;

public class GuessingGame2 {

    static int gamesPlayed;   // العدد الإجمالي للمباريات
    static int gamesWon;      // عدد المباريات التي فاز فيها المستخدم

    public static void main(String[] args) {
       gamesPlayed = 0;
       gamesWon = 0;  // لا فائدة فعلية من ذلك لأن الصفر هو القيمة الافتراضية
       System.out.println("Let's play a game.  I'll pick a number between");
       System.out.println("1 and 100, and you try to guess it.");
       boolean playAgain;
       do {
          playGame();  // استدع البرنامج الفرعي للعب مباراة
          System.out.print("Would you like to play again? ");
          playAgain = TextIO.getlnBoolean();
       } while (playAgain);
       System.out.println();
       System.out.println("You played " + gamesPlayed + " games,");
       System.out.println("and you won " + gamesWon + " of those games.");
       System.out.println("Thanks for playing.  Goodbye.");
    } // end of main()            

    static void playGame() {
        int computersNumber; // العدد العشوائي
        int usersGuess;      // إحدى تخمينات المستخدم
        int guessCount;      // عدد تخمينات المستخدم

        gamesPlayed++;  // أزد العدد الإجمالي للمباريات
        computersNumber = (int)(100 * Math.random()) + 1;

        guessCount = 0;
        System.out.println();
        System.out.print("What is your first guess? ");
        while (true) {
           usersGuess = TextIO.getInt();  // اقرأ تخمين المستخدم
           guessCount++;
           if (usersGuess == computersNumber) {
              System.out.println("You got it in " + guessCount
                      + " guesses!  My number was " + computersNumber);
              gamesWon++; 
              break;  // انتهت المباراة بفوز المستخدم
           }
           if (guessCount == 6) {
              System.out.println("You didn't get the number in 6 guesses.");
              System.out.println("You lose.  My number was " + computersNumber);
               break;  // انتهت المباراة بخسارة المستخدم
           }
           // بلغ المستخدم عما إذا كان تخمينه أكبر أم أصغر من العدد
           if (usersGuess < computersNumber)
              System.out.print("That's too low.  Try again: ");
           else if (usersGuess > computersNumber)
              System.out.print("That's too high.  Try again: ");
        }
        System.out.println();
    } // ‫نهاية البرنامج الفرعي playGame

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

بالمناسبة، لم يُستخدَم أي من المُبدلين public و private مع البرامج الفرعية أو المُتَغيِّرات الساكنة static بالأعلى، فما الذي يَعنيه ذلك؟ في الواقع، إذا لم يُخصَّص أي مُبدل وصول (access modifier) لمُتَغيِّر عام (global variable) أو لبرنامج فرعي (subroutine)، فإنه يُصبِح قابلًا للوصول بأيّ مكان يقع ضِمْن حزمة (package) الصَنْف الحاضن له، ولكن ليس بأي حزم آخرى. تقع الأصناف التي لا تُصَرِّح عن وجودها ضِمْن حزمة معينة بالحزمة الافتراضية (default package)، لذا تستطيع جميع الأصناف ضِمْن الحزمة الافتراضية -وهو ما يَشمَل أغلبية الأصناف بهذا الكتاب- الوصول إلى كِلا المُتَغيِّرين gamesPlayed و gamesWon وكذلك استدعاء البرنامج الفرعي playGame()‎. مع ذلك، يُعدّ اِستخدَام المُبدل private أثناء التَّصْريح عن كُلًا من المُتَغيِّرات الأعضاء والبرامج الفرعية عمومًا واحدًا من الممارسات الجيدة إلا إذا كان هناك سببًا يَدعوك لمخالفة ذلك، كما يُفضَّل تَجَنُّب اِستخدَام الحزمة الافتراضية (default package).

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


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...