مدخل إلى جافا التعبيرات (expressions) في جافا


رضوى العربي

يُلقِي هذا القسم نظرة على التعبيرات (expressions) المُستخدَمة لتمثيل قيمة أو حسابها. قد يكون التعبير (expression) بهيئة قيمة مُصنَّفة النوع (literal) أو مُتْغيِّر (variable) أو استدعاء دالة (function call) أو مجموعة من تلك الأشياء مُجمَّعة معًا بعوامل (operators) مثل + و >. يُمكِنك أن تُسنِد (assign) قيمة تعبير إلى مُتْغيِّر (variable) أو تُمرِّرها كمُعامل (parameter) أثناء استدعاء برنامج فرعي (subroutine call) أو تَدمِجها مع قيم آخرى ضِمْن تعبير آخر أكثر تعقيدًا. يمكنك أيضًا أن تتجاهلها ببعض الحالات إذا كان هذا ما تريد أن تفعله وهو أمر شائع أكثر مما قد تَظُنّ. تُعدّ التعبيرات في العموم جزءًا أساسيًا من البرمجة، ولقد كان تَعامُلنا معها حتى الآن غير رسمي نوعًا ما، ولكننا سنناقشها تفصيليًا خلال هذا القسم باستثناء بعض العوامل (operators) الأقل استخدامًا. تُعدّ كُلًا من القيم المُجرّدة مُصنَّفة النوع (literals) مثل 674 و 3.14 و true و X، والمُتْغيِّرات، واستدعاءات الدوال من أبسط اللبنات الأساسية بالتعبيرات. ينبغي أن تَتَذكَّر أن أي دالة (function) هي عبارة عن برنامج فرعي (subroutine) يُعيد قيمة، ولقد تَعرَّضنا بالفعل لعدة أمثلة منها برامج (routines) الإِدْخَال المُعرَّفة بالصنف TextIO وكذلك الدوال الرياضية بالصَنْف Math.

يَحتوِي الصَنْف Math على مجموعة من الثوابت (constants) الرياضية التي يُمكِن اِستخدَامها ضِمْن أي تعبير رياضي مثل الثابت Math.PI المُمثِل لنسبة محيط أي دائرة إلى طول قُطرها π، وكذلك الثابت Math.E المُمثِل لقيمة أساس اللوغاريتم الطبيعي e. تلك الثوابت (constants) هي عبارة عن مُتْغيِّرات أعضاء (member variables) من النوع double مُعرَّفة بالصَنْف Math، ولأن القيم التي تُمثِلها تلك الثوابت الرياضية تَتَطلَّب عددًا لا نهائيًا من الأرقام لتَخْصِيصها بدقة، اِستخدَمنا أعدادًا تَقْرِيبية لقيم تلك الثوابت. بالإضافة إلى ذلك، يُعرِّف الصَنْف القياسي Integer ثوابتًا (constants) مُتْعلِّقة بالنوع int، فمثلًا، يُمثِل الثابت Integer.MAX_VALUE أكبر عدد يُمكِن للنوع int أن يَحْمله ويُساوِي 2147483647 بينما يُمثِل الثابت Integer.MIN_VALUE أصغر عدد يُمكِن حَمْله ويُساوِي ‎-2147483648. بالمثل، يُعرِّف الصَنْف Double ثوابتًا مُتْعلِّقة بالنوع double فمثلًا يُمثِل الثابت Double.MAX_VALUE أكبر قيمة يُمكِن للنوع double أن يَحملها بينما يُمثِل الثابت Double.MIN_VALUE أصغر قيمة موجبة يُمكِن حَمْلها كما يُعرِّف الصَنْف ثوابتًا آخرى تُمثِل كُلًا من القيمتين اللانهائيتين Double.POSITIVE_INFINITY و Double.NEGATIVE_INFINITY وكذلك القيمة غَيْر المُعرَّفة Double.NaN. فمثلًا، قيمة Math.sqrt(-1)‎ هي Double.NaN.

في الواقع، القيم مُصنَّفة النوع (literals) والمُتْغيِّرات (variables) واِستدعاءات الدوال (function calls) هي مُجرّد تعبيرات (expressions) بسيطة يمكنك دَمْجها عن طريق عوامل (operators) لتَحصُل على تعبيرات أكثر تعقيدًا. تَتَضمَّن تلك العوامل كُلًا من العامل + لحساب حاصل مجموع عددين والعامل > لموازنة قيمتين. عندما تَستخدِم أكثر من عامل ضِمْن تعبير (expression)، فعادةً ما تكون الكيفية التي ستُجمَّع بها تلك العوامل -فيما يُعرَف باسم أولوية (precedence) العوامل- لتَحْصِيل قيمها مَحلّ حيرة وتساؤل. فمثلًا، إذا كان لدينا تعبير مثل A + B * C، فإن قيمة B * C تُحسَب أولًا ثم تُضاف إلى A. يَعنِي ذلك أن عامل الضرب * له أولوية عن عامل الجمع + بشكل افتراضي، فإذا لم يَكُن ذلك هو ما تريده، يُمكِنك ببساطة إضافة أقواس تُحدِّد صراحةً الكيفية التي ستُجمَّع بها العوامل، فمثلًا، يمكنك كتابة ‎(A + B) * C إذا أردت أن تَحسِب حاصل جمع A مع B أولًا ثم تضرب النتيجة في C.

تُوفِّر الجافا عددًا كبيرًا من العوامل (operators) سنَمُرّ عبر الغالبية الأكثر شيوعًا واستخدامًا ببقية هذا القسم.

العوامل الحسابية (arithmetic)

تَتَضمَّن العوامل الحسابية (arithmetic operators) كُلًا من عمليات الجمع والطرح والضرب والقسمة، وتُستخدَم الرموز + و - و * و / بالجافا للإشارة إليها على الترتيب. يُمكِنك اِستخدَام تلك العمليات مع أي قيم من النوع العددي: byte و short و int و long و float و double وكذلك مع أي قيم من النوع char حيث تُعامَل كعدد صحيح (integer) ضِمْن ذلك السياق. تحديدًا عندما يُستخدَم محرف من النوع char مع عامل عددي (arithmetic operator)، يُحوِّله الحاسوب إلى عدده الترميزي باليونيكود (unicode code number). في العموم عندما يُحصِّل الحاسوب أيًا من تلك العمليات بشكل فعليّ، فينبغي أن تَكُون القيمتان المُدْمجتان من نفس النوع، فإذا أراد البرنامج أن يَدْمِج قيمتين من نوعين مختلفين، سيُحوِّل الحاسوب إحداهما إلى نوع الآخرى. فمثلًا عند حِسَاب 37.4 + 10، سيُحوِّل الحاسوب العدد الصحيح 10 إلى نَظيره الحقيقي 10.0 ثُمَّ سيَحسِب قيمة 37.4 + 10.0. يُطلَق على ذلك "تَحْوِيل نوع (type conversion)"، ولا تحتاج عادةً لأن تقلق بشأن ذلك عند حُدوثه ضِمْن تعبير (expression) لأن الحاسوب سيُجريه أتوماتيكيًا.

عندما تَدمِج قيمتين عدديتين بغض النظر عما إذا كان الحاسوب قد اِضطّر لإِجراء عملية تَحْوِيل نوع (type conversion) لأي منهما أم لا، فإن الناتج سيَكُون من نفس النوع. فمثلًا، إذا حَسبت حاصل ضرب عددين صحيحين من النوع int، ستَحصُل على قيمة من النوع int أما إذا حَسبت حاصل ضرب عددين من النوع double، فستَحصُل على قيمة من النوع double. يُعدّ ذلك أمرًا متوقعًا على أية حال، ولكن عندما تَستخدِم عامل القسمة /، فلابُدّ أن تَكُون أكثر حرصًا، لأنه عند حِسَاب حاصل قسمة عددين صحيحين (integers)، دائمًا ما ستَكُون النتيجة عددًا صحيحًا أي ستُهمَل أية كُسور (fractional) قد يَتَضمَّنها خارج القسمة (quotient)، فمثلًا، قيمة 7/2 تُساوِي 3 لا 3.5. لنَفْترِض مثلًا أن N عبارة عن مُتْغيِّر من النوع العددي الصحيح، ستَكُون إذًا قيمة N/100 عددًا صحيحًا (integer) أما قيمة ‎1/N فستُساوِي الصفر لأي N أكبر من الواحد. تُعدّ تلك الحقيقة مصدرًا شائعًا للكثير من الأخطاء البرمجية. يُمكِنك مع ذلك أن تُجبِر الحاسوب على أن يَحسِب الناتج بهيئة عدد حقيقي (real number) من خلال جَعل أحد المُعاملات عددًا حقيقيًا، فمثلًا، عندما يُحصِّل الحاسوب قيمة ‎1.0/N، فإنه سيُحوِّل أولًا N إلى عدد حقيقي لكي يتماشى مع نوع القيمة 1.0، لذلك يَكُون الناتج عددًا حقيقيًا.

تُوفِّر الجافا عاملًا (operator) آخر لحِسَاب باقي قسمة (remainder) عددين على بعضهما، ويُستخدَم % للإشارة إليه. إذا كان كُلًا من A و B أعدادًا صحيحة، يُمثِل A % B باقي قسمة A على B. فمثلًا، ‎7 % 2 تُساوِي 1 بينما ‎34577 % 100 تُساوِي 77 أما ‎50 % 8 فتُساوِي 2. لاحِظ أنه في حالة المُعاملات (operands) السالبة، لا يَعمَل % كما قد تَتَوقَّع من أي عامل باقي قسمة عادي، فمثلًا إذا كانت قيمة A أو B سالبة، فستَكُون قيمة A % B سالبة أيضًا. عادةً ما يُستخدَم العامل % لاختبار ما إذا كان عدد صحيح معين سالبًا أم موجبًا: إذا كانت قيمة N % 2 تُساوِي 0، يَكُون N عددًا موجبًا أما إذا كانت قيمته تُساوِي 1، يَكُون عددًا سالبًا. ويُمكِنك عمومًا أن تَفْحَص ما إذا كان عدد صحيح N قابلًا للقسمة على عدد صحيح آخر M بشكل متساوٍ من خلال فَحْص ما إذا كانت قيمة N % M تُساوِي 0.

يَعمَل العامل % أيضًا مع الأعداد الحقيقية (real numbers)، ففي العموم، يُمثِل A % B مقدار ما يَتبقَّى بعدما تَحذِف من العدد A أكبر قدر ممكن من B. فمثلًا، ‎7.52 % 0.5 يساوي 0.02.

قد تحتاج أحيانًا لأن تَحسِب القيمة السالبة من عدد معين. في هذه الحالة، بدلًا من اِستخدَام تعبير مثل ‎(-1)*X، تُوفِّر الجافا لهذا الغرض عامل إشارة طرح أُحادي (unary) يُكْتَب على الصورة ‎-X. بالمثل، تُوفِّر الجافا عامل إشارة جمع أُحادي يُكْتَب على الصورة ‎+X، ولكنه لا يُنجز أي شيء فعليّ في الواقع.

بالمناسبة، تَذكَّر أنه يُمكِنك أيضًا اِستخدَام العامل + لضمّ (concatenate) قيمة من أي نوع إلى قيمة آخرى عبارة عن سِلسِلة نصية من النوع String. ونظرًا لإمكانية تَحْوِيل أي نوع إلى النوع String أتوماتيكيًا، يُعدّ ذلك مثالًا آخر لعملية تَحْوِيل نوع (type conversion).

الزيادة (increment) والنقصان (decrement)

ستَجِد أن زيادة قيمة مُتْغيِّر (variable) معين بمقدار يُساوِي الواحد أو إنقاصها عملية شائعة للغاية بالبرمجة. يُمكِنك أن تُجرِي ذلك ضِمْن تَعْليمَة إِسْناد (assignment) كالتالي:

counter  =  counter + 1;
goalsScored  =  goalsScored + 1;

تأخذ التَعْليمَة x = x + 1 القيمة السابقة للمُتْغيِّر x وتَحسِب حاصل مجموعها مع العدد واحد ثم تُخزِّن النتيجة كقيمة جديدة للمُتْغيِّر x. في الحقيقة، يُمكِنك تّنْفيذ نفس تلك العملية فقط بكتابة x++‎ أو ‎++x حيث يُغيِّر كُلًا منهما قيمة x بنفس تأثير التَعْليمَة x = x + 1. يُمكِنك إذًا كتابة التَعْليمَتين السابقتين على النحو التالي:

counter++;
goalsScored++;

بالمثل، يمكنك كتابة x--‎ أو ‎--x لطرح العدد واحد من x أي أن x--‎ لها نفس تأثير التعليمة x = x - 1. تُعرَف إضافة مقدار يُساوِي الواحد إلى مُتْغيِّر باسم "الزيادة (increment)" أما عملية الطرح فتُعرَف باسم "النقصان (decrement)"، وعليه يُطلَق على العاملين ++ و -- اسم عاملي الزيادة (increment operator) والنقصان (decrement operator) على الترتيب. يُمكِنك أن تَستخدِم تلك العوامل مع المُتْغيِّرات (variables) التي تنتمي إلى أي من الأنواع العددية وكذلك مع المُتْغيِّرات من النوع char. إذا كان ch يحتوي على المحرف 'A' فإن ch++‎ تُغيِّر قيمته إلى 'B'.

عادةً ما يُستخدَم العاملان ++ و -- بتَعْليمَات مثل x++;‎ أو x--;‎، وتَكُون في تلك الحالة بمثابة أوامر تُغيِّر من قيمة x. علاوة على ذلك، يُمكِنك أيضًا أن تَستخدِم x++‎ أو ‎++x أو x--‎ أو ‎--x كتعبير (expression) أو كجزء ضِمْن تعبير أكبر. اُنظر الأمثلة التالية:

y = x++;
y = ++x;
TextIO.putln(--x);
z = (++x) * (y--);

تُضيِف التَعْليمَة y = x++;‎ مقدارًا يُساوِي الواحد إلى قيمة المُتْغيِّر x كما أنها تُسنِد قيمة معينة إلى y والتي تُعرَّف على أنها القيمة السابقة للمُتْغيِّر x أي قبل إضافة الواحد. فمثلًا، إذا كانت قيمة x هي ٦، فإن التَعْليمَة "y = x++;‎ ستُغيِّر قيمة x إلى ٧. ولأن القيمة السابقة للمُتْغيِّر x تُساوِي ٦، فستُغيِّر التَعْليمَة قيمة y إلى نفس تلك القيمة ٦. من الناحية الآخرى، تُعرَّف قيمة ‎++x على أنها القيمة الجديدة للمُتْغيِّر x أي بعد إضافة مقدارًا يُساوِي الواحد. فمثلًا، إذا كانت قيمة x تُساوِي ٦، فستُغيِّر التَعْليمَة y = ++x;‎ قيم كلًا من x و y إلى ٧. يَعمَل عامل النقصان (decrement operator) -- بنفس الطريقة.

لمّا كان التعبير x = x++‎ يُسنِد القيمة السابقة للمُتْغيِّر x -أي قبل تّنْفيذ الزيادة- إلى نفس ذات المُتْغيِّر x، فإنه في الواقع لا يُغيِّر قيمته. إذا شئنا الدقة، فإن التعبير السابق يزيد (increment) قيمة x بمقدار الواحد ولكنها سرعان ما تَتَغيَّر إلى القيمة السابقة نتيجة لتَعْليمَة الإِسْناد (assignment). لاحِظ أن التعبير x++‎ يختلف عن x + 1 فالتعبير الأول يُغيِّر من قيمة x أما الثاني فلا يُغيِّرها.

قد يَكُون ذلك مُربكًا نوعًا ما، وفي الحقيقة، يقع الطلبة عادةً بالكثير من الأخطاء البرمجية (bugs) نتيجة لذلك الارتباك، ولكن ليس هناك أي داعٍ لذلك فيُمكِنك مثلًا أن تَستخدِم العاملين ++ و -- كتَعْليمَات مُفردة فقط وليس كتعبيرات (expressions)، وهو ما سنلتزم به خلال الأمثلة التالية.

العوامل العلاقية (relational)

تُوفِّر الجافا مُتْغيِّرات منطقية وكذلك تعبيرات (expressions) منطقية يُمكِن اِستخدَامها كشُروط (conditions) قد تؤول إلى أي من القيمتين true أو false. عادةً ما يَتَكوَّن التعبير المنطقي من عامل علاقي (relational) يُوازن بين قيمتين، فيَختبِر مثلًا ما إذا كانت قيمتان متساويتين أو ما إذا كانت قيمة إحداهما أكبر من الآخرى وهكذا. يَتَوفَّر بالجافا العوامل العلاقية التالية == و ‎!=‎ و ‎<‎ و ‎>‎ و ‎<=‎ و ‎>=‎:

A == B       Is A "equal to" B?
A != B       Is A "not equal to" B?
A < B        Is A "less than" B?
A > B        Is A "greater than" B?
A <= B       Is A "less than or equal to" B?
A >= B       Is A "greater than or equal to" B?

تُستخدَم تلك العوامل (operators) لموازنة قيم من أي أنواع عددية. علاوة على ذلك، يُمكِنها أيضًا أن تُستخدَم لموازنة قيم من النوع char. بالنسبة للمحارف (characters)، فإن العوامل ‎<‎ و ‎>‎ مُعرَّفة لتَعمَل وفقًا لقيمها العددية باليونيكود (unicode) وهو ما يختلف عن الترتيب الأبجدي المُعتاد حيث تأتي الأحرف الأبجدية الكبيرة (upper case) بأكملها قبل الأحرف الأبجدية الصغيرة (lower case).

فيما يَتَعلَّق بالتعبيرات المنطقية (boolean expressions)، لا تختلف قيم النوع المنطقي وفقًا للحاسوب عن قيم أي نوع آخر. فيُمكِنك مثلًا أن تُسنِد (assign) تعبيرات منطقية إلى أي مُتْغيِّر منطقي تمامًا مثلما بإِمكانك إِسْناد قيم عددية إلى أي مُتْغيِّر عددي، وكذلك يُمكِن للدوال (functions) أن تُعيد قيم من النوع المنطقي. إلى جانب ذلك، يُمكِنك أن تَستخدِم تلك التعبيرات ضِمْن تَعْليمَتي حَلْقة التَكْرار (loop) والتَفْرِيع (branch)، وهو ما سنُناقشه بالفصل التالي.

بالمناسبة، يُمكِنك أن تَستخدِم العاملين == و ‎!=‎ لموازنة القيم المنطقية أيضًا، وهو ما قد يَكُون مفيدًا في بعض الأحيان. اُنظر المثال التالي:

boolean sameSign;
sameSign = ((x > 0) == (y > 0));

فيما يَتَعلَّق بالقيم من النوع String، لا يُمكِنك أن تَستخدِم العوامل العلاقية < و > و ‎<=‎ و ‎>=‎ لموازنة قيم ذلك النوع، ولكن يُسمَح باِستخدَام كُلًا من == و ‎!=‎ لموازنتها مع أنها لن تُعطِيك النتيجة التي تَتَوقَّعها بسبب الكيفية التي تَتَصرَّف الكائنات (objects) على أساسها. فمثلًا، يَختبِر العامل == ما إذا كان الكائنان (objects) مُخزَّنين بنفس مَوضِع الذاكرة (memory location) بدلًا من أن يَختبِر ما إذا كانت قيمتهما هي نفسها. قد تحتاج إلى إجراء تلك الموازنة لبعض أنواع الكائنات أحيانًا، ولكن نادرًا ما يَكُون ذلك هو الحال مع السَلاسِل النصية (strings) من النوع String. سنَعُود لمناقشة ذلك فيما بَعْد على أية حال. ينبغي عمومًا أن توازن السَلاسِل النصية باِستخدَام برامج فرعية (subroutines) مثل equals()‎ و compareTo()‎ والتي تَعرَّضنا لها بالقسم الفرعي ٢.٣.٣.

إلى جانب ذلك، فإنه عند اِستخدَام كُلًا من العاملين == و ‎!=‎ مع الثابت Double.NaN المُمثِل للقيمة غَيْر المُعرَّفة من النوع double، فإنهما لا يَتَصرَّفان على النحو المُتوقَّع. فمثلًا إن كان x مُتْغيِّرًا من النوع double، فإن تعبيرات مثل x == Double.NaN و x != Double.NaN ستُكون مُساوِية للقيمة false بجميع الحالات سواء كانت x تَحتوِي على Double.NaN أم لا. لذا إذا أردت أن تَختبِر ما إذا كان المُتْغيِّر x يحتوي على القيمة غَيْر المُعرَّفة Double.Nan، يُمكِنك أن تَستخدِم الدالة Double.isNan(x)‎ والتي تُعيد قيمة منطقية.

العوامل المنطقية (logical/boolean)

تَتَركَّب الشُروط (conditions) الأكثر تعقيدًا من عوامل منطقية (boolean operators) هي ‏"and" و "or" و "not" بالإنجليزية مثل الشرط التالي: "If there is a test and you did not study for it…". تُوفِّر الجافا لحسن الحظ تلك العوامل أيضًا كجزء من اللغة.

يَدمِج العامل المنطقي "and" قيمتين من النوع المنطقي boolean لتَكُون النتيجة قيمة من النوع boolean. إذا كانت كلتا القيمتين تُساوِي true، فإن النتيجة النهائية ستُساوِي true أما إذا كانت قيمة أي منهما تُساوِي false، فإن النتيجة ستُساوِي false. يَعمَل الترميز && مُمثِلًا للعامل المنطقي "and" بلغة الجافا، فمثلًا، يؤول التعبير (x == 0) && (y == 0) إلى القيمة المنطقية true إذا كانت قيمة كُلًا من x و y تُساوِي صفرًا.

في المقابل، يَعمَل الترميز || مُمثِلًا للعامل المنطقي "or" بلغة الجافا. يؤول التعبير A || B إلى true إذا كانت قيمة أي من A أو B أو كليهما تُساوِي true بينما يؤول إلى false إذا كانت قيمة كُلًا من A و B تُساوِي false. يَتبِع كُلًا من العاملين && و || مفهوم الدارة القصيرة (short-circuited) أي لا يَكُون تَحْصِيل قيمة المُعامل الثاني ضروريًا إذا أَمْكَن معرفة النتيجة النهائية قبلها. اُنظر الاختبار التالي:

(x != 0) && (y/x > 1)

إذا كانت قيمة x تُساوِي صفر، فسيَكُون حاصل قسمة y/x غَيْر مُعرَّف رياضيًا. ولأن الحاسوب سيُحصِّل قيمة المعامل الأيسر (x != 0) أولًا، وسيَجِدها مُساوية للقيمة false، فإنه سيُدرك أن القيمة النهائية للشرط (‎(x != 0) && anything) ككل لابُدّ وأن تُساوِي false لا محالة، لذا فإنه لا يُحصِّل قيمة المعامل الأيمن، ولا يُنفِّذ عملية القسمة من الأساس. يُعدّ هذا مثالًا على اِستخدَام الدارة القصيرة (short circuit) وهو ما جَنَّبَنا القسمة على صفر.

تَستخدِم الجافا المحرف ! لتمثيل العامل المنطقي "not" والذي هو عامل أحادي (unary) يُكْتَب قَبْل مُعامله الوحيد، فمثلًا إذا كان test مُتْغيِّر منطقي من النوع boolean فإن التعبير التالي:

test = ! test;

سيَعكِس قيمة المُتْغيِّر test ويُغيِّرها من true إلى false ومن false إلى true.

العامل الشرطي (conditional)

تَملُك أي لغة برمجية جيدة بعض الخاصيات الصغيرة الأنيقة نوعًا ما والتي هي في الواقع ليست ضرورية بشكل فعليّ، فكل ما تَفعَله هي أنها تمنحك شعورًا جيدًا عند اِستخدَامها. تُوفِّر الجافا عاملًا شَّرْطيًا (conditional operator) عبارة عن عامل ثلاثي (ternary) أي لديه ٣ مُعاملات (operands)، ويَتَكوَّن من جزئين هما ? و : يُستخدَمان معًا. يُكْتَب العامل بالصياغة التالية:

<boolean-expression> ? <expression1> : <expression2>

يَختبر الحاسوب قيمة التعبير المنطقي ** **أولًا، فإذا كانت قيمته تُساوِي true، فسيؤول التعبير ككل إلى قيمة ، أما إذا كانت قيمته تُساوِي false، فسيؤول إلى . اُنظر المثال التالي:

next = (N % 2 == 0) ? (N/2) : (3*N+1);

بالمثال السابق، إذا كان التعبير N % 2 == 0 يُساوِي true أي إذا كانت قيمة N زوجية، فستُسنَد قيمة N/2 إلى المُتْغيِّر next أما إذا كانت قيمة N فردية، فستُسنَد قيمة (‎3*N+1) إليه. لاحِظ أن الأقواس ضِمْن هذا المثال ليست مطلوبة ولكنها تُسهِل من قراءة التعبير (expression).

عوامل الإسناد (assignment) وتحويل الأنواع (type conversion)

لقد تَعرَّضنا بالفعل للترميز = ضِمْن تَعْليمَات الإِسْناد (assignment statement) المسئولة عن إِسْناد قيمة تعبير (expression) معين إلى مُتْغيِّر (variable). يُعدّ ذلك الترميز = بمثابة عامل (operator) بمعنى أنه من المُمكِن اِستخدَامه كتعبير (expression) بحد ذاته أو كجزء ضِمْن تعبير أكثر تعقيدًا. لاحِظ أن قيمة أي عملية إِسْناد (assignment) مثل A=B تَكُون مُساوِية للقيمة ذاتها المُسنَدة إلى A، لذا إذا أردت أن تُسنِد (assign) قيمة مُتْغيِّر B إلى آخر A، وأن تَفْحَص ما إذا كانت قيمته تُساوِي ٠ بنفس ذات الوقت، يُمكِنك أن تَستخدِم ما يلي:

if ( (A=B) == 0 )...

مع أنه لا يُحبذ القيام بذلك عمومًا.

في العموم، لابُدّ أن يَكُون نوع التعبير (expression) على الجانب الأيمن من أي تَعْليمَة إِسْناد (assignment) من نفس نوع المُتْغيِّر (variable) على جانبها الأيسر. في بعض الحالات، يستطيع الحاسوب أن يُحوِّل قيمة التعبير أتوماتيكيًا لكي تتوافق مع نوع المُتْغيِّر (variable). فمثلًا، بالنسبة لقائمة الأنواع العددية: byte و short و int و long و float و double، يستطيع الحاسوب أن يُحوِّل قيمة من نوع مَذكُور بأول تلك القائمة أتوماتيكيًا إلى قيمة من نوع مَذكُور لاحقًا. اُنظر الأمثلة التالية:

int A;
double X;
short B;
A = 17;
X = A;    // ‫ستحوّل A إلى النوع double
B = A;    // ‫لا يمكن فليس هناك تحويل تلقائي من int إلى short

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

قد تَرغَب أحيانًا بأن تُجرِي تَحْوِيلًا لن يَحدُث أتوماتيكيًا. يُمكِنك القيام بذلك عن طريق ما يُعرَف باسم "التَحْوِيل بين الأنواع (type casting)" وذلك بكتابة اسم النوع المطلوب التَحْوِيل إليه بين قوسين أمام القيمة المُراد تَحْوِيلها. اُنظر المثال التالي:

int A;
short B;
A = 17;
B = (short)A;  // ‫يصح لأن A حولت صراحة إلى قيمة من النوع short

يُمكِنك إجراء عملية "التَحْوِيل بين الأنواع (type casting)" من أي نوع عددي إلى أي نوع عددي آخر، ولكنه قد يتسبَّب بتَغْيِير القيمة العددية للعدد. فمثلًا، ‎(short)100000 تُساوِي ‎-31072. أخذنا العدد الصحيح 100000 من النوع int المُكوَّن من ٤ بايت ثُمَّ تركنا ٢ بايت منه حتى نَحصُل على قيمة من النوع short، وعليه فقد خَسرنا المعلومات الموجودة بتلك البايتات المُلغَاة.

عندما تُجرِي عملية "التحويل بين الأنواع (type-casting)" من عدد حقيقي (real) إلى عدد صحيح (integer)، يُهمَل الجزء الكسري (fractional) منه تلقائيًا. فمثلًا، ‎(int)7.9453 تُساوِي 7. تُعدّ مسألة الحصول على عدد صحيح عشوائي بين ١ و ٦ بمثابة مثال آخر: تُعيد الدالة Math.random()‎ عددًا حقيقيًا يتراوح بين 0.0 و 0.9999، لذا فإن قيمة التعبير 6*Math.random()‎ تَكُون عددًا يتراوح بين 0.0 و 5.999. يُمكِننا الآن أن نَستخدِم عامل التَحْوِيل بين الأنواع (type-cast operator)‏ (int) لتَحْوِيل قيمة ذلك التعبير إلى عدد صحيح كالتالي (int)(6*Math.random(‎)‎)، والتي تُصبِح نتيجته واحدة من قيم الأعداد الصحيحة ٠ و ١ و ٢ و ٣ و ٤ و ٥. والآن، لنَحصُل على عدد يتراوح بين العددين ١ و ٦، يُمكِننا أن نُضيف مقدارًا يُساوِي الواحد كالتالي ‎(int)(6*Math.random()) + 1. لاحِظ أن الأقواس حول 6*Math.random()‎ ضرورية لأنه وبحسب قواعد الأولوية (precedence rules)، سيُطبَق عامل التحويل بين الأنواع (type cast operator) على العدد ٦ فقط إذا لم نَستخدِم أية أقواس.

يُعدّ النوع char بمثابة نوع عددي صحيح (integer) أي يُمكِنك مثلًا أن تُسنِد (assign) قيمة من النوع char إلى مُتْغيِّر من النوع int كما تستطيع أن تُسنِد ثابت (constant) صحيح تتراوح قيمته من ٠ وحتى ٦٥٥٣٥ إلى مُتْغيِّر من النوع char. يُمكِنك حتى أن تُطبِق عملية التَحْوِيل بين الأنواع (type-casting) صراحةً بين النوع char وأي نوع عددي آخر. فمثلًا، ‎(char)97 تُساوِي 'a' أما ‎(int)'+'‎ فتُساوِي 43 بينما (char)('A' + 2) تُساوِي 'C'.

أما بالنسبة للنوع String، فلا يُمكِنك أن تَستخدِم عامل التَحْوِيل بين الأنواع (type-casts) لكي تُحوِّل بين النوع String وأي نوع آخر. في المقابل، يُمكِنك أن تَضُمّ (concatenate) قيم من أي نوع إلى سِلسِلة نصية فارغة لتَحْوِيلها إلى النوع String، فمثلًا، قيمة ‎"" + 42 تُساوِي السِلسِلة النصية "42". على نحوٍ أفضل، يُمكِنك أن تَستخدِم الدالة (function)‏ String.valueOf(x)‎ المُعرَّفة كعضو ساكن (static member) بالصَنْف String حيث تُعيد تلك الدالة قيمة x بعد تَحْوِيلها إلى سِلسِلة نصية من النوع String، فمثلًا، يُعيد الاستدعاء String.valueOf(42)‎ القيمة "42". إذا كان ch مُتْغيِّرًا من النوع char، فإن الاستدعاء String.valueOf(ch)‎ يُعيد سِلسِلة نصية (string) طولها يُساوِي ١ ومُكوَّنة من محرف واحد يُمثِل قيمة ch.

تستطيع أيضًا تَحْوِيل بعض السَلاسِل النصية (strings) إلى قيم من أنواع آخرى. فمثلًا، يُمكِنك تَحْوِيل سِلسِلة نصية مثل "10" إلى القيمة 10 من النوع int وكذلك تَحْوِيل سِلسِلة نصية مثل "17.42e-2" إلى قيمة من النوع double تُساوي 0.1742. تُوفِّر الجافا دوالًا مبنية مُسْبَقًا (built-in functions) لمُعالجة تلك التَحْوِيلات.

يُعرِّف الصَنْف القياسي Integer عضو دالة ساكن (static member function) للتَحْوِيل من النوع String إلى النوع int. لنَفْترِض مثلًا أن str عبارة عن مُتْغيِّر يَتَضمَّن تعبيرًا (expression) من النوع String، سيُحاوِل استدعاء الدالة Integer.parseInt(str)‎ إذًا أن يُحوِّل قيمة ذلك المُتْغيِّر إلى قيمة من النوع int. على سبيل المثال، سيُعيد استدعاء Integer.parseInt("10")‎ القيمة 10 من النوع int. إذا لم تَكُن قيمة المُعامل (parameter) المُمرَّرة للدالة Integer.parseInt تُمثِل قيمة صالحة من النوع int، فسيَحدُث خطأ.

بالمثل، يُعرِّف الصَنْف القياسي Double الدالة Double.parseDouble. لنَفْترِض أن str عبارة عن مُتْغيِّر من النوع String، سيُحاوِل إذًا استدعاء الدالة Double.parseDouble(str)‎ أن يُحوِّل قيمة المُتْغيِّر str إلى قيمة من النوع double، وسيَحدُث خطأ إذا لم تَكُن قيمة str تُمثِل قيمة صالحة من النوع double.

لنعود الآن إلى تَعْليمَات الإِسْناد (assignment statements)، تُوفِّر الجافا تشكيلة متنوعة من عامل الإِسْناد لتَسهِيل الكتابة. فمثلًا، A += B مُعرَّف ليَعمَل بنفس كيفية عَمَل A = A + B. في العموم، يَتوفَّر عامل إِسْناد مشابه لأي عامل (operator) طالما كان مُطبَقًا على مُعاملين (operands) -باستثناء العوامل العلاقية (relational operators)-. اُنظر الأمثلة التالية:

x -= y;     // x = x - y;
x *= y;     // x = x * y;
x /= y;     // x = x / y;
x %= y;     // x = x % y;
q &&= p;    // q = q && p;  (for booleans q and p)

يُمكِن لعامل تَعْليمَة الإِسْناد المُدْمَج ‎+=‎ أن يُطبَق حتى على السَلاسِل النصية من النوع String. لقد طَبَقنا العامل + من قبل على سِلسِلة نصية، وكان ذلك بمثابة عملية ضَمّ (concatenation). لمّا كان التعبير str += x مُكافِئًا تمامًا للتعبير str = str + x، فإنه عند وجود سِلسِلة نصية (string) على الجانب الأيسر من العامل ‎+=‎، تُلحَق القيمة الموجودة على جانبه الأيمن إلى نهاية السِلسِلة النصية، فمثلًا، إذا كانت قيمة str تُساوِي "tire"، تُغيِّر التَعْليمَة str += 'd'‎ قيمة str إلى "tired".

قواعد الأولوية (precedence rules)

إذا اِستخدَمت أكثر من عامل (operator) واحد ضِمْن تعبير (expression)، وفي ذات الوقت لم تَستخدِم أي أقواس لتُشير صراحةً إلى الترتيب الذي ينبغي أن تُحصَّل (evaluate) على أساسه قيمة ذلك التعبير، فلابُدّ عندها إذًا من أن تنتبه لما يُعرَف باسم قواعد الأولوية (precedence rules)، والتي تُستخدَم لتَحْدِيد الترتيب الذي سيتبعه الحاسوب لتَحْصِيل قيمة التعبير. يُنصَح عمومًا باِستخدَام الأقواس لتَجنُّب أي ارتباك مُحتمَل لك أو لقارئ البرنامج في العموم.

تَستعرِض القائمة التالية العوامل (operators) التي ناقشناها بهذا القسم مُرتَّبة تنازليًا بحسب الأولوية (precedence) من الأعلى (يُحصَّل أولًا) إلى الأقل (يُحصَّل أخيرًا):

Unary operators:              ++, --, !, unary -, unary +, type-cast
Multiplication and division:  *,  /,  %
Addition and subtraction:     +,  -
Relational operators:         <,  >,  <=,  >=
Equality and inequality:      ==,  !=
Boolean and:                  &&
Boolean or:                   ||
Conditional operator:         ?:
Assignment operators:         =,  +=,  -=,  *=,  /=,  %=

تَملُك العوامل (operators) ضِمْن نفس السطر بالقائمة السابقة نفس الأولوية (precedence). إذا اِستخدَمت عواملًا لها نفس الأولوية ضِمْن تعبير بدون تَخْصِيص أي أقواس، تُحصَّل كُلًا من العوامل الأحادية (unary) وعوامل الإسناد (assignment) من اليمين إلى اليسار بينما تُحصَّل قيمة بقية العوامل من اليسار إلى اليمين. فمثلًا، تُحصَّل قيمة التعبير A*B/C كما لو كان مَكْتُوبًا كالتالي ‎(A*B)/C بينما تُحصَّل قيمة التعبير A=B=C على النحو التالي A=(B=C)‎. بفَرْض أن قيمة التعبير B=C هي نفسها قيمة المُتْغيِّر B، هل تَعرِف الغرض من التعبير A=B=C ؟

ترجمة -بتصرّف- للقسم Section 5: Details of Expressions من فصل Chapter 2: Programming in the Small I: Names and Things من كتاب Introduction to Programming Using Java.





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


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



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن