رضوى العربي
الأعضاء-
المساهمات
114 -
تاريخ الانضمام
-
تاريخ آخر زيارة
نوع المحتوى
ريادة الأعمال
البرمجة
التصميم
DevOps
التسويق والمبيعات
العمل الحر
البرامج والتطبيقات
آخر التحديثات
قصص نجاح
أسئلة وأجوبة
كتب
دورات
كل منشورات العضو رضوى العربي
-
لقد تَعلَّمت، على مدار الفصلين السابقين، نوعية البرمجة المُستخدَمة أثناء كتابة برنامج فرعي وحيد (subroutine)، فيما أطلقنا عليه اسم "البرمجة في نطاق ضيق". سنُركز أكثر خلال الفصول المُتبقّية من الكتاب على بناء البرامج ضِمْن نطاق أوسع، ولكن ما يزال ما تَعلَّمته حتى الآن هو البذرة الأساسية والضرورية لكل ما ستَتَعرَّض له فيما بَعْد. سنرى، في هذا القسم، كيف يُمكِن تطبيق ما قد تَعلَّمته خلال الفصول السابقة ضِمْن سياق برمجة واجهات المُستخدِم الرسومية (graphical user interface)، والتي تُعرَف اختصارًا باسم GUI، وهو سِّياق مُختلف نوعًا ما عما اعتدته من برامج الطرفيّة النصية. ستَعتمِد برمجة الواجهات الرسومية (GUI) سواء التي سنَتَعرَّض لها خلال هذا القسم أو خلال بقية الكتاب على منصة JavaFX، والتي تَضُمّ مجموعة من الأصناف (classes) المُستخدَمة لكتابة هذه النوعية من البرامج، أيّ أن جميع الأصناف (classes) المذكورة بهذا القسم هي جزء من منصة JavaFX، وبالتالي يَنبغي أن تقوم باستيرادها (import) إلى البرنامج حتى تَتَمكَّن من اِستخدَامها. اُنظر القسم ٢.٦ لمزيد من المعلومات عن تَصرِّيف (compiling) البرامج المُستخدِمة لمنصة JavaFX وكيفية تَشْغِيلها. عند تَشْغِيل برامج واجهات المُستخدِم الرسومية (GUI)، ستُفتَح نافذة واحدة (window) أو أكثر على شاشة الحاسوب الخاصة بك. يُمكِنك، كمبرمج، التَحكُّم الكامل بما يَظهر على تلك النافذة، وكذلك بالكيفية التي يُمكِن للمُستخدِم التَفاعُل (interact) بها مع النافذة. سنَفْحَص خلال هذا القسم أمثلة بسيطة مثل طباعة بعض الأشكال البسيطة كالخطوط والمستطيلات على النافذة بدون أي تَفاعُل من المُستخدِم، فالنقطة المُهمّة، في الوقت الحالي، هو أن تتعرَّف على الطريقة التي تُستخدَم بها أساليب "البرمجة في نطاق ضيق" ضِمْن سياقات اخرى غَيْر برامج الطرفية المُعتمدة على النصوص، وسترى بنفسك أنه يمكن تطبيق نفس تلك الأساليب لكتابة أيّ برنامج فرعي (subroutine) وليس فقط البرنامج main. رسم الأشكال ستحتاج إلى أن تَكُون على دراية ببعض المفاهيم كالبكسل (pixels)، وأنظمة الإِحداثيَّات (coordinate systems)؛ كي يتَسَنَّى لك فهم الرسومات الحاسوبية (computer graphics)، ولذلك سنمر على بعض المفاهيم الأساسية سريعًا. تتكون عامةً شاشة الحاسوب (computer screen) من مربعات صغيرة تُسمى البكسل (pixels)، مُرَتَّبة بصورة صفوف وعواميد، بدقة تَصِل عادةً إلى ١٠٠ بكسل لكل بُوصَة (pixels per inch). تَحتوِي الكثير من الشاشات حاليًا على عدد أكبر بكثير من البكسلات المَلْموسة (physical pixels) لكل بُوصَة، لدرجة أنه مِنْ المُحتمَل لبكسل منصة JavaFX أن يُشير إلى بكسل مَلْموس (physical pixel) بمثل هذه الشاشات عالية الدقة (high-resolution)، ولكنه على الأرجح يُشيِر إلى بكسل مَنطقي (logical pixel)، والتي هي وَحدة قياس تُعادِل ٠.٠١ بوصة تقريبًا. لمّا كان باستطاعة الحاسوب التَحكُّم بلون البكسل، فإنه، في الواقع، يَرسِم (drawing) الأشكال عن طريق تَغْيِير ألوان البكسلات المُفردة (individual pixels). كل بكسل له زوج من الإِحداثيَّات (coordinates)، يُشار إليها عادة باسم الإِحداثيّ x والإِحداثيّ y، وتُستخدَم لتَحْدِيد المَوْضِع الأفقي (horizontal) والرأسي (vertical) للبكسل على الترتيب. عند الرسم بمساحة مستطيلية الشكل على الشاشة، تَكُون إِحداثيَّات البكسل بالرُكْن العُلوِي الأيسر (upper left corner) هي (٠،٠)، وبحيث تزداد قيمة الإِحداثيّ x من اليسار إلى اليمين، بينما تزداد قيمة الإِحداثيّ y من الأعلى إلى الأسفل. يُستخدَم البكسل لتَحْدِيد الأشكال وتَعرَيفها، فعلى سبيل المثال، يُعرَّف أيّ مستطيل من خلال الإِحداثيّ x والإِحداثيّ y بالرُكْن العُلوِي الأيسر للمستطيل، بالإضافة إلى كُلًا من عَرْضه (width)، وارتفاعه (height) بوَحدة البكسل. تَعرَض مساحة الرسم (drawing area) بالصورة التالية نطاق كلًا من الإِحداثيَّات x و y، ويُمثِل العَرْض والارتفاع بها حجم مساحة الرسم بوَحدة البكسل: بفَرْض أن مساحة الرسم (drawing area) -بالأعلى- مُكوَّنة من ٨٠٠*٥٠٠ بكسل، سيكون المستطيل، الواقع بالجزء العُلوِي الأيسر من الصورة، تقريبًا بعَرْض ٢٠٠ بكسل وارتفاع ١٥٠ بكسل، كما يَقع الرُكْن العُلوِي الأيسر (upper left corner) للمستطيل بالإِحداثيَّات (٥٠،٥٠) تقريبيًا. يَتمّ الرسم بلغة الجافا باِستخدَام كائن سِّياق رُسومي (graphics context) من النوع GraphicsContext. يَشتمِل هذا الكائن على بعض البرامج الفرعية (subroutines)، مثل برامج (routines) لرسم الأشكال البسيطة كالخطوط، والمستطيلات، والأشكال البيضاوية، والنصوص. (عندما يَظهر النص على الشاشة، يَرسِم الحاسوب حروف النص مثلما يَرسِم أيّ أشكال آخرى). بالإضافة إلى ذلك، يَشتمِل كائن السِّياق الرُسومي (graphics context) أيضًا على مجموعة من البيانات (data)، مثل نوع الخط المُختار حاليًا للرَسْم ولونه. (يُحدِّد نوع الخط كلًا من حجم وشكل الحروف). تَشتمِل بيانات كائن السِّياق أيضًا على سطح رسم (drawing surface)، وهو ما يَتمّ الرسم عليه، وفي حالتنا، سيكون سطح الرسم هو مساحة مُحتوَى النافذة بدون الحواف (border) وشريط العنوان (title bar)، ولكن تَتوفَّر أسطح رَسْم مختلفة يمكن الرَسْم عليها أيضًا. تُوفِّر منصة JavaFX طريقتين لرسم الأشكال: إِمّا بمَلْئ الشكل (filling) أو بتَحْدِيد حوافه (stroking). مَلْئ الشكل (filling) هو ضَبْط لون كل بكسل بداخله، أمَا تَحْدِيد حواف الشكل (stroking) فهو ضَبْط لون البكسلات الواقعة بحوافه (border)، وهو ما يُشبه عملية سَحب قلم على طول حواف الشكل، وفي هذه الحالة، تُعدّ صفات القلم -كحجمه (width/size) أو ما إذا كان يَستخدِم خط صلب (solid line) أو مُتقطِّع (dashed line)- خاصيات (properties) ضِمْن كائن السِّياق الرُسومي (graphics context). يُخصِّص كائن السِّياق الرُسومي أيضًا لونين مُنفصلين، أحدهما لمَلْئ الأشكال (filling)، والآخر لتَحْدِيد حوافها (stroking). لاحظ اقتصار بعض الأشكال -كالخطوط- على طريقة تَحْدِيد الحواف فقط. يُستخدَم مُتَغيِّر من النوع GraphicsContext لتَمثيِل السِّياق الرُسومي (graphics context)، ويَحمِل هذا المُتَغيِّر عادةً الاسم g. ليس هذا ضروريًا بالطبع، حيث يَتوقَف اسمه بالنهاية على المُبرمج. نَعرِض هنا بعض البرامج الفرعية (subroutines) المُتوفرة ضِمْن كائن السِّياق الرُسومي g. لاحظ أن كل قيم المُعامِلات العددية هي من النوع double: البرنامج الفرعي g.setFill(c): يَضبُط اللون المُستخدَم لمَلْئ الأشكال (filling)، حيث المُعامِل c هو كائن من الصَنْف Color. تَتوفَّر الكثير من الثوابت (constants) المُمثِلة للألوان القياسية (standard colors)، والتي يُمكِن اِستخدَامها كمُعامِل لهذا البرنامج الفرعي. تتراوح الألوان القياسية من الألوان الشائعة مثل Color.BLACK و Color.WHITE و Color.RED و Color.GREEN و Color.BLUE و Color.YELLOW، إلى بعض الألوان الغريبة مثل Color.CORNFLOWERBLUE. (يُمكِنك أيضًا إِنشاء ألوان جديدة مُخصَّصة). على سبيل المثال، إذا أردت مَلْئ الأشكال باللون الأحمر، فإنك ستَستدعِي البرنامج الفرعي g.setFill(Color.RED);. لاحظ أن اللون المُخصَّص أثناء الاستدعاء سيُستخدَم لجميع عمليات المَلْئ التالية وحتى الاستدعاء التالي لنفس البرنامج الفرعي، أما الأشكال المَرسومة مُسْبَّقًا فلا تتأثر بهذا التَغْيِير. البرنامج الفرعي g.setStroke(c): يَضبُط اللون المُستخدَم لتَحْدِيد حواف الأشكال (stroking)، ويَعمَل بصورة مشابهة للبرنامج الفرعي g.setFill. البرنامج الفرعي g.setLineWidth(w): يَضبُط حجم القلم المُستخدَم خلال عمليات تَحْدِيد الحواف التالية (stroking). لاحظ أن المُعامِل w يَستخدِم وَحدة البكسل. البرنامج الفرعي g.strokeLine(x1,y1,x2,y2): يَرسِم خطًا مُمتدًا من إِحداثيَّات نقطة البداية (x1,y1) وحتى إِحداثيَّات نقطة النهاية (x2,y2). يُرسَم الخط باللون الأسود وبحجم ١ بكسل افتراضيًا، ومع ذلك، يُمكِنك تَخْصِيص كلًا منهما باستدعاء g.setStroke() و g.setLineWidth() على الترتيب. البرنامج الفرعي g.strokeRect(x,y,w,h): يَرسِم الحواف الخارجية (stroking) لمستطيل مع جوانبه الأفقية والرأسية، بحيث يَبعُد الرُكْن العُلوِي الأيسر (top-left corner) لهذا المستطيل مسافة قدرها x بوَحدة البكسل عن الحافة اليسرى لمساحة الرسم (drawing area)، ومسافة قدرها y بوَحدة البكسل عن حافتها العُلوِية. يُحدِّد كلًا من المُعامِلين w و h عَرْض المستطيل الأفقي وارتفاعه الرأسي بوَحدة البكسل على الترتيب. يُمكِن ضَبْط لون الخط المُستخدَم وحجمه باستدعاء g.setStroke() و g.setLineWidth() على الترتيب. البرنامج الفرعي g.fillRect(x,y,w,h): يَعمَل بصورة مشابهة للبرنامج الفرعي g.strokeRect() باستثناء أنه يَملْئ المستطيل (filling) بدلًا من رسم حوافه الخارجية (stroking). اِستدعي g.setFill لضَبْط اللون المُستخدَم. البرنامج الفرعي g.strokeOval(x,y,w,h): يَرسِم الحواف الخارجية لشكل بيضاوي. يُرسَم الشكل البيضاوي بحيث يَقع ضِمْن المستطيل الذي كان سيُرسَم في حالة استدعاء g.strokeRect(x,y,w,h) بنفس قيم المُعامِلات. لاحِظ أنه يُمكِنك اِستخدَام نفس القيمة لكُلًا من المُعامِلين w و h لرسم حواف دائرة. البرنامج الفرعي g.fillOval(x,y,w,h): يَعمَل بصورة مشابهة للبرنامج الفرعي g.strokeOval() باستثناء أنه يَملْئ الشكل البيضاوي بدلًا من رَسْم حوافه الخارجية. تُعدّ هذه البرامج الفرعية كافية لرَسْم بعض الصور باِستخدَام الجافا. لنبدأ بشئ بسيط مثل رَسْم عشرة خطوط متوازية، كالتالي: نحتاج أولًا لمجموعة افتراضات هي كالتالي: سيكون طول الخطوط حوالي ٢٠٠ بكسل، والمسافة بين كل خط والخط الذي يَليه حوالي ١٠ بكسل، وأخيرًا، سنفْترِض أن نقطة بداية (start) أول خط تقع بالإِحداثيَّات (١٠٠،٥٠). الآن، كل ما نحتاج إليه لرَسْم خط هو استدعاء البرنامج الفرعي g.strokeLine(x1,y1,x2,y2) بقيم مُعامِلات مناسبة. نلاحِظ أن نقطة البداية (start) لجميع الخطوط لها نفس قيمة الإِحداثيّ x (x-coordinate) وتُساوِي ١٠٠، ومِنْ ثَمَّ، سنَستخدِم قيمة ثابتة تُساوِي ١٠٠ كقيمة للمُعامِل x1. لمّا كانت جميع الخطوط بطول ٢٠٠ بكسل، فإننا سنَستخدِم قيمة ثابتة تُساوِي ٣٠٠ كقيمة للمُعامِل x2. في المقابل، تَختلف إِحداثيَّات y (y-coordinates) بكل خط عن الخط الذي يَليه، ولكن يُمكِننا أن نرى أن قيمة الإِحداثيّ y بنقطتي البداية (start) والنهاية (end) لكل خط منها هو نفسه، وعليه، سنَستخدِم مُتَغيِّر وحيد لكُلًا من قيمتي y1 و y2، هو المُتَغيِّر y. الآن، أصبح أمر الاستدعاء لرَسْم أحد الخيوط كالتالي g.strokeLine(100,y,300,y). اِفْترَضنا قبلًا أن قيمة المُتَغيِّر y لأول خط هي ٥٠، ثم ستزداد تلك القيمة بمقدار ١٠ مع كل انتقال للخط التالي، مما يَعنِي أننا سنحتاج إلى التأكد من أن قيمة y تأخذ القيمة الصحيحة من متتالية الأعداد. يُمكِننا اِستخدَام حَلْقة تَكْرار for، كالتالي: int y; // إحداثي y للخط int i; // المتغير المتحكم بالحلقة y = 50; // تبدأ y بالقيمة 50 لأول خط for ( i = 1; i <= 10; i++ ) { g.strokeLine( 100, y, 300, y ); y = y + 10; // أزد y بمقدار 10 قبل رسم الخط التالي } نستطيع أيضًا اِستخدَام المُتَغيِّر y ذاته كمُتحكِّم بالحَلْقة (loop control variable). لاحِظ أن قيمة y للخط الأخير هي ١٤٠. انظر الشيفرة التالية: int y; for ( y = 50; y <= 140; y = y + 10 ) g.strokeLine( 100, y, 300, y ); إذا أردت تلوين الخطوط باللون الأزرق، اِستدعي البرنامج الفرعي g.setStroke(Color.BLUE) قبل رَسْمها، حيث سيُستخدَم اللون الأسود افتراضيًا إذا قُمت برَسْمها دون ضَبْط اللون. أما إذا أردت أن يَكُون حجم تلك الخطوط ٣ بكسل، اِستدعي البرنامج الفرعي g.setLineWidth(3) قَبْل رَسْمها. لننتقل إلى مثال أكثر تعقيدًا، فمثلًا، لنَرسِم عددًا كبيرًا من الدوائر بشكل عشوائي سواء فيما يَخُص مَوْضِعها (position) أو لونها. لمّا كنا على علم بعدد قليل من الألوان المُتوفرة، فإننا سنختار عشوائيًا واحدًا من الألوان التالية : الأحمر، والأخضر، والأزرق، والأصفر. سنستعمل تَعْليمَة switch بسيطة للاختيار، وذلك بطريقة شبيهة للمثال بالقسم الفرعي ٣.٦.٤: switch ( (int)(4*Math.random()) ) { case 0: g.setFill( Color.RED ); break; case 1: g.setFill( Color.GREEN ); break; case 2: g.setFill( Color.BLUE ); break; case 3: g.setFill( Color.YELLOW ); break; } لمّا كنا نريد للدوائر أن تكون عشوائية التَموْضع، فسنحتاج إلى اختيار مركز الدوائر (center of circles) بصورة عشوائية. بفَرْض أن عَرْض مساحة الرسم (drawing area) وارتفاعها مُعطيين من خلال المُتَغيِّرين width و height على الترتيب، فسينبغي للمَوْضِع الأفقي (horizontal position) للمركز أن يكون قيمة عشوائية تتراوح من القيمة ٠ وحتى width-1. بالمثل، يَنبغي للمَوْضِع الرأسي (vertical position) لمركز الدائرة أن يَكُون قيمة عشوائية تتراوح من القيمة ٠ وحتى height-1. أخيرًا، ما زِلنا بحاجة لتَحْدِيد حجم الدائرة. سنكتفي، في هذا المثال، باِستخدَام نصف قطر (radius) ثابت لجميع الدوائر مُساوِي للقيمة ٥٠ بكسل. تُرسَم الدائرة باِستخدَام التَعْليمَة g.fillOval(x,y,w,h)، لكن، في الواقع، لا يُمثِل المُعامِلان x و y، بهذا الأمر (command)، إِحداثيَّات مركز الدائرة؛ وإنما إِحداثيَّات الرُكْن العُلوِي الأيسر (upper left corner) للمستطيل المرسوم حول الدائرة، ولهذا سنحتاج إلى تَحرِيك مركز الدائرة بمسافة قدرها يُساوِي نصف قطر الدائرة أي ٥٠ بكسل؛ وذلك للحصول على قيم x و y المُناظِرة. في المقابل، يُمثِل المُعامِلان w و h عَرْض وارتفاع المستطيل على الترتيب، واللذين ستكون قيمتهما مُساوِية لضعف نصف قطر الدائرة أي ١٠٠ بكسل بهذا المثال. تُراعِي الشيفرة التالية جميع النقاط المذكورة بالأعلى، وتُستخدَم لرَسْم دائرة عشوائية واحدة: centerX = (int)(width*Math.random()); centerY = (int)(height*Math.random()); g.fillOval( centerX - 50, centerY - 50, 100, 100 ); لاحِظ أن الشيفرة بالأعلى تُستدَعى بَعْد استدعاء الشيفرة المسئولة عن ضَبْط اللون. تبدو الصورة عامةً بشكل أفضل بَعْد تَحْدِيد حافة الدائرة (border) باللون الأسود (stroking)، ولذلك أضيفت الشيفرة التالية: g.setStroke( Color.BLACK ); g.strokeOval( centerX - 50, centerY - 50, 100, 100 ); وأخيرًا، للحصول على عدد كبير من الدوائر، ضُمِّنت الشيفرة بالأعلى داخل حَلْقة تَكْرار for، ونُفِّذت ٥٠٠ مرة، فكانت الرسمة الناتجة عن البرنامج كالتالي: الرسم داخل برنامج كما تعلم، لا يُمكِن لأيّ شيفرة بلغة الجافا أن تكون مُستقلة بذاتها، فلابُدّ لها أن تُكتَب ضِمْن برنامج فرعي (subroutine)، والذي بدوره يَكُون مُعرَّفًا داخل صَنْف (class)، ولهذا تَعرِض الشيفرة التالية التَعرِيف الكامل لبرنامج فرعي (subroutine definition)، والذي يُستخدَم لرَسْم الصورة من المثال السابق : public void drawPicture(GraphicsContext g, int width, int height) { g.setFill(Color.WHITE); g.fillRect(0, 0, width, height); // املأ لون الخلفية بالأبيض // As an example, draw a large number of colored disks. // To get a different picture, erase this code, and substitute your own. int centerX; // احداثي x لمركز القرص int centerY; // احداثي y لمركز القرص int colorChoice; // قيمة اللون العشوائي int count; // المتغير التحكم بالحلقة for (count = 0; count < 500; count++) { centerX = (int)(width*Math.random()); centerY = (int)(height*Math.random()); colorChoice = (int)(4*Math.random()); switch (colorChoice) { case 0: g.setFill(Color.RED); break; case 1: g.setFill(Color.GREEN); break; case 2: g.setFill(Color.BLUE); break; case 3: g.setFill(Color.YELLOW); break; } g.fillOval( centerX - 50, centerY - 50, 100, 100 ); g.setStroke(Color.BLACK); g.strokeOval( centerX - 50, centerY - 50, 100, 100 ); } } // نهاية drawPicture() هذه هي المرة الأولى التي تَتعرَّض فيها لتَعرِيف برنامج فرعي (subroutine definition) -إلى جانب main()-. سنتناول هذا الموضوع تفصيليًا بالفصل التالي، ولكن سنَمر عليه سريعًا هنا، يُتيِح السَطْر الأول من التَعرِيف الولوج لبعض القيم التي يَحتاجها البرنامج الفرعي، وهي السِّياق الرُسومي g، وكلًا من عَرْض وارتفاع مساحة الرسم width و height. يَستقبِل البرنامج الفرعي هذه القيم من مصدر خارجي، ويستطيع اِستخدَامها. ما يَهمّ هنا هو أن تُدرِك أنه لكي تَرِسم شيئًا (يَقصِد الكاتب أن هذا هو هدف البرنامج الفرعي، فاِسم البرنامج الفرعي هو drawPicture)، فستحتاج فقط إلى كتابة مُحتوَى البرنامج الفرعي، مثلما تَكتُب مُحتوَى البرنامج main() عند كتابة برنامج (الهدف من main()). يَنبغي لتَعرِيف البرنامج الفرعي (subroutine definition) أن يَكُون بالصَنْف (class) الذي يُعرِّف البرنامج، وهو في هذه الحالة الصَنْف SimpleGraphicsStarter. شَّغِل البرنامج -مُتاح بالكامل بالملف SimpleGraphicsStarter.java- لترى الرَسمة، كما يُمكِنك اِستخدَام هذا البرنامج كنقطة بداية لرَسْم الصور الخاصة بك. لاحِظ أنك لن تَفهم كل الشيفرة المكتوبة بالبرنامج، لكن ما يزال بإمكانك التَعديل عليها، فلا حاجة إلى فهم الشيفرة بأكملها، كل ما قد يَعَنيك هو الشيفرة الموجودة بالبرنامج الفرعي drawPicture(). اِحذف تلك الشيفرة، وضَعْ مكانها شيفرة الرسوم خاصتك، وستَتمكَّن بعدها من عَرْض رسوماتك. بالمناسبة، قد تُلاحِظ أن الكلمة static مُستخدَمة بتَعرِيف البرنامج الفرعي main()، بعكس البرنامج الفرعي drawPicture()، الذي لا يَستخدِمها، وهو ما يَعنِي أن البرنامج الفرعي drawPicture() موجود بكائن (object) وليس بصَنْف (class). تُعدّ البرامج الفرعية التي تَستخدِم الكلمة static بتَعرِيفعها ساكنة (static)، أما التي لا تَستخدِمها فتُعدّ غَيْر ساكنة (non-static). الفرق بينهما مُهِمّ، ولكنه ليس بالأمر الذي يَنبغي أن تَقْلَق حِياله في الوقت الحاضر؛ حيث سنتناوله تفصيليًا بالفصل الخامس على أية حال. التحريكة (Animation) يَعتمِد التَحرِيك الحاسوبي (computer animation) على متتالية من الصور المُنفصلة، يُطلَق على كُل منها اسم الإطار (frame). تُعرَض هذه الصور بشكل سريع واحدة تلو الآخرى، فإذا كان التَغْيِير بين كل صورة والصورة التي تَليها طفيفًا، ستبدو متتالية الصور وكأنها تَحرِيكة مُستمرة (continuous animation). يُمكِنك اِستخدَام المثال التوضيحي بالملف SimpleAnimationStarter.java كنقطة بداية، حيث يَحتوِي على البرنامج الفرعي drawFrame() المَسؤول عن رَسْم إطار (frame) وحيد ضِمْن تَحرِيكة (animation)، بالإضافة إلى ذلك، يُنفَّذ البرنامج الفرعي drawFrame() أتوماتيكيًا حوالي ٦٠ مرة بالثانية، مما يَضمَن استمرار عَرْض الأُطُر (frames)، أيّ أنك تستطيع إِنشاء تَحرِيكة (animation) بمُجرَّد إضافة الشيفرة إلى هذا البرنامج الفرعي. تستطيع تمييز المرة الحالية من التَّنْفيذ من خلال مُتَغيِّرين إضافيين -إلى جانب السِّياق الرُسومي وكُلًا من عَرْض وارتفاع مساحة الرسم- يَستقبِلهما البرنامج الفرعي، وهما frameNumber و elapsedSeconds؛ حيث يأخذ المُتَغيِّر frameNumber القيم ٠، ١، ٢، ٣، .. والتي تَزداد بمقدار الواحد مع كل اِستدعاء للبرنامج الفرعي، أمَا قيمة المُتَغيِّر elapsedSeconds فتُشيِر إلى عدد الثواني التي مَرَّت على تَّنْفيذ التَحرِيكة حتى الآن. إجمالًا، تَستطيع رَسْم صورة مختلفة في كل مرة يُستدَعى فيها البرنامج الفرعي (subroutine) بالاعتماد على قيمة أيًا من هذين المُتَغيِّرين. سنَرسِم بالمثال التالي مجموعة من المستطيلات المُتداخِلة (nested rectangles)، والتي ستنكمش باتجاه مركز الرَسْمة، مما سيُعطِي انطباعًا زائفًا بوجود حركة لا نهائية (infinite motion). تَعرِض الصورة التالية إِطارًا واحدًا من التَحرِيكة (animation): لنُفكر كيف يُمكِن رَسْم مثل هذه الصورة. عامةً، يُمكِن اِستخدَام حَلْقة التَكْرار while لرَسْم المستطيلات، بحيث تبدأ أولًا برَسْم المستطيل الخارجي، ثُمَّ تنتقل إلى الداخل وهكذا. يَنبغي الآن أن نُفكر بالمُتَغيِّرات التي سنحتاج إليها خلال حَلْقة التَكْرار (loop)، وكذلك بالطريقة التي ستَتغَيَّر بها قيم تلك المُتَغيِّرات من تَكْرار (iteration) معين إلى التَكْرار الذي يليه. ستساعدنا الملاحظات التالية على مَعرِفة تلك المُتَغيِّرات، أولًا، مع كل تَكْرار، يكون المستطيل المرسوم أصغر منه في المرة السابقة، كما أنه يَتحرك للداخل قليلًا. يتركز عامةً الفارق بين أيّ مستطيلين على حجمهما وإِحداثيَّات (coordinates) رُكْنيهما اليساريين العُلوِيين (upper left corners)، ولهذا سنحتاج، أولًا، إلى مُتَغيِّرين لتَمثيِل كلًا من عَرْض المستطيل وارتفاعه، وهما المُتَغيِّران rectWidth و rectHeight على الترتيب. أما بالنسبة لإِحداثيَّات الرُكْن الأيسر العُلوِي x و y، فيُمكِن تَمثيِل كليهما بمُتَغيِّر وحيد للمستطيل الواحد، هو المُتَغيِّر inset؛ لأن قيمتهما مُتساوِية؛ حيث يَبعُد أيّ مستطيل عن حافتي مساحة الرسم (drawing area) بنفس مقدار المسافة. نُلاحِظ أنه مع كل تَكْرار، تَنقُص قيمة كلًا من عَرْض المستطيل rectWidth وارتفاعه rectHeight، بينما تزداد المسافة inset التي يَبعُدها المستطيل عن الحافتين. أخيرًا، تنتهي حَلْقة التَكْرار while عندما يُصبِح عَرْض المستطيل أو ارتفاعه أقل من أو يُساوِي الصفر. اُنظر خوارزمية رَسْم إِطار (frame) وحيد: // املأ مساحة الرسم باللون الأبيض Fill the drawing area with white // اضبط قيمة inset المبدئية للمستطيل الأول الخارجي Set the amount of inset for the first rectangle // اضبط قيمة عرض وارتفاع المستطيل الأول الخارجي Set the width and height for the first rectangle // اضبط اللون المستخدم لتحديد الحواف إلى اللون الأسود Set the stroke color to black // طالما كان العرض والارتفاع أكبر من الصفر while the width and height are both greater than zero: // ارسم مستطيل باستخدام البرنامج الفرعي g.strokeRect draw a rectangle (using the g.strokeRect subroutine) // أزد قيمة inset حتى ينتقل المستطيل التالي إلى الداخل increase the inset (to move the next rectangle over and down) // انقص عرض وارتفاع المستطيل التالي حتى يصبح المستطيل التالي أصغر decrease the width and height (to make the next rectangle smaller) ضُبطَت هذه النسخة من البرنامج بحيث يَبعُد كل مستطيل مسافة قدرها ١٥ بكسل عن المستطيل المُحيِط به، ولهذا فإن قيمة المُتَغيِّر inset تَزداد بمقدار ١٥ بكسل مع كل تَكْرار. في المقابل، يَتقَلص المستطيل حوالي ١٥ بكسل يمينًا ويسارًا، أيّ يَنقُص عَرْض المستطيل بمقدار ٣٠ بكسل. وبالمثل، يَنقُص ارتفاعه بمقدار ٣٠ بكسل مع كل تَكْرار ضِمْن الحلقة. يَسهُل إعادة كتابة الخوارزمية بلغة الجافا، لكن تتبقَى فقط حاجتنا إلى معرفة القيم المبدئية للمُتَغيِّرات inset و width و height لأول مستطيل-(المستطيل الخارجي). لحساب ذلك، سنُفكر في حقيقة كَوْن الصورة متحركة (animated)، أي يَعتمِد ما نَرسِمه بطريقة ما على رقم الإِطار (frame number) الحالي. لمّا كان الرُكْن الأيسر العُلوِي (top-left corner) للمستطيل الخارجي يتحرك للأسفل وللداخل من أيّ إِطار إلى الإِطار الذي يَليه، فإن قيمة المُتَغيِّر inset المبدئية تزداد مع كل إِطار. قد تُفكر إذًا بضَبْط قيمة المُتَغيِّر inset المبدئية إلى القيمة ٠ بالإِطار رقم ٠، وإلى القيمة ١ بالإطار رقم ١ وهكذا. للأسف، لن يكون هذا صالحًا إلى الأبد؛ فعندما تَصِل التحريكة للإِطار ١٥، يَنبغِي أن يَظهر مستطيل خارجي جديد بمساحة الرَسْم (drawing area)، هو في الواقع ليس جديدًا، وإنما أُعيد فقط ضَبْط قيمة المُتَغيِّر inset المبدئية إلى القيمة ٠. إجمالًا، يَنبغِي لقيمة المُتَغيِّر inset أن تأخذ القيم ٠، ١، ٢، ٣،… حتى تَصِل إلى القيمة ١٤، لتُعاد الكَرَّة من جديد، وهو ما يُمكِن إنجازه باِستخدَام الشيفرة التالية: inset = frameNumber % 15; لاحِظ أن المستطيل يَملأ مساحة الرسم باستثناء حافة (border) تُحيِط به، عَرْضها يُساوِي قيمة المُتَغيِّر inset، أي بعبارة آخرى، عَرْض المستطيل هو عَرْض مساحة الرسم مطروحًا منه ضعف قيمة المُتَغيِّر inset، وبالمثل لارتفاعه. انظر شيفرة البرنامج الفرعي drawFrame() كاملة بالأسفل والمسئولة عن تَحرِيك المستطيل: public void drawFrame(GraphicsContext g, int frameNumber, double elapsedSeconds, int width, int height) { g.setFill(Color.WHITE); g.fillRect(0,0,width,height); // املأ مساحة الرسم باللون الأبيض // المسافة بين بين المستطيل الخارجي ومساحة الرسم double inset; double rectWidth, rectHeight; // عرض وطول أحد المستطيلات // اضبط اللون المستخدم لرسم حواف المستطيل g.setStroke(Color.BLACK); // إضافة القيمة 0.5 هو أسلوب للحصول على صورة أكثر وضوحًا inset = frameNumber % 15 + 0.5; rectWidth = width - 2*inset; rectHeight = height - 2*inset; while (rectWidth >= 0 && rectHeight >= 0) { g.strokeRect(inset, inset, rectWidth, rectHeight); inset += 15; // تبعد المستطيلات عن بعضها بمقدار 15 بكسل rectWidth -= 30; rectHeight -= 30; } } البرنامج مُتاح بالكامل بالملف MovingRects.java. يُمكِنك أيضًا الإِطلاع على مثال توضيحي آخر للتحريك (animation) بالملف RandomCircles.java، والذي يُضيِف قرصًا ملونًا (colored disk) بشكل عشوائي مع كل إِطار جديد. سيُظهِر لك هذا المثال أن صورة الإِطار لا تُحذَف تلقائيًا قبل إِعادة رسم الإِطار التالي. ترجمة -بتصرّف- للقسم Section 9: Introduction to GUI Programming من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.
-
تناولنا، بالأقسام السابقة من هذا الفصل، جميع بُنَى التحكُّم (control structures) المُدعَّمة بلغة الجافا، ولكن قبل الانتقال إلى الفصل التالي، سنُلقِي نظرة مبدئية على موضوعين إِضافيين مُرتبطين نوعًا ما ببُنَى التحكُّم. سنُلقِي في هذا القسم نظرة خاطفة على المصفوفات (arrays)، والتي تُعدّ واحدة من أكثر هياكل البيانات (data structure) شيوعًا. بالإضافة إلى ذلك، تَتطلَّب معالجة المصفوفات (array processing) اِستخدَام بُنَى التحكُّم، وهو ما سيمنحك الفرصة لتطبيق بعضًا مما تَعلَّمته عن بُنَى التحكُّم. سنتحدث في الفصل التالي عن الرسومات الحاسوبية (computer graphics)، والتي ستسمح لك أيضًا باستخدام بُنَى التحكُّم، ولكن بسياق مُختلِف نوعًا ما. إنشاء المصفوفات واستخدامها يتكون الهيكل البياني (data structure) من مجموعة من العناصر (items)، مُجمَّعة معًا بحيث يُمكِن التَعامُل معها كوحدة واحدة (unit). تُعدّ المصفوفة (array) هيكلًا بيانيًا (data structure)، تُرتَّب فيه العناصر كمتتالية مُرقَّمَة (numbered sequence)، بحيث يُمكِن الإشارة إلى كل عنصر مُفرد بها بواسطة رقم المَوْضِع (position number) خاصته. تَشترِط لغة الجافا -بخلاف بعض لغات البرمجة الأخرى- أن تكون جميع عناصر (items) المصفوفة من نفس النوع، كما يبدأ العدّ فيها دائمًا من الصفر. ستحتاج إلى التَعرُّف على عدة مصطلحات جديدة لتتمكَّن من الحديث عن المصفوفات: أولًا، طول/حجم المصفوفة (length/size of array) هو عدد العناصر (items) الموجودة بها، أما نوع المصفوفة الأساسي (base type of array) فهو نوع عناصرها المُفردة، وأخيرًا، فهرس العنصر (index) هو رقم المَوْضِع (position number) الخاص بالعنصر داخل المصفوفة. لنفْترِض أنك تريد كتابة برنامج يُمكِنه معالجة الأسماء الخاصة بألف شخص، يَعنِي ذلك أنك في حاجة إلى طريقة للتَعامُل مع كل هذه البيانات. ربما ظننت -قبل تَعرُّفك على هيكل المصفوفة البياني (array)- أن البرنامج سيحتاج إلى ألف مُتَغيِّر ليَحمِل أسماء كل هذه الأشخاص، وأنك ربما ستحتاج إلى ألف تَعْليمَة طباعة كي تَتَمكَّن من طباعة كل هذه الأسماء. سيكون ذلك ضَرْبًا من العبث بكل تأكيد. تستطيع، في الواقع، وضع جميع الأسماء بمصفوفة (array)، يُمثِلها مُتَغيِّر وحيد يَحمِل قائمة الأسماء كاملة. في هذا المثال، لمّا كان هناك ألف اسم مفرد، فإن طول المصفوفة (length) سيُساوِي 1000. ولمّا كان كل عنصر بالمصفوفة من النوع String، فإن نوع المصفوفة الأساسي (base type) هو الصَنْف String. وأخيرًا، سيكون أول اسم داخل المصفوفة بالفهرس 0 (index)، بينما سيكون ثاني اسم بالفهرس 1، وهكذا حتى نَصِل إلى الاسم الأخير، والذي سيكون بالفهرس 999. يُمكِن لنوع المصفوفة الأساسي (base type of array) أن يكون أي نوع مُدعَّم بلغة الجافا، ولكننا سنكتفي حاليًا بمعالجة المصفوفات التي إِمّا أن يكون نوعها الأساسي هو النوع String أو أن يكون واحدًا من الأنواع الأوَّليّة (primitive types) الثمانية. يُطلَق اسم مصفوفة الأعداد الصحيحة (array of ints) على المصفوفات التي يكون نوعها الأساسي هو int، بينما يُطلَق اسم مصفوفة السَلاسِل النصية (array of Strings) على المصفوفات التي يكون نوعها الأساسي هو String. مع ذلك، لا تُعدّ المصفوفة -إذا أردنا تَحرِي الدقة- قائمة من قيم الأعداد الصحيحة (integers) أو قيم السَلاسِل النصية (strings) أو حتى أيّ قيم اخرى، فمن الأفضل أن تُفكِر بها كقائمة من المُتَغيِّرات من النوع العددي int أو كقائمة من المُتَغيِّرات من النوع String أو من أي نوع آخر، فدائمًا ما يكون هناك خَلطًا (confusion) مُحتمَلًا بين اِستخدَام المُتَغيِّرات (variables) كاسم لمَوْضِع ما بالذاكرة (memory location)، واِستخدَامها كاسم للقيمة المُخزَّنة بهذا المَوْضِع. يُعامَل أي مَوْضِع (position) بالمصفوفة كمُتَغيِّر (variable)، فيُمكِن لهذا المَوْضِع أن يَحمِل قيمة من نوع معين (نوع المصفوفة الأساسي) مثلما يُمكِن لأيّ مُتَغيِّر أن يَحمِل قيمة، كما يُمكِن لتلك القيمة أن تَتغيَّر بأيّ وقت مثلما يُمكِن لقيمة أي مُتَغيِّر أن تَتغيَّر. عادة ما يُطلَق اسم عناصر المصفوفة (elements of array) على مجموعة المُتَغيِّرات المُفردة (individual variables) الموجودة بالمصفوفة وتُكَوِّنها إجمالًا. عندما تُستخدَم مصفوفة ببرنامج ما، فيُمكِنك -كما ذَكَرت مُسْبَّقًا- الإشارة إليها ككل باِستخدَام مُتَغيِّر، لكنك عادةً ما ستحتاج إلى الإشارة إلى عناصر المصفوفة المُفردة (elements of array) من خلال اسم معين، والذي يَعتمِد على كُلًا من اسم المصفوفة ككل وفهرس (index) العنصر، فتكون صياغة (syntax) الاسم كالتالي namelist[7]، حيث namelist هو المُتَغيِّر الذي يُمثِل المصفوفة ككل، بينما يُشيِر namelist[7] إلى العنصر الموجود بالفهرس ٧ بتلك المصفوفة. أي يُستخدَم اسم المصفوفة متبوعًا بفهرس العنصر ضِمْن أقواس معقوفة (square brackets) [ ] للإشارة إلى هذا العنصر بتلك المصفوفة. يُعامَل اسم العنصر بهذه الصياغة كأي مُتَغيِّر آخر، فيمكنك أن تُسنِد قيمة إليه، أو أن تَطبَعه، أو أن تَستخدِمه بأيّ تعبير (expression). تَحتوِي أيّ مصفوفة على مُتَغيِّر -نوعًا ما- يُمثِل طولها (length). فعلى سبيل المثال، إذا كان لديك مصفوفة namelist، فإنك تَستطيع اِستخدَام namelist.length للإشارة إلى طولها. ومع ذلك، لا يُمكِنك إسْنَاد (assign) قيمة إلى ذلك المُتَغيِّر؛ لأنه لا يُمكِن تَغْيِير طول المصفوفة. يجب أولًا أن تُصَرِّح (declaration) عن مُتَغيِّر مصفوفة (array variable)؛ حتى تَتَمكَّن من اِستخدَامه للاشارة إلى مصفوفة معينة. تَتوفَّر أنواع المصفوفة (array types) لتَخْصِيص نوع تلك المُتَغيِّرات، وبشكل عام، فإن نوع مصفوفة معينة (array type) يتكون من نوع المصفوفة الأساسي (base type) متبوعًا بزوج من الأقواس المعقوفة (square brackets) الفارغة. فمثلًا، يكون مُتَغيِّر مصفوفة (array variable) من النوع String[] في حالة إشارته إلى مصفوفة سَلاسِل نصية (array of Strings)، بينما يكون من النوع int[] في حالة إشارته إلى مصفوفة أعداد صحيحة (array of ints). اُنظر المثال التالي: String[] namelist; int[] A; double[] prices; يُمكِن للمُتَغيِّرات المُصرَّح عنها بتلك الطريقة الإشارة إلى المصفوفات، لكن لاحِظ أن مُجرَّد التَّصْريح عن المُتَغيِّر (variable declaration) لا يَتسبَّب بالإنشاء الفعليّ للمصفوفة. ينبغي إِسْناد قيمة لمُتَغيِّر المصفوفة (array variable) قبل اِستخدَامه -مثلما هو الحال مع جميع المُتَغيِّرات-، وفي هذه الحالة، تكون القيمة عبارة عن مصفوفة. تَتوفَّر صياغة خاصة (special syntax) للإِنشاء الفعليّ للمصفوفة؛ وذلك لأن المصفوفات -بلغة الجافا- هي بالأساس كائنات (objects)، وهو ما سنؤجل الحديث عنه؛ فهو غَيْر ذي صلة هنا. إجمالًا، يُستخدَم العَامِل new لإنشاء المصفوفات، انظر الأمثلة التالية: namelist = new String[1000]; A = new int[5]; prices = new double[100]; تكون الصياغة كالتالي: <array-variable> = new <base-type>[<array-length>]; يُمكِن اِستخدَام عدد صحيح (integer) أو تعبير من النوع العددي (integer-valued expression) لتَحْدِيد طول المصفوفة . بَعْد تَّنْفيذ تَعْليمَة الإِسْناد A = new int[5];، أَصبح المُتَغيِّر A يُشير إلى مصفوفة مُكوَّنة من ٥ عناصر (elements) من النوع العددي (integer)، هي A[0] و A[1] و A[2] و A[3] و A[4]، كما أصبح A.length يَحمِل القيمة ٥. اُنظر الصورة التالية: عند إِنشاء مصفوفة أعداد صحيحة (array of int)، تُهيَئ (initialized) جميع عناصر المصفوفة أُتوماتيكيًا بحيث تَحمِل القيمة صفر، مما يَعنِي أن قيم جميع عناصر أيّ مصفوفة أعداد تكون مُساوِية للصفر بمُجرَّد إِنشاؤها. في المقابل، تَحمِل جميع عناصر أيّ مصفوفة قيم منطقية (array of boolean) القيمة المنطقية false بمُجرَّد إِنشاؤها. أمّا عناصر أيّ مصفوفة محارف (array of char) فتحتوي على المحرف المقابل لقيمة ترميز اليونيكود (Unicode code) رقم صفر \u0000 بمُجرَّد إِنشاؤها. وأخيرًا، فإن القيمة المبدئية لعناصر أيّ مصفوفة سَلاسِل نصية (array of String) تكون القيمة الفارغة null (تُستخدَم تلك القيمة مع الكائنات [objects] ولن نتَعرَّض لها حتى القسم ٥.١ لاحقًا). المصفوفات وحلقات التكرار For تُعدّ إِمكانية اِستخدَام مُتَغيِّر من النوع العددي (integer) أو حتى اِستخدَام تعبير من النوع العددي (integer-valued expression) كفهرس للعنصر (index of an element) واحدة من أهم مميزات المصفوفات. فعلى سبيل المثال، إذا كان لديك مصفوفة list، ومُتَغيِّر من النوع العددي الصحيح i، فبإمكانك اِستخدَام list أو حتى list[2*i+1] كأسماء مُتَغيِّرات، بحيث تُؤثِر قيمة i على ما يُشيِر إليه المُتَغيِّر فعليًا. يُساعدك ذلك في حالة أردت إِجراء معالجة معينة على جميع عناصر المصفوفة؛ حيث ستستطيع القيام بذلك ضِمْن حَلْقة التَكْرار for. فمثلًا، لطباعة جميع عناصر المصفوفة list، يُمكننا كتابة الآتي: int i; // فهرس المصفوفة for (i = 0; i < list.length; i++) { System.out.println( list[i] ); } في أول مرة تُنْفَّذ فيها الحَلْقة (loop) -أيّ خلال أول تَكْرار (iteration)-، ستكون قيمة i مُساوِية للصفر، أيّ سيُشيِر list إلى list[0]، ولذا تُطبَع القيمة المُخزَّنة بالمُتَغيِّر list[0]. في المرة الثانية، ستكون قيمة i مُساوِية للواحد، ولذا تُطبَع القيمة المُخزَّنة بالمُتَغيِّر list[1]. لمّا كان طول المصفوفة list يُساوِي ٥، فستنتهي حَلْقة التَكْرار (loop) بعد طباعة قيمة المُتَغيِّر list[4]؛ لأن قيمة i ستُصبِح مُساوِية للقيمة ٥، مما يَعنِي أن الشَّرْط الاستمراري (continuation condition) للحلقة i < list.length لم يَعُدْ مُتحقِّقًا بَعْد الآن. لاحِظ أن الشيفرة بالأعلى تُعدّ مثالًا نموذجيًا لاِستخدَام حَلْقة تَكْرار بغرض معالجة مصفوفة. لنفْحَص عدة أمثلة اخرى، بفَرْض أن A هي مصفوفة أعداد حقيقية (array of double)، وكنا نُريد حِسَاب قيمة متوسط (average) جميع عناصر تلك المصفوفة. يُمكِننا ببساطة اِستخدَام حَلْقة التَكْرار for لحِسَاب حاصل مجموع الأعداد، ومِنْ ثَمَّ نُقسمها على طول المصفوفة، كالتالي: double total; // حاصل مجموع الأعداد بالمصفوفة double average; // قيمة متوسط الأعداد int i; // فهرس المصفوفة total = 0; for ( i = 0; i < A.length; i++ ) { // أضف قيمة العنصر برقم الموضع i إلى حاصل المجموع total = total + A[i]; } average = total / A.length; // A.length هي عدد العناصر مِثال آخر هو محاولة إِيجاد أكبر عَدَد بالمصفوفة A من المثال السابق. سنُنشِئ مُتَغيِّر max، بحيث نُخزِن فيه قيمة أكبر عَدَد مرَّرنا به حتى الآن، ثم سنبدأ بالمرور على جميع عناصر المصفوفة، وأينما وَجدنا عَدَد أكبر من قيمة المُتَغيِّر max الحالية، فإننا سنُسنِد ذلك العَدَد إلى المُتَغيِّر max. بعد الانتهاء من معالجة المصفوفة بالكامل، سيَحتوِي المُتَغيِّر max حتمًا على أكبر عدد داخل المصفوفة ككل. ولكن يَبقى السؤال التالي، ما هي القيمة الأوَّليّة للمُتَغيِّر max؟ ربما ببساطة نَستخدِم قيمة أول عنصر بالمصفوفة، أي A[0]، للتهيئة المبدئية للمُتَغيِّر max، ومِنْ ثَمَّ نبدأ عملية البحث عن قيمة أكبر منها بباقي عناصر المصفوفة، أي بدءً من العنصر A[1]: double max; // اكبر عدد حتى الآن max = A[0]; // في البداية، أكبر عدد هو قيمة العنصر A[0] int i; for ( i = 1; i < A.length; i++ ) { if (A[i] > max) { max = A[i]; } } // بالوصول إلى تلك النقطة، يحتوي max على قيمة أكبر عدد قد تَحتاج أحيانًا إلى معالجة بعض عناصر المصفوفة (elements of the array) وليس كلها. تُستخدَم تَعْليمَة if، في هذه الحالة، بداخل حَلْقة التَكْرار for لتَحْدِيد ما إذا كُنت تُريد معالجة العنصر الحالي أم لا. دعنا نُلقِي نظرة أخرى على مسألة حِسَاب قيمة متوسط (average) عناصر مصفوفة معينة، ولكن في هذه المرة، لنفْترِض أننا نُريد حِسَاب قيمة المتوسط فقط للعناصر غَيْر الصفرية (non-zero elements)، أيّ التي لا تحتوي على القيمة صفر. في هذه الحالة، قد يكون عدد تلك العناصر أقل من طول المصفوفة (length)، ولهذا سنحتاج إلى عدّ العناصر غَيْر الصفرية، والتي أُضيفت فعليًا لحاصل المجموع بدلًا من الاعتماد على طول المصفوفة. انظر الشيفرة التالية: double total; // حاصل مجموع الأعداد غير الصفرية بالمصفوفة int count; // عدد الأعداد غير الصفرية double average; // متوسط الأعداد غير الصفرية int i; total = 0; count = 0; for ( i = 0; i < A.length; i++ ) { if ( A[i] != 0 ) { total = total + A[i]; // أضف قيمة العنصر إلى حاصل المجموع count = count + 1; // أزد قيمة العداد } } if (count == 0) { System.out.println("There were no non-zero elements."); } else { average = total / count; // اِقسم حاصل المجموع على عدد العناصر System.out.printf("Average of %d elements is %1.5g%n", count, average); } الجلب العشوائي (Random Access) اِستخدَمت جميع أمثلة معالجة المصفوفات -التي فَحْصناها حتى الآن- الجَلْب المُتتالي (sequential access)، أي عُولَجت عناصر المصفوفة (elements of the array) بنفس ترتيب حُدُوثها بالمصفوفة واحدًا تلو الآخر. مع ذلك، يُعدّ الجَلْب العشوائي (random access) واحدًا من أهم مميزات المصفوفات، حيث تستطيع الولوج لقيمة أيّ عنصر بالمصفوفة بأيّ وقت وبنفس الكفاءة وعلى قَدَم المُساواة. دعنا نُلقِي نظرة على إِحدى المسائل المشهورة والمعروفة باِسم مُعْضِلة يوم الميلاد (birthday problem). بفَرْض وجود مجموعة من الأشخاص داخل حجرة، وليَكُن عَدَدهم هو N، فما هي احتمالية أن يكون لاثنين من هؤلاء الأشخاص نفس يوم الميلاد؟ (بمعنى أنهما قد وُلدَا بنفس اليوم، والشهر، ولكن ليس ضروريًا ولادتهما بنفس العام). يُقَلِّل غالبية الناس من شأن هذه الاحتمالية بشدة. سنَفْحَص، في الواقع، نسخة أخرى شبيهة من نفس السؤال: بفَرْض اختيارك لمجموعة أشخاص بشكل عشوائي، بحيث تَفْحَص يوم ميلادهم، فكم عَدَد الأشخاص الذين ستحتاج إلى فَحْص يوم ميلادهم قبلما تَجِد اثنين لهما نفس يوم الميلاد؟ تَعتمِد الإجابة على مِثل هذا السؤال على عدة عوامل عشوائية بالتأكيد، ولكن مع ذلك، يُمكِننا مُحاكاة هذه التجربة باِستخدَام برنامج، بحيث نُنْفِّذه عدة مرات، وهو ما سيُعْطِينا مُؤِشرًا تقريبيًا لعدد هؤلاء الأشخاص. لمُحاكاة هذه التجربة، سنحتاج إلى معرفة جميع أيام الميلاد التي قد وَجدناها حتى الآن. لمّا كان هناك ٣٦٥ يوم ميلاد مُحتمَل (سنتجاهل الأعوام الكبيسة [leap years])، فإننا سنُنشِئ مصفوفة قيم منطقية (array of boolean) طولها (length) يُساوِي ٣٦٥، بحيث يُناظِر كل عنصر فيها يوم ميلاد مُختلِف، ويُعْلِمنا إذا ما كُنا قد وَجدنا شخصًا بنفس يوم الميلاد المُناظِر للعنصر أم لا. اُنظر الشيفرة التالية: boolean[] used; used = new boolean[365]; بدايةً، ستُرقَّم أيام العام من ٠ إلى ٣٦٤، بحيث يُحدِّد المُتَغيِّر used -من النوع المنطقي- ما إذا سبق وأن وَجدنا شخصًا يوم ميلاده هو رقم اليوم (day number) i أم لا، مما يَعنِي أنه سيَحمِل القيمة المنطقية true في تلك الحالة. مبدئيًا، ستكون جميع قيم عناصر المصفوفة used مُساوِية للقيمة المنطقية false، وهو في الواقع ما يَحدُث أتوماتيكيًا عند إِنشاء المصفوفة. عند اختيارنا لشخص ما، يوم ميلاده هو رقم اليوم i، فإننا سنَفْحَص أولًا قيمة المُتَغيِّر used، فإذا كانت مُساوِية للقيمة المنطقية true، سيَعنِي ذلك أننا قد وَجدنا الشخص الثاني بنفس يوم الميلاد، ومِنْ ثَمَّ فإننا قد انتهينا. أمّا إذا كانت قيمته مُساوِية للقيمة المنطقية false، فسنَضبُط قيمة المُتَغيِّر used إلى القيمة المنطقية true، للإشارة إلى كَوْننا قد وجدنا الشخص الأول بيوم الميلاد ذاك. بَعْد ذلك، سنستمر بتَّنْفيذ البرنامج، فنختار الشخص التالي. اُنظر البرنامج بالأسفل (لاحِظ أننا لم نُحاكِي الأشخاص، فقط أيام الميلاد): public class BirthdayProblem { public static void main(String[] args) { // لتخزين أيام الميلاد التي وجدنا أشخاص ولدوا بها boolean[] used; // عدد الأشخاص الذين تم فحص أيام ميلادهم int count; // القيمة المبدئية لجميع العناصر هي false used = new boolean[365]; count = 0; while (true) { // اختر يوم ميلاد بصورة عشوائية من صفر وحتى 364 int birthday; // يوم الميلاد المختار birthday = (int)(Math.random()*365); count++; System.out.printf("Person %d has birthday number %d%n", count, birthday); if ( used[birthday] ) { // وجدنا يوم الميلاد هذا من قبل، انتهينا break; } used[birthday] = true; } // نهاية while System.out.println(); System.out.println("A duplicate birthday was found after " + count + " tries."); } } // نهاية الصنف BirthdayProblem ينبغي عليك أن تَقضِي بعض الوقت لدراسة البرنامج بالأعلى؛ وذلك حتى تَفهَم طريقة عمله، وطريقة اِستخدَامه للمصفوفة. شَغِّله أيضًا! ستكتشف أن احتمالية تَكْرار أيام الميلاد ربما هي أكبر مما كنت تَتوقَّع. المصفوفات الممتلئة جزئيًا (Partially Full) لنفْترِض أن لدينا تطبيقًا، بحيث يتغَيَّر -أثناء تَّنْفيذه- عَدَد العناصر المطلوب تخزينها بمصفوفة ما. لمّا كان من غَيْر المُمكن تَغْيِير طول/حجم (length/size) المصفوفة، كان لابُدّ لنا من اِستخدَام مُتَغيِّر مُنفصل لعدّ المَواضِع المُستخدَمة فعليًا بالمصفوفة. (تَحتوِي بالطبع جميع المَواضِع بأيّ مصفوفة على قيمة ما، ولكن ما يُهِمّنا هو عَدَد المَواضِع التي تَحتوِي على عناصر صالحة ومفيدة). على سبيل المثال، يَقرأ برنامج ما الأعداد الصحيحة الموجبة (positive integers)، المُدْخَلة مِنْ قِبَل المُستخدِم، بحيث يَتوقَف البرنامج عن القراءة عند إِدْخَال عدد أقل من أو يُساوِي الصفر. يحتاج البرنامج لتَخْزِين الأعداد المُدْخَلة بهدف مُعالجتها لاحقًا (later processing)، ولذلك فإنه يَحتفِظ بها داخل مصفوفة من النوع int[]، هي المصفوفة numbers. بفَرْض أنه لن يتمّ إِدْخَال أكثر من ١٠٠ عدد، فإن حجم (length/size) تلك المصفوفة سيكون ١٠٠. نحتاج الآن إلى الإجابة على السؤال التالي: عند إضافة قيمة عنصر (member) جديد بالمصفوفة، بأيّ مَوْضِع سنضعه تحديدًا؟ للإجابة على هذا السؤال، سنحتاج إلى معرفة عَدَد الأعداد التي تمّ فعليًا قرائتها وتَخْزِينها بالمصفوفة، أيّ عَدَد المَواضِع المُستخدَمة فعليًا بالمصفوفة، ولهذا سنَستخدِم مُتَغيِّر من النوع العددي (integer)، وليَكُن المُتَغيِّر count. سيَعمَل هذا المُتَغيِّر كعَدَّاد (counter)، أيّ أننا سنزيد قيمة هذا المُتَغيِّر بمقدار الواحد في كل مرة نُخزِّن فيها عدد جديد بالمصفوفة. الآن، لمّا كان العدد الفعليّ للعناصر يُساوي قيمة المُتَغيِّر count، فلابُدّ أن تلك العناصر مُخزَّنة بالأرقام المَوْضِعية (position numbers) ٠، ١، …، وحتى count - 1، ولذلك فإن رقم المَوْضِع (position number) المتاح التالي هو قيمة المُتَغيِّر count. وعليه، فإن هذا هو المَوْضِع الذي سنُخزِّن فيه العنصر الجديد. مثال آخر هو برنامج يَقرأ الأعداد المُدْخَلة من قِبَل المُستخدِم، وبحيث يَتوقَف عن القراءة عند إِدْخَال عدد يُساوِي الصفر. الهدف من البرنامج هو طباعة تلك الأعداد بترتيب مُعاكِس (reverse order) للترتيب الأصلي الذي أُدْخلت به. قد يبدو هذا المثال سخيفًا نوعًا ما، ولكنه على الأقل يَتطلَّب أن تكون الأعداد مُخزَّنة بمصفوفة، بعكس أنواع آخرى كثيرة من المعالَجات، والتي يُمكِن إِجراؤها دون الحاجة إلى الاِحتفاظ بقيم الأعداد المُفردة (individual numbers) بذاتها، مثل إِيجاد حاصل مجموع عناصر المصفوفة أو قيمة متوسط تلك العناصر أو قيمة أكبر عدد بها. import textio.TextIO; public class ReverseInputNumbers { public static void main(String[] args) { int[] numbers; // مصفوفة لتخزين القيم المدخلة int count; // عدد الأعداد المخزنة فعليًا بالمصفوفة int num; // أحد الأعداد المدخلة من قبل المستخدم int i; // متغير حلقة for // مصفوفة أعداد صحيحة بطول 100 numbers = new int[100]; // لم يتم إدخال أي أعداد بعد count = 0; System.out.println("Enter up to 100 positive integers; enter 0 to end."); // اقرأ الأعداد وأضفها إلى المصفوفة while (true) { System.out.print("? "); num = TextIO.getlnInt(); // الصفر هو إشارة لانتهاء عملية الإدخال if (num <= 0) { break; } numbers[count] = num; // خزن العدد برقم الموضع count count++; // أزد العداد بمقدار واحد } System.out.println("\nYour numbers in reverse order are:\n"); for ( i = count - 1; i >= 0; i-- ) { System.out.println( numbers[i] ); } } // نهاية main(); } // نهاية الصنف ReverseInputNumbers مِنْ المُهم أن تُدرِك الدور المزدوج الذي يؤديه المُتَغيِّر count. فبالإضافة إلى كَوْنه يَحمِل عدد العناصر التي أُدْخلت فعليًا إلى المصفوفة، فإنه أيضًا يُمثِل فهرس (index) المَوْضِع التالي المُتاح بالمصفوفة. عندما يَحيِن موعد طباعة الأعداد الموجودة بالمصفوفة، يكون رقم المَوْضِع الأخير المُستخدَم فعليًا بالمصفوفة count - 1، ولذا تَطبَع حَلْقة التَكْرار for قيم عناصر المصفوفة بداية من رقم المَوْضِع count - 1، ونزولًا إلى المَوْضِع صفر. يُعدّ هذا مثالًا جيدًا لمعالجة عناصر مصفوفة ما بترتيب مُعاكِس (reverse order). قد تتساءل، ماذا سيَحدُث بالبرنامج إذا حاول المُستخدِم إِدْخَال أكثر من ١٠٠ عدد؟ ستكون النتيجة حُدوث خطأ (error) يَتسبَّب بانهيار (crash) البرنامج؛ فعندما يُدْخِل المُستخدِم العدد رقم ١٠١، سيُحاول البرنامج تَخْزِين ذلك العدد بعنصر مصفوفة number[100]. لكن، في الواقع، هذا العنصر غَيْر موجود؛ حيث يُوجد فقط ١٠٠ عنصر بالمصفوفة، وفهرس آخر عنصر بها هو ٩٩، ولذلك ستؤدي محاولة اِستخدَام number[100] إلى حُدوث اِعتراض (exception) من النوع ArrayIndexOutOfBoundsException. تُعدّ الاعتراضات من هذا النوع مصدرًا شائعًا لحُدوث أخطاء وقت التَّنْفيذ (run-time errors) بالبرامج التي تَستخدِم المصفوفات. المصفوفات ثنائية البعد (Two-dimensional) تُعدّ المصفوفات التي تَعامَلنا معها حتى الآن أحادية البعد (one-dimensional)، مما يَعنِي أن المصفوفة تتكون من متتالية من العناصر، والتي يُمكِن تَخَيُّلها وكأنها مَوْضوعة على خط (line). تَتوفَّر أيضًا المصفوفات ثنائية البعد (two-dimensional)، والتي تُوضَع فيها العناصر داخل شبكة مستطيلة الشكل (rectangular grid). سنمر على هذا الموضوع باختصار هنا، ونعود إليه مُجددًا بالقسم ٧.٥. تُرتَّب عناصر المصفوفة ثنائية البعد (2D/two-dimensional array) بصورة صفوف (rows) وأعمدة (columns). على سبيل المثال، تتكون مصفوفة الأعداد الصحيحة ثنائية البعد (2D array of int) التالية من ٥ صفوف و ٧ أعمدة: تتكون الشبكة ٥*٧ بالأعلى من ٣٥ عنصر. تُرقَّم الصفوف بالمصفوفة ثنائية البعد كالتالي: ٠، ١، ٢، …، وحتى عدد الصفوف ناقص واحد. بالمثل، تُرقَّم العواميد من ٠ وحتى عدد العواميد ناقص واحد. يُمكِن الولوج لأيّ عنصر مُفرد (individual element) بالمصفوفة من خلال رقمي الصف (row number) والعمود (column number) خاصته. (لا تبدو المصفوفة بذاكرة الحاسوب كالصورة المَعروضة بالأعلى، فهي مُجرَّد توضيح للبناء المنطقي [logical structure] للمصفوفة) تُشبه صيغة (syntax) المصفوفات ثنائية البعد (two-dimensional arrays) بلغة الجافا نفس تلك الصيغة المُستخدَمة مع المصفوفات أحادية البعد (one-dimensional arrays)، باستثناء وجود فهرس (index) إِضافي؛ لأن الولوج لأيّ عنصر أَصبح يَتطلَّب كُلًا من رقمي الصف (row number) والعمود (column number). على سبيل المثال، إذا كانت A مصفوفة أعداد صحيحة (array of int) ثنائية البعد، فسيُشيِر A[3][2] إلى العنصر الموجود بالصف رقم ٣ والعمود رقم ٢، والذي يَحمِل العدد ١٧، كما هو موضح بالمصفوفة بالأعلى. كذلك، سيكون نوع مُتَغيِّر المصفوفة، في هذه الحالة، هو كلمة int متبوعة بزوجين من الأقواس المعقوفة (square brackets) الفارغة، أيّ int[][]. يمكنك كتابة التالي للتَّصْريح (declare) عن مُتَغيِّر مصفوفة (array variable)، بالإضافة إلى إِنشاء تلك المصفوفة: int[][] A; A = new int[5][7]; يُنشِئ السطر الثاني مصفوفة ثنائية البعد (2D array) مُكوَّنة من ٥ صفوف و ٧ أعمدة. غالبًا ما تُستخدَم حَلْقات التَكْرار for المُتداخِلة (nested) لمعالجة المصفوفات ثنائية البعد. على سبيل المثال، تَطبَع الشيفرة التالية عناصر المصفوفة A: int row, col; // متغيرات التحكم بالحلقة احدهما للصف والآخر للعمود for ( row = 0; row < 5; row++ ) { for ( col = 0; col < 7; col++ ) { System.out.printf( "%7d", A[row][col] ); } System.out.println(); } يُمكِن أن يكون النوع الأساسي (base type) لمصفوفة ثنائية البعد أيّ شئ، أيّ أنك تستطيع إِنشاء مصفوفات ثنائية البعد من النوع العددي double، أو النوع String، وهكذا. توجد عدة اِستخدَامات طبيعية للمصفوفات ثنائية البعد (2D arrays)، والتي تَكُون فيها الشبكة (grid) واضحة مرئيًا. فمثلًا، قد تُخزِن مصفوفة ثنائية البعد محتويات اللوحة (board) بألعاب مثل الشطرنج أو الدَّامَا (checkers). يُوظِّف مثال آخر، بالقسم الفرعي ٤.٧.٣، مصفوفة ثنائية البعد لحفظ ألوان شبكة (grid) مُكوَّنة من مربعات ملونة. يُمكِنك أيضًا تَوظِيف المصفوفات ثنائية البعد بمسائل لا تكون فيها الشبكة واضحة مرئيًا. فمثلًا، لنفْترِض وجود شركة تَملك ٢٥ مَخزنًا. تَحتفِظ الشركة ببيانات الأرباح التي كَسَبَها كل مَخزن شهريًا طوال عام ٢٠١٨. إذا كانت المخازن مُرقَّمة من ٠ إلى ٢٤، وكانت الشهور الاثنى عشر، أي من يناير ٢٠١٨ وحتى ديسمبر ٢٠١٨، مُرقَّمة من ٠ إلى ١١، فمِنْ ثَمَّ، يُمكِن تَخْزِين هذه البيانات بمصفوفة تَحمِل اسم profit، وتُنشَئ كالتالي: double[][] profit; profit = new double[25][12]; يُمثِل المُتَغيِّر profit[3][2] قيمة أرباح المَخزن رقم ٣ بشهر مارس. بصورة أعم، يُمثِل المُتَغيِّر profit[storeNum][monthNum] قيمة أرباح المَخزن رقم storeNum بالشهر رقم monthNum (تذكر أن الترقيم يبدأ من صفر.) لنفْترِض أن مصفوفة الأرباح profit ممتلئة بالبيانات بالفعل. وعليه، يمكن معالجة هذه البيانات بطرائق كثيرة شيقة. على سبيل المثال، يُمكِن حِسَاب الأرباح الكلية للشركة -جميع المَخازن طوال عام ٢٠١٨- بحِسَاب قيمة حاصل مجموع جميع عناصر المصفوفة، كالتالي: double totalProfit; // الأرباح الكلية للشركة بعام 2018 int store, month; // متغيرات حَلقتي التكرار totalProfit = 0; for ( store = 0; store < 25; store++ ) { for ( month = 0; month < 12; month++ ) totalProfit += profit[store][month]; } تحتاج أحيانًا إلى معالجة صف وحيد أو عمود وحيد بالمصفوفة، وليس المصفوفة بكاملها. على سبيل المثال، تستطيع اِستخدَام حَلْقة التَكْرار التالية لحِسَاب قيمة الأرباح الكلية للشركة بشهر ديسمبر، أيّ بالشهر رقم ١١: double decemberProfit; int storeNum; decemberProfit = 0.0; for ( storeNum = 0; storeNum < 25; storeNum++ ) { decemberProfit += profit[storeNum][11]; } قد تجد أن المصفوفات ثنائية البعد (two-dimensional array) مفيدة في بعض الأحيان، ولكنها عمومًا أقل شيوعًا من المصفوفات أحادية البعد (one-dimensional). وفي الواقع، تَسمَح لغة الجافا كذلك بمصفوفات ذات أبعاد أعلى (higher dimension)، ولكن يَنْدُر اِستخدَامها عمليًا. ترجمة -بتصرّف- للقسم Section 8: Introduction to Arrays من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.
-
بالإضافة إلى بُنَى التحكُّم control structures المُستخدَمة لتَحْدِيد مَسار التحكُّم الطبيعي flow of control بالبرنامج، تُوفِّر لغة الجافا طريقة للتَعامُل مع الحالات الاستثناءية exceptional cases، والتي يُمكِنها تَغْيِير مَسار التحكُّم الطبيعي. فمثلًا، يُعدّ إنهاء terminate البرنامج وما يَتبعه من طباعة رسالة الخطأ هو مَسار التحكُّم الافتراضي في حالة حُدُوث خطأ أثناء التَّنْفيذ، مع ذلك تَسمَح لغة الجافا بالتقاط catch مثل تلك الأخطاء بصورة تَمنع اِنهيار البرنامج crashing، وبحيث يَتمكَّن المُبرمِج من الرد بصورة ملائمة على الخطأ. تُستخدَم تَعْليمَة try..catch لهذا الغرض، والتي سنكتفي بإلقاء نظرة مَبدئية وغَيْر مُكتملة عليها بهذا القسم؛ ففي الواقع يُعدّ موضوع مُعالجة الأخطاء error handling موضوعًا مُعقدًا نوعًا ما، ولذلك سنتناوله تفصيليًا بالقسم ٨.٣، وعندها سنَسْتَعْرِض التَعْليمَة try..catch بمزيد من التفصيل، ونُوضِح قواعد صياغتها syntax كاملة. الاستثناءات يُشير مصطلح الاستثناء exception إلى تلك النوعية من الأحَدََاث التي قد تَرغَب عادةً بمُعالجتها عن طريق تَعْليمَة try..catch. يُفضَّل اِستخدَام هذا المصطلح بدلًا من كلمات مثل خطأ error؛ وذلك لأن الاستثناء في بعض الحالات قد لا يُمثِل خَطأ مِنْ الأساس. ربما يُمكِنك التفكير بالاستثناء exception بعدّه اِستثناءًا بمَسار التحكُّم flow of control الطبيعي للبرنامج، أيّ أنه مُجرَّد وسيلة أخرى لتنظيم البرنامج. تُمثَل الاستثناءات بلغة الجافا باِستخدَام كائنات objects من الصَنْف Exception. تُعرَّف definition عادةً أصناف فرعية subclasses مُشتقَّة من الصَنْف Exception؛ لتمثيل الاستثناءات الفعليّة، بحيث يُمثِل كل صَنْف فرعي subclass نوعًا مختلفًا من الاستثناءات. سنفْحَص بهذا القسم نوعين فقط من الاستثناءات، هما: NumberFormatException و IllegalArgumentException. يُمكِن أن يَحدُث الاستثناء NumberFormatException أثناء محاولة تَحْوِيل سِلسِلة نصية string إلى عَدَد number. تُستخدَم الدالتين Integer.parseInt و Double.parseDouble لإِجراء مثل تلك التَحْوِيلات (انُظر القسم الفرعي ٢.٥.٧). فمثلًا، تَستقبِل الدالة Integer.parseInt(str) مُعامِلًا parameter من النوع String، فإذا كانت قيمة المُتَغيِّر str -المُمرَّرة مثل قيمة لهذا المُعامِل- تُساوِي السِلسِلة النصية "٤٢"، فعندها ستتمكن الدالة من تَحْوِيلها إلى القيمة ٤٢ من النوع int تَحْوِيلًا صحيحًا. في المقابل، إذا كانت قيمة المُتَغيِّر str تُساوِي السِلسِلة النصية fred، فعندها ستَفشَل الدالة؛ لأن تلك السِلسِلة لا تُعدّ تَمثيلًا نصيًا string representation صالحًا لأيّ قيمة ممكنة من النوع العددي int، لذا سيَحدُث استثناء من النوع NumberFormatException في هذه الحالة، وسينهار crash البرنامج إذا لم يُعالَج handle هذا الاستثناء. يُمكِن أن يَحدُث الاستثناء IllegalArgumentException عندما تُمرِّر قيمة غَيْر صالحة كمُعامِل parameter إلى برنامج فرعي subroutine. على سبيل المثال، إذا مَرَّرت قيمة سالبة negative كمُعامِل إلى برنامج فرعي، وكان هذا البرنامج الفرعي يَتطلَّب أن تكون قيمة ذلك المُعامِل أكبر من أو تُساوِي الصفر، فمِنْ المُحتمَل أن يَحدُث استثناء من النوع IllegalArgumentException. على الرغم من شيوع حُدُوث ذلك الاستثناء في مثل تلك الحالات، فما يزال لا يُمكننا الجَزْم بحُدُوثه في كل مرة ستُمرِّر فيها قيمة غَيْر صالحة كمُعامِل لبرنامج فرعي؛ ففي الواقع تَتوقَف طريقة التَعامُل مع القيم غَيْر الصالحة على الشخص الذي كَتَب البرنامج الفرعي. تعليمة try..catch عندما يَحدُث استثناء exception، يُقَال أنه قد بُلِّغ thrown عنه. فعلى سبيل المثال، يُبلِّغ Integer.parseInt(str) عن استثناء من النوع NumberFormatException إذا كانت قيمة المُتَغيِّر str المُمرَّرة إليه غَيْر صالحة. تُستخدَم تَعْليمَة try..catch لالتقاط catch الاستثناءات، ومَنعها من التَسبُّب بانهيار crashing البرنامج. تُكتَب هذه التَعْليمَة بالصياغة syntax التالية في أبسط صورها: try { <statements-1> } catch ( <exception-class-name> <variable-name> ) { <statements-2> } قد يُشير الصَنْف -بالأعلى- إلى الصَنْف NumberFormatException أو IllegalArgumentException أو أيّ صَنْف class آخر طالما كان من النوع Exception. عندما يُنفِّذ الحاسوب تَعْليمَة try..catch، فإنه سيبدأ بتَّنْفيذ التَعْليمَات الموجودة داخل الجزء try -والمُشار إليها بالتعليمة -، فإذا لم يَحدُث استثناء أثناء تَّنْفيذها، فإنه سيتَخَطَّى الجزء catch ليَمضِي قُدُمًا لتَّنْفيذ بقية البرنامج. أما إذا حَدَث استثناء من النوع المُحدَّد وِفقًا للعبارة أثناء التَّنْفيذ، فإنه سيَقفِز مُباشرةً من النقطة التي حَدَث فيها الاستثناء إلى الجزء catch ليُنفِّذ العبارة ، مُتَخطِّيًا بذلك أيّ تَعْليمَات مُتبقِّية ضِمْن العبارة داخل الجزء try. لاحِظ أن تَعْليمَة try..catch بالأعلى ستَلتقِط نوعًا واحدًا فقط من الاستثناءات -النوع المُحدَّد وِفقًا للعبارة -، أما إذا حَدَث استثناء من أي نوع آخر، فسينهار crash البرنامج كالعادة. تُمثِل عبارة كائنًا من نوع الاستثناء المُلتقَط exception object، والذي يَتضمَّن معلومات عن سبب حُدُوث الاستثناء بما في ذلك رسالة الخطأ error message. تستطيع طباعة قيمة هذا الكائن object، على سبيل المثال، أثناء تَّنْفيذ العبارة ، مما سيترتب عليه طباعة رسالة الخطأ. سيَمضِي الحاسوب لتَّنْفيذ بقية البرنامج بَعْد انتهاءه من تَّنْفيذ الجزء catch؛ فقد اُلتقَط catching الاستثناء، وعُولَج handling، وعليه لم يَتَسبَّب بانهيار البرنامج. يُعدّ القوسين { و } المُستخدَمين ضِمْن تَعْليمَة try..catch جزءً أساسيًا من صياغة syntax التَعْليمَة، حتى في حالة اِحتوائها على تَعْليمَة واحدة، وهو ما يختلف عن بقية التَعْليمَات الآخرى التي مَررنا بها حتى الآن، والتي يكون فيها اِستخدَام الأقواس أمرًا اختياريًا في حالة التَعْليمَات المُفردة single statement. بفَرْض أن لدينا مُتَغيِّر str من النوع String، يُحتمَل أن تكون قيمته مُمثِلة لعَدَد حقيقي real صالح، اُنظر الشيفرة التالية: (real)double x; try { x = Double.parseDouble(str); System.out.println( "The number is " + x ); } catch ( NumberFormatException e ) { System.out.println( "Not a legal number." ); x = Double.NaN; } إذا بَلَّغ استدعاء call الدالة Double.parseDouble(str) عن خطأ، فستُنفَّذ التَعْليمَات الموجودة بالجزء catch مع تَخَطِّي تَعْليمَة الخَرْج (output) بالجزء try. يُعالَج الاستثناء، في هذا المثال، بإِسْناد القيمة Double.NaN إلى المُتَغيِّر x. تُشير هذه القيمة إلى عدم حَمْل مُتَغيِّر من النوع double لقيمة عَدَدية. لا تَحتاج دائمًا إلى اِلتقاط الاستثناءات catch exceptions، والاستمرار بتَّنْفيذ البرنامج. على العكس، فقد يُؤدي ذلك أحيانًا إلى حُدُوث فوضى أكبر فيما بَعْد، ولربما يَكُون عندها من الأفضل السماح ببساطة بانهيار crash البرنامج. مع ذلك، يَكُون التَعافِي من أخطاء معينة مُمكنًا في أحيان آخرى. لنفْترِض مثلًا أنك تَرغَب بكتابة برنامج يَحسِب متوسط average متتالية من الأعداد الحقيقية real numbers، التي يُدْخِلها المُستخدِم، وبحيث يُمكِنه الإشارة إلى نهاية المتتالية عن طريق إِدْخَال سَطْر فارغ. في الواقع، يُشبه هذا البرنامج المثال الذي تَعرَّضنا له بالقسم ٣.٣، مع الفارق في اِستخدَام القيمة صفر للإشارة إلى انتهاء المتتالية. قد تُفكِر باِستخدَام الدالة function TextIO.getlnInt() لقراءة دَخْل المُستخدِم input. ولكن لمّا كانت تلك الدالة تَتَخَطَّى الأسطر الفارغة، فإننا ببساطة لن نتَمكَّنْ من تَحْدِيد السَطْر الفارغ، ولذلك سنَستخدِم بدلًا منها الدالة TextIO.getln()، مما سيُمكِّننا من تَحْدِيد السَطْر الفارغ عند إِدْخاله. سنَستخدِم أيضًا الدالة Double.parseDouble لتَحْوِيل الدَخْل -إذا لم يكن سَطْرًا فارغًا- إلى عَدَد حقيقي، وسنستدعيها ضِمْن تَعْليمَة try..catch؛ لتَجَنُّب انهيار البرنامج في حالة إِدْخَال المُستخدِِم عددًا غَيْر صالح. اُنظر شيفرة البرنامج: import textio.TextIO; public class ComputeAverage2 { public static void main(String[] args) { String str; // مدخل المستخدم double number; // مدخل المستخدم بعد تحويله إلى عدد double total; // حاصل مجموع الأعداد المدخلة double avg; // متوسط الأعداد المدخلة int count; // عدد الأعداد المدخلة total = 0; count = 0; System.out.println("Enter your numbers, press return to end."); while (true) { System.out.print("? "); str = TextIO.getln(); if (str.equals("")) { break; // اخرج من الحلقة لأن المستخدم أدخل سطر فارغ } try { number = Double.parseDouble(str); // إذا حدث خطأ، سيتم تخطي السطرين التاليين total = total + number; count = count + 1; } catch (NumberFormatException e) { System.out.println("Not a legal number! Try again."); } } avg = total/count; System.out.printf("The average of %d numbers is %1.6g%n", count, avg); } } استثناءات الصنف TextIO يستطيع الصَنْف TextIO قراءة البيانات من عدة مصادر. (انظر القسم الفرعي ٢.٤.٤). فمثلًا، عندما يُحاوِل قراءة قيمة عددية مُدْخَلة من قِبَل المُستخدِم، فإنه يتحقَّق من كون دَخْل المُستخدِم صالحًا، وذلك باِستخدَام طريقة شبيهة للمثال السابق، أيّ عن طريق اِستخدَام حَلْقة التَكْرار while ضِمْن تَعْليمَة try..catch، وبالتالي لا يُبلِّغ عن استثناء. في المقابل، عندما يُحاوِل القراءة من ملف، فلا توجد طريقة واضحة للتَعَافِي من وجود قيمة غَيْر صالحة بالدَخْل input، ولهذا فإنه يُبلِّغ عن استثناء. يُبلِّغ الصَنْف TextIO -بغرض التبسيط- عن استثناءات من النوع IllegalArgumentException فقط، وذلك بِغَضّ النظر عن نوع الخطأ الفعليّ الذي قد وَاجهه الصَنْف. فمثلًا، إذا حَاولت قراءة ملف تمَّت قراءة جميع محتوياته بالفعل، فسيَحدُث استثناء من الصَنْف IllegalArgumentException. إذا كان لديك رؤية أفضل لكيفية معالجة أخطاء الملفات غَيْر السماح للبرنامج بالانهيار crash، اِستخدِم تَعْليمَة try..catch لالتقاط الاستثناءات من النوع IllegalArgumentException. لنفْحَص البرنامج التالي لحِسَاب قيمة متوسط average مجموعة من الأعداد. في هذا المثال، وبفَرْض وجود ملف يَحتوِي على أعداد حقيقية real numbers فقط، فإن البرنامج سيَقرأ قيم الأعداد من ذلك الملف تباعًا، ويَحسِب كُلًا من حاصل مجموعها ومتوسطها. لمّا كان عَدَد الأعداد الموجودة بالملف غَيْر مَعلوم، فإننا بحاجة إلى معرفة إلى متى سنستمر بالقراءة. أحد الحلول هو أن نستمر حتى نَصِل إلى نهاية الملف، وعندها سيَحدُث استثناء، والذي لا يُمكِن عدّه في تلك الحالة خطأً فعليًا، وإنما هو فقط الطريقة المُتَّبَعة للإشارة إلى عدم وجود بيانات آخرى بالملف، وعليه، يُمكِننا التقاط هذا الاستثناء وإنهاء البرنامج. بعبارة آخرى، سنَقرأ البيانات داخل حَلْقة تَكْرار while لا نهائية، ونَخْرُج منها فقط عند حُدوث استثناء. يُوظِّف هذا المثال الاستثناءات بعدّها جزءً متوقعًا من مَسار التحكُّم flow of control بالبرنامج، وهو ما قد يُمثِل طريقة غَيْر اعتيادية نوعًا ما لاِستخدَام الاستثناءات. سنحتاج إلى معرفة اسم الملف حتى نَتَمكَّن من قراءته. سنَسمَح في الواقع للمُستخدِم بإِدْخَال اسم الملف، بدلًا من تَوْفِيره بالشيفرة hard-coding كقيمة ثابتة؛ وذلك بهدف تَعميم البرنامج. لكن قد يُدخِل المُستخدِم اسم ملف غَيْر موجود أساسًا. وعليه، سيُبلَّغ عن استثناء من النوع IllegalArgumentException عندما نَستخدِم الدالة TextIO.readfile لمحاولة فتح ذلك الملف. يُمكِننا أن نَلتقِط catch هذا الاستثناء، ثُمَّ نَطلُب من المُستخدِم إِدْخَال اسم ملف آخر صالح. اُنظر الشيفرة التالية: import textio.TextIO; public class AverageNumbersFromFile { public static void main(String[] args) { while (true) { String fileName; // اسم الملف المدخل من قبل المستخدم System.out.print("Enter the name of the file: "); fileName = TextIO.getln(); try { TextIO.readFile( fileName ); // حاول فتح الملف break; // إذا نجحت عملية الفتح، أخرج من الحلقة } catch ( IllegalArgumentException e ) { System.out.println("Can't read from the file \"" + fileName + "\"."); System.out.println("Please try again.\n"); } } // الصنف TextIO سيقرأ الملف double number; // عدد مقروء من الملف double sum; // حاصل مجموع الأعداد المقروءة حتى الآن int count; // عدد الأعداد المقروءة حتى الآن sum = 0; count = 0; try { while (true) { // تتوقف الحلقة عند حدوث استثناء number = TextIO.getDouble(); count++; // يتم تخطي هذه العبارة في حالة حدوث استثناء sum += number; } } catch ( IllegalArgumentException e ) { // نتوقع حدوث استثناء عندما نفرغ من قراءة الملف // تم التقاط الاستثناء فقط حتى نمنع انهيار البرنامج // لكن ليس هناك ما نفعله لمعالجة الاستثناء لأنه ليس خطأ أساسا } // تمت قراءة جميع محتويات الملف عند هذه النقطة System.out.println(); System.out.println("Number of data values read: " + count); System.out.println("The sum of the data values: " + sum); if ( count == 0 ) System.out.println("Can't compute an average of 0 values."); else System.out.println("The average of the values: " + (sum/count)); } } ترجمة -بتصرّف- للقسم Section 7: Introduction to Exceptions and try..catch من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.
-
تتوفَّر تَعْليمَتين للتَفْرِيع (branching statements) بلغة الجافا، تناولنا تَعْليمَة if بالقسم السابق، والآن سننتقل إلى تَعْليمَة التَفْرِيع الثانية switch، والتي يُعدّ اِستخدَامها أقل شيوعًا من تَعْليمَة if، ومع ذلك فهي تكون مفيدة أحيانًا للتعبير عن نوع معين من التَفْرِيع المُتعدِّد (multiway branches). تعليمة switch تَفحْص تَعْليمَة switch قيمة تعبير (expression) معين، لتَقفِز بَعْدها مباشرة إلى مكان ما بالتَعْليمَة، يَتقرَّر ذلك المكان وِفقًا للقيمة الناتجة عن التعبير. لا يُمكِنك اِستخدَام أيّ تعبير (expression) مع تَعْليمَة switch؛ حيث يُسمَح بأنواع معينة فقط، منها الأنواع العددية الصحيحة الأوَّليّة (primitive) مثل int و short و byte، في المقابل لا يُسمَح بالأنواع العددية العَشريّة مثل double و float. تستطيع كذلك اِستخدَام تعبيرات من النوع المحرفي الأوَّليّ char والنوع String. وأخيرًا، يُسمَح باِستخدَام التعدادات (enums) أيضًا. اُنظر القسم الفرعي ٢.٣.٤ لقراءة المزيد عن التعدادات (enums). كما ذكرنا مُسْبَّقًا، يَقفِز الحاسوب -أثناء تَّنْفيذه لتَعْليمَة switch- إلى مكان ما ضِمْن التَعْليمَة، لذلك كان لابُدّ من وجود طريقة لتَخْصِيص الأماكن المَسموح بالقفز (نَقْل التَحكُّم بصورة أدق) إليها. تُستخدَم عناوين الحالات (case labels) لهذا الغرض، وتُكتَب بصيغة case constant:، بحيث يكون الثابت (constant) قيمة مُجرَّدة مُصنَّفة النوع (literal)، والتي لابُدّ أن تكون من نفس نوع التعبير المُستخدَم بتَعْليمَة switch. يَعمَل عنوان الحالة (case label) كعلامة للإشارة إلى المكان الذي يَنبغِي للحاسوب القَفز إليه عندما تؤول قيمة التعبير إلى قيمة الثابت المُخصَّصة. يَتوفَّر أيضًا عنوان الحالة الافتراضي default:، والذي يَقفِز الحاسوب إليه إذا لم يَجِد قيمة التعبير ضِمْن قائمة عناوين الحالات (case labels) المُخصَّصة بتَعْليمَة switch. عادة ما تُستخدَم تَعْليمَة switch بالصياغة التالية: switch ( <expression> ) { case <constant-1>: <statements-1> break; case <constant-2>: <statements-2> break; . . // أضف مزيد من الحالات . case <constant-N>: <statements-N> break; default: // حالة افتراضية <statements-(N+1)> } // نهاية تعليمة switch تُطابِق تَعْليمَة switch بالأعلى تَعْليمَة التَفْرِيع المُتعدِّد (multiway branch) if التالية، وتؤدي نفس وظيفتها. تُعدّ تَعْليمَة switch مع ذلك أكثر كفاءة؛ حيث يُقَيِّم الحاسوب -أثناء تَّنْفيذها- تعبيرًا (expression) وحيدًا فقط، ويَقفِز بَعْدها مباشرةً إلى الحالة (case) المَعنيّة. في المقابل، فإنه قد يَضطر -عند مُعالجته لتَعْليمَة if- إلى تَقييم ما قد يَصِل إلى عدد N من التعبيرات، وذلك قَبْل أن يَتمكَن من تَحْدِيد أي من كُتل التَعْليمَات ينبغي له تَّنْفيذها: if ( <expression> == <constant-1> ) { // but use .equals for String!! <statements-1> } else if ( expression == <constant-2> ) { <statements-2> } . . . else if ( <expression> == <constant-N> ) { <statements-N> } else { <statements-(N+1)> } في الواقع، لا تَتطلَّب صياغة (syntax) تَعْليمَة switch اِستخدَام تَعْليمَة break في نهاية كل حالة (case) كالمثال بالأعلى، ولكنك إن تَغافلت عنها -وهو ما يُعدّ سليمًا من ناحية الصياغة-، فسيَمضِي الحاسوب قُدُمًا لتَّنْفيذ التَعْليمَات الخاصة بعنوان الحالة (case label) التالي بعدما ينتهي من تَّنْفيذ تَعْليمَات الحالة الصحيحة. نادرًا ما تكون طريقة التَّنْفيذ تلك هي ما يَنتويه المبرمج، ولذلك اُعتيد اِستخدَام تَعْليمَة break بنهاية كل حالة؛ لإجبار الحاسوب على القفز إلى نهاية تَعْليمَة switch، مُتَخطِّيًا بذلك جميع الحالات الآخرى. ملحوظة قد لا تستطيع استيعابها حتى تَقرأ الفصل التالي: تَحلّ تَعْليمَة return أحيانًا مَحَلّ تَعْليمَات break -المُستخدَمة ضِمْن تَعْليمَة switch- بالبرامج الفرعية (subroutine)، ويؤدي تَّنْفيذها إلى إنهاء كُلًا من تَعْليمَة switch وكذلك البرنامج الفرعي. يُمكِنك تَرك واحدة أو أكثر من عناوين الحالات (case labels) فارغة تمامًا بدون أيّ تَعْليمَات، وحتى بدون تَعْليمَة break، مما سيؤدي إلى وجود متتالية من عناوين الحالات (case label) بثوابت مختلفة. في تلك الحالة، سيَقفِز الحاسوب إلى نفس المكان ويُنفِّذ نفس التَعْليمَات إذا آلت قيمة التعبير إلى أيّ واحدة من تلك الثوابت. ليس هناك أي قواعد لترتيب ثوابت (constants) عناوين الحالات (case label)، ولكن من الضروري ألا تُكرِّر قيمة أيّ ثابت. قد لا يكون المثال التالي مفيدًا بحدّ ذاته، ولكن على الأقل يَسهُل تَتبُّعه بغرض التعلم: switch ( N ) { // بفرض أن N متغير من النوع العددي case 1: System.out.println("The number is 1."); break; case 2: case 4: case 8: System.out.println("The number is 2, 4, or 8."); System.out.println("(That's a power of 2!)"); break; case 3: case 6: case 9: System.out.println("The number is 3, 6, or 9."); System.out.println("(That's a multiple of 3!)"); break; case 5: System.out.println("The number is 5."); break; default: System.out.println("The number is 7 or is outside the range 1 to 9."); } تَستعير لغة الجافا جميع بُنَى التحكُّم (control structures) التي تُدعِّمها من لغتي البرمجة الأقدم: السي C، والسي بلص بلص C++. تُعدّ تَعْليمَة switch بدائية نوعًا ما بالموازنة مع بقية بُنَى التحكُّم (control structures)؛ فمِنْ السهل أن تَقع في العديد من الأخطاء عند اِستخدَامها، ولهذا يَظُنّ الكاتب أنه كان من الضروري لمُصمِّمي لغة الجافا أن يُحسِّنوا من تَعْليمَة switch أثناء تَبَنّيها ضِمْن اللغة. القوائم وتعليمات switch تُعدّ معالجة القوائم (menu) واحدة من أشهر تطبيقات تَعْليمَة switch. ببساطة تُعرَض قائمة (menu) مُكوَّنة من مجموعة من الخيارات (options) -ومُرقَّمَة عادةً على الصورة ١، ٢، …إلخ- على المُستخدِم، والذي يختار واحدة منها، ليتَلقَّى ردًا (response) وفقًا لاختياره. يَعني ذلك أنه لابُدّ للحاسوب من الرد على كل خيار مُحتمَل بطريقة مختلفة، ولهذا يُمكِن اِستعمال رقم الخيار كثابت لعناوين الحالات (case labels) ضِمْن تَعْليمَة switch لتَحْدِيد الرد المناسب. المثال التالي عبارة عن برنامج طرفيّة (command-line program)، وهو في الواقع مُجرَّد نسخة مختلفة من مسألة "تَحْوِيل قياسات الطول" من القسم السابق، حيث يُطلَب من المُستخدِم اختيار وَحدة القياس (unit of measure) التي يَستخدِمها قياس الطول (measurement) المُدْخَل، ولهذا ستُعرَض قائمة (menu) مُرقَّمَة بوَحَدات القياس المتاحة، بحيث يُمكِن للمُستخدِم اختيار إحداها بكتابة رقمها المُناظر كمُدْخَل (input): int optionNumber; // رقم الخيار المختار من قبل المستخدم double measurement; // قياس الطول العددي المعطى من قبل المستخدم double inches; // نفس قياس الطول ولكن بوحدة البوصة. // اعرض قائمة واقرأ رقم الخيار المطلوب System.out.println("What unit of measurement does your input use?"); System.out.println(); System.out.println(" 1. inches"); System.out.println(" 2. feet"); System.out.println(" 3. yards"); System.out.println(" 4. miles"); System.out.println(); System.out.println("Enter the number of your choice: "); optionNumber = TextIO.getlnInt(); // اقرأ قياس الطول المعطى وحوله إلى البوصة switch ( optionNumber ) { case 1: System.out.println("Enter the number of inches: "); measurement = TextIO.getlnDouble(); inches = measurement; break; case 2: System.out.println("Enter the number of feet: "); measurement = TextIO.getlnDouble(); inches = measurement * 12; break; case 3: System.out.println("Enter the number of yards: "); measurement = TextIO.getlnDouble(); inches = measurement * 36; break; case 4: System.out.println("Enter the number of miles: "); measurement = TextIO.getlnDouble(); inches = measurement * 12 * 5280; break; default: System.out.println("Error! Illegal option number! I quit!"); System.exit(1); } // نهاية switch // حول قياس الطول بالبوصة إلى وحدات القياس الأخرى يمكن أيضًا إعادة كتابة المثال السابق بحيث تَستخدَم تَعْليمَة switch تعبيرًا (expression) من النوع String، كالتالي: String units; // وحدة القياس المعطاة من قبل المستخدم double measurement; // قياس الطول العددي المعطى من قبل المستخدم double inches; // نفس قياس الطول ولكن بوحدة البوصة // اقرأ وحدة القياس المعطاة System.out.println("What unit of measurement does your input use?"); System.out.print("Legal responses: inches, feet, yards, or miles : "); units = TextIO.getln().toLowerCase(); // اقرأ قياس الطول المعطى وحوله إلى البوصة System.out.print("Enter the number of " + units + ": "); measurement = TextIO.getlnDouble(); switch ( units ) { case "inches": inches = measurement; break; case "feet": inches = measurement * 12; break; case "yards": inches = measurement * 36; break; case "miles": inches = measurement * 12 * 5280; break; default: System.out.println("Wait a minute! Illegal unit of measure! I quit!"); System.exit(1); } // نهاية switch التعداد (Enum) كتعبير لتعليمات switch كما ذَكَرنا مُسْبَّقًا، يُسمَح باِستخدَام التعدادات (enums) كتعبيرات (expression) لتَعْليمَة switch. وفي هذه الحالة، يجب أن تكون ثوابت عناوين الحالات (case labels) واحدة من القيم المتاحة بنوع التعداد المُستخدَم. فمثلًا، لنفْترِض أن لدينا تعدادًا من النوع Season مُعرَّف كالتالي: enum Season { SPRING, SUMMER, FALL, WINTER } إذا كان التعبير (expression) المُستخدَم ضِمْن تَعْليمَة switch من ذلك النوع، فينبغي أن تكون الثوابت المُستخدَمة بعناوين الحالات (case labels) واحدة من القيم التالية Season.SPRING و Season.SUMMER و Season.FALL و Season.WINTER. لكن لاحظ أنه عندما يُستخدَم تعداد ثابت (enum constant) ضِمْن عنوان حالة (case label)، يُستخدَم فقط الاسم دون إعادة ذِكر نوع التعداد، مثل SPRING، وليس الاسم كاملًا، مثل Season.SPRING. لمّا كان ضروريًا لثوابت عناوين الحالات (case labels) أن تكون من نفس نوع تعبير التَعْليمَة، يستطيع الحاسوب أن يَستنتج نوع تلك الثوابت اعتمادًا على نوع التعبير. فلمّا كان هذا التعبير تعدادًا، افْترَض الحاسوب أن تلك الثوابت لابُدّ وأن تَندرِج تحت قيم نفس نوع التعداد، ولهذا لا حاجة لإعادة كتابة نوع التعداد بالثابت. مع ذلك، يَظِلّ هذا شذوذًا بالصياغة (syntax). على سبيل المثال، يُوضِح المثال بالأسفل طريقة اِستخدَام التعداد Season ضِمْن تَعْليمَة switch، وبفَرْض أن المُتَغيِّر currentSeason من النوع Season: switch ( currentSeason ) { case WINTER: // وليس Season.WINTER System.out.println("December, January, February"); break; case SPRING: System.out.println("March, April, May"); break; case SUMMER: System.out.println("June, July, August"); break; case FALL: System.out.println("September, October, November"); break; } الإسناد المؤكد (Definite Assignment) وتعليمة switch يُعيد التعبير (int)(3*Math.random()) واحدًا من الأعداد الصحيحة ٠ و ١ و ٢ عشوائيًا وباحتمالية مُتساوية، ولذلك تَستخدِمه تَعْليمَة switch -بالمثال التالي- للقيام باختيار عشوائي ضِمْن ثلاث بدائل مُحتمَلة، حيث تُسنَد إحدى القيم الثلاثة "Rock" أو "Paper" أو "Scissors" إلى المُتَغيِّر computerMove، وفقًا لقيمة هذا التعبير وبإحتمالية تُساوِي الثلث لكلًا من القيم الثلاثة. switch ( (int)(3*Math.random()) ) { case 0: computerMove = "Rock"; break; case 1: computerMove = "Paper"; break; case 2: computerMove = "Scissors"; break; } تُعدّ تَعْليمَة switch بالأعلى سليمة تمامًا بهذه الطريقة، لكن بفَرْض قيامنا بإضافة تَعْليمَة لطباعة المُتَغيِّر computerMove كالتالي: String computerMove; switch ( (int)(3*Math.random()) ) { case 0: computerMove = "Rock"; break; case 1: computerMove = "Paper"; break; case 2: computerMove = "Scissors"; break; } System.out.println("The computer's move is " + computerMove); // خطأ!! الآن، أَصبح لدينا خطأ (error) بالسَطْر الأخير! يَرجِع سبب هذا الخطأ إلى ما يُعرَف باسم الإِسْناد المؤكد (definite assignment)، الفكرة ببساطة هي أنه من الضروري أن يَتمكَن مُصرِّف الجافا (compiler) من التأكد من أن أيّ محاولة لاِستخدَام قيمة مُتَغيِّر معين قد سَبَقها بالفعل عملية إِسْناد (assignment) قيمة لهذا المُتَغيِّر. تَعرَّضنا قبلًا لمصطلح الإِسْناد المؤكد (definite assignment) بالقسم الفرعي ٣.١.٤. بهذا المثال تَحْدِيدًا، تُغطِّي الحالات (cases) الثلاثة الموجودة ضِمْن تَعْليمَة switch جميع الاحتمالات، ومع ذلك فإن المُصرِّف (compiler) ليس ذكي كفاية لاستنتاج ذلك، فهو فقط يرى تَعْليمَة switch مُستخدَمة مع تعبير من النوع العددي (integer-valued expression)، وبذات الوقت، يرى أن الحالات (cases) المُخصَّصة ضِمْن التَعْليمَة لا تُغطِّي جميع الأعداد الصحيحة المُحتمَلة، وإنما فقط ثلاثة منها. أحد الحلول البسيطة هو استبدال الحالة الافتراضية default: بالحالة (case) الأخيرة الموجودة ضِمْن تَعْليمَة switch. لمّا كان تَخْصِيص الحالة الافتراضية default يَعني بالضرورة أن جميع القيم المُحتمَلة لتعبير التَعْليمَة switch سوف تُغطََّى لا محالة، استطاع المُصرِّف (compiler) التأكد من حُدوث إِسْناد مُؤكد للمُتَغيِّر computerMove. انظر المثال التالي: String computerMove; switch ( (int)(3*Math.random()) ) { case 0: computerMove = "Rock"; break; case 1: computerMove = "Paper"; break; default: computerMove = "Scissors"; break; } System.out.println("The computer's move is " + computerMove); // OK! صياغة جديدة لتعليمة switch أُضيفت نسخة جديدة من تَعْليمَة switch إلى الإصدار ١٤ من لغة الجافا. تَستخدِم النسخة الجديدة السهم (arrow) -> بعد عنوان الحالة (case label)، بدلًا من النقطتان الرأسيتان (colon) :، وتَسمَح بتَعْليمَة واحدة فقط بعد السهم. إذا أردت كتابة أكثر من تَعْليمَة لنفس عنوان الحالة، يُمكِنك اِستخدَام تَعْليمَة كُتليّة (block statement) بتَضْمِين التَعْليمَات المطلوبة بين قوسين. لم تَعُدْ في حاجة إلى اِستخدَام تَعْليمَة break بنهاية كل حالة، وهو ما ساعد كثيرًا على تَجنُّب الخطأ الشائع من انتقال التَحكُّم غَيْر المقصود من الحالة (case) الصحيحة -بعد الانتهاء من تَّنْفيذها- إلى الحالة التالية بدلًا من إنهاء التَعْليمَة، وذلك في حالة عدم إضافة تَعْليمَة break. وأخيرًا، لم تَعُدْ مُضطرًا لاِستخدَام قيمة واحدة فقط لكل عنوان حالة (case label)؛ فقد أصبح بالإمكان تَخْصِيص أكثر من قيمة لنفس الحالة (case)؛ بحيث يُفصَل بينهما بفاصلة (comma). يُمكن إعادة كتابة تَعْليمَة switch الموجودة بأول مثال بهذا القسم بالصياغة الجديدة كالتالي: switch ( N ) { // بفرض أن N متغير من النوع العددي case 1 -> System.out.println("The number is 1."); case 2, 4, 8 -> { System.out.println("The number is 2, 4, or 8."); System.out.println("(That's a power of 2!)"); } case 3, 6, 9 -> { System.out.println("The number is 3, 6, or 9."); System.out.println("(That's a multiple of 3!)"); } case 5 -> System.out.println("The number is 5."); default -> System.out.println("The number is 7 or is outside the range 1 to 9."); } تُعدّ النسخة الجديدة من وجهة نظر الكاتب إضافة ضخمة للغة. لاحِظ أنه ما تزال الصيغة (syntax) الأصلية من تَعْليمَة switch مُتاحة باللغة. إلى جانب التحسِّينات على تَعْليمَة switch، أُضيف أيضًا إلى لغة الجافا تعبيرًا من النوع switch (switch expression)، والذي -وكأيّ تعبير آخر (expression)- يُمكن تَحْصِيل قيمته (evaluation) بحيث تؤول إلى قيمة مُفردة (single value). تتشابه صياغة (syntax) هذا التعبير الجديد إلى حد كبير مع تَعْليمَة switch، مع فارق أن كل حالة أَصبحَت تُخصِّص تعبيرًا (expression) لا تَعْليمَة (statement). اُنظر المثال التالي: String computerMove = switch ( (int)(3*Math.random()) ) { case 1 -> "Rock"; case 2 -> "Paper"; default -> "Scissors"; }; لابُدّ أن يؤول التعبير من النوع switch (switch expression) إلى قيمة دائمًا، ومِنْ ثَمَّ فإنه تقريبًا دائمًا ما يَتضمَّن الحالة الافتراضية (default case). يُمكِن للتعبير (expression) الخاص بحالة (case) معينة أن يُستبدَل بكتلة (block) تَشتمِل على مجموعة من التَعْليمَات، وعِندها تُحسَب قيمة هذه الحالة باِستخدَام تَعْليمَة yield، مثل yield 42;، وليس بأي من التَعْليمَتين return أو break. ترجمة -بتصرّف- للقسم Section 6: The switch Statement من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.
-
تتوفَّر تَعْليمَتين للتَفْرِيع (branching statements) بلغة الجافا، وفي هذا القسم، سنتناول تَعْليمَة التَفْرِيع الأولى if، والتي قد سبق وتَعرَّضنا لها بالفعل بالقسم ٣.١. تُكتب تَعْليمَة التَفْرِيع الشَّرْطيّة if بالصياغة التالية: if ( <boolean-expression> ) <statement-1> else <statement-2> تُسمى هذه الصياغة من if بتَعْليمَة التَفْرِيع الثنائي (two-way branch)، وكما هو مُتوقَّع، يُمكِن للتعليمتين و -بالأعلى- أن تَكونا كُتلَتي البِنْية (block statement). لاحِظ أنه من المُمكن حَذْف الجزء else المُكوَّن من الكلمة else والتعليمة . مشكلة تَّدَلِّي else لمّا كانت تَعْليمَة if هي بالنهاية مُجرَّد تَعْليمَة، فإنه من المُمكن لإحدى التعليمتين أو أو حتى كلتيهما أن يَكونا تَعْليمَة if بذاتهما، ولكن قد يتَسبَّب ذلك بحُدوث مشاكل في بعض الأحيان. فمثلًا، إذا اِستخدَمت تَعْليمَة if بدون الجزء else محل التعليمة ، كما في الشيفرة التالية: if ( x > 0 ) if (y > 0) System.out.println("First case"); else System.out.println("Second case"); قد يُعطيك اِستخدَامك للمسافات البادئة (indentation) -ببداية كل سطر من الشيفرة- انطباعًا خاطئًا بأن الجزء else هو النصف الآخر من تَعْليمَة if (x > 0)، لكن هذا غَيْر صحيح. لا يُعطِي الحاسوب في الواقع أيّ اعتبار لتلك المسافات، وإنما يَتبِّع -في هذه الحالة بالتَحْدِيد- قاعدة بسيطة، وهي ربْط الجزء else بأقرب تَعْليمَة if ليس لديها جزء else خاص بها، مما يَعني أنه سيَربُط الجزء else بتَعْليمَة if (y > 0)؛ لأنها الأقرب. ولهذا يَقرأ الحاسوب التَعْليمَات بالأعلى كما لو كانت مُرَتَّبة على النحو التالي: if ( x > 0 ) if (y > 0) System.out.println("First case"); else System.out.println("Second case"); تستطيع مع ذلك إجبار الحاسوب على اِستخدَام التفسير الآخر بتَضْمِين تَعْليمَة if الداخلية داخل كُتلَة (block)، كالتالي: if ( x > 0 ) { if (y > 0) System.out.println("First case"); } else System.out.println("Second case"); تختلف بالطبع نتائج تَعْليمَتي if السابقتين. فمثلًا إذا كانت قيمة المُتَغيِّر x أقل من أو تُساوِي الصفر، لا تَطبع التَعْليمَة الأولى شيئًا، بينما تَطبع الثانية السِلسِلة النصية "Second case". التفريع المتعدد (Multiway Branching) من الجهة الآخرى، تُصبِح الأمور أكثر تشويقًا عندما تَستخدِم تَعْليمَة if محل التعليمة ، وفي هذه الحالة، ستبدو تَعْليمَة التَفْرِيع (branching) كالتالي -ربما بدون الجزء else الأخير-: if ( <boolean-expression-1> ) <statement-1> else if ( <boolean-expression-2> ) <statement-2> else <statement-3> ولمّا كان الحاسوب لا يَهتم بطريقة تَنْسِيق الشيفرة على الصفحة، فتقريبًا دائمًا ما يُكتَب المثال السابق بالصيغة التالية: if ( <boolean-expression-1> ) <statement-1> else if ( <boolean-expression-2> ) <statement-2> else <statement-3> تُسمى هذه الصياغة من if -بالأعلى- بتَعْليمَة التَفْرِيع الثلاثي (three-way branch)، وينبغي أن تُفكر بها كتَعْليمَة واحدة؛ فبالنهاية، ستُنفَّذ واحدة فقط من التعليمات الثلاثة و و أثناء تَّنْفيذ تلك التَعْليمَة. تُحسَب أولًا قيمة التعبير ، فإذا كانت تؤول إلى القيمة المنطقية true، سيُنفِّذ الحاسوب التعليمة ، ثم سيَقفِز مباشرة إلى نهاية تَعْليمَة if الخارجية مُتخَطِّيًا بذلك العبارتين الآخريتين تمامًا. أما إذا آلت قيمتها إلى القيمة المنطقية false، فإنه سيتَخَطَّى التعليمة فقط، وينتقل إلى تَعْليمَة if الثانية المُتداخِلة (nested)؛ ليَفحْص قيمة التعبير ، ويُقرِّر ما إذا كان سيُنفِّذ التعليمة أم . مثلًا، يَطبع المثال التالي واحدة من ثلاث رسائل مُحتمَلة وفقًا لقيمة المُتَغيِّر temperature: if (temperature < 50) System.out.println("It's cold."); else if (temperature < 80) System.out.println("It's nice."); else System.out.println("It's hot."); على سبيل المثال، إذا كان المُتَغيِّر temperature يَحمِل القيمة ٤٢، فسيَتحقَّق أول شَّرْط، ومِنْ ثَمَّ تُطبَع الرسالة "It's cold"، لينتقل بَعْدها الحاسوب إلى ما بعد التَعْليمَة مُتخَطِّيًا بقيتها، وبدون حتى محاولة اختبار الشَّرْط الثاني. أما إذا كانت قيمته تُساوِي ٧٥، فلن يتحقَّق أول شَّرْط، لذلك سينتقل الحاسوب لاختبار الشَّرْط الثاني، الذي سيتحقَّق لتتم طباعة الرسالة "It's nice"، ثم سيتَخَطَّى بقية التَعْليمَة. أخيرًا إذا كانت قيمته تُساوِي ١٧٣، لا يتحقَّق أي من الشَّرْطين، وتُطبَع الرسالة "It's hot" (لو لم تَكن دوائره الكهربية قد اِحترقَت بفِعْل الحرارة). يُمكنك الاستمرار بإضافة المزيد من عبارات else-if بأيّ عَدَد، وفي تلك الحالة، يُطلَق عليها اسم تَعْليمَة التَفْرِيع المُتعدِّد (multiway branches)، كالتالي: if ( <test-1> ) <statement-1> else if ( <test-2> ) <statement-2> else if ( <test-3> ) <statement-3> . . // أضف المزيد . else if ( <test-N> ) <statement-N> else <statement-(N+1)> الاختبارات ** **-بالأعلى- هي تعبيرات منطقية (boolean expressions)، تُحسَب قيمها واحدة تلو الآخرى، إلى أن تؤول إحداها إلى القيمة المنطقية true، فيُنفِّذ عندها الحاسوب التعليمة > التي تُناظِر الاختبار > المُتحقِّق ويتَخَطَّى البقية. أمّا إذا لم تؤول أي واحدة منها إلى القيمة المنطقية true، تُنفَّذ عندها التعليمة (n+1)> الموجودة ضِمْن الجزء else. لاحِظ أنه دائمًا ما ستُنفَّذ واحدة فقط لا غَيْر من كل هذه التعليمات >، وهذا هو سبب تسميتها باسم تَعْليمَة التَفْرِيع المُتعدِّد (multiway branch). يمُكنك أيضًا حَذْف الجزء else الأخير، ولكن -في تلك الحالة- إذا لم تتحقَّق أيّ من التعبيرات المنطقية، وآلت جميعا إلى القيمة المنطقية false، فلن تُنفَّذ أيّ عبارة نهائيًا. يُمكن بالطبع لأيّ من التعليمات > أن تكون كُتليّة البِنْية (block)، أي مُكوَّنة من أيّ عدد من التَعْليمَات طالما أُحيطَت بزوج من الأقواس. > قد يَنْتابك شُعور بالارتباك لوجود الكثير من قواعد الصياغة (syntax) الجديدة هنا، ولكن أَضمَن لك أنه مع قليل من الممارسة، ستعتاد عليها. يُمكنك أيضًا أن تُلقي نظرة على المُخطط التالي، والذي يُوضِح مَسار التحكُّم (flow control) أثناء تَّنْفيذ الصيغة الأعم من تَعْليمَة if..else if والمعروفة باسم تَعْليمَة التَفْرِيع المُتعدِّد: أمثلة لنفْترَض أن لدينا ثلاثة مُتَغيِّرات x و y و z من النوع العددي int، يَحتوِي كل منها على قيمة عددية معينة. المطلوب هو طباعة قيم المُتَغيِّرات الثلاثة بحيث تكون مُرَتَّبة ترتيبًا تصاعديًا. فمثلًا، إذا كانت قيم المُتَغيِّرات هي ٤٢ و ١٧ و ٢٠، فلابُدّ من طباعتها على الترتيب ١٧ و ٢٠ و ٤٢. لحل هذه المسألة، سنحاول الإجابة على السؤال التالي: "لنتخيل أن لدينا قائمة مُرَتَّبة بالفعل، فبأيّ مكان يَنبغي للمُتَغيِّر x أن يقع؟" ببساطة، إذا كانت قيمة المُتَغيِّر x أصغر من قيمة كلا المُتَغيِّرين y و z، فإنه سيأتي بأول القائمة، أما إذا كان أكبر من كليهما، فإنه سيأتي بذيل القائمة، وأخيرًا، سيأتي بوسط القائمة في أيّ حالة غَيْر تلك الحالتين. يُمكن التعبير عن الحالات الثلاثة السابقة بتَعْليمَة التَفْرِيع الثلاثي if، مع ذلك ما يزال أمامنا تحديًا لإيجاد طريقة نُحدِّد من خلالها ترتيب طباعة المُتَغيِّرين y و z. انظر الشيفرة الوهمية (pseudocode): if (x < y && x < z) { // اطبع x متبوعة بكلا من y و z بترتيبها الصحيح output x, followed by y and z in their correct order } else if (x > y && x > z) { // اطبع y و z بترتيبها الصحيح متبوعين بقيمة x output y and z in their correct order, followed by x } else { // اطبع x بين y و z بترتيبها الصحيح output x in between y and z in their correct order } يتطلب تَحْدِيد ترتيب طباعة المُتَغيِّرين y و z تَعْليمَة if آخرى داخلية. انظر الشيفرة التالية بلغة الجافا: if (x < y && x < z) { // إذا كانت x بالبداية if (y < z) System.out.println( x + " " + y + " " + z ); else System.out.println( x + " " + z + " " + y ); } else if (x > y && x > z) { // إذا كانت x بالنهاية if (y < z) System.out.println( y + " " + z + " " + x ); else System.out.println( z + " " + y + " " + x ); } else { // إذا كانت x في الوسط if (y < z) System.out.println( y + " " + x + " " + z); else System.out.println( z + " " + x + " " + y); } حَاوِل اختبار ما إذا كانت الشيفرة بالأعلى ستَعمَل بشكل سليم حتى في حالة وجود أكثر من مُتَغيِّر يَحمِل نفس القيمة. عمومًا إذا كان هناك مُتَغيِّرين بنفس القيمة، فإنه لم يَعُدْ مهمًا بأيّ ترتيب سيَتِمّ طباعتهما. بخلاف اللغات الطبيعية التي تَسمَح بكتابة جملة مثل "إذا كان x أصغر من y و z"، لا يُمكنك أن تَفعَل الشيء نفسه -أيّ كتابة التعبير if (x < y && z)- بلغة الجافا. لا تَسير الأمور بهذه البساطة، فلابُدّ أن يَستقبِل العَامِل (operator) المنطقي && مُعامِلين منطقيين (boolean)، ولهذا ستحتاج إلى إِجراء الاختبارين x<y و x<z بشكل منفصل، ثم تُطبِّق العَامِل المنطقي && على نتيجتيهما. ربما تَتَّخذ منحى آخر لحل نفس المسألة، وذلك بمحاولة الإجابة على السؤال: "بأيّ ترتيب يَنبغي طباعة المُتَغيِّرين x و y؟". بمُجرَّد أن تتَوصَّل لإجابة، سيتبقَّى فقط أن تُحدِّد مكان المُتَغيِّر z. انظر الشيفرة التالية بلغة الجافا: if ( x < y ) { // x comes before y if ( z < x ) // z comes first System.out.println( z + " " + x + " " + y); else if ( z > y ) // z comes last System.out.println( x + " " + y + " " + z); else // z is in the middle System.out.println( x + " " + z + " " + y); } else { // y comes before x if ( z < y ) // z comes first System.out.println( z + " " + y + " " + x); else if ( z > x ) // z comes last System.out.println( y + " " + x + " " + z); else // z is in the middle System.out.println( y + " " + z + " " + x); } تتوفَّر عادةً عدة طرائق وأساليب مختلفة لحل نفس المشكلة. فمثلًا، بالمثال السابق، تَعرَّضنا لحلّين فقط ضِمْن عدة حلول مُحتمَلة آخرى كثيرة. فمثلًا، قد يقوم حل آخر ثالث بتبديل قيمة (swap) المُتَغيِّرين x و y إذا كانت قيمة x أكبر من قيمة y، ومِنْ ثَمَّ سيكون من الضروري طباعة قيمة المُتَغيِّر x قبل y. سنحاول أخيرًا كتابة برنامج كامل يَستخدِم تَعْليمَة if بطريقة مُشوِّقة. هدف البرنامج عمومًا هو تَحْوِيل قياسات الطول (measurements of length) من إحدى وَحَدات قياس الطول (units of measure) إلى آخرى، فمثلًا من المِيل (miles) إلى اليَارْدَة (yards)، أو من البُوصَة (inches) إلى القَدَم (feet). سنحتاج بالتأكيد أن نكون أكثر تحديدًا بخُصوص مُتطلَّبات البرنامج؛ فما تزال المسألة غَيْر واضحة المعالِم حتى الآن، ولهذا سنفْترَض أن البرنامج يتَعامَل فقط مع وَحَدات القياس التالية: البُوصَة، والقَدَم، واليَارْدَة، والمِيل؛ وسيكون من السهل إضافة المزيد منها فيما بَعْد. سيُدْخِل المُستخدِم قياس الطول بأي وَحدة من وَحَدات القياس المسموح بها، كأن يَكتُب "١٧ قدم" أو "٢.٧٣ ميل"، ربما بَعْدها قد تَطلُب من المُستخدِم إِدْخَال اسم وَحدة القياس المطلوب التَحْوِيل إليها، بحيث تُطبَع كخَرْج (output) للبرنامج. لكننا في الواقع سنكتفي بطباعة قياس الطول بجميع وَحَدات القياس المسموح بها؛ وذلك في محاولة لتبسيط المسألة. انظر الخوارزمية التالية المبدئية: // اقرأ كلا من قياس الطول ووحدة القياس المستخدمة Read the user's input measurement and units of measure // حول قيمة قياس الطول المعطاة إلى وحدات القياس الأخرى Express the measurement in inches, feet, yards, and miles // اطبع قياسات الطول بالوحدات الأربعة Display the four results لابُدّ أن يَستقبِل البرنامج مُدْخَلين (input) من المُستخدِم، هما قياس الطول ذاته بالإضافة إلى وَحدة القياس المُستخدَمة (units of measure)، ولهذا تُستخدَم الدالتين TextIO.getDouble() و TextIO.getlnWord()؛ لقراءة كلًا من قياس الطول العددي واسم وَحدة القياس من نفس السَطْر على الترتيب. سنبدأ دائمًا بتَحْوِيل قياس الطول المُعْطى إلى إِحدى وَحَدات قياس الطول -ولتكن البُوصَة-؛ وذلك لتوحيد الخطوات التالية المسئولة عن التَحْوِيل إلى وَحَدات القياس الأخرى. يَلزَمنا الآن فقط تَحدِّيد وَحدة القياس التي أَدْخَل بها المُستخدِم قياس الطول؛ وذلك لنتمكن من تَحْوِيلها إلى وَحدة البُوصَة. بَعْد إجراء ذلك التَحْوِيل، سيكون من السهل تَحْوِيل قياس الطول بوَحدة البُوصَة إلى كلًا من القَدَم، واليَارْدَة، والمِيل. اُنظر الخوارزمية التالية: Let measurement = TextIO.getDouble() Let units = TextIO.getlnWord() // إذا اسم وحدة القياس المعطى هو البوصة if the units are inches Let inches = measurement // إذا اسم وحدة القياس المعطى هو القدم else if the units are feet Let inches = measurement * 12 // 12 inches per foot // إذا اسم وحدة القياس المعطى هو الياردة else if the units are yards Let inches = measurement * 36 // 36 inches per yard // إذا اسم وحدة القياس المعطى هو الميل else if the units are miles Let inches = measurement * 12 * 5280 // 5280 feet per mile // إذا لم يكن اسم وحدة القياس المعطى أي من القيم الأربعة السابقة else // اسم وحدة القياس المعطى غير صالح The units are illegal! // اطبع رسالة خطأ واوقف المعالجة Print an error message and stop processing // إجراء التحويلات Let feet = inches / 12.0 Let yards = inches / 36.0 Let miles = inches / (12.0 * 5280.0) // اعرض النتائج Display the results لمّا كنا سنقرأ اسم وَحدة القياس المُعْطاة إلى المُتَغيِّر units، وهو من النوع String، فسنحتاج إلى إجراء موازنة نصية (comparison) لتَحدِّيد وَحدة القياس المُناظِرة. تُستخدَم الدالة units.equals("inches") لفَحْص ما إذا كانت قيمة المُتَغيِّر units تُساوِي السِلسِلة النصية "inches"، لكن لا يَنطوِي إِلزام المُستخدِم على إِدْخَال الكلمة "inches" حرفيًا على أفضل تجربة للمُستخدِم (user experience)؛ فمن الأفضل أن نَسمَح له بكتابة كلمات أخرى مثل "inch" أو "in"، ولذلك سنُجري الفَحْص على التعبير المنطقي التالي: units.equals("inches") || units.equals("inch") || units.equals("in") وذلك للسماح بالاحتمالات الثلاثة. يُمكننا أيضًا أن نَسمَح له بكتابة وَحدة القياس بحروف كبيرة (upper case) مثل "Inches" أو "IN" إِمّا عن طريق تَحْوِيل السِلسِلة النصية المُعْطاة units إلى حروف صغيرة (lower case) قبل إجراء الفَحْص، أو باِستخدَام الدالة units.equalsIgnoreCase للموازنة بدلًا من units.equals. تَسمَح النسخة النهائية من البرنامج للمُستخدِم بتَكْرار عمليتي الإِدْخَال والطباعة أكثر من مرة؛ حيث سيَتوقَف البرنامج فقط عند إِدْخَال القيمة صفر. كل ما تَطلبه الأمر هو إِحاطة الخوارزمية بالأعلى ضِمْن حَلْقة التَكْرار while، والتأكد من إِيقافها عندما يُدخِل المُستخدِم القيمة صفر. اُنظر الشيفرة بالكامل: import textio.TextIO; public class LengthConverter { public static void main(String[] args) { double measurement; // قياس الطول المعُطى String units; // اسم وحدة قياس المعطاة // قياس الطول بوحدات القياس الأربعة المتاحة double inches, feet, yards, miles; System.out.println("Enter measurements in inches, feet, yards, or miles."); System.out.println("For example: 1 inch 17 feet 2.73 miles"); System.out.println("You can use abbreviations: in ft yd mi"); System.out.println("I will convert your input into the other units"); System.out.println("of measure."); System.out.println(); while (true) { // اقرأ مُدخَل المستخدم System.out.print("Enter your measurement, or 0 to end: "); measurement = TextIO.getDouble(); if (measurement == 0) break; // اِنهي حلقة while units = TextIO.getlnWord(); units = units.toLowerCase(); // صغر الحروف // حول قياس الطول المعطى إلى وحدة البوصة if (units.equals("inch") || units.equals("inches") || units.equals("in")) { inches = measurement; } else if (units.equals("foot") || units.equals("feet") || units.equals("ft")) { inches = measurement * 12; } else if (units.equals("yard") || units.equals("yards") || units.equals("yd")) { inches = measurement * 36; } else if (units.equals("mile") || units.equals("miles") || units.equals("mi")) { inches = measurement * 12 * 5280; } else { System.out.println("Sorry, but I don't understand \"" + units + "\"."); continue; // عد إلى بداية حلقة while } // حوِّل قياس الطول بوحدة البوصة إلى وحدات القياس الآخرى feet = inches / 12; yards = inches / 36; miles = inches / (12*5280); // اطبع قياس الطول بوحدات القياس الأربعة System.out.println(); System.out.println("That's equivalent to:"); System.out.printf("%14.5g inches%n", inches); System.out.printf("%14.5g feet%n", feet); System.out.printf("%14.5g yards%n", yards); System.out.printf("%14.5g miles%n", miles); System.out.println(); } // نهاية تعليمة while System.out.println(); System.out.println("OK! Bye for now."); } // نهاية برنامج main } // نهاية الصنف LengthConverter لمّا كنا غَيْر مُتحكِّمين بقيم الأعداد الحقيقية (real numbers) المُدْخَلة؛ حيث تَعتمِد على المُستخدِم الذي ربما قد يَرغَب بإِدْخَال قياسات صغيرة جدًا أو ربما كبيرة جدًا، فكان لابُدّ من صياغة الخَرْج باِستخدَام مُحدِّدات الصيغة (format specifiers). اُستخدِم تحديدًا مُحدِّد الصيغة g -بالبرنامج-، والذي يَعمَل كالتالي: إذا كان العَدَد كبيرًا جدًا أو صغيرًا جدًا، فإنه يَطبَعه بصيغة أسّية (exponential form)، أمّا إن لم يَكُن كذلك، فإنه يَطبَعه بالصيغة الرقمية (decimal form) العادية. تَذَكَّر أن القيمة 5، مثلًا، بمُحدِّد الصيغة %14.5g تُشير إلى العدد الكلي للأرقام المعنوية (significant digits) المَطبُوعة، وبذلك سنَحصُل دائمًا على نفس العَدَد من الأرقام المعنوية (significant digits) بالخَرْج بِغَضّ النظر عن قيمة العَدَد المُدْخَل. يَختلف ذلك عن مُحدِّد الصيغة f، والذي يَطبَع الخَرْج بصيغة رقمية (decimal form)، ولكن تُشير فيه القيمة 5، مثلًا، بمُحدِّد الصيغة %14.5f إلى عَدَد الأرقام بَعْد العلامة العَشريّة (decimal point)، أيّ إذا كان لدينا العَدَد 0.000000000745482 فإن مُحدِّد الصيغة f سيَطبَعه إلى 0.00000، بدون أيّ أرقام معنوية (significant digits) على الإطلاق، أما مُحدِّد الصيغة g فسيَطبَعه 7.4549e-10. التعليمة الفارغة (Empty Statement) تُوفِّر لغة الجافا تَعْليمَة آخرى يُطلَق عليها اسم التَعْليمَة الفارغة (empty statement)، وهي ببساطة مُجرَّد فاصلة منقوطة ;، وتَطلُب من الحاسوب أن يَفعَل "لا شيء". اُنظر المثال التالي: if (x < 0) { x = -x; }; عملية إِضافة فاصلة منقوطة (semicolon) بَعْد القوس {، كما بالمثال السابق، هي عملية صحيحة تمامًا وِفقًا لقواعد بناء الجملة (syntax)، لكن، في هذه الحالة، لا يعدّها الحاسوب جزءًا من تَعْليمَة if، وإنما يُعامِلها معاملة تَعْليمَة فارغة (empty statement) مستقلة بذاتها. عمومًا ليس هذا الغرض من تَوفَّر التَعْليمَة الفارغة بلغة الجافا، ولن تجدها مُستخدَمة بهذه الطريقة إلا نادرًا، وإنما ستَستخدِمها أحيانًا عندما تُريد أن تَطلُب من الحاسوب ألا يَفعَل شيئًا. على سبيل المثال: if ( done ) ; // تعليمة فارغة else System.out.println( "Not done yet."); تَطلُب الشيفرة بالأعلى من الحاسوب ألا يَفعَل شيئًا إذا كانت قيمة المُتَغيِّر المنطقي done مساوية للقيمة المنطقية true، وأن يَطبَع السِلسِلة النصية "Not done yet" إذا كانت قيمته مُساوية للقيمة المنطقية false. لا يمكنك في هذه الحالة أن تَحذِف الفاصلة المنقوطة (semicolon)؛ لأن قواعد الصياغة بلغة الجافا (Java syntax) تَتَطلَّب وجود تَعْليمَة بين if و else. يُفضِّل الكاتب على الرغم من ذلك اِستخدَام كُتلَة (block) فارغة أي اِستخدَام قوسين فارغين في تلك الحالات. عادةً ما تَتسبَّب إضافة التَعْليمَة الفارغة ; دون قصد بحُدوث أخطاء (errors) يَصعُب إِيجادها. انظر المثال التالي: for (i = 0; i < 10; i++); System.out.println("Hello"); يَطبَع المثال بالأعلى كلمة "Hello" مرة واحدة فقط وليس عشر مرات كما قد تَظُنّ -بالأخص مع وجود المسافة البادئة (indentation) ببداية السطر الثاني-. السبب ببساطة هو وجود فاصلة منقوطة ; بنهاية تَعْليمَة for بنهاية السَطْر الأول. تُعدّ هذه الفاصلة المنقوطة ; -في هذه الحالة- تَعْليمَة بحد ذاتها، هي التَعْليمَة الفارغة (empty statement)، وهي في الواقع ما يتم تَّنْفيذه عشر مرات، أي أن حَلْقة التَكْرار for بالأعلى تَفعَل "لا شئ" عشر مرات. في المقابل، لا تُعدّ التَعْليمَة System.out.println جزءً من تَعْليمَة for أساسًا، ولذلك تُنفَّذ مرة واحدة فقط بعد انتهاء تَعْليمَة حَلْقة for. ترجمة -بتصرّف- للقسم Section 5: The If Statement من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.
-
سنتناول في هذا القسم نوعًا آخر من الحَلْقات، هو تَعْليمَة الحَلْقة for. ينبغي أن تُدرك أنه يُمكن لأيّ حَلْقة تَكْرارية (loop) عامةً أن تُكتَب بأيّ من التَعْليمَتين for و while، وهو ما يَعني أن لغة الجافا لم تتَحَصَّل على أيّ مِيزَة وظيفية إضافية بتَدْعِيمها لتَعْليمَة for. لا يَعني ذلك أن تَعْليمَة for غَيْر مُهمة، على العكس تمامًا، ففي الواقع، قد يَتجاوز عَدَد حَلْقات for المُستخدَمة ببعض البرامج عَدَد حَلْقات while. (كما أن الكاتب على معرفة بأحد المبرمجين والذي لا يَستخدِم سوى حَلْقات for). الفكرة ببساطة أن تَعْليمَة for تكون أكثر ملائمة لبعض النوعيات من المسائل؛ حيث تُسهِل من كتابة الحَلْقات وقرائتها بالموازنة مع حَلْقة while. حلقة التكرار For عادةً ما تُستخدَم حَلْقة التَكْرار while بالصياغة التالية: <initialization> while ( <continuation-condition> ) { <statements> <update> } مثلًا، اُنظر لهذا المثال من القسم ٣.٢: years = 0; // هيئ المتغير while ( years < 5 ) { // شرط حلقة التكرار // نفذ التعليمات الثلاثة التالية interest = principal * rate; principal += interest; System.out.println(principal); // حدث قيمة المتغير years++; } ولهذا أضيفت تَعْليمَة for للتسهيل من كتابة هذا النوع من الحَلْقات، حيث يُمكِن إِعادة كتابة حَلْقة التَكْرار بالأعلى باستخدام تَعْليمَة for، كالتالي: for ( years = 0; years < 5; years++ ) { interest = principal * rate; principal += interest; System.out.println(principal); } لاحظ كيف دُمجَت كُُلًا من تعليمات التهيئة ، والشَّرْط الاستمراري لحَلْقة التَكْرار ، والتَحْدِيث جميعًا بسَطْر واحد هو السَطْر الأول من حَلْقة التَكْرار for. يُسهِل ذلك من قراءة حَلْقة التَكْرار وفهمها؛ لأن جميع تَعْليمَات التحكُّم بالحَلْقة (loop control) قد ضُمِّنت بمكان واحد بشكل منفصل عن مَتْن الحَلْقة الفعليّ المطلوب تَكْرار تَّنْفيذه. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن تُنفَّذ حَلْقة التَكْرار for بالأعلى بنفس الطريقة التي تُنفَّذ بها الشيفرة الأصلية، أي تُنفَّذ أولًا تعليمة التهيئة مرة وحيدة قبل بدء تَّنْفيذ الحَلْقة، ثم يُفحَص الشَّرْط الاستمراري للحَلْقة قَبْل كل تَكْرار (iteration/execution) لمَتْن الحَلْقة، بما في ذلك التَكْرار الأوَّليّ (first iteration)، بحيث تَتوقَف الحَلْقة عندما يؤول هذا الشَّرْط إلى القيمة false. وأخيرًا، تُنفَّذ تعليمة التَحْدِيث بنهاية كل تَكْرار (iteration/execution) قَبْل العودة لفَحْص الشَّرْط من جديد. تُكتَب تَعْليمَة حَلْقة التَكْرار for بالصياغة التالية: for ( <initialization>; <continuation-condition>; <update> ) <statement> أو كالتالي إذا كانت التعليمة كُتليّة البِنْية (block statement): for ( <initialization>; <continuation-condition>; <update> ) { <statements> } يُمكِن لأيّ تعبير منطقي (boolean-valued expression) أن يُستخدَم محل الشَّرْط الاستمراري . يُمكِن لأيّ تعبير (expression) -طالما كان صالحًا كتَعْليمَة برمجية- أن يُستخدَم محل تعليمة التهيئة ، وفي الواقع غالبًا ما تُستخدَم تَعْليمَة تَّصْريح (declaration) أو إِسْناد (assignment). يُمكِن لأي تَعْليمَة بسيطة (simple statement) أن تُستخدَم محل تعليمة التَحْدِيث ، وعادةً ما تكون تَعْليمَة زيادة/نقصان (increment/decrement) أو إِسْناد (assignment). وأخيرًا، يُمكِن لأي من تلك التعليمات الثلاثة بالأعلى أن تكون فارغة. لاحظ أنه إذا كان الشَّرْط الاستمراري فارغًا، فإنه يُعامَل وكأنه يُعيد القيمة المنطقية true، أي يُعدّ الشَّرْط مُتحقِّقًا، مما يعني تَّنْفيذ مَتْن حَلْقة التَكْرار (loop body) بشكل لا نهائي (infinite loop) إلى أن يتم إيقافها لسبب ما، مثل اِستخدَام تَعْليمَة break. يُفضِّل بعض المبرمجين في الواقع تَّنْفيذ الحَلْقة اللا نهائية (infinite loop) باِستخدَام الصياغة for (;;) بدلًا من while (true). يُوضح المخطط (diagram) التالي مَسار التحكُّم (flow control) أثناء تَّنْفيذ حَلْقة التَكْرار for: عادةً ما تُسْنِد تعليمة التهيئة قيمة ما إلى مُتَغيِّر معين، ثم تُعدِّل تعليمة التَحْدِيث قيمة هذا المُتَغيِّر إِمّا بواسطة تَعْليمَة إِسْناد (assignment) وإِمّا بعملية زيادة/نُقصان (increment/decrement)، وتُفْحَص تلك القيمة من خلال الشَّرْط الاستمراري لحَلْقة التَكْرار (continuation condition)، فتَتوقَف الحَلْقة عندما يؤول الشَّرْط إلى القيمة المنطقية false. يُطلق عادة على المُتَغيِّر المُستخدَم بهذه الطريقة اسم المُتحكِّم بالحَلْقة (loop control variable). في المثال بالأعلى، كان المُتحكِّم بالحَلْقة هو المُتَغيِّر years. تُعدّ حَلْقة العَدّ (counting loop) هي النوع الأكثر شيوعًا من حَلْقات التَكْرار for، والتي يأخذ فيها المُتَغيِّر المُتحكِّم بالحَلْقة (loop control variable) قيم جميع الأعداد الصحيحة (integer) الواقعة بين قيمتين إحداهما صغرى (minimum) والآخرى عظمى (maximum). تُكتَب حَلْقة العَدّ كالتالي: for ( <variable> = <min>; <variable> <= <max>; <variable>++ ) { <statements> } يُمكِن لأيّ تعبير يُعيد عددًا صحيحًا (integer-valued expressions) أن يُستخدَم محل و ، ولكن تُستخدَم عادةً قيم ثابتة (constants). يأخذ المُتَغيِّر -المشار إليه باستخدام بالأعلى والمعروف باسم المُتحكِّم بالحَلْقة (loop control variable)- القيم المتراوحة بين و ، أي القيم <min>+1 و <min>+2 ..وحتى . وغالبًا ما تُستخدَم قيمة هذا المُتَغيِّر داخل المَتْن (body). مثلًا، حَلْقة التَكْرار for بالأعلى هي حَلْقة عَدّ يأخذ فيها المُتَغيِّر المُتحكِّم بالحَلْقة years القيم ١ و ٢ و ٣ و ٤ و ٥. تطبع الشيفرة التالية قيم الأعداد من ١ إلى ١٠ إلى الخَرْج القياسي (standard output): for ( N = 1 ; N <= 10 ; N++ ) System.out.println( N ); مع ذلك، يُفضِّل مبرمجي لغة الجافا بدء العَدّ (counting) من ٠ بدلًا من ١، كما أنهم يميلون إلى اِستخدَام العَامِل > للموازنة بدلًا من <=. تطبع الشيفرة التالية قيم الأعداد العشرة ٠، ١، ٢، …، ٩، كالتالي: for ( N = 0 ; N < 10 ; N++ ) System.out.println( N ); يُعدّ اِستخدَام عَامِل الموازنة > بدلًا من <= أو العكس مصدرًا شائعًا لحُدوث الأخطاء بفارق الواحد (off-by-one errors) بالبرامج. حاول دائمًا أن تأخذ وقتًا للتفكير إذا ما كنت تَرغَب بمعالجة القيمة النهائية أم لا. يُمكنك أيضًا إجراء العَدّ التنازلي، وهو ما قد يكون أسهل قليلًا من العَدّ التصاعدي. فمثلًا، لإجراء عَدّ تنازلي من ١٠ إلى ١. ابدأ فقط بالقيمة ١٠، ثم اِنقص المُتَغيِّر المُتحكِّم بالحَلْقة (loop control variable) بدلًا من زيادته، واستمر طالما كانت قيمة المُتَغيِّر أكبر من أو تُساوِي ١: for ( N = 10 ; N >= 1 ; N-- ) System.out.println( N ); في الواقع، تَسمَح صيغة (syntax) تَعْليمَة for بأن تَشتمِل كُلًا من تعليمتي التهيئة والتَحْدِيث على أكثر من تعبير (expression) مربوطين بفاصلة (comma). يَعنِي ذلك أنه من الممكن الإبقاء على أكثر من عَدَّاد بنفس الوقت، فمثلًا قد يكون لدينا عَدَّاد تصاعدي من ١ إلى ١٠، وآخر تنازلي من ١٠ إلى ١، كالتالي: for ( i=1, j=10; i <= 10; i++, j-- ) { // اطبع قيمة i بخمس خانات System.out.printf("%5d", i); // اطبع قيمة j بخمس خانات System.out.printf("%5d", j); System.out.println(); } كمثال أخير، نريد اِستخدَام حَلْقة التَكْرار for لطباعة الأعداد الزوجية (even numbers) الواقعة بين العددين ٢ و ٢٠، أي بالتحديد طباعة الأعداد ٢، ٤، ٦، ٨، ١٠، ١٢، ١٤، ١٦، ١٨، ٢٠. تتوفَّر أكثر من طريقة للقيام بذلك، نسْتَعْرِض منها أربعة حلول ممكنة (ثلاث منها هي حلول نموذجية تمامًا)؛ وذلك لبيان كيف لمسألة بسيطة مثل تلك المسألة أن تُحلّ بطرائق مختلفة: // (1) // المتغير المتحكم بالحلقة سيأخذ القيم من واحد إلى عشرة // وبالتالي سنطبع القيم 2*1 و 2*2 و ... إلى 2*10 for (N = 1; N <= 10; N++) { System.out.println( 2*N ); } // (2) // المتغير المتحكم بالحلقة سيأخذ القيم المطلوب طباعتها مباشرة // عن طريق إضافة 2 بعبارة التحديث بدلًا من واحد for (N = 2; N <= 20; N = N + 2) { System.out.println( N ); } // (3) // مر على جميع الأرقام من اثنين إلى عشرين // ولكن اطبع فقط الأعداد الزوجية for (N = 2; N <= 20; N++) { if ( N % 2 == 0 ) // is N even? System.out.println( N ); } // (4) // فقط اطبع الأعداد المطلوبة مباشرة // غالبًا سيغضب منك الأستاذ في حالة إطلاعه على مثل هذا الحل for (N = 1; N <= 1; N++) { System.out.println("2 4 6 8 10 12 14 16 18 20"); } من المهم أن نُعيد التأكيد على أنه -باستثناء تَعْليمَة التَّصْريح عن المُتَغيِّرات (variable declaration)- ليس مُمكنًا بأي حال من الأحوال تَّنْفيذ أي تَعْليمَة برمجية، بما في ذلك تَعْليمَة for، بشكل مستقل، وإنما ينبغي أن تُنفَّذ إما داخل البرنامج (routine) الرئيسي main أو داخل إحدى البرامج الفرعية (subroutine)، والمُعرَّفة ضِمْن صَنْف معين (class). لابُدّ أيضًا من التَّصْريح (declaration) عن أي مُتَغيِّر قبل إمكانية اِستخدَامه، بما في ذلك المُتَغيِّر المُتحكِّم بالحَلْقة (loop control variable) المُستخدَم ضِمْن حَلْقة التَكْرار for. صَرَّحنا عن هذا المُتَغيِّر بكونه من النوع العددي الصحيح (int) بالأمثلة التي فحصناها حتى الآن بهذا القسم. مع ذلك، فإنه ليس أمرًا حتميًا، فقد يكون من نوع آخر. فمثلًا، تَستخدِم حَلْقة التَكْرار for -بالمثال التالي- مُتَغيِّرًا من النوع char، وتعتمد على إمكانية تطبيق عَامِل الزيادة ++ على كُلًا من الحروف والأرقام: char ch; // المتغير المتحكم بالحلقة; for ( ch = 'A'; ch <= 'Z'; ch++ ) // اطبع حرف الأبجدية الحالي System.out.print(ch); System.out.println(); مسألة عد القواسم (divisors) سنُلقِي الآن نظرة على مسألة أكثر جدية، والتي يُمكِن حلّها باِستخدَام حَلْقة التَكْرار for. بفَرْض أن لدينا عددين صحيحين موجبين (positive integers) N و D. إذا كان باقي قسمة (remainder) العدد N على العدد D مُساوٍ للصفر، يُقال عندها أن الثاني قَاسِمًا (divisor) للأول أو أن الأول مُضاعَفًا (even multiple) للثاني. بالمثل، يُقال -بتعبير لغة الجافا- أن العدد D قَاسِمًا (divisor) للعدد N إذا تَحقَّق الشَّرْط N % D == 0، حيث % هو عَامِل باقي القسمة. يَسمَح البرنامج التالي للمُستخدِم بإِدْخَال عدد صحيح موجب (positive integer)، ثُمَّ يَحسِب عَدَد القواسم (divisors) المختلفة لذلك العَدَد. لحِساب عَدَد قواسم (divisors) عَدَد معين N، يُمكننا ببساطة فَحْص جميع الأَعْدَاد التي يُحتمَل أن تكون قَاسِمًا للعَدَد N، أيّ جميع الأَعْدَاد الواقعة بدايةً من الواحد ووصولًا للعَدَد N (١، ٢، ٣، … ،N). ثم نَعدّ منها فقط تلكم التي أَمكنها التقسيم الفعليّ للعَدَد N تقسيمًا مُتعادلًا (evenly). على الرغم من أن هذه الطريقة ستُؤدي إلى نتائج صحيحة، فلربما هي ليست الطريقة الأكثر كفاءة لحلّ هذه المسألة. تَسْتَعْرِض الشيفرة التالية الخوارزمية بالشيفرة الوهمية (pseudocode): // اقرأ قيمة عددية موجبة من المستخدم N Get a positive integer, N, from the user // هيئ عداد القواسم Let divisorCount = 0 // لكل عدد من القيمة واحد وحتى القيمة العددية المدخلة testDivisor for each number, testDivisor, in the range from 1 to N: // إذا كان العدد الحالي قاسم للعدد المدخل if testDivisor is a divisor of N: // أزد قيمة العداد بمقدار الواحد Count it by adding 1 to divisorCount // اطبع قيمة العداد Output the count تَسْتَعْرِض الخوارزمية السابقة واحدة من الأنماط البرمجية (programming pattern) الشائعة، والتي تُستخدَم عندما يكون لديك مُتتالية (sequence) من العناصر، وتَرغَب بمعالجة بعضًا من تلك العناصر فقط، وليس كلها. يُمكِن تَعْمِيم هذا النمط للصيغة التالية: // لكل عنصر بالمتتالية for each item in the sequence: // إذا نجح العنصر الحالي بالاختبار if the item passes the test: // عالج العنصر الحالي process it يُمكننا تَحْوِيل حَلْقة التَكْرار for الموجودة ضِمْن خوارزمية عَدّ القواسم بالأعلى (divisor-counting algorithm) إلى لغة الجافا كالتالي: for (testDivisor = 1; testDivisor <= N; testDivisor++) { if ( N % testDivisor == 0 ) divisorCount++; } بإمكان الحواسيب الحديثة تَّنْفيذ حَلْقة التَكْرار (loop) بالأعلى بسرعة، بل لا يَسْتَحِيل حتى تَّنْفيذها على أكبر عَدَد يُمكن أن يَحمله النوع int، والذي يَصِل إلى ٢١٤٧٤٨٣٦٤٧، ربما حتى قد تَستخدِم النوع long للسماح بأعداد أكبر، ولكن بالطبع سيستغرق تَّنْفيذ الخوارزمية وقتًا أطول مع الأعداد الكبيرة جدًا، ولذلك تَقَرَّر إِجراء تعديل على الخوارزمية بهدف طباعة نقطة (dot) -تَعمَل كمؤشر- بَعْد كل مرة ينتهي فيها الحاسوب من اختبار عشرة مليون قَاسِم (divisor) مُحتمَل جديد. سنضطر في النسخة المُعدَّلة من الخوارزمية إلى الإبقاء على عَدَّادين (counters) منفصلين: الأول منهما لعَدّ القواسم (divisors) الفعليّة التي تحصَّلَنا عليها، والآخر لعَدّ جميع الأعداد التي اُختبرت حتى الآن. عندما يَصل العَدَّاد الثاني إلى قيمة عشرة ملايين، سيَطبع البرنامج نقطة .، ثُمَّ يُعيد ضَبْط قيمة ذلك العَدَّاد إلى صفر؛ ليبدأ العَدّ من جديد. تُصبِح الخوارزمية باِستخدَام الشيفرة الوهمية كالتالي: // اقرأ عدد صحيح موجب من المستخدم Get a positive integer, N, from the user Let divisorCount = 0 // عدد القواسم التي تم العثور عليها Let numberTested = 0 // عدد القواسم المحتملة والتي تم اختبارها // اقرأ رد المستخدم إلى المتغير str // لكل عدد يتراوح من القيمة واحد وحتى قيمة العدد المدخل for each number, testDivisor, in the range from 1 to N: // إذا كان العدد الحالي قاسم للعدد المدخل if testDivisor is a divisor of N: // أزد عدد القواسم التي تم العثور عليها بمقدار الواحد Count it by adding 1 to divisorCount // أزد عدد الأعداد المحتملة التي تم اختبارها بمقدار الواحد Add 1 to numberTested // إذا كان عدد الأعداد المحتملة المختبر يساوي عشرة ملايين if numberTested is 10000000: // اطبع نقطة print out a '.' // أعد ضبط عدد الأعداد المختبرة إلى القيمة صفر Reset numberTested to 0 // اطبع عدد القواسم Output the count وأخيرًا، يُمكننا تَحْوِيل الخوارزمية إلى برنامج كامل بلغة الجافا، كالتالي: import textio.TextIO; public class CountDivisors { public static void main(String[] args) { int N; // القيمة العددية المدخلة من قبل المستخدم int testDivisor; // عدد يتراوح من القيمة واحد وحتى N int divisorCount; // عدد قواسم N التي عثر عليها حتى الآن int numberTested; // عدد القواسم المحتملة التي تم اختبارها // اقرأ قيمة عدد صحيح موجبة من المستخدم while (true) { System.out.print("Enter a positive integer: "); N = TextIO.getlnInt(); if (N > 0) break; System.out.println("That number is not positive. Please try again."); } // عِدّ القواسم واطبع نقطة بعد كل عشرة ملايين اختبار divisorCount = 0; numberTested = 0; for (testDivisor = 1; testDivisor <= N; testDivisor++) { if ( N % testDivisor == 0 ) divisorCount++; numberTested++; if (numberTested == 10000000) { System.out.print('.'); numberTested = 0; } } // اعرض النتائج System.out.println(); System.out.println("The number of divisors of " + N + " is " + divisorCount); } // نهاية البرنامج main } // نهاية الصنف CountDivisors حلقات for المتداخلة كما ذَكَرنا مُسْبَّقًا، فإن بُنَى التحكُّم (control structures) بلغة الجافا هي ببساطة تَعْليمَات مُركَّبة، أي تَتضمَّن مجموعة تَعْليمَات. في الحقيقة، يُمكن أيضًا لبِنْية تحكُّم (control structure) أن تَشتمِل على بِنْية تحكُّم أخرى أو أكثر، سواء كانت من نفس النوع أو من أيّ نوع آخر، ويُطلَق عليها في تلك الحالة اسم بُنَى التحكُّم المُتداخِلة (nested). لقد مررنا بالفعل على عدة أمثلة تَتضمَّن هذا النوع من البُنَى، فمثلًا رأينا تَعْليمَات if ضِمْن حَلْقات تَكْرارية (loops)، كما رأينا حَلْقة while داخلية (inner) مُضمَّنة بداخل حَلْقة while آخرى خارجية. لا يَقتصر الأمر على هذه الأمثلة؛ حيث يُسمَح عامةً بدمج بُنَى التحكُّم بأي طريقة ممكنة، وتستطيع حتى القيام بذلك على عدة مستويات من التَدَاخُل (levels of nesting)، فمثلًا يُمكن لحَلْقة while أن تَحتوِي على تَعْليمَة if، والتي بدورها قد تَحتوِي على تَعْليمَة while آخرى؛ فلغة الجافا Java لا تَضع عامةً أي قيود على عدد مستويات التَدَاخُل المسموح بها، ومع ذلك يَصعُب عمليًا فهم الشيفرة إذا اِحْتَوت على أكثر من عدد قليل من مستويات التَدَاخُل (levels of nesting). تَستخدِم كثير من الخوارزميات حَلْقات for المُتداخِلة (nested)، لذا من المهم أن تفهم طريقة عملها. دعنا نَفحْص عدة أمثلة، مثلًا، مسألة طباعة جدول الضرب (multiplication table) على الصورة التالية: 1 2 3 4 5 6 7 8 9 10 11 12 2 4 6 8 10 12 14 16 18 20 22 24 3 6 9 12 15 18 21 24 27 30 33 36 4 8 12 16 20 24 28 32 36 40 44 48 5 10 15 20 25 30 35 40 45 50 55 60 6 12 18 24 30 36 42 48 54 60 66 72 7 14 21 28 35 42 49 56 63 70 77 84 8 16 24 32 40 48 56 64 72 80 88 96 9 18 27 36 45 54 63 72 81 90 99 108 10 20 30 40 50 60 70 80 90 100 110 120 11 22 33 44 55 66 77 88 99 110 121 132 12 24 36 48 60 72 84 96 108 120 132 144 نُظِّمت البيانات بالجدول إلى ١٢ صف و ١٢ عمود. اُنظر الخوارزمية التالية -بالشيفرة الوهمية (pseudocode)- لطباعة جدول مُشابه: for each rowNumber = 1, 2, 3, ..., 12: // اطبع بسَطر منفصل المضاعفات الاثنى عشر الأولى من قيمة المتغير Print the first twelve multiples of rowNumber on one line // اطبع محرف العودة الى بداية السطر Output a carriage return في الواقع، يُمكن للسطر الأول بمَتْن حَلْقة for بالأعلى "اطبع بسَطر منفصل المضاعفات الاثنى عشر الأولى من قيمة المتغير الحالية" أن يُكتَب على صورة حَلْقة for أخرى منفصلة كالتالي: for N = 1, 2, 3, ..., 12: Print N * rowNumber تحتوي الآن النسخة المُعدَّلة من خوارزمية طباعة جدول الضرب على حَلْقتي for مُتداخِلتين، كالتالي: for each rowNumber = 1, 2, 3, ..., 12: for N = 1, 2, 3, ..., 12: Print N * rowNumber // اطبع محرف العودة الى بداية السطر Output a carriage return يُمكن اِستخدَام مُحدِّدات الصيغة (format specifier) عند طباعة خَرْج ما (output)؛ بهدف تخصيص صيغة هذا الخَرْج، ولهذا سنَستخدِم مُحدِّد الصيغة %4d عند طباعة أيّ عَدَد بالجدول؛ وذلك لجعله يَحْتلَّ أربعة خانات دائمًا دون النظر لعَدَد الخانات المطلوبة فعليًا، مما يُحسِن من شكل الجدول النهائي. بفَرْض أنه قد تم الإعلان عن المُتَغيِّرين rowNumber و N بحيث يَكُونا من النوع العددي int، يمكن عندها كتابة الخوارزمية بلغة الجافا، كالتالي: for ( rowNumber = 1; rowNumber <= 12; rowNumber++ ) { for ( N = 1; N <= 12; N++ ) { // اطبع الرقم بأربع خانات بدون طباعة محرف العودة الى بداية السطر System.out.printf( "%4d", N * rowNumber ); } // اطبع محرف العودة الى بداية السطر System.out.println(); } ربما قد لاحظت أن جميع الأمثلة التي تَعْرَضنا لها -خلال هذا القسم- حتى الآن تَتعامَل فقط مع الأعداد، لذلك سننتقل خلال المثال التالي إلى معالجة النصوص (text processing). لنفْترِض أن لدينا سِلسِلة نصية (string)، ونريد كتابة برنامج لتَحْدِيد الحروف الأبجدية (letters of the alphabet) الموجودة بتلك السِلسِلة. فمثلًا، إذا كان لدينا السِلسِلة النصية "أهلًا بالعالم"، فإن الحروف الموجودة بها هي الألف، والباء، والعين، واللام، والميم، والهاء. سيَستقبِل البرنامج، بالتَحْدِيد، سِلسِلة نصية من المُستخدِم، ثم يَعرِض قائمة بكل تلك الحروف المختلفة الموجودة ضمن تلك السِلسِلة، بالإضافة إلى عَدَدها. كالعادة، سنبدأ أولًا بكتابة الخوارزمية بصيغة الشيفرة الوهمية (pseudocode)، كالتالي: // اطلب من المستخدم إدخال سلسلة نصية Ask the user to input a string // اقرأ رد المستخدم إلى المتغير str Read the response into a variable, str // هيئ عداد لعدّ الحروف المختلفة Let count = 0 (for counting the number of different letters) // لكل حرف أبجدي for each letter of the alphabet: // إذا كان الحرف موجودًا بالسلسلة النصية if the letter occurs in str: // اطبع الحرف Print the letter // أزد قيمة العداد Add 1 to count // اطبع قيمة العداد Output the count سنَستخدِم الدالة TextIO.getln() لقراءة السَطْر الذي أَدْخَله المُستخدِم بالكامل؛ وذلك لحاجتنا إلى معالجته على خطوة واحدة. يُمكننا تَحْوِيل سَطْر الخوارزمية "لكل حرف أبجدي" إلى حَلْقة التَكْرار for كالتالي for (letter='A'; letter<='Z'; letter++). في المقابل، سنحتاج إلى التفكير قليلًا بالطريقة التي سنكتب بها تَعْليمَة if الموجودة ضِمْن تلك الحَلْقة. نُريد تَحْدِيدًا إيجاد طريقة نتحقَّق من خلالها إذا ما كان الحرف الأبجدي الحالي بالتَكْرار (iteration) letter موجودًا بالسِلسِلة النصية str أم لا. أحد الحلول هو المرور على جميع حروف السِلسِلة النصية str حرفًا حرفًا، لفَحْص ما إذا كان أيًا منها مُساو لقيمة الحرف الأبجدي الحالي letter، ولهذا سنَستخدِم الدالة str.charAt(i) لجَلْب الحرف الموجود بموقع معين i بالسِلسِلة النصية str، بحيث تَتراوح قيمة i من الصفر وحتى عدد حروف السِلسِلة النصية، والتي يُمكن حِسَابها باِستخدَام التعبير str.length() - 1. سنواجه مشكلة أخرى، وهي إمكانية وجود الحرف الأبجدي بالسِلسِلة النصية str على صورتين، كحرف كبير (upper case) أو كحرف صغير (lower case). فمثلًا قد يكون الحرف A على الصورة A أو a، ولذلك نحن بحاجة لفَحْص كلتا الحالتين. قد نتجنب، في المقابل، هذه المشكلة بتَحْوِيل جميع حروف السِلسِلة النصية str إلى الحروف الكبيرة (upper case) قبل بدء المعالجة، وعندها نستطيع فَحْص الحروف الكبيرة (upper case) فقط. والآن نُعيد صياغة الخوارزمية كالتالي: // اطلب من المستخدم إدخال سلسلة نصية Ask the user to input a string // اقرأ رد المستخدم إلى المتغير str Read the response into a variable, str // كَبِّر حروف السلسلة النصية Convert str to upper case // هيئ عداد لعدّ الحروف المختلفة Let count = 0 for letter = 'A', 'B', ..., 'Z': for i = 0, 1, ..., str.length()-1: if letter == str.charAt(i): // اطبع الحرف Print letter // أزد قيمة العداد Add 1 to count // اخرج من الحلقة لتجنب إعادة عدّ الحرف أكثر من مرة break Output the count لاحظ اِستخدَامنا لتَعْليمَة break داخل حَلْقة تَكْرار for الداخلية؛ حتى نتجنب طباعة حرف الأبجدية الحالي letter وعَدّه مجددًا إذا كان موجودًا أكثر من مرة بالسِلسِلة النصية. تُوقِف تَعْليمَة break حَلْقة التَكْرار for الداخلية فقط (inner loop)، وليس الخارجية (outer loop)، والتي ينتقل الحاسوب في الواقع إلى تَّنْفيذها بمجرد خروجه من الحَلْقة الداخلية، ولكن مع حرف الأبجدية التالي. حاول استكشاف القيمة النهائية للمُتَغيِّر count في حالة حَذْف تَعْليمَة break. تَسْتَعْرِض الشيفرة التالية البرنامج بالكامل بلغة الجافا: import textio.TextIO; public class ListLetters { public static void main(String[] args) { String str; // السطر المدخل من قبل المستخدم int count; // عدد الحروف المختلفة الموجودة بالسلسلة النصية char letter; System.out.println("Please type in a line of text."); str = TextIO.getln(); str = str.toUpperCase(); count = 0; System.out.println("Your input contains the following letters:"); System.out.println(); System.out.print(" "); for ( letter = 'A'; letter <= 'Z'; letter++ ) { int i; // موضع الحرف بالسِلسِلة النصية for ( i = 0; i < str.length(); i++ ) { if ( letter == str.charAt(i) ) { System.out.print(letter); System.out.print(' '); count++; break; } } } System.out.println(); System.out.println(); System.out.println("There were " + count + " different letters."); } // نهاية البرنامج main } // نهاية الصنف ListLetters في الواقع، تتوفَّر الدالة str.indexOf(letter) المبنية مُسْبَّقًا (built-in function)، والمُستخدَمة لاختبار ما إذا كان الحرف letter موجودًا بالسِلسِلة النصية str أم لا. إذا لم يكن الحرف موجودًا بالسِلسِلة، ستُعيد الدالة القيمة -1، أما إذا كان موجودًا، فإنها ستُعيد قيمة أكبر من أو تُساوي الصفر. ولهذا كان يمكننا ببساطة إجراء عملية فَحْص وجود الحرف بالسِلسِلة باِستخدَام التعبير if (str.indexOf(letter) >= 0)، بدلًا من اِستخدَام حَلْقة تَكْرار مُتداخِلة (nested loop). يتضح لنا من خلال هذا المثال كيف يمكننا اِستخدَام البرامج الفرعية (subroutines)؛ لتبسيط المسائل المعقدة ولكتابة شيفرة مَقْرُوءة. ترجمة -بتصرّف- للقسم Section 4: The for Statement من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.
-
تُصنَّف التَعْليمات البرمجية (statements) بأي لغة برمجة -ومنها الجافا Java- إلى تَعْليمات بسيطة (simple) وأخرى مُركَّبة (compound). تُعدّ التَعْليمات البسيطة -مثل تَعْليمَة الإِسْناد (assignment) وتَعْليمَة اِسْتدعاء البرامج الفرعية (subroutine call)- اللَبِنة الأساسية لأيّ برنامج. في المُقابل، تتكون التَعْليمات المُركَّبة -مثل تَعْليمَة حَلْقة التَكْرار while وتَعْليمَة التَفْرِيع الشَّرطيّة if- من عدد من التَعْليمات البسيطة، وتُعرَف باسم بُنَى التحكُّم (control structures)؛ نظرًا لأنها تَتَحكَّم بترتيب تَّنْفيذ التَعْليمات. سنتناول بُنَى التحكُّم (control structures) المُدَعَّمة بلغة الجافا Java بشئٍ من التفصيل خلال الأقسام الخمسة التالية، بحيث نبدأ في هذا القسم بتَعْليمتي الحَلْقة while و do..while. سنتَعرَّض، خلال ذلك، لكثير من الأمثلة البرمجية التي تُوظِّف بُنَى التحكُّم تلك، كما أننا وبينما نقوم بذلك، سنُطبِّق التقنيات المُتَّبَعة بتصميم الخوارزميات (algorithms)، والتي قد تَعرَّضنا لها بالفصل السابق. تَعْليمَة while تُكتَب تَعْليمَة حَلْقة التَكْرار (loop) while -والتي قد تَعرَّضت لها مُسْبقًا بالقسم ٣.١- بالصياغة التالية: while ( <boolean-expression> ) <statement> لا يُشترَط أن تتكوَّن التعليمة -المُشار إليها بالأعلى، والمعروفة باسم مَتْن حَلْقة التَكْرار (loop body)- من تَعْليمَة واحدة فقط، فبالطبع يُمكنها أن تكون كُتليّة البِنْية (block statement)، بحيث تَتضمَّن عدة تَعْليمَات مُحَاطة بزوج من الأقواس. يَتكرَّر تَّنْفيذ مَتْن الحَلْقة (body of the loop) طالما كان التعبير -المُشار إليه بالأعلى- مُتحقِّقًا. تتكون تلك العبارة من تَعبير منطقي (boolean expression)، وتُعرَف باسم الشَّرْط الاستمراري (continuation condition) لحَلْقة التَكْرار أو باسم اختبار حَلْقة التَكْرار. نحتاج الآن لإِيضاح عدة نقاط بشيء من التفصيل. أولًا، ماذا يَحدُث إذا لم يَتحقَّق شَّرْط حَلْقة التَكْرار ولو لمرة واحدة على الأقل، بمعنى أنه لم يَكُن متحقِّقًا قبل التَّنْفيذ الأوَّليّ لمَتْن الحَلْقة؟ في هذه الحالة، لا يُنفَّذ مَتْن الحَلْقة (body) نهائيًا، وهو ما يعني أن مَتْن حَلْقة while يُمكِن أن يُنفَّذ أيّ عدد من المرات بما في ذلك الصفر. ثانيًا، ماذا لو كان شَّرْط حَلْقة التَكْرار مُتحقِّقًا، لكن وبينما يُنفَّذ مَتْن الحَلْقة لم يَعُدْ الشَّرْط كذلك؟ هل يَتوقَف تَّنْفيذ الحَلْقة بمجرد حُدُوث ذلك؟ ببساطة لا؛ حيث يَستمِر الحاسوب بتَّنْفيذ مَتْن الحَلْقة بالكامل حتى يَصِل إلى نهايته، وعِندها فقط يَقفز عائدًا إلى بداية حَلْقة التَكْرار لفَحْص شَّرْطها (continuation condition) مُجددًا، ومِنْ ثَمَّ، يَستطيع في تلك اللحظة فقط إِيقاف الحَلْقة وليس قبل ذلك. لنفْحَص إِحدى المشكلات التي تُحلّ باِستخدَام حَلْقة التَكْرار while: حِساب قيمة متوسط (average) مجموعة من الأعداد الصحيحة الموجبة (positive integers)، والتي يَتم إِدْخالها من قِبَل المُستخدِم. تُحسَب قيمة المتوسط (average) عمومًا بحِساب حاصل مجموع الأعداد ثم قِسمته على عَدَدها. سيَطلب البرنامج من المُستخدِم إِدْخال عدد صحيح (integer) وحيد في المرة، وسيَحتفِظ دومًا بقيمة المجموع الكلي للأعداد الصحيحة المُدْخَلة حتى اللحظة الراهنة، وكذلك عَدَدها، كما سيُبقي هذه القيم مُحْدَثة مع كل عملية إِدْخال. هاك خوارزمية البرنامج (algorithm) مَكتوبة بأسلوب الشيفرة الوهمية (pseudocode): Let sum = 0 Let count = 0 // طالما لا يوجد المزيد من الأعداد الصحيحة للمعالجة while there are more integers to process: // اقرأ عدد صحيح Read an integer // أضف قيمته إلى المتغير sum Add it to the sum // أزد قيمة العداد Count it // اقسم المجموع sum على count لحساب قيمة المتوسط Divide sum by count to get the average // اطبع قيمة المتوسط Print out the average لكن كيف سنتحقَّق فعليًا من شَّرْط الحَلْقة بالأعلى "طالما لا يوجد المزيد من الأعداد الصحيحة للمعالجة"؟ أحد الحلول هو إِبلاغ المُستخدِم بأن يُدخِل القيمة صفر بعد أن يَنتهِي من إِدْخال جميع البيانات الفعليَّة. ستَنجح تلك الطريقة نظرًا لأننا نَفترِض أن البيانات الفعليَّة المُدْخَلة لابُدّ وأن تَكون من الأعداد الصحيحة الموجبة، وهو ما يَعني أن الصفر يُعدّ قيمة غيْر صالحة من الأساس. لاحِظ أن الصفر هنا ليس قيمة بحدّ ذاتِها يَنبغي تَضْمِينها مع الأعداد المطلوب حِساب مُتوسطها، وإِنما هي فقط مطلوبة كعلامة للإشارة إلى نهاية البيانات الفعليَّة. يُطلق أحيانًا على قِيَم البيانات المُستخدَمة بطريقة مُشابهة اسم بَيَان البداية/النهاية (sentinel value). سنُعدِّل الآن اختبار حَلْقة التَكْرار while ليُصبِح "طالما العدد الصحيح المُدخل لا يساوي الصفر". ستُواجهنا مشكلة آخرى، عندما يُنفَّذ اختبار حَلْقة التَكْرار (loop) لأول مرة، أي قَبْل التَّنْفيذ الأوَّليّ لمَتْن الحَلْقة، لن يكون هناك عدد صحيح قد قُرأ بَعْد، بل ليس هناك أيّ بيانات قد قُرِأت أساسًا، مما يَعني أنه لا يوجد "عدد صحيح مُدخل". ولهذا، يُصبِح اختبار الحَلْقة "طالما العدد الصحيح المُدخل لا يساوي الصفر" -ضِمْن هذا السياق- غَيْر ذي مَعنى. لذلك يَنبغي القيام بأمر ما قَبل البدء بتَّنْفيذ حَلْقة التَكْرار while للتأكد من صلاحية الاختبار حتى مع أول تَّنْفيذ له. تُعرَف هذه العملية باسم التهيئة المبدئية (priming the loop) لحَلْقة التَكْرار، وفي هذه الحالة بالتحديد، يُمكننا ببساطة تهيئة الحَلْقة عن طريق قراءة أول عدد صحيح مُدْخَل قَبل بدء تَّنْفيذ حَلْقة التَكْرار. هاك الخوارزمية (algorithm) المُعدَّلة: Let sum = 0 Let count = 0 // اقرأ عدد صحيح Read an integer // طالما العدد الصحيح المُدخل لا يساوي الصفر while the integer is not zero: // أضف قيمته إلى المتغير sum Add the integer to the sum // أزد قيمة العداد Count it // اقرأ عدد صحيح Read an integer // اقسم المجموع sum على count لحساب قيمة المتوسط Divide sum by count to get the average // اطبع قيمة المتوسط Print out the average لاحِظ إعادة ترتيب تَعْليمَات مَتْن حَلْقة التَكْرار (loop body)؛ لأنه لمّا أصْبَحت أول محاولة لقراءة عدد صحيح (integer) تَحدُث قَبْل بداية حَلْقة التَكْرار (loop)، كان لابُدّ لمَتْن الحَلْقة من أن يبدأ أولًا بمُعالجة ذلك العدد. ثُمَّ بنهاية المَتْن، سيُحاول الحاسوب قراءة عدد صحيح جديد، ليَقفِز بَعْدها عائدًا لبداية الحَلْقة ليَختبِر العدد الجديد المَقروء للتو. عندما يَقرأ الحاسوب قيمة بَيَان النهاية (sentinel value)، ستَتوقَف الحَلْقة، ومِنْ ثَمَّ لن تُعالَج تلك القيمة، كما أنها لن تُضاف إلى حاصل المجموع sum، أو هذه هى الطريقة التي ينبغي أن تَعمَل بها الخوارزمية على الأقل؛ لأن قيمة بَيَان النهاية ليست جزءً من البيانات الفعليَّة كما ذَكَرنا مُسْبَّقًا. لا تَعمَل الخوارزمية الأصلية طِبقًا لذلك بطريقة صحيحة -بفَرْض إِمكانية تَّنْفيذها بدون إِجراء التهيئة المبدئية (priming) بطريقة ما-؛ لأنها ستَحسِب حاصل مجموع جميع الأعداد الصحيحة وكذلك عَدَدها (count) بما يتَضمَّن قيمة بَيَان النهاية (sentinel value). لمّا كانت تلك القيمة -المُتفق عليها- تُساوي الصفر، فإن حاصل المجموع سيظِلّ لحسن الحظ صحيحًا، ولكن في المقابل سيكون عَدَدها (count) خطأ بفارق واحد. يُطلَق على الأخطاء المُشابهة اسم الأخطاء بفارق الواحد (off-by-one errors)، وهي واحدة من أكثر الأخطاء شيوعًا. اِتضح أن العَدّ أصعب مما قد يبدو عليه. سنُحوِّل الآن الخورازمية إلى برنامج كامل، نُلاحِظ أولًا أنه لا يُمكِن اِستخدَام التَعْليمَة average = sum/count; لحساب قيمة المتوسط (average)؛ لأنه لمّا كانت قيمة كُلًا من المُتَغيِّرين sum و count من النوع العددي الصحيح int، فإن حاصل قِسمة الأول على الثاني sum/count ستكون أيضًا من النوع العددي الصحيح int، في حين ينبغي للمتوسط أن يكون عددًا حقيقيًا (real number). لقد واجهنا هذه المشكلة مِن قَبْل، ويَتلخَّص حَلّها بضرورة تَحْوِيل واحد من القيمتين إلى النوع double؛ وذلك لإجبار الحاسوب على حِسَاب قيمة حاصل القِسمة (quotient) كعَدَد حقيقي (real number). يُمكِن القيام بذلك عن طريق إجراء عملية التَحْوِيل بين الأنواع (type-casting) على أحد المُتَغيِّرين على الأقل، بحيث يُحوَّل إلى النوع double عن طريق اِستخدَام (double)sum. ولهذا يَحسِب البرنامج قيمة المتوسط باستخدام التعبير average = ((double)sum) / count;. يَتوفَّر حلّ آخر، وهو التَّصْريح (declaration) عن المُتَغيِّر sum بحيث يكون أساسًا من النوع double. حُلَّت مشكلة آخرى بالبرنامج التالي، وهي أنه في حالة إِدْخال المُستخدِم القيمة صفر كأول قيمة مُدْخَل، فلن تتوفَّر أيّ بيانات للمُعالجة، ولهذا يُمكننا اختبار حُدُوث تلك الحالة بفَحْص ما إذا كانت قيمة المُتَغيِّر count ما تزال مُساوية للصفر حتى بَعْد انتهاء حَلْقة التَكْرار while. قد تبدو هذه المشكلة ثانوية، ولكن لابُدّ أن يُغطي أيّ مبرمج مُنتبِه جميع الحالات المُمكنة. انظر شيفرة البرنامج بالكامل: import textio.TextIO; public class ComputeAverage { public static void main(String[] args) { int inputNumber; // إحدى القيم المدخلة من قبل المستخدم int sum; // حاصل مجموع الأعداد الصحيحة الموجبة int count; // عدد الأعداد الصحيحة double average; // قيمة متوسط الأعداد الصحيحة الموجبة // هيئ قيمة كلا من متغير المجموع والعداد sum = 0; count = 0; // اقرأ مدخل من المستخدم لمعالجته System.out.print("Enter your first positive integer: "); inputNumber = TextIO.getlnInt(); while (inputNumber != 0) { // أضف قيمة المتغير للمجموع الحالي sum += inputNumber; // أزد قيمة العداد بمقدار الواحد count++; System.out.print("Enter your next positive integer, or 0 to end: "); inputNumber = TextIO.getlnInt(); } // اعرض النتائج if (count == 0) { System.out.println("You didn't enter any data!"); } else { average = ((double)sum) / count; System.out.println(); System.out.println("You entered " + count + " positive integers."); System.out.printf("Their average is %1.3f.\n", average); } } // نهاية البرنامج main } // نهاية الصنف ComputeAverage تعليمة do..while تَفحْص تَعْليمَة حَلْقة التَكْرار while الشَّرْط الاستمراري لحَلْقة التَكْرار (continuation condition) ببداية الحَلْقة، ولكن أحيانًا ما يكون من الأنسب فَحْصه بنهاية الحَلْقة لا بدايتها، وهذا في الواقع ما تُوفِّره تَعْليمَة do..while. تَتشابه صياغة (syntax) تَعْليمَة الحَلْقة do..while مع تَعْليمَة while تمامًا باستثناء بعض التَغْيِيرات الطفيفة، حيث تُنْقَل كلمة while مصحوبة مع الشَّرْط إلى نهاية الحَلْقة، في حين تُضاف كلمة do في بداية الحَلْقة بدلًا منها. تُكتَب تَعْليمَة حَلْقة التَكْرار do..while بالصياغة التالية: do <statement> while ( <boolean-expression> ); إذا كانت التعليمة كُتلَة (block) بذاتها، تُصاغ التَعْليمَة كالتالي: do { <statements> } while ( <boolean-expression> ); لاحِظ وجود الفاصلة المَنقوطة (semicolon) ; بنهاية التَعْليمَة do..while. تُعدّ هذه الفاصلة جزءً أساسيًا من التَعْليمَة do..while، مثلما تُعدّ الفاصلة بنهاية أي تَعْليمَة أخرى -كالإِسْناد (assignment) أو التَّصْريح (declaration)- جزءً أساسيًا منها. ولهذا سيؤدي حَذْف الفاصلة المنقوطة ;، في هذه الحالة، إلى التَسبُّب بحُدوث خطأ في بناء الجملة (syntax error). ينبغي عمومًا أن تنتهي أيّ تَعْليمَة برمجية -بلغة الجافا Java- إِمّا بفاصلة منقوطة ; أو بقوس مُغلِق {. عندما يُنفَّذ الحاسوب تَعْليمَة حَلْقة التَكْرار do..while، فإنه يبدأ أولًا بتَّنْفيذ مَتْنها (loop body)-التَعْليمَة أو مجموعة التَعْليمَات بداخل الحَلْقة-، ثم يَحسِب قيمة التعبير المنطقي (boolean expression)، فإذا كانت مُساوية للقيمة المنطقية true، فإنه يَقفِز عَائدًا إلى بداية الحَلْقة ويُعيد تَّنْفيذ مَتْنها، لتستمر العملية بَعْدها بنفس الطريقة. أما إذا كانت قيمة التعبير المنطقي مُساوية للقيمة المنطقية false، فإنه يَتوقَف عن تَّنْفيذ الحَلْقة، وينتقل إلى الجزء التالي من الشيفرة. لاحظ أنه لمّا كان شَّرْط الحَلْقة لا يُفحْص إلا بنهاية الحَلْقة، فإن مَتْن حَلْقة التَكْرار do..while دائمًا ما يُنفَّذ مرة واحدة على الأقل. على سبيل المثال، اُنظر الشيفرة الوهمية (pseudocode) التالية لبرنامج لعبة. ستجد أن استخدام تَعْليمَة الحَلْقة do..while أكثر مُلائمة في هذه الحالة؛ لأنها ستَضمَن أن المُستخدِم قد لعِبَ مباراة واحدة على الأقل. علاوة على ذلك، فإن شَّرْط حَلْقة التَكْرار بالأسفل لن يكون له أيّ مَعنى في حالة تَّنْفيذه في بداية الحَلْقة. do { // العب مباراة Play a Game // اسأل المستخدم إذا ما كان يريد اللعب مرة آخرى Ask user if he wants to play another game // اقرأ رد المستخدم Read the user's response // أعد التكرار طالما كان رد المستخدم هو نعم } while ( the user's response is yes ); سنُحوِّل الآن هذه الشيفرة الوهمية إلى لغة الجافا. بدايةً ولكي نتجنب الخَوْض في تفاصيل ليست ذا أهمية، سنفترض أن لدينا صَنْف (class) يَحمِل اسم Checkers. أحد أعضاء (member) هذا الصَنْف هو البرنامج الفرعي الساكن (static subroutine) playGame(). يُغلِّف هذا البرنامج الفرعي التفاصيل الخاصة باللعبة، ويُمثِل استدعائه إجراء مباراة دَّامَا (checkers) واحدة مع المُستخدِم. يُمكننا الآن اِستخدَام تَعْليمَة استدعاء البرنامج الفرعي (subroutine call) الساكن Checkers.playGame(); كبديل عن سَطْر الشيفرة "العب مباراة". سنَستخدِم الصنف TextIO لتَلَقِّي رد المُستخدِم على سؤال من النوع نعم أم لا؛ حيث يُوفِّر هذا النوع الدالة (function) TextIO.getlnBoolean()، والتي تَسمَح للمُستخدِم بإِدْخال إحدى القيمتين "Yes/No" ضِمْن عدة ردود أُخرى صالحة، بحيث يؤول الرد "Yes" إلى القيمة المنطقية true، بينما يؤول الرد "No" إلى القيمة المنطقية false. سنحتاج الآن إلى مُتَغيِّر لتخزين رد المُستخدِم، والذي سيكون بطبيعة الحال من النوع المنطقي (boolean). يُمكن كتابة الخوارزمية كالتالي: // تساوي True في حالة رغب المستخدم باللعب مجددًا boolean wantsToContinue; do { Checkers.playGame(); System.out.print("Do you want to play again? "); wantsToContinue = TextIO.getlnBoolean(); } while (wantsToContinue == true); وفقًا لشَّرْط حَلْقة التَكْرار بالأعلى، فإنه عندما تَتَساوى قيمة المُتَغيِّر المنطقي wantsToContinue مع القيمة المنطقية false، سيكون ذلك إشارة (signal) إلى ضرورة تَوقُف حَلْقة التَكْرار. يُطلَق عادةً على المُتَغيِّرات المنطقية (boolean variables) المُستخدَمة بهذه الطريقة اسم راية (flag) أو مُتَغيِّر راية -هي متغيرات تُضبَط (set) بمكان ما بالشيفرة لتُفحَص قيمتها بمكان آخر-. بالمناسبة، عادةً ما يَسخَر بعض المبرمجين -ربما قد يَصفهم البعض بالمُتَحذلِقين- من شَّرْط حَلْقة التَكْرار while (wantsToContinue == true)؛ وذلك لأنه مُكافئ تمامًا للشَّرْط while (wantsToContinue)؛ فاختبار ما إذا كان التعبير wantsToContinue == true يُعيد القيمة المنطقية true لا يَختلف نهائيًا عن اختبار ما إذا كان المُتَغيِّر wantsToContinue يَحمِل القيمة true ويُعيدها. بالمثل، -وبفَرْض أن لدينا مُتَغيِّر اسمه flag من النوع المنطقي (boolean variable)- يُكافئ التعبير flag == false، والذي يُعدّ أقل فَجاجة من التعبير السابق، تمامًا التعبير !flag، حيث الحرف ! هو عَامِل النفي المنطقي (negation operator). يُفضَّل عمومًا كتابة while (!flag) بدلًا من while (flag == false)، وبالمثل، كتابة if (!flag) بدلًا من if (flag == false). على الرغم من أن اِستخدَام تَعْليمَة حَلْقة التَكْرار do..while أحيانًا ما يكون أكثر ملائمة في بعض المسائل، فعمومًا لا يَجعل وجود نوعين مختلفين من تَعْليمَة الحَلْقة (loops) لغة البرمجة أكثر قوة؛ فبالنهاية، أي مشكلة تُحلّ بإحداهما، ستجد أنه من الممكن حلّها بالآخرى. في الواقع، بفرض أن العبارة تُمثل أي كُتلَة شيفرة، فإن التالي: do { <doSomething> } while ( <boolean-expression> ); يُكافئ تمامًا: <doSomething> while ( <boolean-expression> ) { <doSomething> } بالمثل: while ( <boolean-expression> ) { <doSomething> } يُكافئ: if ( <boolean-expression> ) { do { <doSomething> } while ( <boolean-expression> ); } بدون أي تَغْيِير بمَقصِد البرنامج نهائيًا. تعليمتي break و continue تَسمَح لك صيغتي حَلْقتي التَكْرار while و do..while باختبار الشَّرْط الاستمراري (continuation condition) إِمّا ببداية الحَلْقة أو بنهايتها على الترتيب. ولكن أحيانًا سترغب في إجراء عملية اختبار شَّرْط أثناء تَّنْفيذ الحَلْقة، أيّ داخل مَتْن الحَلْقة نفسها، أو حتى قد ترغب بإجراء أكثر من عملية اختبار بأماكن مختلفة داخل نفس الحَلْقة. تُستخدَم تَعْليمَة break بلغة الجافا لإيقاف تَّنْفيذ أيّ حَلْقة تَكْرار (loop)، والخروج منها، وذلك بمجرد استدعائها بأيّ مكان داخل الحَلْقة. تُكتَب كالتالي: break; عندما يُنفِّذ الحاسوب تَعْليمَة break داخل حَلْقة (loop)، فإنه سيَقفِز مُباشرةً خارج الحَلْقة، ويُنفِّذ الشيفرة التالية بالبرنامج والموجودة أسفل الحَلْقة. انظر المثال التالي: while (true) { // يبدو وكأنها ستنفذ بشكل لا نهائي System.out.print("Enter a positive number: "); N = TextIO.getlnInt(); // إما أن تكون قيمة المدخل سليمة أو اقفز خارج الحلقة if (N > 0) break; System.out.println("Your answer must be > 0."); } // اكمل هنا بعد break في المثال بالأعلى، إذا أَدْخَل المُستخدِم عدد أكبر من الصفر، سيتحقَّق الشَّرْط، وستُنفَّذ تَعْليمَة break، ولهذا سيَقفِز الحاسوب مباشرة خارج الحَلْقة إلى ما بَعْدها. في المقابل، إذا لم يتحقَّق الشَّرْط، ستُطبَع السِلسِلة النصية "Your answer must be > 0"، ثم سيَقفِز الحاسوب عَائدًا إلى بداية الحَلْقة ليَقرأ مُدْخَل جديد. قد يبدو السَطْر الأول من الشيفرة بالأعلى while (true) غَيْر مألوف نوعًا ما، ولكنه في الواقع سليم تمامًا. يُسمَح عمومًا لأيّ تعبير منطقي (boolean-valued expression) بأن يكون شَّرْطًا لحَلْقة التَكْرار، أيّ لابُدّ فقط أن تؤول قيمته النهائية إلى قيمة منطقية؛ ليَفحْصها الحاسوب لمعرفة ما إذا كانت مُساوية للقيمة true أم للقيمة false. تُعدّ القيمة المُجرَّدة (literal) true تعبيرًا منطقيًا، والذي يؤول دائمًا إلى القيمة المنطقية true، ولذلك يُمكِن اِستخدَامها كشَّرْط للحَلْقة. يُستخدَم الشَّرْط while (true) لكتابة حَلْقة لا نهائية (infinite loop) أو حَلْقة يُفْترَض الخروج منها باِستخدَام تَعْليمَة break. يُسمَح باِستخدَام حَلْقات التَكْرار المُتداخِلة (nested loops)، بمَعنى وجود تَعْليمَة حَلْقة تَكْرار داخل أُخرى، ولذلك لابُدّ لنا من فهم طريقة عَمَل تَعْليمَة break ضِمْن هذا السياق. كقاعدة عامة، تُوقِف تَعْليمَة break حَلْقة التَكْرار الأقرب لها فقط، بمَعنى أنها إذا اُستخدِمت بداخل الحَلْقة الداخلية (inner loop)، فإنها ستُوقِف فقط تلك الحَلْقة، لا الحَلْقة الخارجية (outer loop) التي تَشَمَلها. أما إذا أردت إيقاف تَّنْفيذ الحلقة الخارجية، يُمكِنك اِستخدَام ما يُعرَف باسم تَعْليمَة break المُعنوَنة (labeled break). تَسمَح تَعْليمَة break المُعنوَنة بتحديد صريح للحَلْقة المطلوب الخروج منها. اِستخدَام هذه التَعْليمَة المُعنوَنة غَيْر شائع، ولذلك سنمر عليها سريعًا. تَعمَل العناوين (labels) كالتالي: يُمكِنك ببساطة عَنوَنة أيّ حَلْقة تَكْرار بوَضْع مُعرَّف (identifier) مَتبوع بنقطتان رأسيتان : بمقدمة الحَلْقة. فمثلًا، يمكن عَنوَنة حَلْقة تَكْرار while باِستخدَام mainloop: while.... تستطيع الآن اِستخدَام تَعْليمَة break المُعنوَنة بأيّ مكان داخل هذه الحَلْقة عن طريق اِستخدَام الصيغة break mainloop;، وذلك بهدف الخروج من هذه الحَلْقة تحديدًا. على سبيل المثال، تَفحْص الشيفرة بالأسفل ما إذا كانت السِلسِلتين النصيتين s1 و s2 تحتويان على حرف مُشتَرَك. في حالة تَحقُّق الشَّرْط، ستُسْنَد القيمة المنطقية false إلى مُتَغيِّر الراية nothingInCommon، ثُمَّ تُسْتَدعى تَعْليمَة break المُعنوَنة لإيقاف المعالجة عند تلك النقطة: boolean nothingInCommon; // افترض أن السلسلتين لا يشتركان بأي حرف nothingInCommon = true; // متغيرات حلقتي التكرار والتي ستأخذ قيم حروف السلسلتين int i,j; i = 0; bigloop: while (i < s1.length()) { j = 0; while (j < s2.length()) { // إذا كان هناك حرفًا مشتركا if (s1.charAt(i) == s2.charAt(j)) { // اضبط قيمة المتغير إلى القيمة false nothingInCommon = false; // اخرج من الحلقتين break bigloop; } j++; // Go on to the next char in s2. } i++; //Go on to the next char in s1. } تُعدّ تَعْليمَة continue مرتبطة نوعًا ما بتَعْليمَة break، لكنها أقل اِستخدَامًا منها. تتَخَطَّى تَعْليمَة continue الجزء المُتبقي من التَكْرار الحالي (current iteration) فقط، أي أنها لا تتَسبَّب بالقفز خارج الحَلْقة تمامًا إلى ما بَعْدها كتَعْليمَة break؛ وإنما تَقفِز عائدة إلى بداية نفس الحَلْقة لتَّنْفيذ التَكْرار التالي (next iteration) -بالطبع إذا كان الشَّرْط الاستمراري (continuation condition) لحَلْقة التَكْرار ما زال مُتحقِّقًا. عندما تُستخدَم تَعْليمَة continue داخل حَلْقة مُتداخِلة (nested loop)، فإنها، مثل تَعْليمَة break، تُجرِي هذه العملية على حَلْقة التَكْرار الأقرب لها فقط، أيّ الحَلْقة الداخلية (inner nested loop). وبالمثل، تتوفَّر تَعْليمَة continue المُعنوَنة (labeled continue)، وذلك لتخصيص حَلْقة التَكْرار المراد إجراء عملية continue عليها. يمكن اِستخدَام تَعْليمَتي break و continue داخل جميع حَلْقات التَكْرار مثل while، و do..while، و for. سنتحدث عن الأخيرة تفصيليًا بالقسم التالي. يُمكن أيضًا اِستخدَام تَعْليمَة break للخروج من تَعْليمَة switch، وهو ما سنتناوله بالقسم ٣.٦. يُسمَح أيضًا باِستخدَامها داخل تَعْليمَة التَفْرِيع الشَّرْطيّة if إذا كانت تَعْليمَة التَفْرِيع موجودة إِمّا ضِمْن حَلْقة تَكْراريّة أو ضِمْن تَعْليمَة switch، ولكن عندها لا يكون المقصود هو الخروج من تَعْليمَة التَفْرِيع if، وإنما الخروج من التَعْليمَة التي تَشتَملها، أي من تَعْليمَة الحَلْقة أو من تَعْليمَة switch. بالمثل، يُمكِن اِستخدَام تَعْليمَة continue داخل تَعْليمَة if بنفس الطريقة، وتتبِّع نفس القواعد بالأعلى. ترجمة -بتصرّف- للقسم Section 3: The while and do..while Statements من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.
-
قد تَكُون البرمجة (programming) صعبة نوعًا ما، وذلك كغَيْرها من الأنشطة المُفيدة والجديرة بالاهتمام، ومع ذلك، فهي عادة ما تَكُون مُجْزيّة ومُمتعة. عند كتابة برنامج (program)، لابُدّ أن تُخبِر الحاسوب بكُل تَفصيلة صغيرة يَنبغي له تَّنْفيذها، وبصورة صحيحة تمامًا؛ وذلك لأنه سيَتَّبِع البرنامج كما هو مَكتوب تمامًا وبطريقة عمياء. إذن، كيف تُكتَب البرامج غَيْر البسيطة؟ في الواقع، إنه ليس أمرًا غامضًا، فالأمر بِرُمَّته يَعتمِد فقط على تَعَلُّم طريقة التفكير الصحيحة. يُعدّ البرنامج (program) تعبيرًا عن فكرة (idea)، حيث يبدأ المُبرمج بفكرة عامة عن مُهِمّة (task) معينة يُريد من الحاسوب تَّنْفيذها، وعادةً ما يكون المُبرمج على عِلم بكيفية تَّنْفيذ تلك المُهِمّة يدويًا، أو لديه تَصَوُّر عام على الأقل. تتمحور المشكلة حول طريقة تَحْوِيل هذا التَصَوُّر العام إلى إِجراء (procedure)، واضح، ومُكتمِل، ومُحدَّد الخُطوات لتََّنْفيذ تلك المُهِمّة. يُسمَى مثل هذا الإِجراء باسم "الخوارزمية (algorithm)"، والتي هي إِجراء واضح، ومُحدَّد الخُطوات، والذي لابُدّ أن ينتهي دائمًا بَعْد عَدَد مُتناهي (finite number) من الخُطوات؛ فنحن لا نُريد عدّ الإِجراءات التي قد تَستمِر للأبد. لاحِظ أن الخوارزمية (algorithm) تَختلف عن البرنامج (program)، فالبرنامج يُكتَب بلغة برمجية معينة، أما الخوارزمية، فتُكتَب بأيّ لغة بما فيها الإنجليزية، وهي أَشْبَه بالفكرة وراء البرنامج، ويُقصَد بالفكرة هنا مجموعة الخُطوات المطلوب القيام بها حتى يَتمّ تَّنْفيذ المُهِمّة، وليس مُجرَّد مُلخَّص لما ينبغي للمُهِمّة إِنجازه في النهاية. عند وصف الخوارزمية، ليس من الضروري أن تَكُون الخُطوات مُفَصَّلة، وذلك طالما كانت واضحة بما فيه الكفاية لبَيَان أن تَّنْفيذها سوف يُنجِز المُهِمّة المطلوبة، ولكن بالطبع لا يُمكِن التعبير عنها كبرنامج (program) فِعليّ بدون مَلْئ جميع تلك التفاصيل. من أين تأتي الخوارزميات؟ ينبغي عادةً تطويرها، وهو ما يَتطلَّب كثيرًا من التفكير والعمل الجاد. يُمكِن القول أن عملية تطوير الخوارزميات (algorithm development) هي مهارة تُكتسَب مع المُمارسة المستمرة، ولكن، مع ذلك، تَتَوفَّر بعض التقنيات (techniques) والقواعد الإرشادية (guidelines) التي يُمكِنها مُساعدتك. سنتناول في هذا القسم بَعضًا منها، وبالأخص ما يَتعلَّق بالبرمجة "في نطاق ضيق"، كما سنعود للحديث عن نفس هذا الموضوع أكثر من مرة بالفصول القادمة. الشيفرة الوهمية والتصميم المتدرج عند البرمجة "في نطاق ضيق"، فأنت مُقيَّد نوعًا ما باِستخدَام عَدَد قليل من الأوامر، وهي كالتالي: المُتَغيِّرات (variables)، وتَعْليمَات الإِسْناد (assignment statements)، والبرامج (routines) الخاصة بعَمليتي الإِدْخال (input) والإِخراج (output). قد يَتوفَّر لك أيضًا اِستخدَام بعض البرامج الفرعية (subroutines)، والكائنات (objects)، أو رُبما حتى بعض اللَبِنات الأساسية الآخرى، ولكن بشَّرْط أن تَكُون قد كُتِبَت مُسْبَّقًا إِمّا بواسطتك أو بواسطة شخص آخر (لاحِظ أن برامج الإِدْخال والإِخراج تَقع ضِمْن هذا التصنيف). والآن، تَستطِيع بناء مُتتالية مِنْ تلك التَعْليمَات البسيطة، أو ربما قد تَدمجهم دِاخل بُنَى تحكُّم (control structures) أكثر تعقيدًا، مثل تَعْليمَة حَلْقة التَكْرار (loops) while، وتَعْليمَة التَفْرِيع الشَّرْطيّة if. لنفْترِض أننا نريد برمجة الحاسوب ليُنفِّذ مُهِمّة (task) معينة، يُمكِننا البدء بكتابة تَوصِيف (description/specification) مَبدئي لتلك المُهِمّة، بحيث يُلخِّص وظيفة الخوارزمية (algorithm) المطلوب تَطْويرها، ثُمَّ نُضيف مزيدًا من الخطوات والتفاصيل إلى ذلك التَوصِيف تدريجيًا، وعبر سِلسِلة من التصميمات المُحسَّنة، إلى أن نَصِل إلى الخوارزمية الكاملة التي يُمكِن ترجمتها مباشرة إلى لغة برمجية. تُكتَب عادة تلك التَوصِيفات باِستخدَام ما يُعرَف باسم الشيفرة الوهمية (pseudocode)، وهي مجموعة من التَعْليمَات العَاميَّة، التي تُحاكِي بِنْية اللغات البرمجية، بصورة مُبسَّطة، وبدون قواعد الصيغة (syntax) الصارمة المُعتادة بالشيفرة الفعليّة. يُطلَق على هذا الأسلوب من كتابة البرامج اسم التصميم المُتدرج (stepwise refinement)، ويُصنَّف ضِمْن استراتيجيات التصميم من أعلى لأسفل (top-down design). لنفْحَص كيفية تَطبيق التصميم المتدرج لكتابة إحدى البرامج التي قد تَعرَّضنا لها خلال القسم السابق. يَحسِب هذا البرنامج قيمة الاستثمار خلال خمسة أعوام. يُمكِن تَوصِيف مُهِمّة البرنامج بالعبارة التالية: "اِحسب قيمة الاستثمار واطبعها لكل عام من الأعوام الخمسة التالية، بحيث تُحدَّد قيمة الاستثمار المبدئي وسعر الفائدة مِن قِبَل المُستخدِم". ربما نُفكر بكتابة الشيفرة الوهمية التالية: // اقرأ مدخل المستخدم Get the user's input // احسب قيمة الاستثمار بعد عام Compute the value of the investment after 1 year // اطبع القيمة Display the value // احسب قيمة الاستثمار بعد عامين Compute the value after 2 years // اطبع القيمة Display the value // احسب قيمة الاستثمار بعد ثلاث أعوام Compute the value after 3 years // اطبع القيمة Display the value // احسب قيمة الاستثمار بعد أربع أعوام Compute the value after 4 years // اطبع القيمة Display the value // احسب قيمة الاستثمار بعد خمس أعوام Compute the value after 5 years // اطبع القيمة Display the value على الرغم من أن الخوارزمية بالأعلى سليمة وستؤدي الغرض منها، لكنها مُكرَّرة، وهو ما يَعنِي ضرورة اِستخدَام حَلْقة تَكْرار (loop)؛ لأنها ستُمكِّننا من كتابة شيفرة مُعمَّمة أكثر وبعَدَد سطور أقل. يُقصَد بالتَعْميم (generalization) هنا أنه يُمكِن اِستخدَام نفس حَلْقة التَكْرار بغض النظر عن عدد الأعوام المطلوب مُعالجتها. والآن، سنُعيد كتابة مُتتالية الخطوات السابقة كالتالي: // اقرأ مدخل المستخدم Get the user's input // طالما ما يزال هناك عدد من الأعوام للمعالجة while there are more years to process: // احسب قيمة الاستثمار بعد العام التالي Compute the value after the next year // اطبع القيمة Display the value على الرغم من أن الخوارزمية (algorithm) بالأعلى سليمة، لكنها مُوجَزة، وربما مُبهَمة بشكل أكثر من اللازم. يَحتاج الحاسوب عمومًا إلى تَعْليمَات واضحة وصريحة، ولهذا سنحتاج، مثلًا، إلى شرح الخطوات: "اقرأ مُدْخَل المُستخدِم" و "احسب قيمة الاستثمار بَعْد العام التالي" و "ما يزال هناك عَدَد من الأعوام للمعالجة". فمثلًا، نستطيع إعادة تَوصِيف الخطوة "اِقْرأ مُدْخَل المُستخدِم" إلى التالي: // اسأل المستخدم عن قيمة الاستثمار المبدئي Ask the user for the initial investment // اقرأ مدخل المستخدم Read the user's response // اسأل المستخدم عن قيمة سعر الفائدة Ask the user for the interest rate // اقرأ مدخل المستخدم Read the user's response أمَا بخُصوص الخطوة "احسب قيمة الاستثمار بَعْد العام التالي"، فسنحتاج إلى معرفة طريقة حِسَابها (ينبغي أن تَطلُب في تلك الحالة مزيد من التوضيح من أستاذك أو مُديرك)، ولكن دَعْنَا الآن نفْترِض أن قيمة الاستثمار تُحسَب بإضافة قيمة فائدة معينة إلى قيمة الاستثمار السابقة، وبالتالي يُمكِننا إِعادة كتابة حَلْقة التَكْرار while كالتالي: // طالما ما يزال هناك عدد من الأعوام للمعالجة while there are more years to process: // احسب الفائدة Compute the interest // أضف الفائدة إلى قيمة الاستثمار Add the interest to the value // اطبع القيمة Display the value نحتاج الآن إلى توضيح الاختبار الموجود بالخطوة "ما يزال هناك عَدَد من الأعوام للمعالجة"، وهو ما يُمكِن القيام به عن طريق عدّ الأعوام بأنفسنا، سنَستخدِم عَدَّادًا قيمته تُساوِي الصفر، ثم نُزيِد قيمة هذا العَدَّاد بمقدار الواحد بَعْد كل مرة نُعالِج فيها عامًا جديدًا، ونَتوقَف عندما تُصبِح قيمة العَدَّاد مُساوِية للعَدَد المطلوب من الأعوام. يُطلَق على ذلك عادةً اسم "حَلْقة العدّ (counting loop)"، وهي أحد الأنماط الشائعة، ولذلك تَوقَّع أن تَستخدِم شيئًا مُشابهًا بكثير من البرامج. تُصبِح الآن حَلْقة التَكْرار while كالتالي: // ابدأ بعدد أعوام يساوي الصفر years = 0 // طالما عدد الأعوام أقل من الخمسة while years < 5: // أزد عدد الأعوام بمقدار الواحد years = years + 1 // احسب الفائدة Compute the interest // أضف الفائدة إلى تلك القيمة Add the interest to the value // اطبع القيمة Display the value نَحتاج إلى أن نكون أكثر توضيحًا بخُصوص طريقة حِسَاب الفائدة، وسنفْترِض أنها تُساوِي حاصل ضرب سعر الفائدة بقيمة الاستثمار الحالية. نُضيِف هذا الإيضاح إلى ذلك الجزء من الخوارزمية المسئول عن قراءة مُدْخَلات المُستخدِم، وبهذا، نَحصُل على الخوارزمية الكاملة: // اسأل المستخدم عن قيمة الاستثمار المبدئي Ask the user for the initial investment // اقرأ مدخل المستخدم Read the user's response // اسأل المستخدم عن قيمة سعر الفائدة Ask the user for the interest rate // اقرأ مدخل المستخدم Read the user's response // ابدأ بعدد أعوام يساوي الصفر years = 0 // طالما عدد الأعوام أقل من الخمسة while years < 5: // أزد عدد الأعوام بمقدار الواحد years = years + 1 // احسب الفائدة بحيث تساوي حاصل ضرب القيمة مع سعر الفائدة Compute interest = value * interest rate // أضف الفائدة إلى تلك القيمة Add the interest to the value // اطبع القيمة Display the value وَصلنا إلى النقطة التي يُمكِن معها الترجمة المُباشرة إلى لغة برمجة مناسبة، فقط نحتاج إلى اِختيار أسماء المُتَغيِّرات (variables)، وتَقْرير نص العبارات التي سنَطبَعها للمُستخدِم، وهكذا. نستطيع الآن كتابة الخوارزمية (algorithm) بلغة الجافا كالتالي: double principal, rate, interest; // التصريح عن المتغيرات int years; System.out.print("Type initial investment: "); principal = TextIO.getlnDouble(); System.out.print("Type interest rate: "); rate = TextIO.getlnDouble(); years = 0; while (years < 5) { years = years + 1; interest = principal * rate; principal = principal + interest; System.out.println(principal); } ما زال أمامنا بعض التَحسِّينات الإضافية، مِن بينها تَضْمِين هذه الشيفرة داخل برنامج كامل، وإضافة التعليقات (comments)، وطِباعة المَزيد من المعلومات للمُستخدِم، ولكنه يظِلّ نفس البرنامج بالقسم السابق. في حين تَستخدِم خوارزمية الشيفرة الوهمية (pseudocode algorithm) المسافات البادئة (indentation) لتوضيح التَعْليمَات الواقعة ضِمْن حَلْقة التَكْرار (loop)، تُهمِل لغة الجافا هذه المسافات البادئة تمامًا، ولهذا أَضفنا قوسين معقوصين (curly brackets/braces) {} لتوضيح أيّ مجموعة تَعْليمَات (statements) تقع ضِمْن حَلْقة التَكْرار. إذا لم تَستخدِم هذه الأقواس بشيفرة الجافا، فإن الحاسوب سيفْترِض أن التَعْليمَة الوحيدة الواقعة ضِمْن حَلْقة التَكْرار هي years = years + 1;، أمَا بقية التَعْليمَات فسيُنفِّذها مرة واحدة فقط بَعْد انتهاء حَلْقة التَكْرار. للأسف، لا يُبلِّغ الحاسوب عن هذه النوعية من الأخطاء، بنفس الطريقة التي يُبلِّغ بها عن حُدوث خطأ في حالة عدم اِستخدَام القوسين الهلاليين (rounded brackets/parentheses) () حول (years < 5)؛ وذلك لأن تلك الأقواس مطلوبة وِفقًا لصيغة (syntax) تَعْليمَة while، أمَا قوسيّ المعقوصين {}، فإنها مطلوبة فقط لأغراض دَلاليّة (semantics)، أيّ أغراض مُتعلِّقة بالمعنى. يَستطيع الحاسوب عمومًا تَمييز أخطاء بناء الجملة (syntax errors) فقط، لا الأخطاء الدَلاليّة (semantic errors). لاحِظ أن التَوصِيف الأصلي للمسألة التالي لم يَكُن مُكتملًا: ينبغي لك عمومًا، قَبْل بدء كتابة أيّ برنامج، أن تتأكد من أن لديك التَوصِيف الكامل لوظيفة البرنامج المطلوب كتابته، فلابُدّ أن تَعرِف المعلومات التي سيَقرأها البرنامج (input) وأيّ خَرْج (output) ينبغي أن يَطبعُه، وكذلك الحِسَابات التي ينبغي له القيام بها. ربما يُمكِننا إعادة تَوصِيف نفس البرنامج السابق بصورة أكثر معقولية كالتالي: مسألة متتالية "3N+1" لنفْحَص مثالًا آخرًا لم نتَعرَّض له من قَبْل، السؤال هنا عبارة عن مَسألة رياضية مُجرَّدة، والتي يَعُدّها الكاتب تَحْدِيدًا واحدة مِن تمارينه البرمجية المُفضلة. سنبدأ هذه المرة، بعكس المثال السابق، بتَوصِيف كامل (specification) لمُهِمّة (task) البرنامج: اُكْتُب برنامجًا يَقرأ عَدَدًا صحيحًا موجبًا من المُستخدِم، ثُمَّ يَطبَع مُتتالية الأعداد "3N+1"، بحيث تبدأ من العَدَد المُدْخَل، كما يَنبغي للبرنامج أن يعدّ عَدَد عناصر المُتتالية ويَطبعها." اُنظر الخوارزمية المبدئية التالية، والتي تُوضِح فقط التَصَوُّر العام لمثل هذا البرنامج: // اقرأ عدد صحيح موجب من المستخدم Get a positive integer N from the user. // احسب قيمة كل عنصر بالمتتالية، واطبعه وعدّه Compute, print, and count each number in the sequence. // اطبع عدد عناصر المتتالية Output the number of terms. يَتضح لنا أن الخطوة الثانية تَحتوِي على المَضمون الفِعليّ للبرنامج، وبالطبع تحتاج إلى مزيد من الإيضاح. لمّا كنا نُريد الاستمرار بحِسَاب قيم عناصر المُتتالية حتى تُصبِح قيمة N الحالية مُساوِية للعَدَد ١، فإننا سنحتاج ببساطة إلى اِستخدَام حَلْقة تَكْرار (loop)، ولذلك دَعْنَا نُعيد صياغة نفس الجملة السابقة بحيث تَتوافق مع حَلْقة التَكْرار while. إننا في حاجة إلى مَعرِفة متى نَستمِر بتَّنْفيذ حَلْقة التَكْرار ومتى نُوقِّفها، في الواقع، سنستمر طالما كانت قيمة N الحالية لا تُساوِي ١، ولهذا يُمكِننا إعادة كتابة خوارزمية الشيفرة الوهمية (pseudocode algorithm) كالتالي: // اقرأ عدد صحيح موجب من المستخدم Get a positive integer N from the user; // طالما كانت قيمة `N` الحالية لا تساوي 1 while N is not 1: // احسب قيمة عنصر المتتالية التالي واسنده إلى N Compute N = next term; // اطبع قيمة N Output N; // عدّ عنصر المتتالية Count this term; // اطبع عدد عناصر المتتالية Output the number of terms; لمّا كان حِسَاب قيمة عنصر المُتتالية التالي يَعتمِد على ما إذا كانت قيمة N الحالية هي عَدَد زوجي (even) أم فردي (odd)، يَعنِي ذلك أن الحاسوب بحاجة إلى تَّنْفيذ حَدَثين مُختلفين لكل حالة، وهو ما يَعنِي أن اِستخدَام تَعْليمَة التَفْرِيع الشَّرْطيّة if بات ضروريًا؛ وذلك للاختيار ما بين تلك الحالتين، اُنظر الخوارزمية بَعْد التعديل: // اقرأ عدد صحيح موجب من المستخدم Get a positive integer N from the user; // طالما كانت قيمة `N` الحالية لا تساوي 1 while N is not 1: // إذا كان N عددًا زوجيًا if N is even: // احسب قيمة العنصر التالي وأسنده إلى N Compute N = N/2; // إذا كان N عددًا فرديًا else // احسب قيمة العنصر التالي وأسنده إلى N Compute N = 3 * N + 1; // اطبع قيمة N Output N; // عدّ عنصر المتتالية Count this term; // اطبع عدد عناصر المتتالية Output the number of terms; انتهينا تقريبًا، يَتبقَّى فقط العدّ (counting)؛ وذلك لطباعة عَدَد عناصر المُتتالية. يَعنِي العدّ ببساطة أن تبدأ بالقيمة صفر، ثم تُضيف المقدار واحد في كل مرة يَكُون لديك فيها ما تعدّه، ولهذا نحتاج إلى مُتَغيِّر (variable) للقيام بالعدّ، يُعرَف باسم العَدَّاد (counter). يَنبغي ضَبْط قيمة ذلك المُتَغيِّر إلى القيمة صفر قَبْل بداية الحَلْقة (loop)، بحيث تَزداد (increment) تلك القيمة أثناء تَّنْفيذ الحَلْقة. (يُعدّ ذلك أحد الأنماط الشائعة [common pattern]، ولذلك تَوقَّع أن تراه بكثير من البرامج). تُصبِح الخوارزمية، بَعْد إضافة العَدَّاد (counter)، كالتالي: // اقرأ عدد صحيح موجب من المستخدم Get a positive integer N from the user; // اضبط قيمة العداد إلى القيمة صفر Let counter = 0; // طالما كانت قيمة N الحالية لا تساوي 1 while N is not 1: // إذا كان N عددًا زوجيًا if N is even: // احسب قيمة العنصر التالي وأسنده إلى N Compute N = N/2; // إذا كان N عددًا فرديًا else // احسب قيمة العنصر التالي وأسنده إلى N Compute N = 3 * N + 1; // اطبع قيمة N Output N; // أزد قيمة العداد بمقدار 1 Add 1 to counter; // اطبع عدد عناصر المتتالية Output the counter; ما يزال أمامنا مشكلة أخيرة بخُصوص الخطوة الأولى، وهي كيف نتأكد من أن المُستخدِم قد أَدْخَل عَدَدًا صحيحًا موجبًا؟ ففي الواقع، قد يُدْخِل المُستخدِم عَدَدًا سالبًا أو صفرًا، وعندها سيستمر تَّنْفيذ البرنامج للأبد؛ لأن القيمة المُدْخَلة N، في تلك الحالة، لن تُصبِح أبدًا مُساوِية للواحد. ربما قد لا يُعدّ ذلك مشكلة ضخمة في تلك الحالة تَحْدِيدًا، ولكن، مع ذلك، ينبغي عمومًا محاولة كتابة برامج غَيْر قابلة للخطأ. نستطيع حَل تلك المُشكلة عن طريق الاستمرار بقراءة الأعداد إلى أن يُدْخِل المُستخدِم عددًا صحيحًا موجبًا. // اطلب من المستخدم إدخال عدد صحيح موجب Ask user to input a positive number; // أسند القيمة المدخلة إلى N Let N be the user's response; // طالما N ليست موجبة while N is not positive: // اطبع رسالة خطأ Print an error message; // اقرأ قيمة أخرى واسندها إلى N Read another value for N; // اضبط قيمة العداد إلى القيمة صفر Let counter = 0; // طالما كانت قيمة `N` الحالية لا تساوي 1 while N is not 1: // إذا كان N عددًا زوجيًا if N is even: // احسب قيمة العنصر التالي وأسنده إلى N Compute N = N/2; // إذا كان N عددًا فرديًا else // احسب قيمة العنصر التالي وأسنده إلى N Compute N = 3 * N + 1; // اطبع قيمة N Output N; // أزد قيمة العداد بمقدار 1 Add 1 to counter; // اطبع عدد عناصر المتتالية Output the counter; لاحِظ أن حَلْقة while الأولى ستنتهي فقط عندما تُصبِح قيمة N زوجية. عند محاولة كتابة شيفرة التَوصِيف التالي: "إذا كانت قيمة N غَيْر زوجية، اُطلب من المُستخدِم إِدْخَال عدد آخر"، يقع الكثير من المبرمجين، وبالأخص المبتدئين، في خطأ اِستخدَام تَعْليمَة التَفْرِيع if بدلًا من تَعْليمَة حَلْقة التَكْرار while. تَظهر المشكلة تَحْدِيدًا عندما يُدْخِل المُستخدِم عددًا غَيْر زوجي مرة آخرى. لمّا كانت تَعْليمَة التَفْرِيع if تُنفَّذ مرة واحدة فقط، فإنه لا يَتمّ فَحْص مُدْخَل المُستخدِم إلا مرة واحدة فقط، مما يعني أن البرنامج سينتقل إلى تَّنْفيذ التَعْليمَة التالية بغض النظر عما إذا كانت قيمة المُدْخَل الثاني للمُستخدِم زوجية أم لا، وهو ما يَتسبَّب بحُدوث حَلْقة لا نهائية (infinite loop) كما ذَكرنا آنفًا. أمَا في حالة اِستخدَام حَلْقة التَكْرار while، فإن الحاسوب سيَقفِز (أو سيَنقِل التَحكُّم بتعبير أدق) إلى بداية الحَلْقة بَعْد كل عملية إِدْخَال؛ لاختبار ما إذا كانت القيمة المُدْخَلة زوجية أم لا، مما يَعنِي أنه سيستمر في طلب إِدْخَال عَدَد جديد إلى أن يُدْخِل المُستخدِم قيمة مقبولة، أيّ عدد زوجي. وبالتالي، في حالة انتقال البرنامج إلى تَّنْفيذ ما بَعْد حَلْقة while، فإن قيمة N هي زوجية حتمًا. ها هو نفس البرنامج بشيفرة الجافا. لاحِظ اِستخدَام العَامِلين (operators) <= بمعنى "أقل من أو يُساوِي" و != بمعنى "لا يُساوِي"، بالإضافة إلى اِستخدَام التعبير N % 2 == 0؛ لاختبار ما إذا كانت قيمة N زوجية. نُوقِشَت كل هذه العَوامِل في القسم ٢.٥. import textio.TextIO; public class ThreeN1 { public static void main(String[] args) { int N; // لحساب العناصر بالمتتالية int counter; // لعد عدد عناصر المتتالية System.out.print("Starting point for sequence: "); N = TextIO.getlnInt(); while (N <= 0) { System.out.print( "The starting point must be positive. Please try again: " ); N = TextIO.getlnInt(); } // نعلم أن N هي عدد صحيح موجب عند هذه النقطة counter = 0; while (N != 1) { if (N % 2 == 0) N = N / 2; else N = 3 * N + 1; System.out.println(N); counter = counter + 1; } System.out.println(); System.out.print("There were "); System.out.print(counter); System.out.println(" terms in the sequence."); } // نهاية main } // نهاية الصنف ThreeN1 مُلاحظتان أخيرتان على هذا البرنامج: أولًا، ربما لاحَظت أن البرنامج لم يَطبَع قيمة أول عنصر بالمُتتالية -أيّ قيمة N المُدْخَلة من قِبَل المُستخدِم-، وكذلك لم يعدّها. هل هذا خطأ؟ يَصعُب القول. ربما ينبغي أن نَطرح سؤالًا آخر: هل كان تَوصِيف البرنامج (specification) صريحًا بما يَكفي بخُصوص تلك النقطة؟ في الواقع، للإجابة على مثل هذا السؤال، ستَحتاج إلى طَلَب مزيد من الإيضاح من أستاذك/مديرك. يُمكِن عمومًا حل هذه المشكلة -في حال كانت- بسهولة، فقط اِستبدل السَطْرين التاليين بتَعْليمَة counter = 0 قَبْل حَلْقة التَكْرار while : System.out.println(N); // print out initial term counter = 1; // and count it ثانيًا، لماذا تُعدّ هذه المسألة تَحْدِيدًا مثيرة؟ في الواقع، يَجِدْ كثير من علماء الرياضيات والحاسوب هذه المسألة مُشوقة؛ بسبب سؤال بسيط، يَخُص تلك المسألة، والذي لم يَتوصَّلوا للإجابة عليه بَعْد. السؤال هو "هل عملية حِسَاب قيم مُتتالية '3N+1' دومًا ما ستنتهي بَعْد عَدَد مُتناهي (finite) من الخطوات لجميع قيم N المبدئية؟" على الرغم من سهولة حِسَاب قيم المُتتاليات بشكل مُفرد، لم يُجِبْ أحد على السؤال الأعم حتى الآن، أيّ بصياغة آخرى، لا أحد يَعلم ما إذا كان من الصحيح تسمية عملية حِسَاب قيم مُتتالية "3N+1" بـ"الخوارزمية (algorithm)"؛ فبالنهاية، لابُدّ لأيّ خوارزمية أن تَنتهي بَعْد عَدَد مُتناهي من الخطوات. لاحظ: يَنطبق ذلك على الأعداد الصحيحة (integers) بمفهومها الرياضي، وليس القيم من النوع العددي الصحيح int! بمعنى أننا نفْترِض هنا أن قيمة N قد تَكُون أيّ عدد صحيح مُمكن مهما كَبُر، وهو ما لا يَنطبق على مُتَغيِّر من النوع int داخل برنامج بلغة الجافا. إذا أَصبحت قيمة N كبيرة جدًا ليَتمّ تَمثيلها بمُتَغيِّر من النوع int (32 بت)، فلا يُمكِن عدّ قيم خَرْج البرنامج صحيحة رياضيًا، أيّ أن البرنامج لا يَحسِب قيم متتالية "3N+1" بشكل صحيح عندما تَكُون قيمة N كبيرة. اُنظر تمرين ٨.٢. كتابة الشيفرة (coding) والاختبار (testing) وتنقيح الأخطاء (debugging) بَعْد انتهائك من تَطوير خوارزمية البرنامج (algorithm)، سيَكُون من اللطيف لو كان بإمكانك الضغط فقط على زِر معين؛ لتَحصُل بَعْدها على برنامج قابل للتَّنْفيذ (working program) بصورة ممتازة. في الواقع، عملية تَحْوِيل الخوارزمية إلى شيفرة بلغة الجافا لا تَتمّ دومًا بمثل هذه السَلاسَة لسوء الحظ، وحتى عندما تَصِل إلى تلك المرحلة من الحُصول على برنامج قابل للتَّنْفيذ (working program)، فإنه غالبًا ما يَكُون قابلًا للتَّنْفيذ بمعنى أنه يُنفِّذ "شيء ما"، لا بالضرورة الشيء الذي تريده أن يَفعَله. بَعْد الانتهاء من تصميم البرنامج (program design)، يَحيِن موعد كتابة الشيفرة (coding): أيّ ترجمة التصميم إلى برنامج مكتوب بلغة الجافا أو بأيّ لغة برمجية اخرى. مَهمَا كنت حَريصًا أثناء كتابة الشيفرة، فعادةً ما ستَجِدْ بعض أخطاء بناء الجملة (syntax errors) طريقها إلى الشيفرة، ولذلك سيَرفُض مُصرِّف (compiler) الجافا البرنامج، وسيُبلِّغك عن نوع معين من رسائل الخطأ (error message). لاحِظ أنه في حين يستطيع المُصرِّف اكتشاف أخطاء بناء الجملة (syntax errors) دائمًا، فإنه لسوء الحظ ليس بنفس الكفاءة في اكتشاف مَاهية الخطأ، بل أنه قد لا يَتَمكَّن، في بعض الأحيان، من معرفة مكان حُدوث الخطأ الفِعليّ، فمثلًا، قد يَتسبَّب وجود خطأ إملائي أو نَسْيَان قوس "{" بالسطر رقم ٤٥ بتَوَقُّف المُصرِّف بالسطر ١٠٥. يَظِلّ، مع ذلك، الفهم الجيد لقواعد صياغة (syntax rules) اللغة البرمجية مع اِتباع بعض القواعد الإرشادية البرمجية البسيطة الطريقة الأفضل لتَلافِي كثير من تلك الأخطاء. لنسْتَعْرِض بعضًا من تلك القواعد، أولًا، لا تَكتُب أبدًا قوس حَاصِرة "{" بدون كتابة زوجه الآخر "}"، ثُمَّ عُد بَعْدها لكتابة التَعْليمَات بينهما؛ وذلك لأن نَسْيَان قوس أو إضافة قوس في غَيْر مَحَلّه يُعدّ من أكثر الأخطاء التي يَصعُب اكتشافها خاصة بالبرامج الضخمة. ثانيًا، اِستخدِم دائما المسافات البادئة (indentation) لتَنسيق الشيفرة، وإن عَدَّلت البرنامج، عَدِّل أيضًا المسافات البادئة بحيث تُصبِح مُتوافقة مع التَعْدِيل الجديد. ثالثًا، اِستخدِم نَمط تَسمية (naming scheme) ثابت؛ حتى لا تُعانِي بَعْد ذلك بينما تَتَذكَّر ما إذا كان اسم مُتَغيِّر ما (variable) هو "interestrate" أم "interestRate". رابعًا، عندما يُبلِّغك المُصرِّف بأكثر من رسالة خطأ (error message) واحدة، لا تُحاوِل إصلاح رسالة الخطأ الثانية حتى تَنتهي من إِصلاح الأولى؛ لأن المُصرِّف عادةً ما يَرتبك بعد إِيجاده لأول خطأ، ولذلك قد تَكُون رسائل الخطأ التالية هي مُجرَّد تَخمينات. وأخيرًا، وربما هي النصيحة الأفضل: خُذ الوقت الكافي لفِهم الخطأ قبل مُحاولة إِصلاحه؛ فالبرمجة، بالنهاية، ليست علمًا تجريبيًا (experimental science). إذا تمّ تَصرِيف برنامجك بنجاح، لا يَعنِي ذلك أنك قد انتهيت؛ فمن الضروري أن تَختبر (test) البرنامج لتتأكد مما إذا كان يَعمَل بشكل صحيح، وهو ما لا يَقْتصِر على مُجرَّد الحُصول على الخَرْج الصحيح (output) لعينة المُدْخَلات (inputs) التي أَعطاك إِياها الأستاذ، بل ينبغي للبرنامج أن يَعمَل بشكل سليم لجميع المُدْخَلات المقبولة، وفي حالة اِستقباله لمُدَْخَل غَيْر صالح، فينبغي أن يُوبِخ البرنامج المُستخدِم بلطف، لا أن يَنَهار (crashing) تمامًا. عمومًا، ينبغي أن تَختبِر البرنامج على نطاق واسع من المُدْخَلات. قد تُحاوِل أيضًا إِيجاد مجموعة المُدْخَلات التي بإِمكانها اِختبار جميع الوظائف التي أَدْرَجتها بالشيفرة. في حالة كتابة برامج كبيرة، حَاوِل تقسيمها إلى عدة مراحل، بحيث تَختبِر كل مرحلة قَبْل البدء بالمرحلة التالية، حتى لو اضطررت لكتابة شيفرة إِضافية تقوم بالاختبار مثل أن تَستدعِي أحد البرامج الفرعية التي قُمت بكتابتها للتو؛ فأنت حتمًا لا تُريد أن يَنتهي بك الحال بخُمسمائة سَطْر جديد من الشيفرة مَصحوبة بخطأ ما في مكان ما. الغرض من الاختبار (testing) هو مُحاولة العُثور على الأخطاء البرمجية (bugs)، وهي -بعكس أخطاء التَصرِيف (compilation errors)- أخطاء دَلاليّة (semantic errors)، أيّ تَكُون في صورة سُلوك غَيْر سليم. المُحزن في الأمر هو أنك غالبًا ما ستَجِدهم. تستطيع، مع ذلك، تَقْليل -لا التَفادِي التام- هذه النوعية من الأخطاء البرمجية (bugs)، من خلال الانتباه أثناء قيامك بكُلًا من التصميم (design)، وكتابة الشيفرة (coding). عندما تَكتشف خطأً برمجيًا (bug)، يَحيِن موعد تَنْقِيح الأخطاء (debugging)، بمعنى تَعَقُّب سبب الخطأ بالشيفرة بهدف التخلص منه. لاحظ أنك لن تُصبِح خبيرًا بتَنْقِيح الأخطاء إلا من خلال المُمارسة المستمرة؛ فهي مَهارة تكتسبها، مثلها مثل جميع نواحي البرمجة الآخرى، ولذلك لا تَكُن خائفا منها، وإنما تَعلَّم منها. تُعدّ القدرة على قراءة الشيفرة واحدة من أهم مهارات تَنْقِيح الأخطاء الاساسية، والمقصود بقراءة الشيفرة هنا: القدرة على تَنْحية تَصوراتك المُسْبَّقة عما ينبغي للشيفرة أن تقوم به، وبدلًا من ذلك، تَعقُّب الطريقة التي يُنفِّذها بها الحاسوب خَطوة بخَطوة؛ لتُدرِك ما تَقوم به فِعليًا. في الواقع، هذا ليس بالأمر السهل. ما يزال الكاتب يَذكُر تلك المرة التي قضى فيها عدة ساعات يَبحث عن خطأ برمجي ليَكتشِف بالنهاية أن سَطْرًا ما بالشيفرة، كان قد نَظَر إليه عشرات المرات، يَحتوِي على القيمة ١ بدلًا من الحرف i، أو تلك المرة التي كَتَب فيها برنامجًا فرعيًا (subroutine) اسمه هو WindowClosing، والذي كان سيُؤدي غرضه تمامًا لولا أن الحاسوب كان يَبحث عن البرنامج الفرعي windowClosing (بحرف w صغير). قد يُساعِدك أحيانًا الاستعانة بشخص آخر لفَحْص الشيفرة، خاصة وأنه لن يَملك نفس تَصوراتك المُسْبَّقة عنها. أحيانًا ما يَكُون مُجرَّد العثور على ذلك الجزء من البرنامج الذي يَكمُن فيه الخطأ مُشكلة بحد ذاته، ولهذا تُوفِّر مُعظم بيئات التطوير البرمجية (programming environments) برنامجًا يُسمَى مُنقِّح الأخطاء (debugger)؛ لمساعدتك بالعثور على الأخطاء البرمجية (bugs)، بحيث يَتمّ تَشْغِيل البرنامج (program) تحت تَحكُّم ذلك المُنقِّح، والذي يَسمَح لك بضَبْط ما يُعرَف باسم نقاط المُقاطعة (breakpoints)، وهي نقاط بالبرنامج سيَتوقَّف عندها المُنقِّح مؤقتًا (pause)؛ حتى تَتَمكَّن من فَحْص قيم مُتَغيِّرات البرنامج عند تلك النقطة، مما سيُساعدك في العُثور على المكان الذي بدأت فيه الأخطاء البرمجية بالظهور أثناء تَّنْفيذ البرنامج. بمُجرَّد تَحْدِيد ذلك الجزء من البرنامج الذي يَكمُن فيه الخطأ البرمجي (bug)، يَسمَح لك المُنقِّح أيضًا بتَّنْفيذ البرنامج سَطْرًا سَطْرًا، ومِنْ ثَمَّ، تستطيع مشاهدة ما يَحدُث تفصيليًا. يَعترِف الكاتب أنه لا يَستخدِم مُنقِّح الأخطاء دائمًا، وإنما يَتبِع المنهج التقليدي لتَنْقِيح الأخطاء، وذلك بإضافة تَعْليمَات تَنْقِيحيّة (debugging statements) داخل البرنامج، والتي هي مُجرَّد تَعْليمَات خَرْج تَطبَع معلومات عن حالة (state) البرنامج. عادةً ما تُكتَب أيّ تَعْليمَة تَنْقِيحيّة على الصورة التالية: System.out.println("At start of while loop, N = " + N); ينبغي لنص الخَرْج أن يُساعِدك على تَحْدِيد مكان تَعْليمَة الطباعة المسئولة عن ذلك الخَرْج، وكذلك معرفة قيم المُتَغيِّرات (variables) المُهمة. ستكتشف، في بعض الأحيان، أن الحاسوب لم يَمر حتى على ذلك الجزء من البرنامج الذي كنت تَظن أنه يُنفِّذه. تَذكَّر أن الهدف هو اكتشاف أول نُقطة بالبرنامج أَصبَحت فيها حالة البرنامج (state) مُخالِفة للحالة المُتوقَّعة، فهذا هو مكان الخطأ البرمجي (bug). وأخيرًا، تَذكَّر القاعدة الذهبية لتَنْقِيح الأخطاء: إذا كنت مُتأكدًا تمامًا أن كُل شئ بالبرنامج سليم، ومع ذلك ما يزال لا يَعمَل بالصورة المطلوبة، فلابُدّ أن واحدًا من تلك الأشياء هو ببساطة خطأ. ترجمة -بتصرّف- للقسم Section 3.1 Blocks, Loops, and Branches من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.
-
تُعدّ لغة الجافا لغة قياسية ومعيارية إلى حد بعيد، ومع ذلك، تختلف الإجراءات اللازمة لإنشاء برامج الجافا وتَصْرِيفها وتَعْدِيلها من بيئة برمجة (programming environment) إلى آخرى اختلافًا كبيرًا. تَتَوفَّر طريقتان في العموم هما: بيئة سطر الأوامر (command line environment) حيث يَكْتُب المُستخدِم أوامرًا (commands) يَرُد عليها الحاسوب، والآخرى هي بيئة تَطْوِير مُتكاملة (integrated development environment - IDE) والتي يَعتمِد فيها المُستخدِم على كُلًا من الفأرة ولوحة المفاتيح للتَفاعُل مع واجهة مُستخدِم رُسومية (graphical user interface). في حين تَتَوفَّر بيئة سطر أوامر واحدة للغة الجافا، فهنالك الكثير من بيئات التَطْوِير المُتكاملة منها Eclipse و NetBeans و IntelliJ IDEA و BlueJ. سنتناول خلال هذا القسم المعلومات الضرورية لكي تَتَمكَّن من تَصْرِيف (compile) الأمثلة وتَشْغِيلها باِستخدَام كُلًا من سطر الأوامر وبيئتي Eclipse و BlueJ. اِحرِص دائمًا على قراءة الأمثلة وتَصْريِفها وتَشْغِيلها. ستَجِد الشيفرة المصدرية لكُلًا من البرامج التوضيحية وحلول تمارين كل فصل بموقع الكتاب الالكتروني. يُعدّ الإصدار ١٤ هو الإصدار الحالي من الجافا، ولكن سيَعتمِد الكتاب على الإصدار ٨. في الحقيقة، لقد حَدَثَت الكثير من التَعْدِيلات على لغة الجافا وكذلك سياسة شركة أوراكل (Oracle) منذ الإصدار ٨. أدت تلك التَغْيِيرات إلى جَعْل مُهِمّة تَثْبِيت الجافا وتَجهِيزها للعمل أكثر صعوبة، ولذلك سنُحاوِل شرح المعلومات الضرورية لتَثْبِيتها بهذا القسم. أصبحت الإصدارات الجديدة من الجافا تُصدَر بصورة أسرع مما كانت عليه في الماضي. يُعدّ بعضها إصدارات مُدعَّمة طويلة الأمد (long-term support - LTS) أي تَستمِر بتَلَقِّي التحديثات الأمنية (security updates) وتحديثات إصلاح الأخطاء البرمجية (bug fixes) لفترة أطول من الزمن. يُعدّ الإصدارين ٨ و ١١ من الجافا من الإصدارات المُدعَّمة طويلة الأمد أما الإصدارات ٩ و ١٠ و ١٢ و ١٣ و ١٤ فليست كذلك. في الواقع، الإصدارات ٨ و ١١ و ١٤ هي إصدارات مُدعَّمة رسميًا إلى الآن. عدة تطوير جافا JDK و JavaFX يُعرَف نظام التطوير بلغة الجافا باسم "عُدة تطوير جافا (Java Development Kit - JDK)". تَعتمِد الأمثلة بهذا الكتاب على الإصدار ٨ من الجافا أو الإصدارات الأحدث، لذا ينبغي أن تُثبته على حاسوبك. تَتَوفَّر الجافا بنسختين: عُدة تَطوِير (JDK) وبيئة تّنْفيذ (Java Runtime Environment - JRE). تُستخدَم نسخة بيئة التّنفيذ (JRE) لتَشغِيل البرامج، لكن ليس بإمكانها تَصْرِيفها (compile). في المقابل، يُمكِنك اِستخدَام نسخة عُدة التطوير (JDK) لتَصرِيف البرامج كما أنها تَتَضمَّن بيئة تّنْفيذ (JRE). لقد أصبح من الصعب إيجاد ملف بيئة تّنْفيذ مُنفصِل منذ الإصدار ١١ من الجافا ولكن ما يزال ذلك المُصطلح مُستخدَمًا على أية حال. تَتَضمَّن عُدة تطوير جافا (JDK) بيئة لسطر الأوامر (command line environment) ستحتاج إليها للعمل مع الجافا. لاحظ أنه لابُدّ من أن تُحمِّل عدة تطوير جافا (JDK) حتى لو كنت تنوي اِستخدَام بيئة تطوير متكاملة (IDE). طُوِّرت لغة الجافا بواسطة شركة صن ميكروسيستمز (Sun Microsystems) ثم اِستحوَذت عليها مؤسسة أوراكل (Oracle). يُمكِنك أن تُحمِّل نسخة أوراكل من عُدة تَطوِير جافا (JDK) من موقعها الإلكتروني مباشرةً، ولكنها أصبحت منذ الإصدار الحادي عشر تَستهدِف الأغراض التجارية لذا ربما قد تُفضِّل الاعتماد على نسخة OpenJDK فيما يتعلق بالأغراض الشخصية والتعليمية، فهي تمتلك نفس وظائف نسخة أوراكل كما أنها مُوزعة برخصة مجانية ومفتوحة المصدر (open-source). يَفترِض ما يلي اِستخدَامك لنسخة OpenJDK، فإذا كنت قد حمَّلت نسخة أوراكل، ستَجِد تَعْليمَات التثبيت بموقعها الإلكتروني. يُمكِنك أن تُحمِّل نسخة OpenJDK من موقع https://jdk.java.net التابع لأوراكل ولكنه يُوفِّر خيارات محدودة، لذا قد تُفضِّل تَحْمِيله من موقع AdoptOpenJDK حيث يُوفِّر نُسخًا لنطاق أوسع من المنصات كما أنه يُوفِّر أداة تثبيت للجافا لنظامي ماك (Mac) وويندوز (Windows) مما يُسهِل من عملية التثبيت على تلك المنصات لذا فقد يَكُون ذلك هو خيارك الأفضل لتثبيت JDK. أما بالنسبة لنظام لينكس (Linux)، فعادةً ما تستطيع تثبيت JDK من مستودع برمجيات (software repositories) النظام. لاحِظ أنه على الرغم من أن كُلًا من البرامج التوضيحية والتمارين التي سنتناولها هنا مُتناسبة مع الإصدارات الأقدم من JDK حتى الإصدار الثامن، لكن يُنصَح عمومًا بالاعتماد على أحدث إصدار منه أو على إصدار مُدعم على المدى البعيد. منذ مايو ٢٠٢٠، يُعدّ الإصدار الحالي للجافا هو ١٤. تَعتمِد تطبيقات واجهة المُستخدِم الرسومية (GUI programs) على مكتبة برمجية تُعرَف باسم JavaFX. إذا كنت تَستخدِم أداة OpenJDK، فينبغي عليك أن تُحمِّل تلك المكتبة بصورة مُنفصلة عن الأداة. أما بالنسبة لنُسخ أوراكل من أداة JDK، فلقد كانت مكتبة JavaFX مُضْمَّنة بها حتى الإصدار الحادي عشر من الجافا أي إذا كنت تَستخدِم إصدارًا أحدث، سينبغي عليك تَحْمِيلها أيضًا بصورة مُنفصِلة. تستطيع عمومًا تَحْمِيل مكتبة JavaFX من هنا. ملحوظة: ينبغي أن تَكُون عُدة تطوير جافا إف إكس (JavaFX SDK) مُتناسبة مع نظام التَشغِيل الخاص بك (لينكس، وماك، وويندوز) كما ينبغي أن تَكُون مُتوافقة مع إصدار أداة OpenJDK الذي تَستخدِمه. إذا كنت قد حَمَّلت أداة تثبيت JDK لنظام ويندوز (Windows) أو ماك (Mac) من موقع أوراكل (Oracle) أو موقع AdoptOpenJDK، فتستطيع ببساطة أن تَنقُر نقرة مزدوجة على ملف أداة التثبيت إذا لم يَكُن قد شُغِّل أتوماتيكيًا بالفعل لبدء عملية التثبيت أما إذا كنت قد حَمَّلت ملفات OpenJDK و JavaFX، فستَجِد كُلًا منهما بهيئة ملف أرشيفي (archive file) مَضغُوط ينبغي أن تَستخرِج محتوياته أولًا. يُمكِنك أن تَنقُر على أيقونة الملف الأرشيفي نقرة مُزدوجة مما سيُؤدي إلى اِستخرَاج محتوياته أو فَتْح برنامج يُساعِدك على اِستخرَاج محتوياته. ستَجِد بَعْدها مُجلدًا يحمل اسم مشابه لـ"jdk-11.0.7" أو "javafx-sdk-11.0.2"، والذي تستطيع وَضْعُه بأي مكان بالحاسوب ولكن ينبغي أن تَعرِف مساره. إذا كنت تَستخدِم نظام لينكس (Linux)، فعادةً ما يُوفِّر مستودع برمجيات (software repositories) لينكس إصدارًا حديثًا من كُلًا من OpenJDK و JavaFX مما سيُمكِّنك من تثبيتهما باِستخدَام أداة تثبيت البرمجيات (Linux software installer). يعَتمِد ذلك على إصدار لينكس المُستخدَم لكن في العموم يُمكِنك البحث عن أي حزم (packages) تَحتوِي أسمائها على كلمات مثل "openjdk" و "openjfx" كما أنك ستحتاج أيضًا إلى مَعرِفة مكان مجلد عدة تطوير JavaFX الذي يَقَع عادةً بالمسار "/usr/share/openjfx". إذا كُنت تَستخدِم نظام ماك (Mac) وحَمَّلت OpenJDK كملف أرشيفي، فينبغي أن تَستخرِج محتوياته، وستَجِد عندها مُجلدًا اسمه هو "jdk-11.0.7.jdk". يُنصَح بنَقْل مجلد JDK إلى المسار "/Library/Java/JavaVirtualMachines" وهو ما يَتَطلَّب بعض الصلاحيات الإدارية. يَعتمِد نظام ماك (Mac) بشكل تلقائي على أحدث إصدار من JDK بالمسار "Library/Java/JavaVirtualMachines/". أما إذا كنت تَستخدِم نظام ويندوز (Windows)، فعادةً ما تُثبَّت عُدة تطوير جافا (JDK) بالمسار "C:\Program Files\Java" لذلك يُمكِنك نَقْل مجلد OpenJDK إلى نفس ذلك المسار. بيئة سطر الأوامر تَختلِف بيئة سطر الأوامر (command line environment) عن واجهات المُستخدِم الرُسومية (graphical user interfaces) إلى حد كبير. في حين أن غالبية مُستخدمِي الحواسيب العصرية مُعتادون على الواجهات الرُسومية حيث يَجِدُون بيئة سطر الأوامر غَيْر بديهية، ولكن يُمكِنك بقليلٍ من المُمارسة أن تَتَعلَّم أساسيات بيئة سطر الأوامر وستَجِدها أكثر إنتاجية بالموازنة مع نظيرتها من الواجهات الرُسومية. في العموم، يُعدّ تَعلُّم كيفية اِستخدَام سطر الأوامر (command line) أمرًا مهمًا بالأخص لطلبة علوم الحاسوب (computer science)، ولكن يُمكِنك تَخطِّيه إن كُنت تَنوِي الاِعتمَاد بالكامل على واحدة من بيئات التطوير المتكاملة (IDE). ينبغي أن تَفتَح نافذة كتابة الأوامر أولًا لكي تَتَمكَّن من اِستخدَام بيئة سطر الأوامر (command line environment). إذا كنت تَستخدِم نظام ويندوز (Windows)، فيُمكِنك فَتْح تلك النافذة عن طريق تَشْغِيل برنامج اسمه "cmd" أما إذا كنت تَستخدِم نظام ماك (Mac)، فيُمكِنك تَشْغِيل برنامج الطرفية (terminal) والذي ستَجِده بمُجلد الأدوات (Utilities) داخل مُجلد التطبيقات (applications) أما إذا كنت تَستخدِم نظام لينكس (Linux)، فهنالك أكثر من طريقة، فيُمكِنك مثلًا أن تَستخدِم برنامجًا اسمه "xterm" لكن حَاوِل البحث عن برنامج الطرفية (terminal) بقائمة التطبيقات (applications) أولًا. عند فَتْح نافذة الأوامر (command window) بأي حاسوب، سيَظهَر محث (prompt) حيث تستطيع أن تَكْتُب أمرًا (command) ثم تَضغَط على مفتاح العودة الى بداية السطر. سيُنفِّذ الحاسوب عندها ذلك الأمر ثم سيَعرِض الخرج (output) الناتج عن الأمر بنفس النافذة. سيَظهَر بَعْدها المحث مرة آخرى لكي يُمكِّنك من كتابة أمر جديد. يُعدّ "المجلد الحالي (current directory)" أو "مجلد العمل (working directory)" واحدًا من المفاهيم الأساسية ببيئة سطر الأوامر (command line environment)، وعادةً ما يَكُون اسم المجلد الحالي جزءًا من اسم محث الأوامر (command prompt). (لاحِظ أن الكلمتين "directory" أو "folder" يحملان نفس المعنى). يَحتوِي المجلد الحالي (current directory) على ملفات تُستخدَم مِن قِبَل الأوامر المُدْخَلة. إذا كنت تَستخدِم نظام ويندوز، يُمكِنك اِستخدَام الأمر dir لتَحصُل على قائمة بالملفات الموجودة بالمجلد الحالي أما إذا كنت تَستخدِم نظام لينكس أو ماك، فيُمكِنك اِستخدِم الأمر ls لنفس الغرض. عند فَتْح النافذة للمرة الأولى، ستَجِد أن المُجلد الحالي يُشير إلى المجلد الرئيسي للمُستخدِم (home directory) أي حيث ملفاته الشخصية مُخزَّنة. يُمكِنك أن تُغيِّر المجلد الحالي باِستخدَام الأمر cd متبوعًا باسم المُجلد الذي تود أن تَستخدِمه. فمثلًا إذا كان المجلد الحالي يُشير إلى مجلدك الرئيسي (home directory)، يُمكِنك أن تَكْتُب الأمر cd Desktop لكي تُغيِّره إلى مُجلد سطح المكتب (Desktop directory) الخاص بك وبالطبع ينبغي أن تَضغَط على مفتاح العودة إلى بداية السطر لكي يُنفَّذ الأمر. يُمكِنك أن تُنشِئ مجلدًا (directory/folder) بمُجلدك الرئيسي (home directory) لتَخْزِين برامج الجافا الخاصة بك، وليَكُن اسم المجلد هو "javawork". تستطيع بالطبع أن تُنشِئه باِستخدَام الواجهة الرسومية، ولكننا سنُنشِئه هنا باِستخدَام سطر الأوامر (command line). اِتبع الخطوات التالية ببساطة: أولًا، اِفتح نافذة الأوامر (command window). إذا كُنت تريد أن تُنشِئه بمجلد آخر غَيْر مُجلدك الرئيسي (home directory)، فينبغي أن تَستخدِم الأمر cd -كما أوضحنا مُسبَقًا- للانتقال إلى ذلك المجلد. بَعد ذلك، يُمكِنك أن تَكْتُب الأمر mkdir javawork لكي تُنشِئ المجلد الجديد. وقتما تريد العمل على أحد برامج الجافا الخاصة بك ضِمْن ذلك المجلد، تستطيع فَتْح نافذة الأوامر (command window) وكتابة الأمر cd للانتقال إلى المجلد "javawork". يُعدّ الأمرين javac و java من أبسط الأوامر لاِستخدَام الجافا من خلال سطر الأوامر. يُستخدَم الأمر javac لتَصرِيف (compile) الشيفرة المصدرية المَكْتُوبة بلغة الجافا بينما يُستخدَم الأمر java لتَشْغِيل البرامج. تَقَع تلك الأوامر بمجلد اسمه "bin" داخل مجلد عُدة تطوير جافا (JDK). إذا ثَبَّت كل شيء بصورة سليمة، فينبغي أن يتعرَّف الحاسوب على هذه الأوامر عند كتابتها بسطر الأوامر (command line). حَاوِل مثلًا أن تَكْتُب كُلًا من الأمرين java -version و javac -version بسطر الأوامر. إذا كان كل شيء مُثبَّتًا على نحو سليم، فستَعرِض تلك الأوامر إصدار الجافا المُثبَّت أما إذا كان هنالك مشكلة ما، فستَحصُل على رسالة مثل "الأمر غَيْر موجود". بالنسبة لنظام لينكس، إذا كنت قد ثَبَّت الجافا من مستودع برمجيات النظام (software repositories)، فينبغي أن يَكون كل شيء مَضبُوطًا على نحوٍ سليم. يَنطبِق الأمر نفسه على نظام ماك إذا كنت قد اِستخدَمت أداة التثبيت من موقع AdoptOpenJDK أو إذا كنت قد نَقْلَت مجلد OpenJDK إلى المسار "/Library/Java/JavaVirtualMachines/". يَعتمِد الأمرين javac و java بنظام ماك على أحدث إصدار من عدة تطوير جافا (JDK) ضِمْن ذلك المجلد. بالنسبة لنظام ويندوز، إذا كنت قد اِستخدَمت أداة تثبيت AdoptOpenJDK، فينبغي أن يَكُون كل شيء مضبوطًا بصورة افتراضية. في المقابل، إذا كنت قد اِستخدَمت نُسخة أوراكل من عُدة تطوير جافا (Oracle JDK) أو ملف JDK مضغُوط، فلابُدّ أن تُضيف مسار مجلد "bin" الموجود بمجلد عُدة تطوير جافا (JDK) يدويًا إلى مسار النظام (system path). تُوضِح تَعْليمَات تثبيت نسخة أوراكل بنظام ويندوز طريقة القيام بذلك. اُنظر تحديدًا قسم "ضَبْط مُتْغيِّر البيئة PATH" بتَعْليمَات تثبيت JDK بنظام ويندوز. سنُجرِّب الآن الأمر javac، يُمكِنك أن تَضَع مثلًا نُسخة من الملف HelloWorld.java بمجلد العمل (working directory). إذا كنت قد حمَّلت نسخة الكتاب المُتاحة عبر الإنترنت، ستَجِد ذلك الملف بالمجلد "source". تستطيع أن تَستخدِم الواجهة الرسومية لنَسْخ ذلك الملف ولصقه إلى مجلد العمل. أو على نحوٍ بديل، يُمكِنك فَتْح ذلك الملف بالموقع الإلكتروني للكتاب واِستخدَام أمر المُتصفح "Save As" لحِفظ نسخة منه بمُجلد العمل الخاص بك. اُكْتب الآن الأمر التالي: javac HelloWorld.java سيُصرِّف ذلك الأمر الملف HelloWorld.java وسيُنشِئ ملف بايتكود اسمه HelloWorld.class بنفس المجلد. إذا نجح الأمر في عملية التصريف، لن تَحصُل على أي خَرْج، وسيَظهَر محث الأوامر (command prompt) مرة آخرى ليُخبرك بكَوْنه جاهزًا لاستقبال أي أمر آخر. يُمكِنك عندها كتابة الأمر java لتَشْغِيل البرنامج كالتالي: java HelloWorld ينبغي أن يَعرِض الحاسوب الرسالة "Hello World!". على الرغم من أن البرنامج مُخزَّن بالملف HelloWorld.class، يَستخدِم الأمر java اسم الصَنْف (class) -أي HelloWorld- لا اسم الملف. تَستخدِم الكثير من البرامج التوضيحية التي سنتعرض لها الصنف TextIO لقراءة مدخلات المُستخدِم (اُنظر القسم الفرعي ٢.٤.٣). ولأن الصنف TextIO ليس جزءًا قياسيًا من لغة الجافا، لابُدّ إذًا من أن تتيحه لأي برنامج يَستخدِمه. يَعنِي ذلك أن مجلد العمل (working directory) الخاص بك ينبغي أن يحتوي على مجلد اسمه "textio" بداخله ملف اسمه TextIO.java، والذي يُمكِنك نسخه من المجلد "source" أو تحميله من الموقع الالكتروني، ولكن تَأكَّد من وَضْعه داخل مجلد اسمه "textio" بنفس مجلد البرنامج الذي يَستخدِم الصَنْف TextIO. يُمكِنك الآن أن تُشغِّل البرنامج التوضيحي Interest2.java لتختبر مُدْخَلات البرنامج. اِستخدِم الأمر التالي أولًا لتَصرِيف (compile) البرنامج: javac Interest2.java إذا نجح الأمر السابق، فإنه سيُنشِئ ملفًا مُصرَّفًا (compiled) اسمه Interest2.class، وكذلك سيُنشِئ ملفًا اسمه TextIO.class بالمجلد "textio" إذا لم يَكُن موجودًا. في العموم، لا يُصرِّف الأمر javac الملف المُخصَّص فقط وإنما يُصرِّف أيضًا أية ملفات جافا آخرى مطلوبة. تستطيع الآن تَشْغِيل البرنامج بكتابة الأمر التالي: java Interest2 سيَطلُب البرنامج منك أن تُدْخِل بعض المعلومات لذا ينبغي أن تَكْتُب إجاباتك بنافذة الأوامر (command window) مع الضَغْط على محرف العودة بنهاية كل سطر. عندما ينتهي البرنامج، سيَظهَر محث الأوامر (command prompt) مرة آخرى مما يَعنِي إمكانية كتابة أمر جديد. لأن الصَنْف TextIO لا يُعرِّف البرنامج main()، لا يُمكِن تّنْفيذه كبرنامج أي لا يَكُون للأمر java TextIO أي معنى. يُمكِنك تَشْغِيل جميع البرامج التي سنَتعرَّض لها هنا بنفس الطريقة باستثناء تلك التي تَستخدِم مكتبة JavaFX والتي سنُوضِح طريقة تَشْغِيلها بالقسم الفرعي التالي. ستحتاج إلى مُحرر نصي (text editor) لتُنشِئ برامجك الخاصة. سيَسمَح لك أي مُحرر نصي في العموم بكتابة مستندات (documents) تَحتوِي على نص مُجرّد (plain text) ومن ثَمَّ حِفظها. لابُدّ أن تَحفظ تلك المُستندات كنص مُجرّد بدون أي ترميز (encoding) أو تنسيق خاص. في المقابل، لا يُمكِنك أن تَستخدِم مُعالجات النصوص (Word processor) إلا إذا ضَبْطتها لكي تحفظ المستندات بهيئة نص مُجرّد (plain text). سيُمكِّنك أي محرر نصي جيد من البرمجة على نحو أفضل. يأتي نظام لينكس مثلًا مع مجموعة من محررات النصوص (text editors). أما بالنسبة لنظام ويندوز، فيُمكِنك أن تَستخدِم برنامج notepad أو قد تبحث عن شيء أفضل قليلًا. بالنسبة لنظام ماك، يُمكِنك أن تُحمِّل تطبيق BBEdit مجانًا. بالإضافة إلى تلك البرامج، قد تُفضِّل استخدام برامج متعددة المنصات (cross-platform) مثل المحرر النصي jedit أو مُحرر البرمجة الشائع Atom. يُمكِنك الآن أن تَكْتُب برنامجًا خاصًا بك باتباع الخطوات التالية: اِفتح نافذة سطر الأوامر (command line window) ثم استخدِم الأمر cd لكي تنتقل إلى مجلد العمل (working directory) حيث تُخزِّن ملفات الشيفرة المصدرية (source code). اُنقُر نقرة مزدوجة على أيقونة برنامج مُحرر النصوص (text editor) أو اختره من قائمة "ابدأ (Start)" لتَشْغِيله ثم اُكْتُب شيفرة البرنامج بنافذة المُحرر أو قد تُعدِّل على ملف شيفرة مصدرية موجود مُسْبَقًا، وأخيرًا اِحفظ الملف. يَحمِل أي ملف شيفرة مصدرية بلغة الجافا اسمًا يَتطابَق مع اسم الصَنْف (class) المُعرَّف بالملف وينتهي بالامتداد .java. بَعْد حِفظ الملف بمجلد العمل (working directory)، اِستخدِم الأمر javac لتَصْرِيف (compile) الملف كما ذَكَرنا مُسْبَقًا. إذا كان هنالك أية أخطاء بقواعد بناء الجملة (syntax errors) بشيفرة البرنامج، ستَظهَر رسائل خطأ بنافذة الأوامر (command window). ستُخبرك كل رسالة منها برقم سطر ضِمْن ملف حيث عَثَر الحاسوب على الخطأ. ينبغي إذًا أن تعود مرة آخرى إلى المُحرر لتَصْليح ذلك الخطأ، وبعدها ينبغي أن تَحفظ تلك التَغْيِيرات وتُجرِّب الأمر javac مُجددًا. يُفضل دومًا أن تُحاول إصلاح أول مجموعة صغيرة من رسائل الأخطاء، فعادةً ما يؤدي تَصْلِيحها إلى اختفاء بعض الأخطاء الآخرى. عندنا ينجح الأمر javac بعملية التَصرِيف، لن تَحصُل على أية رسائل نهائيًا أو قد تَحصُل على بعض "التحذيرات (warnings)" التي لا تَمنَع تَشْغِيل البرنامج. يُمكِنك الآن أن تَستخدِم الأمر java لتَشْغِيل البرنامج كما ذَكَرنا مُسْبَقًا. بمُجرّد تَصرِيف البرنامج، يُمكِنك أن تُشغِّله وقتما تُريد وبأي عدد من المرات دون الحاجة لإعادة تَصرِيفه (compile). ها هو مُلخَّص كل شيء تقريبًا: ابقي كُلًا من نافذة المحرر ونافذة سطر الأوامر مفتوحة. عَدِّل محتويات الملف ثم اِحفظه وأخيرًا حَاوِل تَصْرِيفه (compile) إلى أن تَختفِي جميع أخطاء بناء الجملة (syntax errors). ينبغي دومًا أن تَحفظ التَغْيِيرات التي أَجْرَيتها على الملف قَبْل أن تُحاوِل إعادة تَصْرِيفه حيث يرى المُصرِّف (compiler) آخر نُسخة محفوظة من الملف وليس النُسخة المَعرُوضة بنافذة المُحرر. بَعْدما تُشغِّل البرنامج، قد تَعثُر على بعض الأخطاء الدلالية (semantic errors)، وعندها يَنبغِي أن تعود إلى حَلْقة "عَدِّل واِحفظ وصَرِّف" في محاولة للعُثور على المشكلة وإصلاحها. استخدام JavaFX بسطر الأوامر تَتَكوَّن مكتبة JavaFX من مجموعة من الأصناف (classes) المَكْتُوبة بلغة الجافا، والتي يُمكِنها أن تُستخدَم لإنشاء برامج واجهة مُستخدِم رُسومية (GUI). سنَتَعرَّض لتلك المكتبة بالقسم ٣.٩ للمرة الأولى، وسنتناولها تفصيليًا بكُلًا من الفصلين ٦ و ١٣. بالإضافة إلى ذلك، ستَجِدها مُستخدَمة ضِمْن الأمثلة التوضيحية بعدة فصول آخرى. لقد كانت مكتبة JavaFX جزءًا قياسيًا من نسخة أوراكل من عُدة تطوير جافا (JDK) الإصدار ٨. إذا كنت قد ثَبَّت ذلك الإصدار، فأنت لست بحاجة للقيام بأي شيء آخر لاِستخدَامها. في المقابل، إذا كنت قد ثَبَّت OpenJDK أو JDK للإصدار ١١ أو أحدث، فينبغي عليك أن تُجْرِي عدة أشياء لأن مكتبة JavaFX أصبحت تُوزَّع منذ ذلك الإصدار كمجموعة من الوحدات (modules) بشكل مُستقِل. تُخزَّن الوحدات (modules) -كما سنُوضِح بالقسم الفرعي ٤.٦.٤- بهيئة ملفات .jar بالمجلد الفرعي "lib" بمجلد عُدة تطوير جافا إف إكس (Java SDK). عندما تَستخدِم أي من الأمرين java أو javac مع برنامج يَعتمِد على مكتبة JavaFX، ينبغي أن تُمرِّر مسار وحدات مكتبة JavaFX إلى ذلك الأمر. سنُوضِح بهذا القسم كيفية اِستخدَام JavaFX بسطر الأوامر بالإصدار ١١ أو أحدث. يُستخدَم الخياران --module-path و --add-modules لتَخْصِيص وحدات (modules) مكتبة JavaFX. يُخصِّص الخيار الأول المُجلد الذي يحتوي على ملفات .jar أما الثاني فيُخصِّص الوحدات (modules) المُستخدَمة ضِمْن البرنامج بشكل فعليّ. يُمكِنك ضَبْط قيمة الخيار --add-modules إلى القيمة "ALL-MODULE-PATH" مما سيُؤدي إلى إتاحة مكتبة JavaFX بالكامل أما الخيار --module-path، فينبغي أن تَضبُطه إلى مسار المجلد "lib" الذي يَحتوِي على ملفات .jar. لنَفترِض مثلًا أن اسم مجلد JavaFX هو "openjfx-sdk-11"، وبأنه موجود بالمجلد الرئيسي "/home/eck"، سيَكُون مسار المجلد "lib" هو "/home/eck/openjfx-sdk-11/lib" أي يُمكِننا عندها كتابة أمر javac التالي لتَصرِيف برامج JavaFX: javac --module-path=/home/eck/openjfx-sdk-11/lib --add-modules=ALL-MODULE-PATH ينبغي أن تُضيف إليه أسماء ملفات .java التي تُريد أن تُصرِّفها. يَتَطلَّب الأمر java نفس تلك الخيارات ليُشغِّل برامج JavaFX. يُمكِنك أن تَستخدِم الاختصار -p بدلًا من --module-path كالتالي: javac -p /home/eck/openjfx-sdk-11/lib --add-modules=ALL-MODULE-PATH إذا لم تَستطِع مَعرِفة مسار عُدة تطوير جافا إف إكس (JavaFX SDK)، اِفتح نافذة الأوامر (command window) واستخدِم الأمر cd لكي تنتقل إلى مجلد "lib" الخاص بعُدة تطوير جافا إف إكس. إذا كنت تَستخدِم نظام ماك أو لينكس، فيُمكِنك أن تَكْتُب الأمر pwd لكي تَطبَع مسار مجلد العمل (working directory) أما إذا كنت تَستخدِم نظام ويندوز، اِستخدِم الأمر cd بدون تَخْصِيص أي مجلد لكي تَطبَع المسار. أخيرًا، اِستخدِم خَرْج ما سبَق كقيمة للخيار --module-path. قد يبدو أمر java عند اِستخدَامه مع javafx كما يلي: java -p C:\Users\eck\openjfx-sdk-11\lib --add-modules=ALL-MODULE-PATH إذا اِحتوَى المسار على مسافة أو أي محارف خاصة آخرى، فلابُدّ أن تُحيطه بعلامتي اقتباس. لأن الأمر بالأعلى طويلًا نوعًا ما، سيَكُون من الأفضل بالتأكيد لو أمَكْنَنا تَجنُّب إعادة كتابته بكل مرة نُنفِّذه فيها. إذا كنت تَستخدِم نظام لينكس أو ماك، يُمكِنك أن تُعرِّف اسمًا مستعارًا (alias) يَعمَل كاختصار لأمر معين. على سبيل المثال، يُمكِنك أن تُعرِّف الاسم المستعار jfxc كاختصار لأمر تَصرِيف البرامج المُستخدِمة لمكتبة JavaFX. لاحِظ أنه من الضروري اِستخدَام المسار الكامل لكُلًا من الأمر javac ومجلد "lib" الخاص بمكتبة JavaFX أثناء تعريف الاسم المستعار. سيبدو تَعرِيفه مشابهًا لما يلي (لابُدّ من كتابته ضِمْن سطر واحد): alias jfxc='/home/eck/jdk-11.0.7/bin/javac --module-path=/home/eck/javafx-sdk-11/lib --add-modules=ALL-MODULE-PATH' بالمثل، يُمكِنك أن تُعرِّف الاسم المستعار jfx كاختصار لأمر تَشْغِيل البرامج المُستخدِمة لمكتبة JavaFX: alias jfx='/home/eck/jdk-11.0.7/bin/java --module-path=/home/eck/javafx-sdk-11/lib --add-modules=ALL-MODULE_PATH' لابُدّ أن تُضيِف تَعرِيفات الأسماء المستعارة (aliases) بالأعلى إلى ملف .bashrc بنظام لينكس وملف .bash_profile بنظام ماك بمُجلد المُستخدِم الرئيسي (home directory) لكي تَحتفِظ بها بصورة دائمة. عادةً ما يَكون ذلك الملف موجودًا، وإذا لم تَجِده، فيُمكِنك أن تُنشئه ببساطة. يُنفَّذ ذلك الملف بكل مرة تُفْتَح فيها نافذة طرفية (terminal) أيّ لن تُفعَّل أية تَعْدِيلات تُجرِيها عليه حتى تَفتَح طرفية جديدة. يبدأ اسم الملف بنقطة (period) مما يَعنِي كَوْنه ملف مَخفِي (hidden file) لا يُعرَض افتراضيًا بمُتصفِح الملفات. إذا كنت تَستخدِم نظام لينكس، يُمكِنك أن تُظهِر أية ملفات مخفية بمُتصفِح الملفات من خلال إِعدادات الضَبْط. (ألقي نظرة على قائمة "View"). ليس الأمر بنفس السهولة إذا كُنت تَستخدِم نظام ماك. في العموم، تستطيع دومًا أن تَستخدِم الأمر ls -a بنافذة سطر الأوامر (command line) لكي تَعرِض قائمة بأسماء الملفات الموجودة بمجلد معين بما في ذلك أية ملفات مخفية. إذا واجهتك مشكلة مع أي ملف مَخفِي (hidden file)، أَعِد تسميته ليُصبِح ملفًا عاديًا ثم عدِّله كما تشاء وأخيرًا أَعِد تسميته مرة آخرى. يُستخدَم الأمر mv لإعادة تسمية الملفات مثل mv .bash_profile temp و mv temp .bash_profile. في المقابل، لا يُوفِّر نظام ويندوز ملفًا لنافذة الأوامر cmd كالملفات .bashrc أو .bash_profile. كحل بديل، قد تُنشِئ ملف نص برمجي (script file) يُنفِّذ أمرًا. فمثلًا لتَصْرِيف برامج JavaFX، انشِئ ملفًا اسمه jfxc.bat يَحتوِي على سطر مُماثِل لما يلي: javac -p C:\Users\eck\openjfx-sdk-11\lib --add-modules=ALL-MODULES-PATH $* لكن بالطبع باِستخدَام مسار JavaFX الخاص بحاسوبك. تُمثِل $* مُدْخَلات الأمر javac. قد يَكُون الملف المُدْخَل موجودًا بالمجلد الحالي (current directory) أو بأي مكان آخر بمسار النظام (system path) مثل مجلد "bin" الخاص بعُدة تطوير جافا (JDK). تستطيع الآن أن تَستخدِم jfxc كأمر لتَصرِيف (compiling) البرامج المُستخدِمة لمكتبة JavaFX كالتالي: jfxc MyJavaFXProgram.java يُمكِنك أن تُطبِّق الشيء نفسه مع الأمر java. بيئة تطوير إكلبس المتكاملة (Eclipse) تَتَضمَّن بيئات التطوير المُتكاملة (Integrated Development Environment - IDE) كل شيء قد تحتاج إليه لإنشاء البرامج وتَصْرِيفها وتشْغِيلها ضِمن حزمة واحدة مُدعَّمة بواجهة مُستخدِم رُسومية (graphical user interface) مألوفة لغالبية مُستخدِمي الحاسوب. تَتَوفَّر مجموعة من بيئات التطوير المتكاملة لبرامج الجافا تتراوح من مجرد برامج بسيطة تعمل كغلاف لعُدة تطوير جافا (JDK) إلى تطبيقات معقدة تتميز بالعديد من الخاصيات. نظرًا لصعوبة تَعلُّم كُلًا من بيئات التطوير المتكاملة (IDE) ومفاهيم البرمجة نفسها في نفس الوقت، لا يُحبَّذ في العموم اِستخدَامها من قِبَل المُبرمجين المُبتدئين. سنتناول هنا بيئة تطوير إكلبس المُتكاملة (Eclipse IDE) بشيء من التفصيل وكذلك البديل الأكثر بساطة BlueJ على نحوٍ مختصر نوعًا ما. تتميز بيئات التطوير المُتكاملة (IDEs) بالكثير من السمات المفيدة حتى للمبرمج المبتدئ ولكنه عادةً ما سيتَجاهلها خاصة بالبداية. إذا لم تَكُن تَستخدِم نُسخة أوراكل من عُدة تطوير جافا (JDK) إصدار ٨ أو ٩ أو ١٠، فينبغي أن تَضْبُط بعض الإعدادت الآخرى لكي تَتَمكَّن من اِستخدَام إكلبس (Eclipse) مع البرامج المُستخدِمة لمكتبة JavaFX، وهو ما سنتناوله بالقسم الفرعي التالي. خلال هذا القسم، سنُناقش كيف يُمكِننا أن نَستخدِم إكلبس لكي نُنشِئ برامجًا تَعتمِد فقط على أصناف جافا القياسية (standard classes). يُمكِنك أن تُحمِّل بيئة تطوير إكلبس المُتكاملة (Eclipse IDE) من موقع إكلبس. حمِّل حزمة بيئة إكلبس لمطوري الجافا (Eclipse IDE for Java Developers) إذا كنت تَستخدِم نظام ويندوز أو لينكس، سيَكُون الملف المُحمَّل بهيئة ملف أرشيفي مَضغُوط. اِستخرِج مُحتوياته إلى مجلد بأي مكان ثم اُنقُر نقرة مزدوجة على أيقونة تطبيق إكلبس (Eclipse) التي ستَجِدها بذلك المجلد لتَشْغِيل البرنامج. إذا كنت تَستخدِم نظام ماك، فسيَكُون امتداد الملف المُحمَّل هو .dmg، يُمكِنك فتحه وسحبه إلى أي مكان تُفضِّله (ربما مُجلد التطبيقات). إكلبس (Eclipse) هو برنامج مجاني مَكْتُوب بلغة الجافا، ويَتَطلَّب عُدة تطوير جافا (JDK) إصدار ٨ أو أحدث لذا لابُدّ أن تُثبِّت عُدة تطوير جافا (JDK) قَبْل أن تُحاوِل تَشْغِيله. ملحوظة لمُستخدمِي ماك: منذ يونيو ٢٠١٩، تَسبَّب خطأ برمجي (bug) بإكلبس (Eclipse) بمنع تَشْغِيله مع عدة تطوير جافا (JDK) إصدار ١٢، لذا إذا كنت تُريد اِستخدَام إكلبس (Eclipse)، يُفضَّل أن تَعتمِد على الإصدار ١١ من JDK إلى أن يُصلَّح ذلك الخطأ. سيَطلُب منك برنامج إكلبس عند تَشْغِيله لأول مرة تَخْصِيص مساحة عمل (workspace) عبارة عن مجلد لتَخْزِين الملفات التي ستَعمَل عليها. يُمكِنك أن توافق على الاسم الافتراضي أو قد تُخصِّص أي اسم آخر. في الواقع، يُمكِنك أن تَستخدِم أكثر من مساحة عمل واحدة (workspaces) ثم تختار واحدة منها عند تَشْغِيله. عند فَتْح مساحة عمل (workspace) لأول مرة، ستَظهَر شاشة ترحيب كبيرة تَتَضمَّن روابط إلى عدة دروس إلى جانب التوثيق (documentation). اِضغَط على زر "X" الموجود إلى جوار كلمة "Welcome" لإغلاق الشاشة. يُمكِنك فَتْحها لاحقًا باختيار "Welcome" من قائمة "Help". تَتَكوَّن واجهة برنامج إكلبس (Eclipse) الرُسومية من نافذة كبيرة مُجزَّئة إلى عدة أقسام (section) يَتَكوَّن كُلًا منها من عَرْض (view) واحد أو أكثر. يُوضِح كل عَرْض (view) نوعًا مُختلفًا من المعلومات، فيُمكِن أن يَكُون بهيئة مُحرر نصي (text editor) أو مساحة لعمليات الخَرْج والدَخْل (I/O) أو قائمة بالمشروعات الخاصة بك. إذا كان هناك أكثر من عَرْض (view) ضِمْن قسم (section) واحد، فسيَظهَر شريط نوافذ (tabs) بالجزء العلوي من ذلك القسم يَسمَح لك باختيار العَرْض (view) الذي تَرغَب بإظهاره. يُطلَق اسم "منظور (perspective)" على مجموعة عُروض (views). يَستخدِم إكلبس (Eclipse) منظورات (perspectives) مختلفة لمجموعات مختلفة من العُروض (views) تَخُص أنواعًا مختلفة من المعلومات. ستحتاج عمومًا إلى ما يُعرَف باسم "منظور الجافا (java perspective)" لتَصْرِيف البرامج وتَشْغِيلها، وهو في الواقع المنظور الافتراضي. عندما تُصبِح أكثر خبرة، ستَستخدِم على الأغلب "منظور تَنْقِيح الأخطاء (debug perspective)" للعُثور على الأخطاء الدلالية (semantic errors) بالبرامج. توجد أزرار صغيرة بشريط أدوات إكلبس (Eclipse) للتَبْدِيل بين المنظورات (perspectives) المختلفة. يَتَكوَّن "منظور الجافا (java perspective)" من مساحة كبيرة بمنتصف النافذة تَحتوِي على عَرْض (view) واحد أو أكثر بهيئة مُحرر نصي (text editor). في الواقع، ستُنشِي البرامج وتُعدِّلها بتلك المساحة. على الجانب الأيسر منها، ستَجِد عَرْض بهيئة مُستكْشِف حزم (package explorer) يَعرِض قائمة بمشروعات الجافا وبملفات الشيفرة المصدرية (source code). على الجانب الأيمن منها، ستَجِد عدة عروض (views) آخرى غَيْر مُهِمّة لذلك يُمكِنك أن تُغلِقها بالنَقْر على "X" الموجود بجانب اسم كُلًا منها. بالإضافة إلى ذلك، ستَجِد عدة عروض (views) آخرى مفيدة أسفل مساحة التَعْدِيل. إذا أغلقت عَرْضًا (views) مُهِمًّا عن طريق الخطأ كمُستكِشف الحزم (package explorer)، يُمكِنك أن تُظهِره مُجددًا عن طريق اختياره من القائمة الفرعية "Show View" من قائمة "Window". يُمكِنك إعادة ضَبْط النافذة بالكامل إلى محتوياتها الافتراضية عن طريق اختيار "Reset Perspective" من قائمة "Window". لبدء العمل على إكلبس (Eclipse)، ينبغي أولًا أن تُنشِئ مشروعًا (project): اِختر الأمر "Java Project" من القائمة الفرعية "New" بقائمة "File". ستَظهَر نافذة أمامك، املأ حقل "اسم المشروع (Project Name)" بأي شيء تريده ثم اِضغَط على زر "Finish". قد يسألك إكلبس عما إذا كنت تُريد أن تُنشِئ ملف module-info.java، ولأننا لن نَستخدِم أية وحدات (modules) بأي برنامج نهائيًا، اُنقر دومًا على "Don't Create". اُنظر القسم الفرعي ٤.٦.٤ لمزيد من المعلومات عن الوحدات (modules). لابُدّ أن يَظهَر المشروع الآن بعَرْض (view) "مُستكِشف الحزم (package explorer)". اُنقُر على علامة المربع أو المثلث الصغيرة الموجودة إلى جانب اسم المشروع لترى مُحتوياته. إذا كنت تَستخدِم الإعدادات الافتراضية، ينبغي أن تَجِد مُجلد اسمه "src" مُخصَّص لملفات الشيفرة المصدرية لذلك المشروع، وستَجِد أيضًا "مكتبات بيئة تّنْفيذ الجافا (JRE System Library)" مُكوَّنة من مجموعة من الأصناف القياسية المَبنية مُسْبَقًا (standard built-in classes). لتَشْغيِل أي من برامج الجافا المَعرُوضة هنا، ينبغي أن تَنسَخ ملف الشيفرة المصدرية (source code) للبرنامج إلى مشروع الجافا بتطبيق إكلبس (Eclipse). إذا كُنت قد حمَّلت ذلك الملف إلى حاسوبك، تستطيع ببساطة أن تَنسخُه وتُلصقه إلى نافذة إكلبس كما يلي: اُنقُر بزر الفأرة الأيمن على أيقونة الملف ثُمَّ اِختَر "اِنسَخ (Copy)" من القائمة ثُمَّ اُنقُر بزر الفأرة الأيمن على مُجلد "src" بالمشروع بنافذة إكلبس واِختَر "اِلصِق (Paste)". تَأكَّد من لَصْقُه بمُجلد "src" لا مجلد المشروع نفسه، فالملفات الموجودة خارج مُجلد "src" لا تُعامَل كملفات شيفرة مصدرية. بدلًا من ذلك، يُمكِنك أن تَسحَب أيقونة الملف من نافذة مُتصفِح الملفات إلى مجلد "src" بنافذة إكلبس. لكَوْن الأمثلة هنا مُعتمِدة على الصَنْف TextIO غَيْر القياسي، ينبغي إذًا أن تُضيِف الملف TextIO.java إلى المشروع بحيث يَقَع ضِمْن حزمة اسمها textio. إذا كُنت قد حمَّلت ذلك الملف إلى حاسوبك بمُجلد اسمه "textio" كما أَوضَحنا مُسْبَقًا، يُمكِنك نَسخُه ولَصقُه إلى مجلد المشروع "src". بدلًا من ذلك، يُمكِنك أن تُنشِئ الحزمة textio باختيار الأمر "New/Package" من قائمة "File". سيُنشِئ ذلك مجلدًا جديدًا اسمه "textio" بالمشروع، وعليه، تستطيع نَسْخ الملف TextIO.java بداخله. أيًا كانت الطريقة المُستخدَمة، لابُدّ أن يَحتوِي مجلد المشروع "src" على الملف TextIO.java داخل مجلد ثانوي اسمه هو textio. إذا وَضعَت ملفًا بمكان غَيْر صحيح عن طريق الخطأ، يُمكِنك سَحْبه من مكانه إلى أي مكان آخر بـ"مستكشف الحزم (package explorer)". بَعْدما تُضيِف برنامج الجافا إلى مشروعك، اُنقُر نقرة مزدوجة على اسم الملف الموجود بعَرْض "مُستكِشف الحزم (package explorer)" لفَتْح البرنامج ثم اُنقُر بزر الفأرة الأيمن على نافذة المُحرّر أو على اسم الملف بمُستكِشف الحزم واِختَر "Java Application" من القائمة الفرعية "Run As" لتّنْفيذ البرنامج. إذا كان البرنامج مُصمَّمًا ليَكْتُب إلى الخَرْج القياسي (standard output)، فسيَظهَر الخَرْج بالعَرْض "Console" بالقسم الواقع أسفل المُحرّر بنافذة إكلبس. إذا كان البرنامج يَستخدِم أي من الصَنْفين TextIO أو Scanner لقراءة أية مُدْخَلات (input)، ستَضطّر إلى كتابة المُدْخَلات المطلوبة بالعَرْض "Console". لاحِظ أنك ستحتاج للضَغْط على ذلك العَرْض قبل البدء بالكتابة لكي تتأكَّد من إرسال المحارف التي ستَكْتُبها إلى القسم الصحيح من نافذة إكلبس. تَتَوفَّر طريقة أسهل لتَشْغيل البرنامج: اِضغَط على زر "شَغِّل (Run)" بشريط أدوات إكلبس. وفقًا للسياق، سيؤدي ذلك إلى تَشْغِيل البرنامج المَعرُوض بنافذة المُحرّر أو البرنامج المُختار بمُستكِشف الحزم (package explorer) أو أحدث برنامج كان مُشغَّلًا للتو. ملحوظة: عندما تُشغِّل برنامجًا بإكلبس، فليس هناك خُطوة مُنفصِلة لعملية التَصرِيف (compilation) حيث تُصرَّف ملفات البرنامج أتوماتيكيًا. يُمكِنك أن تُضيِف أكثر من برنامج إلى نفس المشروع أو قد تُنشِئ مشروعات إضافية لتَنْظِيم العمل على نحو أفضل. تَذكَّر دومًا أن تُضيِف نُسخة من الصَنْف TextIO.java بمجلد اسمه textio بأي مشروع يَستخدِمه. إذا كنت تريد أن تُنشِئ برنامج جافا جديد بإكلبس، لابُدّ إذًا من أن تُنشِئ صنفًا (class) جديدًا: اُنقُر بزر الفأرة الأيمن على اسم المشروع بمُستكِشف المشروع (Project Explorer) ثم اُنقُر على القائمة الفرعية "New" ثم اِختَر "Class". بدلًا من ذلك، تستطيع أن تَنقُر على واحدة من الأيقونات الصغيرة بصندوق الأدوات أعلى نافذة إكلبس. بالنافذة المفتوحة، اُكْتُب اسم الصنف (class) -وليس اسم ملف الشيفرة المصدرية- الذي تريد أن تُنشئه بشَّرط أن يَكُون عبارة عن اسم مُعرف هوية (identifier) صالح. تَتَضمَّن النافذة صندوق إدخال آخر بعنوان "حزمة (Package)" حيث يُمكِنك أن تُخصِّص اسم الحزمة (package) المطلوب إضافة الصَنْف إليها. تَستخدِم غالبية الأمثلة بهذا الكتاب "الحزمة الافتراضية (default package)". اُترك صندوق الإدخال فارغًا لكي تَستخدِم الحزمة الافتراضية، ولكن يُمكِنك أيضًا أن تُخصِّص اسم حزمة إذا أردت. أخيرًا، اضغط على زر "Finish" لإنشاء الصنف، والذي ينبغي أن يَظهَر بالمجلد "src" داخل مجلد فرعي مُناظِر للحزمة (package) المُخصَّصة كما سيُفتَح الملف الجديد أتوماتيكيًا بمساحة التعديل (editing area) لكي تََتَمكَّن من البدء بالكتابة. يُوفِّر إكلبس خاصيات مُتعدّدة ستُساعدك حتمًا أثناء كتابة الشيفرة. على سبيل المثال، يَظهَر خط أحمر خشن أسفل أي خطأ في بناء الجملة (syntax error)، وفي بعض الحالات، تَظهَر علامة خطأ إلى الجانب الأيسر من نافذة المُحرر، والتي تَعرِض وصفًا للخطأ إذا حومت (hover) مؤشر الفأرة عليها أو على الخطأ نفسه. لا ينبغي أن تُحاوِل التَخلُّص من كل خطأ يَظهَر أثناء كتابة الشيفرة خاصة وأن بعضها يَختفِي تلقائيًا بَعْدما تُكمِل كتابة البرنامج. إذا كانت علامة الخطأ بهيئة مصباح كهربي، يَعنِي ذلك أن إكلبس يَعرِض محاولة لإصلاح الخطأ أتوماتيكيًا. اِضغَط على علامة المصباح أو حوم (hover) مؤشر الفأرة على الخطأ الفعليّ للحصول على قائمة بالحلول المُحتمَلة. يُمكِنك الضَغْط على أي منها لتطبيق الحل. على سبيل المثال، إذا اِستخدَمت مُتْغيِّرًا (variable) بدون أن تُصرِّح عنه، سيَعرِض إكلبس إمكانية التَصْرِيح (declare) عنه أتوماتيكيًا مما يعني السماح لإكلبس بكتابة أنواع محددة من الشيفرة. ملحوظة: لن تَفهَم جميع الحلول التي يقترحها إكلبس حتى تَتَعلَّم المزيد عن لغة الجافا لذا لا تُحاوِل تطبيق أي حل لا تفهمه فعادةً ما سيَجعَل ذلك الأمور اسوأ في النهاية. يَعثُر إكلبس (Eclipse) أيضًا على أخطاء التهجئة بالتعليقات (comments) ويَضَع خطًا أحمرًا خشنًا أسفلها. حوم مؤشر الفأرة على واحدًا من تلك الأخطاء لتَجِد قائمة بالحلول الصحيحة المُحتمَلة. يتميز إكلبس بخاصية أخرى أساسية هي مُساعد المُحتوى (content assist) المُستخدَمة لعَرْض التكميلات المُحتمَلة لما تَكْتبه بتلك اللحظة. يُمكِنك استدعاء المساعد بالضَغْط على زري "Control-Space". على سبيل المثال، إذا كُنت تَكْتب جُزءًا من مُعرِّف هوية (identifier) ثم ضَغْطت على زري "Control-Space"، ستَظهَر قائمة بكل مُعرِّفات الهوية المتاحة البادئة بالمحارف التي كُنت قد كَتْبَتها. اِستخدِم مفتاحي الأسهم (arrow keys) "لأعلى" و "لأسفل" لاختيار أي عنصر بالقائمة ثم اضغط "Enter". يُمكِنك أيضًا أن تَنقُر على أي عنصر باِستخدَام الفأرة لاختياره أو قد تَضغَط على "Escape" لإخفاء القائمة. إذا ضَغطَت على "Control-Space" وكان هنالك احتمالًا تَكْميليًا واحدًا، فسيُكتَب أتوماتيكيًا. يظهر مساعد المحتوى (content assist) تلقائيًا بصورة افتراضية عند كتابة نقطة أو عدة محارف آخرى، فمثلًا، إذا كَتَبَت "TextIO." ثم اِنتظرت لجزء من الثانية، ستَظهَر قائمة بجميع البرامج الفرعية (subroutines) المُعرََّفة بالصَنْف TextIO. يُمكِنك إلغاء التَفْعِيل التلقائي لتلك الخاصية من خلال إعدادات إكلبس (جافا > المحرر > مساعد المحتوى). ما يزال بإمكانك استدعائه يدويًا باستخدام زري "Control-Space". بمُجرد أن يُصبِح برنامجك خالٍ من الأخطاء، يُمكِنك أن تُشغِّله كما أوضحنا بالأعلى. إذا وجدت مشكلة عند تَشْغِيله، عُد إلى المُحرّر لتجُرِي التعديلات المطلوبة ثُمَّ جَرِّب تَشْغِيله مرة آخرى. استخدام JavaFX ببيئة تطوير إكلبس (Eclipse) إذا كنت تَستخدِم إصدارً من عُدة تطوير جافا (JDK) لا يَحتوِي على مكتبة JavaFX، فلابُد من أن تَضبُط مشروعات إكلبس لكي تَتَمكَّن من اِستخدَام تلك المكتبة. تَتَوفَّر في الواقع الكثير من الطرائق للقيام بذلك. سنَعرِض واحدة منها هنا والتي ليست بالضرورة أصحها أو أفضلها، فنحن فقط نحتاج إلى تَطْوِير بعض البرامج البسيطة المُعتمِدة على JavaFX. سنَضبُط إحدى إعدادات مساحة العمل (workspace) بإكلبس (Eclipse). تَعمَل هذه الطريقة مع كُلًا من الإصدار ١١ من الجافا وبيئة إكلبس (Eclipse) حتى إصدار يونيو ٢٠١٩. قد لا تتوافق هذه الطريقة مع الإصدارات الأحدث من كُلًا منهما. اِختَر الأمر "Preferences" من قائمة "Windows" بنظامي لينكس وويندوز أو من قائمة "Eclipse" بنظام ماك لفَتْح نافذة الإِعدادات المُفضَّلة. اُنقُر على علامة المربع أو المثلث الصغيرة الموجودة إلى جانب كلمة "Java" بالقائمة على يسار النافذة لفَتْح قسم "Java" ثم اُنقُر على "بيئات تّنْفيذ جافا المُثبَّتة (Installed JREs)". ستَظهَر قائمة ببيئات الجافا (Java environments) التي تَعرَّف عليها إكلبس بالفعل. يُمكِنك القيام بأمر من اثنين: اِضغَط على الزر "Add" لإضافة بيئة تّنْفيذ جافا (JRE) جديدة أو اِختَر واحدة من تلك الموجودة بالقائمة واِضغَط على "Edit". إذا اِختَرت إضافة بيئة تّنْفيذ جديدة، سيَظهَر صندوق يسألك عن نوع بيئة التّنْفيذ (JRE Type)، اِختَر "Standard VM" ثم اِضغَط على "Next". سيَظهَر صندوق مُشابه للصورة بالأسفل. في المقابل، إذا اِختَرت التَعْدِيل على بيئة تّنْفيذ (JRE) موجودة، فإن الحقلين "مسار بيئة التّنْفيذ (JRE Home)" و "اسم بيئة التنفيذ (JRE name)" سيَكُونا مملوئين بالفعل أما حقل "مكتبات بيئة تّنْفيذ الجافا (JRE system libraries)" فسيَكُون فارغًا. إذا اِختَرت إضافة بيئة تّنْفيذ جافا (JRE) جديدة، ستَحتاج إلى تَخْصِيص "مسار بيئة التّنْفيذ (JRE Home)". لابُدّ أن يَحتوِي المجلد الذي يُشير إليه ذلك المسار على مجلد "bin" بعُدة تطوير جافا. اِضغَط على زر "Directory" لفَتْح مُتصفِح الملفات ثم اِختَر المجلد. يُمكِنك اختيار عُدة تطوير جافا (JDK) مُثبَّتة على الحاسوب أو لا. بالمثال المُوضَّح بالصورة، كانت عُدة تَطوِير جافا (JDK) مُثبَّتة على نظام لينكس. إذا كنت تَستخدِم ويندوز أو لينكس، اِختَر مجلد JDK الرئيسي أما إذا كنت تَستخدِم نظام ماك، فاِختَر مجلد "Home" الموجود بمجلد "Content" الموجود بمجلد JDK الرئيسي أي لابُدّ أن يَنتهِي المسار بـ"/Content/Home". إذا أَدْخَلت مُجلد عُدة تطوير (JDK) صالح، فسيَملأ إكلبس كُلًا من "اسم بيئة التّنْفيذ (JRE name)" و"مكتبات النظام (system library)" تلقائيًا. لابُدّ أن تَكون مكتبة JavaFX مُتاحة للبرنامج خلال زمني التَصْرِيف (compile time) والتَشْغِيل (run time). بالنسبة لزمن التصريف (compile time)، يُمكِنك أن تُضيِف ملفات المكتبة .jar إلى مكتبات النظام (system libraries): اِضغَط على زر "Add External JARs" ثم اِنتقِل إلى مجلد "lib" الموجود بمجلد عُدة تطوير جافا إف إكس (JavaFX SDK) المُحمَّلة أو المُثبَّتة. ستَجِد سبعة ملفات مُنتهيّة بالامتداد .jar، اِخترها جميعًا ثم اُنقُر على "OK". ينبغي أن تَظهَر جميعًا بقائمة "مكتبات بيئة تّنْفيذ الجافا (JRE system libraries)". بالنسبة لزمن التَشْغِيل (run time)، ينبغي أن تُضيِف عدة وسائط (arguments) إلى صندوق "Default VM arguments" يدويًا. يَتَضمَّن ذلك الصندوق عدة خيارات ستُضَاف إلى الأمر java عندما تُشغِّل برنامجًا باِستخدَام تلك النُسخة من عُدة التطوير (JDK). ينبغي أن تَكْتُب نفس الخيارات التي كنت تَستخدِمها مع سطر الأوامر (command line) كما ناقشنا بالأعلى، فمثلًا، قيمة الخيار -p أو --module-path هي مسار المجلد "lib" الخاص بمكتبة JavaFX أما قيمة الخيار --add-modules فيُمكِن أن تَكُون "ALL-MODULE-PATH". عندما تَنتهِي من ضَبْط كل شيء، اُنقُر على "Finish" ثم اُنقُر على "Apply and Close" بنافذة "الإعدادات المُفضَّلة (Preferences)" الرئيسية. عندما تُنشِئ مشروع جافا جديد بمساحة العمل (workspace)، اِختَر بيئة التّنْفيذ (JRE) التي أَضفتها أو عدَّلتها للتو بنافذة إِنشاء المشروع. ينبغي الآن أن تَكُون قادرًا على اِستخدَام مكتبة JavaFX ضِمْن ذلك المشروع. إذا واجهتك مشكلة، اِفحَص إعدادات بيئة تّنْفيذ الجافا (JRE). ملحوظة: ليس هنالك أي ضرر من اِستخدَام بيئة تّنفيذ (JRE) مُدعِّمة لمكتبة JavaFX مع البرامج العادية التي لا تَستخدِم تلك المكتبة. بتعبير آخر، يُمكِنك أن تَستخدِم نفس الإعدادات السابقة مع كل البرامج التي سنتناولها لاحقًا. بيئة التطوير BlueJ تُعدّ بيئة التطوير BlueJ مُوجَّهة للمبتدئين الراغبين بتَعلُّم البرمجة. هي في العموم أقل تعقيدًا بكثير من بيئة إكلبس (Eclipse) ولكنها مع ذلك تُوفِّر مجموعة من الخاصيات المفيدة للأغراض التعليمية. يُمكِنك أن تُحمِّل بيئة BlueJ من موقعها الرسمي. يتوافق إصدارها الحالي مع الإصدار ١١ من كُلًا من الجافا و جافا إف إكس (JavaFX) كما يَتَوفَّر إصدار سابق متوافق مع الإصدار ٨ من الجافا. عندما تُشغِّل BlueJ لأول مرة، سيَطلُب منك إدخال مسار كُلًا من JDK و JavaFX. تستطيع الآن أن تُصرِّف البرامج المُستخدِمة لمكتبة JavaFX وتُشغِّلها باستخدام بيئة BlueJ. يُمكِنك أن تُنشِئ مشروعًا جديدًا باختيار الأمر "New Project" من قائمة "Project". لاحِظ أن المشروع ببيئة BlueJ عبارة عن مجلد. عندما تُنشِئ أي مشروع جديد، سيتعيَّن عليك اختيار اسم ذلك المجلد. ستُنشِئ بيئة BlueJ المجلد وستَفتَح نافذة تَعرِض مُحتوياته بهيئة أيقونات. يُمكِنك أن تُضيِف أي ملف .java إلى المشروع بسَحبُه من نظام الملفات (file system) إلى النافذة. سيُنسَخ الملف إلى مجلد المشروع وسيَظهَر أيضًا على النافذة. في المقابل، يُمكِنك أن تَنسَخ أي ملف مُباشرةً إلى مجلد المشروع، ولكنك لن تراه بيئة BlueJ حتى تُعيد فَتْح المشروع. عندما تُعيد تَشْغِيل بيئة BlueJ، سيَظهَر آخر مشروع كُنت تَعمَل عليه، ولكن يُمكنِك طبعًا فَتْح أي مشروع آخر من أحد الأوامر (command) بقائمة "Project". ستَجِد زرًا بنافذة المشروع لإنشاء صَنْف (class) جديد. عند النَقْر عليه، يُضيِف BlueJ أيقونة صَنْف إلى النافذة ويُنشِئ ملف شيفرة مصدرية بامتداد .java بمُجلد المشروع لكنه لا يَفتَحه تلقائيًا. يُمكِنك أن تَنقُر على أيقونة أي ملف نقرةً مزدوجة لفَتْحه، وسيَظهَر عندها مُحرّر يَعرِض مُحتوياته ضِمْن نافذة مُنفصلة. ستُلاحِظ أن أي صَنْف (class) جديد يَتَضمَّن افتراضيًا شيفرة لن تَرغَب بها عادةً. يُمكِنك حَذفَها وإضافة البرنامج main() بدلًا منها. يَختلِف مُحرّر بيئة BlueJ عن مُحرّر Eclipse بعدة أمور: أولًا، لا يَعرِض مُحرّر BlueJ أية أخطاء (errors) أثناء كتابة الشيفرة وإنما يُبلِّغ عنها عند محاولة تَصْرِيف (compile) البرنامج. ثانيًا، لا يَقترِح المُحرّر أية حلول تلقائية لأي من تلك الأخطاء. ثالثًا، يُوفِّر مُحرّر BlueJ مُساعد مُحتوى (content assist) بدائي يَقْتصِر دوره على عَرْض البرامج الفرعية (subroutines) المُعرَّفة بصَنْف (class) أو كائن (object). اِضغَط على زري "Control-Space" لاستدعائه بَعْد كتابة النقطة التي تأتي بَعْد اسم الصَنْف أو الكائن. تَحتوِي نافذة المُحرّر على زر لتَصْرِيف (compile) البرنامج المَعرُوض بينما تَحتوِي نافذة المشروع على زر آخر لتَصْرِيف جميع الأصناف (classes) ضِمْن المشروع. لابُدّ أن تُصرِّف (compile) البرنامج أولًا لكي تَتَمكَّن من تَشْغِيله. اُنقُر بزر الفأرة الأيمن على أيقونة البرنامج المُصرَّف. ستَظهَر قائمة، اِختَر منها "void main(String[] args)" لتَشْغِيل البرنامج ثم اُنقُر على "OK". ستُفتَح بَعْد ذلك نافذة مُنفصلة تَستقبِل مُدْخَلات البرنامج وتَعرِض مُخْرَجاته. يُمكِنك أن تُشغِّل أي برنامج فرعي (subroutine) وليس فقط البرنامج main ببيئة التَطوِير BlueJ، وهو ما يُعدّ واحدًا من أفضل مميزاتها. إذا كان لديك صَنْف (class) يَحتوِي على برامج فرعية (subroutines) آخرى غير البرنامج main، فستَجِدها ضِمْن القائمة المَعرُوضة بَعْد النَقْر بزر الفأرة الأيمن على أيقونة الصَنْف. إذا كان البرنامج (routine) يَتَطلَّب أية مُعاملات (parameters)، فسيُسمَح لك بإِدْخَالها عَبْر نافذة، وإذا كان دالة (function) -أي يُعيد قيمة-، فسيَظهَر صندوق آخر بَعْد تّنْفِيذ البرنامج ليُخبرِك بقيمته المُعادة (return value) مما يَسمَح لك بإجراء اختبار لأي برنامج فرعي (subroutine) بشكل مُستقِل. بالإضافة إلى ذلك، تستطيع أيضًا أن تُنشِئ كائنًا (object) من صَنْف معين ببيئة BlueJ، وستَظهَر بَعْدها أيقونة للكائن (object) أسفل نافذة المشروع. يُمكِنك أن تَنقُر بزر الفأرة الأيمن على أيقونة الكائن لتَحصُل على قائمة بجميع البرامج الفرعية (subroutines) المُعرَّفة بداخله. بالطبع لن يَكُون ذلك مفيدًا لك حتى نَصِل إلى الفصل الخامس حيث سنَتَعلَّم البرمجة كائنية التوجه (object-oriented programming). الحزم (packages) يَقَع أي صَنْف (class) بالجافا ضِمْن شيء يُطلق عليه اسم "حزمة (package)". تُعرَّف الأصناف التي لا يُعلَّن صراحةً عن وقوعها ضِمْن حزمة داخل "الحزمة الافتراضية (default package)" تلقائيًا. تَقَع جميع أصناف الجافا القياسية (standard classes) ضِمْن حزم مُسمَاة (named packages)، فمثلًا، الأصناف String و System مُعرَّفة ضِمْن حزمة java.lang، والتي تُستورَد (import) جميع أصنافها تلقائيًا إلى أي ملف جافا. في المقابل، ينبغي أن تَستخدِم المُوجِّه import لاستيراد الأصناف المُعرَّفة بأي حزم آخرى غَيْر حزمة java.lang. على سبيل المثال، يَقَع الصَنْف TextIO بحزمة اسمها textio لذا ينبغي أن تَستورِدها إلى أي برنامج يرغب باِستخدَامها. سنُناقش الحزم (packages) على نحو مُفصَّل بالقسم ٤.٦. تحتاج الآن فقط إلى مَعرِفة بعض الحقائق البسيطة. وفقًا للقواعد الإرشادية الرسمية للجافا، لا يُنصَح باِستخدَام الحزمة الافتراضية (default package)، ولكننا سنَعتمِد عليها بغالبية الأمثلة؛ لأنه من الأسهل دومًا للمبرمجين المبتدئين تَجنُّب أي حديث عن الحزم (packages) قَدْر الإمكان. إذا كنت تُنشِئ صنفًا (class) وحَاوَل إكلبس (Eclipse) أن يَضَعه ضِمْن حزمة، يُمكِنك حَذْف اسم الحزمة من نافذة إنشاء الصَنْف لتُجبره على اِستخدَام الحزمة الافتراضية (default package). في المقابل، إذا كان لديك صَنْف داخل حزمة (package) بالفعل، فستبدأ شيفرة ذلك الصَنْف بسطر يُخصِّص اسم الحزمة التي يَقَع ضِمْنها الصَنْف، فمثلًا، إذا كان الصَنْف بحزمة اسمها test.pkg، فسيَكُون أول سطر من ملف الصَنْف كالتالي: package test.pkg; على سبيل المثال، يبدأ ملف الشيفرة المصدرية للصَنْف TextIO بالسطر package textio;. لقد اِختَرنا تَعرِيف الصَنْف TextIO ضِمْن حزمة مُخصَّصة لأنه لا يُمكِن لأي صَنْف (class) يَقَع ضِمْن حزمة (package) غَيْر افتراضية أن يَستخدِم صَنْفًا من الحزمة الافتراضية (default package) أي إذا كان الصَنْف TextIO ضِمْن الحزمة الافتراضية، فيُمكِنك اِستخدَامه بالبرامج الواقعة ضِمْن الحزمة الافتراضية فقط. في الواقع، لقد كان الصَنْف TextIO مُعرَّفًا داخل الحزمة الافتراضية (default package) بالنُسخ الأقدم من هذا الكتاب. ملحوظة: ستُواجِه بعض التَعْقيدات إذا استخدَمت الحزم (packages) ببيئة سطر الأوامر (command-line environment). فلنَفْترِض مثلًا أنك عرَّفت برنامجًا ضِمْن حزمة اسمها text.pkg، لابُدّ إذًا أن يَقَع ملف ذلك البرنامج بمجلد فرعي اسمه "pkg" داخل مجلد آخر اسمه "test" والذي يَقَع بدوره داخل مُجلد العمل الرئيسي. في نفس الوقت، لابُدّ أن تَعمَل بمجلد العمل الرئيسي (main directory) أثناء تَصرِيف أي برنامج أو تّنْفِيذه. لذا عندما تُصرِّف (compile) ملف الشيفرة المصدرية لذلك البرنامج، ستَضطّر إلى كتابة اسم المجلد. على سبيل المثال، لتَصرِيف البرنامج الوَاقِع ضِمْن الحزمة test.pkg، ستَضطّر إلى اِستخدَام الأمر javac test/pkg/ClassName.java إذا كنت تَستخدِم لينكس أو ماك أو الأمر javac test\pkg\ClassName.java إذا كنت تَستخدِم ويندوز. بالنسبة لتّنْفِيذ البرنامج، فستَستخدِم الأمر java test.pkg.ClassName مع نقطة تَفصِل اسم الحزمة (package) عن اسم الصَنْف (class). الأمر jshell تُعدّ أداة سطر الأوامر jshell جزءًا قياسيًا من عُدة تطوير جافا (JDK) إصدار ٩ أو أحدث. إذا كان لديك أيًا من تلك الإصدارات وتستطيع اِستخدَام الأمرين java و javac بسطر الأوامر (command line)، فبإمكانك أيضًا اِستخدَام أداة jshell. إذا كنت تَستخدِم نظام ماك، فقد تحتاج إلى تَخْصِيص المسار الكامل للملف التّنْفيذي jshell الموجود بمُجلد "bin" بعُدة تَطوِير جافا. يَسمَح لك الأمر jshell بكتابة شيفرة جافا وتّنفِيذها دون الحاجة لإنشاء ملف .java وكتابة البرنامج main. اُكتُب الأمر jshell بنافذة سطر الأوامر لتَشْغِيله. ستَحصُل بَعْدها على محث الأمر jshell حيث ستَتَمكَّن من كتابة تَعْليمَة (statement) أو تعبير (expression) بالجافا. إذا أَدْخَلت تَعْليمَة، فإنها ستُنفَّذ أما إذا أَدْخَلت تعبيرًا، فستُطبَع قيمته. لاحِظ أنك لا تحتاج إلى كتابة فاصلة منقوطة (semicolon) بنهاية السطر. يَعرِض المثال التالي جلسة (session) قصيرة لاِستخدَام الأمر jshell: $ jshell | Welcome to JShell -- Version 11.0.7 | For an introduction type: /help intro jshell> System.out.println("Hello World") Hello World jshell> int x = 42 x ==> 42 jshell> x * x $3 ==> 1764 jshell> /exit | Goodbye ستُساعدك أداة jshell بلا شك بينما تَتَعلَّم الجافا وتُجرِّب مميزاتها، ولكننا لن نتعمَّق في الحديث عنها. يُمكِنك مع ذلك الإطلاع على توثيقها بموقع أوراكل. ترجمة -بتصرّف- للقسم Section 6: Programming Environments من فصل Chapter 2: Programming in the Small I: Names and Things من كتاب Introduction to Programming Using Java.
-
يُلقِي هذا القسم نظرة على التعبيرات (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.
-
خدمات استدعاء المنصة Platform Invocation Services يُشار إلى الشيفرة التي تُنفَّذ داخل بيئة التَّنفيذ المشتركة (CLR) باسم الشيفرة المُدارة (managed code) بخلاف الشيفرة التي تُنفَّذ خارجها، والتي يُشار إليها بطبيعة الحال باسم الشيفرة غير المُدارة (unmanaged code). تَتوفَّر العديد من الطرائق لتَسهِيل العمليات بين الشيفرات (interoperability) من كِلَا النوعين. تُعدّ خدمات اِستدعاء المنصة Platform Invocation Services إحداها. استدعاء شيفرة غير مُدارة من أُخرى مُدارة يُمكِنك استدعاء إحدى الدوال غير المُدارة مثل دوال واجهة ويندوز لبرمجة التطبيقات Windows API -في حالة عدم وجودها بمكتبات الأصناف المُدارة (class libraries)- عن طريق التََّصرِيح عن تابع خارجي ساكن (static extern) يَحمِل نفس بَصمة الدالة المطلوب استدعائها. سيُعدّ هذا التابع بمثابة مُمثِل للدالة غير المُدارة بحيث تستطيع استدعائها باستدعائه. لتحقيق ذلك، لابُدّ أن يُزَخرَف هذا التابع بالسمة DllImportAttribute مع تمرير اسم مكتبة dll التي توجد بها تلك الدالة كمُعامِل للسمة. عند استدعائك لهذا التابع، ستُحمِّل خدمات استدعاء المنصة Platform Invocation Services مكتبة dll المُخصَّصة، ثم تَستدعِي الدالة غير المُدارة المُناظِرة للتابع. ستَحتاج إلى تَضمِين فضاء الاسم System.Runtime.InteropServices. انظر المثال التالي: using System.Runtime.InteropServices; class PInvokeExample { [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern uint MessageBox(IntPtr hWnd, String text, String caption, int options); public static void test() { MessageBox(IntPtr.Zero, "Hello!", "Message", 0); } } اِستعِن بتوثيق pinvoke.net قبل التَّصرِيح عن تابع خارجي مُمثِل لإحدى دوال واجهة ويندوز لبرمجة التطبيقات Windows API، فغالبًا ستَجِدْ معلومات عن الطريقة المُلائمة للتَّصرِيح عنه مع جميع الأنواع المطلوبة كمُعامِلات أو كنوع للقيمة المُعادة بالإضافة إلى بعض الأمثلة التوضيحية. ترتيب الأنواع (Marshalling) في حالة وجود مُعامِلات للدوال غير المُدارة أو قيم مُعادَة منها، فغالبًا ما تَتَمَكَّن خدمات استدعاء المنصة Platform Invocation Services من تحويل أنواع .NET المُدارة -بالتحديد الأنواع البسيطة- إلى أنواع المكتبة المُستخدَمة ضِمْن الاستدعاء والعكس تحويلًا أتوماتيكيًا وبدون تعليمات إضافية. تُعرَف هذه العملية باسم الترتيب (Marshalling). أما إذا كان المُعامِل من نوع مُركَّب مثل struct أو union، فستَحتَاج إلى التَّصرِيح عن صنف جديد struct أو class بشيفرة الـ c# الخاصة بك. يَعمَل هذا الصنف كمُمثِل للنوع غير المُدار، ولابُدّ أن يُزخرَف بالسمة StructLayoutAttribute لإعلام المُرَتِّب (marshaler) بطريقة رَبْط الحُقول (mapping). قد تحتاج أيضًا إلى مزيد من التَخصِيص. ترتيب النوع union يَستعرِض المثال التالي تَّصرِيح مكتبة c++ عن صنف union مُكَّون من حقول من نوع القيمة فقط: typedef union { char c; int i; } CharOrInt في هذه الحالة، تستطيع اِستخدَام LayoutKind.Explicit كقيمة لمُعامِل السمة StructLayout، كالتالي: [StructLayout(LayoutKind.Explicit)] public struct CharOrInt { [FieldOffset(0)] public byte c; [FieldOffset(0)] public int i; } أما إذا كان الصنف union مُكَّون من حقول من نَوعي القيمة والمَرجِع، كالتالي: typedef union { char text[128]; int i; } TextOrInt; في هذه الحالة، لا يُمكِنك مجرد اِستخدَام السمة FieldOffset كالمثال الأسبق. وإنما ستحتاج في الغالب إلى تَخصِيص عملية الترتيب (marshaling). مع ذلك، يُعدّ المثال بالأعلى بسيطًا نوعا ما، وتَتوفَّر طريقة مُبسَّطة باِستخدَام القيمة LayoutKind.Sequential كقيمة لمُعامِل السمة StructLayout مما يعني طريقة تسلسُليّة لرَبْط الحقول (mapping). قد تحتاج بعض أنواع حقول الصنف إلى الزَخرَفة باستخدَام السمة MarshalAs لتَخصِيص النوع غير المُدار المُناظِر. [StructLayout(LayoutKind.Sequential)] public struct TextOrInt { [MarshalAs(UnmanagedType.ByValArray, SizeConst = 128)] public byte[] text; public int i { get { return BitConverter.ToInt32(text, 0); } } } ترتيب النوع struct يستعرض المثال التالي تَّصرِيح مكتبة c++ عن صنف struct: typedef struct _PERSON { int age; char name[32]; } PERSON, *LP_PERSON; void GetSpouse(PERSON person, LP_PERSON spouse); بصورة مشابهة للأنواع unions. ستحتاج إلى تَعرِيف نوع جديد مُزخرَف باستخدام السمة StructLayout يُمثِل نَظيره غير المُدار، كالتالي: [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct PERSON { public int age; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string name; } [DllImport("family.dll", CharSet = CharSet.Auto)] public static extern bool GetSpouse(PERSON person, ref PERSON spouse); ترتيب حقل مصفوفة غير مَعْلومة الحجم (إرسال) بصمة c++: typedef struct { int length; int *data; } VECTOR; void SetVector(VECTOR &vector); في هذه الحالة، ينبغي أن يُمرَّر حقل المصفوفة غير مَعْلومة الحجم كقيمة من النوع IntPtr مع الاستدعاء الصريح للتابعين Marshal.AllocHGlobal() و Marshal.FreeHGlobal() من أجل تَخصِيص مساحة بالذاكرة لهذا الحقل وتَّفرِيغها على الترتيب، كالتالي: [StructLayout(LayoutKind.Sequential)] public struct VECTOR : IDisposable { int length; IntPtr dataBuf; public int[] data { set { FreeDataBuf(); if (value != null && value.Length > 0) { dataBuf = Marshal.AllocHGlobal(value.Length * Marshal.SizeOf(value[0])); Marshal.Copy(value, 0, dataBuf, value.Length); length = value.Length; } } } void FreeDataBuf() { if (dataBuf != IntPtr.Zero) { Marshal.FreeHGlobal(dataBuf); dataBuf = IntPtr.Zero; } } public void Dispose() { FreeDataBuf(); } } [DllImport("vectors.dll")] public static extern void SetVector([In]ref VECTOR vector); ترتيب حقل مصفوفة غير مَعْلومة الحجم (استقبال) بصمة C++: typedef struct { char *name; } USER; bool GetCurrentUser(USER *user); عند استقبال شيفرة مُدارة لمصفوفة غير مَعْلومة الحجم من أُخرى غير مُدارة، فإن مساحة الذاكرة التي تَشغَلها المصفوفة تكون بطبيعة الحال مُخصَّصة بواسطة دوال الشيفرة غير المُدارة. في هذه الحالة، ينبغي للشيفرة المُدارة أن تَستقبِل البيانات إلى مُتغير من النوع IntPrt ثم تَقرَّأ بيانات المُخزِّن المؤقت (buffer) إلى مصفوفة مُدارة. إذا كانت المصفوفة من النوع string، يُمكن استخدام التابع Marshal.PtrToStringAnsi() المُخَصَّص لهذا الغرض. [StructLayout(LayoutKind.Sequential)] public struct USER { IntPtr nameBuffer; public string name { get { return Marshal.PtrToStringAnsi(nameBuffer); } } } [DllImport("users.dll")] public static extern bool GetCurrentUser(out USER user); ترتيب مصفوفة إذا كانت مصفوفة من نوع بسيط: [DllImport("Example.dll")] static extern void SetArray( [MarshalAs(UnmanagedType.LPArray, SizeConst = 128)] byte[] data); إذا كانت مصفوفة من النوع string: [DllImport("Example.dll")] static extern void SetStrArray(string[] textLines); ترجمة -وبتصرف- للفصل Platform Invoke من كتاب .NET Framework Notes for Professionals
-
لقد رأينا مدى سهولة اِستخدَام دوال (functions) مثل System.out.print و System.out.println لعَرْض نص معين للمُستخدِم، ولكن لا يُمثِل ذلك سوى جزءًا صغيرًا من موضوع مُخْرَجات النصوص. تَعتمِد غالبية البرامج عمومًا على البيانات التي يُدخِلها المُستخدِم أثناء زمن تَشْغِيل البرنامج لذا لابُدّ من فهم الكيفية التي ينبغي أن نَتَعامَل على أساسها مع كُلًا من المُدْخَلات والمُخْرَجات. سنشرح خلال هذا القسم كيف يُمكِننا قراءة البيانات المُدْخَلة من قِبَل المُستخدِم كما سنناقش المُخْرَجات على نحو أكثر تفصيلًا، وأخيرًا سنتناول اِستخدَام الملفات كوسيلة للمُدْخَلات والمُخْرَجات. الخرج البسيط والخرج المنسق تُعدّ الدالة System.out.print(x) واحدة من أبسط دوال الخَرْج حيث تَستقبِل مُعاملًا x عبارة عن قيمة أو تعبير (expression) من أي نوع. إذا لم تَكُن قيمة المُعامل x المُمرَّرة سِلسِلةً نصية من النوع String، فإنها ستُحوَّل أولًا إلى قيمة من النوع String ثم ستُرسَل إلى مقصد خَرْج (output destination) يُعرَف باسم "الخَرْج القياسي (standard output)" مما يَعنِي أنها ستُعرَض للمُستخدِم. لاحِظ أنه في حالة برامج واجهة المُستخدِم الرسومية (GUI)، ستُرسَل تلك السِلسِلة إلى مكان لا يُحتمَل للمُستخدِم أن يراه. بالإضافة إلى ذلك، يُمكِننا أيضًا أن نُعيد توجيه الخَرْج القياسي (standard output) لكي يَكْتُب إلى مقصد خَرْج (output destination) مختلف، ولكن فيما هو مُتَعلِّق بالبرنامج التالي، سيتولَّى الكائن System.out مسئولية عَرْض النص للمُستخدِم. تُرسِل الدالتان System.out.println(x) و System.out.print نفس النص إلى الخَرْج، ولكن تُضيف الأولى سطرًا جديدًا (line feed) إلى نهاية النص أي سيُعرَض الخَرْج التالي بسطر جديد. يُمكِنك أيضًا أن تَستخدِم الدالة System.out.println() بدون أية مُعاملات (parameter) لإخراج سطر جديد أي يُكافِئ استدعاء الدالة System.out.println(x) ما يلي: System.out.print(x); System.out.println(); ربما تَكُون قد لاحظت أن System.out.print تُخرِج أعدادًا حقيقية مُكوَّنة من عدد أرقام مناسب بَعْد العلامة العشرية، فمثلًا، تُخرِج العدد π كالتالي "3.141592653589793" أما الأعداد المُمثِلة للنقود فتُخرِجها على الصورة التالية "1050.0" أو "43.575". قد تُفضِّل إِخراج تلك الأعداد بهيئة مختلفة مثل "3.14159" و "1050.00" و "43.58"، وعندها ينبغي أن تَستخدِم ما يُعرَف باسم "الخَرْج المُنسَّق (formatted output)" والذي سيُساعدك على التَحكُّم بكيفية طباعة الأعداد الحقيقية وغيرها من القيم الآخرى. تَتَوفَّر العديد من خيارات التنسيق سنُناقِش هنا أبسطها وأكثرها شيوعًا. يُمكِنك اِستخدَام الدالة System.out.printf للحصول على خَرْج مُنسَّق. يُعدّ الاسم printf اختصارًا للكلمتين "print formatted" أي "اِطبَع بصورة مُنسَّقة"، وهو في الواقع مأخوذ من لغتي البرمجة C و C++. تَستقبِل الدالة System.out.printf مُعاملًا (parameter) واحدًا أو أكثر بحيث يكون مُعاملها الأول عبارة عن سِلسِلة نصية من النوع String تُحدِّد صياغة الخَرْج (output format)، ويُطلَق عليها اسم "صِيْغة سلسلة التنسيق (Format String)" أما بقية المُعاملات فتُخصِّص القيم المطلوب إرسالها للخَرْج. تَطبَع التَعليمَة التالية عددًا بصياغة تتناسب مع قيمة نقدية بالدولار بحيث يُمثِل amount مُتْغيِّرًا من النوع double: System.out.printf( "%1.2f", amount ); تتكوَّن أي صِيْغة سلسلة تنسيق (format string) من "مُحدِّد تنسيق (format specifiers)" واحد أو أكثر بحيث يكون كُلًا منها مسئولًا عن صيغة الخَرْج لقيمة واحدة تُمرَّر عبر مُعامل آخر إضافي. في المثال أعلاه، احتوت صيغة سلسلة التنسيق على مُحدِّد تنسيق واحد هو %1.2f. يبدأ أي مُحدِّد تنسيق عمومًا بعلامة نسبة مئوية % وينتهي بحرف أبجدي يُحدِّد نوع الخَرْج المطلوب كما قد يَتَضمَّن معلومات تنسيق آخرى تُكْتَب بينهما، فمثلًا، ها هي بعض من مُحدِّدات التنسيق المُحتمَلة: %d و %12d و %10s و %1.2f و %1.8g. يُشير الحرف "d" بمُحدِّديّ التنسيق %d و %12d إلى أن القيمة المطلوب كتابتها عبارة عن عدد صحيح (integer) أما "12" بالمُحدِّد الثاني فتُشير إلى أقل عدد من الخانات ينبغي للخَرْج أن يحتلّه، فمثلًا، إذا تَطلَّب إخراج عدد صحيح عدة خانات عددها أقل من ١٢، ستُضَاف خانات فارغة إضافية إلى مُقدمة ذلك العدد لكي يُصبِح مجموع الخانات بالنهاية مساويًا لأقل عدد مُمكِن أي ١٢. يُقال عندها أن الخَرْج "وَاقِع على مُحاذاة اليمين بحقل طوله ١٢" أما إذا تطلَّب إخراجه عددًا أكبر من ١٢ خانة، ستُطبَع جميع الأرقام بدون أي خانات إضافية. لاحِظ أن مُحدِّديّ التنسيق %d و %1d لهما نفس المعنى أي إخراج القيمة بهيئة عدد صحيح بأي عدد ضروري من الخانات. يُعدّ الحرف "d" اختصارًا لكلمة "decimal" كما تستطيع اِستخدَام الحرف "x" بدلًا منه لإِخراج عدد صحيح بصياغة ست عشريّة (hexadecimal). يُمكِنك أيضًا اِستخدَام الحرف "s" بنهاية مُحدِّد تنسيق (format specifier) مع أي قيمة من أي نوع، مما يَعنِي ضرورة إخراج تلك القيمة بصيغتها الافتراضية كما لو أننا لم نَستخدِم خرجًا مُنسَّقًا من الأساس، فمثلًا، يُمكننا أن نُخصِّص مُحدِّد تنسيق مثل %20s بحيث يُمثِل العدد "20" أقل عدد ممكن من المحارف. يُعدّ الحرف "s" اختصارًا لكلمة "string" ويُمكِن استخدامه للقيم من النوع String أو مع أي قيم من أي نوع آخر بحيث تُحوَّل تلك القيم إلى النوع String بالطريقة المُعتادة. تُعدّ مُحدِّدات تنسيق القيم من النوع double أكثر تعقيدًا بقليل. يُستخدَم الحرف "f" بمُحدِّد تنسيق مثل %1.2f لإخراج عدد بصيغة عدد عشري مُكوَّن من فاصلة عائمة (floating-point form) أي يحتوي على عدة أرقام بعد العلامة العشرية. يُخصِّص العدد "2" بذلك المُحدِّد عدد الأرقام المُستخدَمة بَعْد العلامة العشرية أما العدد "1" فيُخصِّص أقل عدد من المحارف يلزم إخراجه. تعني القيمة "1" بذلك المَوضِع إمكانية إخراج أي عدد ضروري من المحارف. كمثال آخر، يُخصِّص المُحدِّد %12.3f صيغة عدد عشري مُكوَّن من ثلاثة أرقام بَعْد العلامة العشرية بحيث يُطبَع على محاذاة اليمين داخل حقل طوله ١٢. بالنسبة لكُلًا من الأعداد الكبيرة جدًا والصغيرة جدًا، فينبغي أن تُكْتَب بصيغة أسية (exponential format) مثل 6.00221415e23 وهو ما يُمثِل حاصل ضرب 6.00221415 في ١٠ مرفوعة للأس ٢٣. يُخصِّص مُحدِّد صيغة مثل %15.8e خَرْجًا بصيغة أُسية بحيث تُشير "8" إلى عدد الأرقام المُستخدَمة بَعْد العلامة العشرية. لاحِظ أنه إذا كنت تَستخدِم "g" بدلًا من "e"، فسيَكوُن الخرج بصيغة أُسية فقط للقيم الصغيرة جدًا والكبيرة جدًا أما القيم الآخرى فستُعرَض بصيغة عدد عشري كما ستُشير القيمة "8" بمُحدِّد صيغة مثل %1.8g إلى عدد الأرقام الكلي بما في ذلك كُلًا من الأرقام قَبْل وبَعْد العلامة العشرية. بالنسبة لخرج القيم العددية، فيُمكِنك أن تُضيف فاصلة , إلى مُحدِّد الصيغة لتقسيم أرقام العدد إلى مجموعات مما يُسهِل من قراءة الأعداد الكبيرة. بالولايات المتحدة الأمريكية، تَتَكوَّن كل مجموعة من ثلاثة أرقام يَفصِل بينها محرف فاصلة، فمثلًا إذا كانت قيمة x تُساوِي واحد بليون، فسيَكُون الخَرْج الناتج عن System.out.printf("%,d",x) هو 1,000,000,000. بدول آخرى، قد يختلف المحرف الفاصل وكذلك عدد الأرقام بكل مجموعة. لابُدّ من كتابة الفاصلة ببداية مُحدِّد الصيغة (format specifier) أي قبل تَخْصِيص عَرْض الحقل (field width) كالتالي %,12.3f. إذا أردت محاذاة الخرج إلى اليسار بدلًا من اليمين، يمكنك إضافة علامة سالبة إلى بداية مُحدِّد الصيغة كالتالي %-20s. إلى جانب مُحدِّدات الصيغة (format specifiers)، قد تَتَضمَّن صيغ سَلاسِل التنسيق (format string) المُمرَّرة إلى الدالة printf محارفًا آخرى عادية تُنسَخ إلى الخَرْج كما هي. يَكُون ذلك مناسبًا لتَضْمِين بعض القيم بمنتصف سِلسِلة نصية. فلنَفْترِض مثلًا أن كُلًا من x و y عبارة عن مُتْغيِّرات (variables) من النوع int، يُمكِننا إذًا كتابة التالي: System.out.printf("The product of %d and %d is %d", x, y, x*y); عندما تُنفَّذ تلك التَعْليمَة، فستَحِلّ قيمة x محلّ أول حُدوث لمُحدِّد الصيغة %d بالسِلسِلة النصية (string) بينما ستَحِلّ قيمة y محلّ الحُدوث الثاني، وأخيرًا ستَحِلّ قيمة التعبير x*y محلّ الحُدوث الثالث. فمثلًا، قد يَكُون الخَرْج كالتالي "The product of 17 and 42 is 714" (علامتي الاقتباس ليست ضِمْن الخَرْج). إذا أردت أن تُخرج علامة نسبة مئوية، فينبغي أن تَكْتُب مُحدِّد الصيغة %% بصيغة سِلسِلة التنسيق (format string) أما إذا كنت تريد أن تُخرِج سطرًا جديدًا، فاستخدِم مُحدِّد الصيغة %n. بالمثل من السَلاسِل النصية العادية، يُمكِنك أيضًا أن تَستخدِم خطًا مائلًا عكسيًا \ لإخراج محارف خاصة كعلامة الاقتباس المزدوجة أو المحرف "tab". مثال لمدخل نصي اعتادت الجافا لسبب غَيْر مفهوم أن تُصعِب من قراءة البيانات المُدْخَلة من قِبَل مُستخدِم البرنامج. كما ذكرنا من قبل، يتَوفَّر الكائن System.out بصورة مُسْبَقة ويُستخَدم لعَرْض الخَرْج للمُستخدِم حيث يَتَضمَّن البرنامج الفرعي (subroutine) System.out.print لذلك الغرض. يَتَوفَّر أيضًا الكائن System.in لأغراض قراءة البيانات المُدْخَلة من قِبَل المُستخدِم، ولكنه يُوفِّر خدمات إِدْخَال بدائية جدًا تَتَطلَّب بعض المهارات البرمجية المُتقدمة بالجافا لاستخدامه بكفاءة. قدمت الجافا منذ إصدارها الخامس الصَنْف Scanner مما سَهَّل قليلًا من عملية قراءة البيانات المُدْخَلة من قِبَل المُستخدِم، ولكنه يَتطلَّب بعض المعرفة بالبرمجة كائنية التوجه (object-oriented programming) لكي تَتَمكَّن من اِستخدَامه، لذلك ربما لا يَكُون الحل الأمثل حاليًا بينما ما نزال نبدأ خطواتنا الأولى. بالإضافة إلى ذلك، قدمت الجافا منذ إصدارها السادس الصَنْف Console لأغراض التواصل مع المُستخدِم، ولكنه يُعاني هو الآخر من بعض المشاكل، فهو أولًا غَيْر متاح دائمًا للاِستخدَام كما أنه قادر على قراءة السَلاسِل النصية (strings) فقط وليس الأعداد. يرى الكاتب أن الصَنْفين Scanner و Console لا يقومان بالأشياء على نحوٍ صحيحٍ تمامًا، لذلك فإنه سيَعتمِد على صَنْف خاص TextIO عَرَّفه بنفسه، ولكن سنتعرَّض أيضًا إلى الصَنْف Scanner بنهاية هذا القسم في حال أردت أن تبدأ باِستخدَامه. يُمكِنك أن تُنشِئ أصنافًا (classes) جديدة تَتَضمَّن برامجًا فرعيةً (subroutines) غَيْر مُتاحة بالجزء القياسي من لغة الجافا. بمُجرّد إِنشائك لصنف جديد، يُمكِنك أن تَستخدِم البرامج الفرعية (subroutines) المُعرَّفة بداخله بنفس الكيفية التي تَستخدِم بها البرامج المبنية مُسْبَقًا (built-in). على سبيل المثال، يَحتوِي الصَنْف TextIO الذي عرَّفه الكاتب على برامج فرعية (subroutines) لقراءة أي مُدْخَلات يَكْتُبها المُستخدِم من خلال كائن الدَخْل القياسي (standard input) System.in وذلك بدون الحاجة لأي مَعرِفة مُتقدمة بالجافا والتي يَتَطلَّبها كُلًا من اِستخدَام الصنف Scanner والاِستخدَام المباشر للكائن System.in. لاحِظ أن الصَنْف TextIO مُعرَّف بـ"حزمة (package)" اسمها textio مما يَعنِي وقوع الملف TextIO.java بمجلد اسمه textio كما يَعنِي أنه من الضروري لأي برنامج يَرغَب باِستخدَام الصَنْف TextIO من أن "يستورد (import)" ذلك الصَنْف من الحزمة textio أولًا بكتابة مُوجِّه الاستيراد import التالي: import textio.TextIO; لابُدّ أن يأتي الموجِّه بالأعلى قَبْل عبارة public class المُستخدَمة لبدء البرنامج. لاحِظ أن غالبية أصناف الجافا القياسية (standard classes) مُعرَّفة ضِمْن حزم (packages) ينبغي اِستيرادها (import) بنفس الطريقة إلى البرامج التي تَستخدِمها. لكي تَتَمكَّن من اِستخدَام الصَنْف TextIO ضِمْن برنامج، لابُدّ أن تَتَأكَّد أولًا من تَوفُّر الصَنْف (class) بذلك البرنامج. يختلف ما يَعنِيه ذلك بحسب بيئة برمجة الجافا (Java programming environment) المُستخدَمة، ولكن ينبغي عمومًا أن تُضيف المجلد textio -الذي يحتوي على الملف TextIO.java- إلى مجلد البرنامج الرئيسي. اُنظر القسم ٢.٦ لمزيد من المعلومات عن كيفية اِستخدَام الصَنْف TextIO. تُعدّ برامج (routines) الإِدْخَال المُعرَّفة بالصَنْف TextIO أعضاء دوال ساكنة (static member functions) ضِمْن ذلك الصنف (ناقشنا ما يعنيه ذلك بالقسم السابق). إذا كنت تَرغَب بكتابة برنامج يَقرأ عددًا صحيحًا مُدْخَلًا من قِبَل المُستخدِم، فيُمكِنك إذًا أن تَستخدِم الصَنْف TextIO حيث يَتَضمَّن عضو دالة ساكن مُعرَّف خصيصًا لذلك الغرض اسمه هو getlnInt. نظرًا لأن تلك الدالة (function) مُعرَّفة ضِمْن الصَنْف TextIO، فلابُدّ إذًا أن تَستخدِم الاسم TextIO.getlnInt للإشارة إليها ضِمْن البرنامج. لا تَستقبِل تلك الدالة أية مُعاملات (parameters)، لذا يَكُون استدعائها على الصورة TextIO.getlnInt(). يُعيد ذلك الاستدعاء قيمة من النوع int تُمثِل القيمة التي أَدْخَلها المُستخدِم، والتي ستَرغَب عادةً في اِستخدَامها بشكل أو بآخر لذا سنُسنِدها إلى مُتْغيِّر (variable). بِفَرض أن userInput عبارة عن مُتْغيِّر (variable) من النوع int صَرَّحنا عنه بتَعْليمَة تّصْرِيح (declaration statement) مثل int userInput;، نستطيع إذًا أن نُسنِد إليه القيمة المُعادة باِستخدَام تَعْليمَة الإِسْناد (assignment statement) التالية: userInput = TextIO.getlnInt(); عندما يُنفِّذ الحاسوب التَعْليمَة بالأعلى، فإنه سينتظر إلى أن يُدْخِل المُستخدِم قيمة من النوع العددي الصحيح أي لابُدّ أولًا أن يُدْخِل المُستخدِم عددًا ثم يَضغَط على مفتاح العودة إلى بداية السطر (return) لكي يُكمِل البرنامج عمله. ستُعيد الدالة (function) المُستدعَاة بالأعلى القيمة التي أَدْخَلها المُستخدِم، والتي ستُخزَّن بالمُتْغيِّر userInput. يَستخدِم البرنامج التالي الدالة TextIO.getlnInt لقراءة العدد المُدْخَل من قِبَل المُستخدِم ثم يَعرِض مربع ذلك العدد (لاحِظ مُوجّه الاستيراد [import] بأول سطر): import textio.TextIO; public class PrintSquare { public static void main(String[] args) { int userInput; // العدد الذي أدخله المستخدم int square; // قيمة مربع العدد الذي أدخله المستخدم System.out.print("Please type a number: "); userInput = TextIO.getlnInt(); square = userInput * userInput; System.out.println(); System.out.println("The number that you entered was " + userInput); System.out.println("The square of that number is " + square); System.out.println(); } // نهاية main() } // نهاية الصنف PrintSquare يَعرِض البرنامج بالأعلى الرسالة النصية "Please type a number:" عند تَشْغِيله ثم يَنتظِر إلى أن يَتَلقَّى إجابة مُكوَّنة من عدد يتبعه محرف العودة إلى بداية السطر (carriage return). لاحِظ أنه من الأفضل دومًا لأي برنامج أن يَطبَع سؤالًا للمُستخدِم قبلما يُحاوِل أن يقرأ أي قيم مُدْخَلة؛ لأنه في حالة غياب ذلك، قد لا يَتَمكَّن المُستخدِم من معرفة نوع القيمة التي ينتظرها الحاسوب بل إنه قد لا يَتَمكَّن حتى من ملاحظة كَوْن البرنامج ينتظره لأن يُدْخِل قيمة في العموم. دوال الإدخال بالصنف TextIO يَتَضمَّن الصَنْف TextIO تشكيلة متنوعة من الدوال (functions) لقراءة الأنواع (types) المختلفة التي يُمكِن للمُستخدِم أن يُدْخِلها. تَستعرِض القائمة التالية عددًا من تلك الدوال: j = TextIO.getlnInt(); // اقرأ قيمة من النوع int y = TextIO.getlnDouble(); // اقرأ قيمة من النوع double a = TextIO.getlnBoolean(); // اقرأ قيمة من النوع boolean c = TextIO.getlnChar(); // اقرأ قيمة من النوع char w = TextIO.getlnWord(); // اقرأ كلمة واحدة كقيمة من النوع String s = TextIO.getln(); // اقرأ سطرًا مدخلًا بالكامل كقيمة من النوع String لكي تُصبِح أي من تَعْليمَات الإِسْناد (assignment) بالأعلى صالحة، ينبغي أولًا أن تُصرِّح (declare) عن المُتْغيِّر الموجود على جانبها الأيسر كما ينبغي للمُتْغيِّر أن يَكُون من نفس النوع المُعاد (return type) من الدالة على الجانب الأيمن. لا تَستقبِل تلك الدوال أية مُعاملات (parameters)، وتُعيد قيم تُمثِل ما أَدْخَله المُستخدِم أثناء تَشْغِيل البرنامج، والتي ينبغي أن نُسنِدها (assign) إلى مُتْغيِّرات (variable) حتى نَتَمكَّن من اِستخدَامها ضِمْن البرنامج، حيث سنَتَمكَّن عندها من اِستخدَام أسماء تلك المُتْغيِّرات للإشارة إلى مُدْخَلات المُستخدِم. عندما تَستدعِي واحدة من تلك الدوال (functions)، فإنها ستُعيد دومًا قيمة صالحة من النوع المُحدَّد، ففي حالة أَدْخَل المُستخدِم قيمة غَيْر صالحة كمُدْخَل أي إذا طلب البرنامج قيمة من النوع int ولكن المُستخدِم أَدْخَل محرفًا (character) غَيْر عددي أو أَدْخَل عددًا يَقَع خارج النطاق المسموح به للقيم التي يُمكِن تَخْزِينها بمُتْغيِّر (variable) من النوع int، فسيَطلُب الحاسوب من المُستخدِم أن يُعيد إِدْخَال القيمة وسيتصرف كما لو أنه لمْ يَر القيمة غَيْر الصالحة نهائيًا. تَسمَح الدالة TextIO.getlnBoolean() للمُستخدِم بكتابة أي من القيم التالية: true و false و t و f و yes و no و y و n و 1 و 0 كما تَسمَح باِستخدَام كُلًا من الأحرف الأبجدية الكبيرة (upper case) والصغيرة (lower case). في جميع الأحوال، يُفسَّر مُدْخَل المُستخدِم على أساس كَوْنه مُكافِئ للقيمة true أو للقيمة false. عادةً ما تُستخدَم الدالة TextIO.getlnBoolean() لقراءة إجابة المُستخدِم على أسئلة "نعم أم لا". يُوفِّر الصَنْف TextIO دالتي إِدْخَال (input functions) تُعيد كلتاهما قيمة من النوع String هما: getlnWord() و getln(). تُعيد الأولى getlnWord() سِلسِلة نصية (string) مُكوَّنة من محارف (characters) فقط بدون أي فراغات (spaces)، حيث تَتَخطَّى عند اِستدعَائها أي فراغ (spaces) أو محرف عودة إلى بداية السطر (carriage return) يُمكِن أن يَكُون المُستخدِم قد أَدْخَله ثم تبدأ بقراءة المحارف إلى أن تَصِل إلى أول فراغ أو أول محرف عودة إلى بداية السطر، وعندها تُعيد سِلسِلة نصية من النوع String مُكوَّنة من المحارف المقروءة. في المقابل، تُعيد دالة الإِدْخَال الثانية getln() سِلسِلة نصية (string) مُكوَّنة من أية محارف أَدْخَلها المُستخدِم بما في ذلك أي فراغات إلى أن تَصِل إلى محرف عودة إلى بداية السطر (carriage return) أي أنها تَقرَأ سطرًا كاملًا من النص المُدْخَل. في حين يقرأ الحاسوب محرف العودة إلى بداية السطر (carriage return) إلا أنه لا يَعُدّه جزءًا من السِلسِلة النصية المُدْخَلة حيث يُهمله الحاسوب بَعْد قراءته. لاحِظ أنه إذا اِكتفَى المُستخدِم بالضغط على محرف العودة إلى بداية السطر بدون أن يَكْتُب أي شيء قبلها، ستُعيد عندها الدالة TextIO.getln() سِلسِلة نصية فارغة "" لا تَحتوِي على أية محارف نهائيًا. لا تتخطَّى دالة الإِدْخَال TextIO.getln() أية فراغات أو محارف نهاية السطر قبل قراءة القيمة المُدْخَلة. يختلف ذلك عن دوال الإِدْخَال الآخرى getlnInt() و getlnDouble() و getlnBoolean() و getlnChar() والتي تُشبه الدالة getlnWord() فيما يَتَعلَّق بتخطّيها لأية فراغات أو محارف عودة إلى بداية السطر (carriage returns) قبل بدئها بقراءة القيمة الفعليّة المُدْخَلة. عندما تَتَخطَّى إحدى تلك الدوال (functions) محرف نهاية السطر، فإنها تَطبَع العلامة '?' كإشارة للمُستخدِم بأن البرنامج ما يزال يَتَوقَّع بعض المُدْخَلات الآخرى. بالإضافة إلى ذلك، إذا أَدْخَل المُستخدِم محارفًا إضافية بنفس سطر القيمة الفعليّة المُدْخَلة، سيُهمِل الحاسوب جميع تلك المحارف الإضافية مع محرف العودة إلى بداية السطر (carriage return) الموجود بنهاية السطر. يترتب على ذلك أنه إذا اِستدعَى برنامج معين دالة إِدْخَال (input function) آخرى، فسيَكُون المُستخدِم مُضطّرًا لأن يُدْخِل سطرًا جديدًا حتى لو كان قد أَدْخَل عدة قيم بالسطر الأول. قد لا يَكُون إهمال الحاسوب لمُدْخَلات المُستخدِم بتلك الطريقة أمرًا جيدًا، ولكنه في الواقع الحل الأسلم بغالبية البرامج. باِستخدَامنا للصَنْف TextIO لكُلًا من المُدْخلات والمخرجات، نستطيع الآن أن نُحسِّن برنامج حساب قيمة الاستثمار من القسم ٢.٢، فيُمكِننا مثلًا أن نَسمَح للمُستخدِم بأن يُدْخِل قيمًا مبدئية لكُلًا من الاستثمار ومُعدّل الفائدة مما سيُعطِينا برنامجًا أكثر فائدة نَرغَب بتَشْغِيله أكثر من مرة. يَستخدِم البرنامج التالي خَرْجًا مُنسَّقًا (formatted output) لطباعة القيم النقدية بالصياغة المناسبة: import textio.TextIO; public class Interest2 { public static void main(String[] args) { double principal; // قيمة الاستثمار double rate; // قيمة معدل الفائدة السنوية double interest; // الفائدة المكتسبة خلال العام System.out.print("Enter the initial investment: "); principal = TextIO.getlnDouble(); System.out.print("Enter the annual interest rate (as a decimal): "); rate = TextIO.getlnDouble(); interest = principal * rate; // إحسب فائدة العام principal = principal + interest; // أضفها إلى المبلغ الأساسي System.out.printf("The amount of interest is $%1.2f%n", interest); System.out.printf("The value after one year is $%1.2f%n", principal); } // نهاية main() } // نهاية الصنف Interest2 قد تتساءل عن سبب وجود برنامج خَرْج (output routine) وحيد System.out.println بإِمكانه طباعة جميع أنواع القيم بينما تَتَوفَّر برامج إِدْخَال (input routine) متعددة لكل نوع على حدى. في الواقع، نظرًا لأن دالة الخَرْج (output function) تَستقبِل القيمة المطلوب طباعتها بهيئة مُعامل، فإن الحاسوب يُمكِنه أن يُحدِّد نوع القيمة بفَحْص قيمة المُعامل. في المقابل، لا تَستقبِل برامج الإِدْخَال (input routines) أية مُعاملات (parameters) لذا كان من الضروري اِستخدَام اسم مختلف لكل برنامج من برامج الإِدْخَال حتى يَتَمكَّن الحاسوب من التمييز بينها. الملفات كمدخلات ومخرجات يُرسِل System.out الخَرْج إلى مقصد خَرْج (output destination) يُعرَف باسم "الخَرْج القياسي (standard output)". يُمثِل الخَرْج القياسي مقصدًا واحدًا فقط ضِمْن عدة مقاصد خَرْج مُحتمَلة، فيُمكِنك مثلًا أن تَكْتُب البيانات إلى ملف مُخزَّن ضِمْن قرص صلب، وهو ما يتميز بعدة أمور منها أن البيانات ستَبقَّى مُخزَّنة بالملف إلى ما بَعْد انتهاء البرنامج كما ستَتَمكَّن من طباعة الملف أو إرساله عبر البريد الالكتروني إلى شخص آخر أو حتى تَعْدِيله باستخدام برنامج آخر. على نحو مُشابه، يُمثِل System.in مصدرًا واحدًا مُحتمَلًا للمُدْخَلات. يستطيع الصَنْف TextIO أن يقرأ من ويَكْتُب إلى الملفات. يَتَضمَّن الصنف TextIO دوال الخَرْج TextIO.put و TextIO.putln و TextIO.putf والتي تَعمَل بنفس طريقة عَمَل الدوال System.out.print و System.out.println و System.out.printf على الترتيب. علاوة على ذلك، يُمكِن أيضًا اِستخدَامها لإرسال نصوص إلى ملفات أو إلى أي مقاصد خَرْج آخرى. عندما تَستخدِم أي من دوال الخَرْج TextIO.put أو TextIO.putln أو TextIO.putf، يُرسَل الخَرْج إلى مقصد الخَرْج الحالي (output destination)، والذي يُمثِل ما يُعرَف باسم "الخَرْج القياسي (standard output)" على نحو افتراضي، ولكن تستطيع تَغْيِيره باِستخدَام برامج فرعية (subroutines) مُعرَّفة بالصَنْف TextIO، فمثلًا، يُمكِنك أن تَستخدِم التَعْليمَة التالية لكي تَكْتُب إلى ملف اسمه "result.txt": TextIO.writeFile("result.txt"); بمُجرّد تّنْفيذ التَعْليمَة السابقة، سيُرسَل الخَرْج الناتج عن أي من تَعْليمَات الصَنْف TextIO إلى ملف اسمه هو "results.txt" بدلًا من أن تُرسَل إلى الخَرْج القياسي (standard output). لاحظ أنه إذا لم يَكُن هنالك أي ملف يَحمِل نفس الاسم، فسيُنشَئ واحدًا جديدًا أما في حالة وجوده، فستُحذَف محتوياته السابقة دون أي تحذير. بَعْد استدعاء TextIO.writeFile، سيَتَذكَّر الصَنْف TextIO ذلك الملف وسيُرسِل أي خَرْج ناتج عن الدالة TextIO.put أو عن أي دوال خَرْج آخرى إلى ذلك الملف أتوماتيكيًا. إذا أردت أن تعود مجددًا إلى الكتابة إلى الخَرْج القياسي (standard output)، يُمكِنك استدعاء ما يلي: TextIO.writeStandardOutput(); يَطلُب البرنامج بالأسفل من المُستخدِم أن يُجيب على بضعة أسئلة ثم يُخرِج تلك الإجابات إلى ملف اسمه "profile.txt". يَستخدِم البرنامج الصَنْف TextIO لإرسال الخَرْج إلى كُلًا من الخَرْج القياسي (standard output) والملف المذكور، ولكن يُمكِن أيضًا اِستخدَام System.out لإرسال الخَرْج إلى الخَرْج القياسي (standard output). import textio.TextIO; public class CreateProfile { public static void main(String[] args) { String name; // اسم المستخدم String email; // البريد الإلكتروني للمستخدم double salary; // المرتب السنوي للمستخدم String favColor; // اللون المفضل للمستخدم TextIO.putln("Good Afternoon! This program will create"); TextIO.putln("your profile file, if you will just answer"); TextIO.putln("a few simple questions."); TextIO.putln(); /* اقرأ إجابات المستخدم */ TextIO.put("What is your name? "); name = TextIO.getln(); TextIO.put("What is your email address? "); email = TextIO.getln(); TextIO.put("What is your yearly income? "); salary = TextIO.getlnDouble(); TextIO.put("What is your favorite color? "); favColor = TextIO.getln(); // اكتب بيانات المستخدم المدخلة إلى الملف profile.txt TextIO.writeFile("profile.txt"); // يرسل الخرج التالي إلى الملف TextIO.putln("Name: " + name); TextIO.putln("Email: " + email); TextIO.putln("Favorite Color: " + favColor); TextIO.putf( "Yearly Income: %,1.2f%n", salary); /* اطبع رسالة أخيرة إلى الخرج القياسي */ TextIO.writeStandardOutput(); TextIO.putln("Thank you. Your profile has been written to profile.txt."); } } إذا أردت أن تَسمَح للمُستخدِم بأن يختار الملف المطلوب اِستخدَامه للخَرْج، يُمكِنك مثلًا أن تَطلُب منه أن يَكْتُب اسم الملف، ولكن يَكُون ذلك عُرضة للخطأ كما أن المُستخدِمين عادةً ما يكونوا مُعتادين على نوافذ اختيار الملفات، لهذا سنَستخدِم التَعْليمَة التالية: TextIO.writeUserSelectedFile(); ستَعرِض التَعْليمَة السابقة واجهة مُستخدِم رسومية (graphical user interface) عبارة عن نافذة اختيار ملف تقليدية. ستَسمَح تلك النافذة للمُستخدِم بأن يختار ملف خَرْج معين أو بأن يُغلِقها دون اختيار أي ملف. تتميز تلك الطريقة بإمكانية تَنبِيه المُستخدِم في حالة كان مُوشِكًا على استبدال ملف موجود بالفعل. تُعيد الدالة TextIO.writeUserSelectedFile قيمة من النوع boolean ستُساوِي true في حالة اختيار ملف وستُساوِي false في حالة غلق النافذة. قد يَفْحَص برنامج معين القيمة المُعادة (return value) من تلك الدالة ليَعرِف ما إذا كان عليه أن يَكْتُب البيانات إلى ملف أم لا. إلى جانب قراءة المُدْخَلات من "الدَخْل القياسي (standard input)"، يستطيع الصَنْف TextIO القراءة من ملف. ستحتاج فقط إلى تَخْصِيص مصدر المُدْخَلات (input source) الخاصة بالدوال المُعرَّفة بالصَنْف TextIO، وهو ما يُمكِنك القيام به إما باِستخدَام التَعْليمَة TextIO.readFile("data.txt") للقراءة من ملف اسمه "data.txt" أو باِستخدَام TextIO.readUserSelectedFile() والتي تُظهِر واجهة مُستخدِم رسومية (GUI) عبارة عن نافذة اختيار ملف تَسمَح للمُستخدِم بأن يختار ملفًا. بََعْد انتهائك من ذلك، سيُقرَأ أي دَخْل بَعْدها من الملف المُختار لا من مُدْخَلات المُستخدِم. يُمكِنك استدعاء الدالة TextIO.readStandardInput() للعودة مرة آخرى إلى الوضع الافتراضي أي قراءة المُدْخَلات المَكْتُوبة من قِبَل المُستخدِم. إذا كان البرنامج يَقَرأ المُدْخَلات من الدَخْل القياسي (standard input)، فسيَتَمكَّن المُستخدِم إذًا من تصحيح أية أخطاء ضِمْن تلك المُدْخَلات أما إذا كان يَقَرأها من ملف، فلن يَكُون ذلك مُمكِنًا لذا إذا عُثِر على بيانات غَيْر صالحة، سيَحدُث خطأ وسينهار (crash) البرنامج. قد تَحدُث بعض الأخطاء أيضًا عند مُحاولة الكتابة إلى ملف ولكنها تُعدّ أقل شيوعًا. يَتَطلَّب الفهم الكامل للمُدْخَلات والمُخْرَجات بلغة الجافا مَعرِفة بمفاهيم البرمجة كائنية التوجه (object oriented programming)، لذا سنعود مجددًا إلى نفس هذا الموضوع بالفصل الحادي عشر بَعْدما نَكُون قد ناقشنا تلك المفاهيم. تُعدّ إمكانيات الصَنْف TextIO فيما يَتَعلَّق بقراءة الملفات وكتابتها بدائية نوعًا ما، ولكنها كافية لغالبية التطبيقات كما أنها ستَمنَحك بعض الخبرة في التَعامُل مع الملفات. خصائص أخرى بالصنف TextIO تَعرَّضنا لمجموعة من دوال الإِدْخَال المُعرَّفة بالصَنْف TextIO والتي بإِمكانها قراءة قيمة واحدة فقط من سطر مُدْخَل. إذا أردت قراءة أكثر من مُجرّد قيمة واحدة من نفس السطر المُدْخَل، فمثلًا، قد تَرغَب بتَمْكِين المُستخدِم من كتابة سطر مثل "42 17" لكي يُدْخِل العددين ٤٢ و ١٧. يُوفِّر الصَنْف TextIO لحسن الحظ دوال إِدْخَال (input functions) آخرى مُعرَّفة خصيصًا لذلك الغرض. اُنظر الأمثلة التالية: j = TextIO.getInt(); // اقرأ قيمة من النوع int y = TextIO.getDouble(); // اقرأ قيمة من النوع double a = TextIO.getBoolean(); // اقرأ قيمة من النوع boolean c = TextIO.getChar(); // اقرأ قيمة من النوع char w = TextIO.getWord(); // اقرأ كلمة واحدة كقيمة من النوع String تبدأ أسماء الدوال بالأعلى بكلمة "get" بدلًا من كلمة "getln" التي تُعدّ اختصارًا للكلمتين "get line" أي "اِجلب سطرًا" وهو ما ينبغي أن يُذكِّرك بأنها تقرأ سطرًا مُدْخَلًا بالكامل. في المقابل، تقرأ الدوال بدون "ln" قيمة مُدْخَلة واحدة بنفس الطريقة لكنها لا تُهمِل بقية السطر المُدْخَل بل تَحتفِظ به كما هو ضِمْن ذاكرة داخلية تُعرَف باسم "مُخزِّن المُدْخَلات المُؤقت (input buffer)". عندما يحتاج الحاسوب إلى قراءة قيمة مُدْخَلة آخرى، فإنه سيَفْحَص أولًا مُخزِّن المُدْخَلات المؤقت قبلما يَطلُب من المُستخدِم إِدْخِال أي قيم جديدة، مما سيَسمَح بقراءة عدة قيم من سطر مُدْخَل واحد. بتعبير أكثر دقة، يقرأ الحاسوب دومًا من مُخزِّن المُدْخَلات المؤقت، ففي المرة الأولى التي سيُحاوِل فيها البرنامج قراءة قيمة مُدْخَلة من المُستخدِم، سينتظر الحاسوب إلى أن ينتهي المُستخدِم من كتابة سطر كامل من المُدْخَلات، ثُمَّ سيَحتفِظ الصَنْف TextIO بذلك السطر ضِمْن مُخزِّن مُدْخَلات مُؤقت (input buffer) إلى أن يُقرأ أو يُهمَل بواسطة إحدى دوال "getln". سيَضطّر المُستخدِم إلى إِدْخَال سطر جديد فقط عندما يُصبِح مُخزِّن المُدْخَلات المُؤقت (buffer) فارغًا. ستَتَخطَّى دوال الإِدْخَال (input functions) المُعرَّفة بالصَنْف TextIO أية فراغات (spaces) أو محارف عودة إلى بداية السطر (carriage returns) عند محاولتها قراءة القيمة المُدْخَلة التالية، ولكنها مع ذلك لن تَتَخطَّى أية محارف آخرى. فمثلًا، إذا أَدْخَل المُستخدِم السطر "42,17" بينما كنت تُحاوِل قراءة قيمتين من النوع int، فسيَتَمكَّن الحاسوب من قراءة العدد الأول بشكل سليم، ولكنه عندما يُحاوِل قراءة العدد الثاني، فستُقابله فاصلة ,، وهو ما يُعدّ خطأ لذا سيَطلُب الحاسوب من المُستخدِم أن يُعيد إدخال العدد الثاني أي إذا أردت أن تَسمَح للمُستخدِم بأن يُدْخِل عدة قيم ضِمْن سطر واحد، فينبغي أن تُخبره بأن عليه أن يَستخدِم فراغًا لا فاصلة بين كل عدد والذي يليه. أما إذا كنت تُفضِّل السماح له باِستخدَام فاصلة (comma)، فاِستخدِم الدالة getChar() لقرائتها أولًا قَبْل أن تُحاوِل قراءة العدد الثاني. يُوفِّر الصَنْف TextIO دالة آخرى لإِدْخَال محرف واحد هي TextIO.getAnyChar() والتي لا تَتَخطَّى أية محارف نهائيًا، فهي ببساطة تقرأ المحرف التالي المُدْخَل من قِبَل المُستخدِم حتى لو كان ذلك المحرف فراغًا أو محرف عودة إلى بداية السطر (carriage return). فمثلًا، إذا أَدْخَل المُستخدِم محرف عودة إلى بداية السطر، فسيَكُون المحرف المُعاد من الدالة getAnyChar() عبارة عن محرف إضافة سطر جديد \n. يُوفِّر الصَنْف TextIO دالة (function) آخرى هي TextIO.peek() تَفْحَص المحرف المُدْخَل التالي دون قرائته بصورة فعلية أي أنه سيَظِلّ متاحًا للقراءة حتى بَعْدما تَفْحَصه باِستخدَام تلك الدالة، وعليه ستَتَمكَّن من مَعرِفة قيمة المُدْخَل التالي وربما حتى اتِّخاذ إِجراءات مختلفة وفقًا لقيمته. يُوفِّر الصَنْف TextIO مجموعة آخرى من الدوال (functions) يُمكِنك الإطلاع عليها بملف الشيفرة المصدرية TextIO.java كما أنها مرفقة بتعليقات (comments) للمساعدة. يتَّضح لنا أن المُدْخَلات أكثر تعقيدًا من المُخْرَجات إلى حد ما، ولكن يُعدّ اِستخدَامها بسيطًا بأغلب التطبيقات من الناحية العملية أما إذا كنت تُحاوِل القيام بشيء مميز، فقد تحتاج إلى المزيد من التفاصيل. يُنصَح عمومًا بالاعتماد على برامج الإِدْخَال (input routines) التي يَحتوِي اسمها على كلمة "getln" وليس كلمة "get" لأنها أبسط بكثير إلا إذا كانت هنالك حاجة لقراءة أكثر من قيمة ضِمْن سطر مُدْخَل واحد. استخدام الصنف Scanner للمدخلات يُسهِل الصَنْف TextIO من قراءة مُدْخَلات المُستخدِم، ولكن ينبغي أن تُتيِح الملف TextIO.java لأي برنامج يَستخدِمه لأنه ليس صَنْفًا قياسيًا (standard class). في المقابل، يُعدّ الصَنْف Scanner جزءًا قياسيًا من الجافا أي سيَكُون متاحًا دائمًا أينما أردت اِستخدَامه لذا قد تُفضِّل اِستخدَامه. يُستخدَم الصَنْف Scanner لقراءة مُدْخَلات المُستخدِم، ويتميز في العموم بسمات رائعة، ولكن ربما يَكُون اِستخدَامه صعبًا نوعًا ما حيث يَتَطلَّب مَعرِفة ببعض قواعد الصيغة (syntax) التي لن نَتَعرَّض لها حتى نَصِل إلى الفصلين الرابع والخامس. سنَذكُر هنا طريقة اِستخدَامه سريعًا دون الخوض بأية تفاصيل، لذا لا تَقْلَق إن لم تفهم كل ما هو مَكْتُوب هنا. سنناقش الصَنْف Scanner تفصيليًا بالقسم الفرعي ١١.١.٥. أولًا، لمّا كان الصنف Scanner مُعرَّف بحزمة java.util، ينبغي أن تُضيف مُوجّه (directive) الاستيراد import التالي إلى بداية ملف الشيفرة بالبرنامج الخاص بك أي قَبْل كلمة public class: import java.util.Scanner; ثم سنُضِيف التَعْليمَة التالية إلى بداية البرنامج main(): Scanner stdin = new Scanner( System.in ); تُنشِي تلك التَعْليمَة مُتْغيِّرًا (variable) اسمه stdin من النوع Scanner. تُعدّ كلمة stdin اختصارًا للكلمتين "standard input" مما يَعنِي "دَخْل قياسي"، ولكن يُمكِنك أن تَستخدِم أي اسم آخر للمُتْغيِّر إن أردت. تستطيع اِستخدَام المُتْغيِّر stdin للوصول إلى تشكيلة من البرامج الفرعية (subroutines) المُختصَّة بقراءة مُدْخَلات المُستخدِم، فمثلًا، تقرأ الدالة stdin.nextInt() قيمة واحدة مُدْخَلة من النوع int ثُم تُعيدها كقيمة للدالة أي تُشبه TextIO.getInt() إلى حد كبير باستثناء أمرين: أولًا، إذا كانت القيمة المُدْخَلة مِن قِبَل المُستخدِم غَيْر صالحة، فلن تَطلُب الدالة stdin.nextInt() من المُستخدِم أن يُعيد إِدْخَال قيمة جديدة وإنما ستَتسبَّب بحُدوث انهيار (crash). ثانيًا، لابُدّ من أن يُدْخِل المُستخدِم فراغًا أو محرف نهاية السطر بَعْد قيمة العدد الصحيح المُدْخَلة. في المقابل، تَتَوقَّف الدالة TextIO.getInt() عن قراءة المُدخلات مع أول محرف لا يُمثِل رقمًا تُقابِله. علاوة على ذلك، تَتَوفَّر توابعًا (methods) لقراءة الأنواع الآخرى مثل stdin.nextDouble() و stdin.nextLong() و stdin.nextBoolean() (تَقبَل الدالة stdin.nextBoolean() القيم true و false فقط). تقرأ تلك البرامج الفرعية (subroutines) أكثر من قيمة واحدة ضِمْن سطر لذا فهي أَشْبه للنُسخ "get" من البرامج الفرعية المُعرَّفة بالصَنْف TextIO وليس النُسخ "getln"، فمثلًا، يُعدّ التابع stdin.nextLine() مكافئًا للتابع TextIO.getln() أما التابع stdin.next() فيُكافِئ TextIO.getWord() حيث يُعيد كلاهما سِلسِلة نصية (string) من المحارف غَيْر الفارغة. يَستعرِض المثال التالي البرنامج Interest2.java مجددًا ولكن باِستخدَام الصَنْف Scanner بدلًا من الصَنْف TextIO: import java.util.Scanner; public class Interest2WithScanner { public static void main(String[] args) { Scanner stdin = new Scanner( System.in ); // أنشئ كائن جديد double principal; // قيمة الاستثمار double rate; // معدل الفائدة السنوي double interest; // الفائدة المكتسبة خلال العام System.out.print("Enter the initial investment: "); principal = stdin.nextDouble(); System.out.print("Enter the annual interest rate (as a decimal): "); rate = stdin.nextDouble(); interest = principal * rate; // إحسب فائدة العام principal = principal + interest; // أضف إلى الأساسي System.out.printf("The amount of interest is $%1.2f%n", interest); System.out.printf("The value after one year is $%1.2f%n", principal); } // نهاية main() } // نهاية الصنف Interest2WithScanner بالبرنامج أعلاه، أضفنا سطرًا لاِستيراد الصَنْف Scanner وآخرًا لإِنشاء الكائن stdin. علاوة على ذلك، بدلًا من التابع TextIO.getlnDouble()، اِستخدَمنا stdin.nextDouble()، والذي يُعدّ في العموم مُكافئًا للتابع TextIO.getDouble() ولكن كل ذلك لا يَعنِينا طالما كان المُستخدِم سيُدْخِل عددًا واحدًا فقط بكل سطر. سنستمر بالاعتماد على الصَنْف TextIO فيما يَتَعلَّق بالمُدْخَلات في العموم، ولكننا سنَستخدِم الصَنْف Scanner مرة آخرى بعدة أمثلة ضِمْن التمارين المُرفقة بنهاية الفصل كما سنُناقِشه تفصيليًا فيما بَعْد. ترجمة -بتصرّف- للقسم Section 4: Text Input and Output من فصل Chapter 2: Programming in the Small I: Names and Things من كتاب Introduction to Programming Using Java.
-
يُوفِّر إطار عمل .NET الصنف SpeechRecognitionEngine بفضاء الاسم System.Speech.Recognition لتَميِيز الكلام تلقائيًا. يُدعِّم مُمَيِّز الكلام -من النوع SpeechRecognitionEngine- عِدة أحَدَاث (events) تُثار تلقائيًا عند حُدوث ظرف معين. يُمكِن لمُعالِجات الأحَدَاث (event handlers) التَسجيل بتلك الأحداث، مما يَسمَح بإجراء العمليات المطلوبة بشكل غَيْر مُتزامِن (asynchronous). يُعدّ الحَدَث SpeechRecognized أحد أهم أحداث النوع SpeechRecognitionEngine، والذي يُثَار عندما يَستقبِل مُمَيِّز الكلام دَخْلًا يَتوَافق مع إحدى قواعده المُفعَّلة. تُخصَّص تلك القواعد من خلال كائن من النوع Grammar يُمرَّر للتابع LoadGrammar لوضع قيود على عملية تَميِيز الكلام. تَعرِض الشيفرة بالأسفل طريقة اِستخدَام النوع SpeechRecognitionEngine لبناء مُعالِج للحَدَث SpeechRecognized يقوم بإرسال الكلام المُمَيَّز إلى مَجْرى الخَرْج. تنشئة مُمَيِّز الكلام كالتالي: SpeechRecognitionEngine recognitionEngine = new SpeechRecognitionEngine(); التَسجيل بالحَدث SpeechRecognized كالتالي: recognitionEngine.SpeechRecognized += delegate(object sender, SpeechRecognizedEventArgs e) { Console.WriteLine("You said: {0}", e.Result.Text); }; بدء تشغيل المُمَيِّز كالتالي: recognitionEngine.SetInputToDefaultAudioDevice(); recognitionEngine.RecognizeAsync(RecognizeMode.Multiple); تمييز مقيد بمجموعة جمل باستخدام GrammarBuilder SpeechRecognitionEngine recognitionEngine = new SpeechRecognitionEngine(); GrammarBuilder builder = new GrammarBuilder(); builder.Append(new Choices("I am", "You are", "He is", "She is", "We are", "They are")); builder.Append(new Choices("friendly", "unfriendly")); recognitionEngine.LoadGrammar(new Grammar(builder)); recognitionEngine.SpeechRecognized += delegate(object sender, SpeechRecognizedEventArgs e) { Console.WriteLine("You said: {0}", e.Result.Text); }; recognitionEngine.SetInputToDefaultAudioDevice(); recognitionEngine.RecognizeAsync(RecognizeMode.Multiple); تمييز حر بدون قيود (free text) باستخدام DictationGrammar SpeechRecognitionEngine recognitionEngine = new SpeechRecognitionEngine(); recognitionEngine.LoadGrammar(new DictationGrammar()); recognitionEngine.SpeechRecognized += delegate(object sender, SpeechRecognizedEventArgs e) { Console.WriteLine("You said: {0}", e.Result.Text); }; recognitionEngine.SetInputToDefaultAudioDevice(); recognitionEngine.RecognizeAsync(RecognizeMode.Multiple); ملاحظات: مُعامِلات التابع LoadGrammar: grammar: من النوع Grammar يُشير إلى القواعد التي يَنبغي تَحميلها. مثلًا، يُستخدَم كائن من النوع DictationGrammar -المُشتَق من النوع Grammar- لتمييز حُر دون قيود. مُعامِلات باني النوع Grammar: builder: من النوع GrammarBuilder. مُعامِلات التابع GrammarBuilder.Append: choices: من النوع Choices. يُلحِق هذا التابع مُكَوِّنًا جديد بتَسَلسُل القواعد (grammar sequence)، مع السَماح بعدة بدائل (choices) ضِمْن ذلك المُكَوِّن. يعني ذلك أنه عند اِستقبَال دَخْل صوتي من المُستخدِم، يُمكِن للمُمَيِّز اتباع عدة بدائل من القواعد. مُعامِلات باني النوع Choices: choices: مصفوفة من البدائل. مُعامِلات التابع RecognizeAsync: mode: من النوع تعداد RecognizeMode، لتحديد عدد عمليات التَميِيز المُمكن إجرائها إِمّا Single أو Multiple ترجمة -وبتصرف- للفصل SpeechRecognitionEngine class to recognize speech من كتاب .NET Framework Notes for Professionals
-
تحدثنا بالقسم السابق عن الأنواع الأساسية (primitive types) الثمانية بالإضافة إلى النوع String. هنالك فارق جوهري بينهما، وهو أن القيم من النوع String عبارة عن كائنات (objects). على الرغم من أننا لن نناقش الكائنات تفصيليًا حتى نَصِل إلى الفصل الخامس، فما يزال من المفيد أن نَطَّلِع قليلًا عليها وعلى مفهوم الأصناف (classes) المُرتبِط بها إلى حد كبير. لن يُمكِّننا استيعاب مفاهيم أساسية كالكائنات (objects) والأصناف (classes) على اِستخدَام السَلاسِل النصية من النوع String فقط، وإنما سيَفتَح لنا الباب لاستيعاب مفاهيم برمجية آخرى مُهِمّة مثل البرامج الفرعية (subroutines). الدوال (functions) والبرامج الفرعية (subroutines) المبنية مسبقًا تَذَكَّر أن البرنامج الفرعي (subroutine) ما هو إلا مجموعة من التَعْليمَات (instructions) مُضمَّنة معًا تحت اسم معين، ومُصمَّمة لتكون مسئولة عن إنجاز مُهِمّة واحدة مُحدَّدة. سنتعلَّم أسلوب كتابة البرامج الفرعية (subroutines) بالفصل الرابع، ومع ذلك يُمكِنك أن تُنجز الكثير فقط باستدعاء البرامج الفرعية التي كُتبَت بالفعل بواسطة مبرمجين آخرين. يُمكِنك أن تَستدعِي برنامجًا فرعيًا ليُنجز المُهِمّة المُوكَلة إليه باِستخدَام تَعْليمَة استدعاء برنامج فرعي (subroutine call statement). يُعرَّف أي برنامج فرعي بالجافا ضِمْن صَنْف (class) أو كائن (object)، وتَتَضمَّن بعض الأصناف القياسية بلغة الجافا برامجًا فرعية مُعرَّفة مُسْبَقًا يُمكِنك اِستخدَامها. فمثلًا، تحتوي القيم من النوع String التي هي عبارة عن كائن على برامج فرعية مبنية مُسْبَقًا يُمكِنها معالجة السَلاسِل النصية (strings)، والتي يُمكِنك أن تَستدعِيها دون فهم طريقة كتابتها أو كيفية عملها. في الواقع، هذا هو الغرض الأساسي منها: أي برنامج فرعي هو صندوق أسود (black box) يُمكِن اِستخدَامه بدون مَعرِفة ما يحدث داخله. لنَفْحَص أولًا البرامج الفرعية (subroutines) التي تُعدّ جزءًا من صَنْف (class). يُستخدَم أي صَنْف عمومًا لتجميع بعض المُتْغيِّرات (variables) والبرامج الفرعية (subroutines) المُعرَّفة بذلك الصنف معًا، ويُعرَف كلاهما باسم "أعضاء الصَنْف الساكنة (static members)". لقد رأينا مثالًا لذلك بالفعل: إذا كان لدينا صنف يُعرِّف برنامجًا، فإن البرنامج main() هو عضو ساكن (static member) بذلك الصَنْف. ينبغي أن تُضيف الكلمة المحجوزة static عندما تُعرِّف عضو ساكن مثل الكلمة static بالتَصْرِيح public static void main.... عندما يحتوي صنف (class) معين على مُتْغيِّر أو برنامج فرعي ساكن (static)، يُعدّ اسم الصنف جزءًا من الاسم الكامل لذلك المُتْغيِّر أو لذلك البرنامج الفرعي. على سبيل المثال، يحتوي الصنف القياسي System على البرنامج الفرعي exit لذا يُمكِنك أن تُشير إليه باستخدام الاسم System.exit والذي يَتَكوَّن من كُلًا من اسم الصنف المُتْضمِّن للبرنامج الفرعي متبوعًا بنقطة ثم باسم البرنامج الفرعي نفسه. يَستقبِل البرنامج الفرعي exit مُعامِلًا من النوع int، لذلك يُمكِنك اِستخدَامه بكتابة تَعْليمَة استدعاء برنامج فرعي (subroutine call statement) كالتالي: System.exit(0); يُنهِي الاستدعاء System.exit البرنامج ويُغلِق آلة جافا الافتراضية (Java Virtual Machine)، لذا يُمكِنك أن تَستخدِمه إذا كنت تُريد إنهاء البرنامج قبل نهاية البرنامج main(). تُشير قيمة المُعامِل المُمرَّرة إلى سبب إغلاق البرنامج، فإذا كانت تُساوِي القيمة ٠، يَعنِي ذلك أن البرنامج قد انتهى بشكل طبيعي. في المقابل، تَعنِي أي قيمة آخرى انتهاء البرنامج لوجود خطأ مثل الاستدعاء System.exit(1). تُرسَل قيمة المُعامِل المُمرَّرة إلى نظام التشغيل والذي عادةً ما يتجاهلها. يُعدّ الصَنْف System واحدًا فقط من ضِمْن مجموعة من الأصناف القياسية التي تأتي مع الجافا. كمثال آخر، يَتَضمَّن الصنف Math مُتْغيِّرات ساكنة (static variables) مثل Math.PI و Math.E والتي قيمها هي الثوابت الرياضية π و e على الترتيب كما يُعرِّف عددًا كبيرًا من الدوال (functions) الحسابية. يُنفِّذ أي برنامج فرعي (subroutine) في العموم مُهِمّة مُحدّدة. لبعض البرامج الفرعية، تَحسِب المُهِمّة قيمة إحدى البيانات ثم تُعيدها، وفي تلك الحالة، تُعرَف تلك البرامج الفرعية باسم الدوال (functions)، ونقول عندها أن تلك الدالة تُعيد (return value) قيمة، والتي يُفْترَض استخدامها بطريقة ما ضِمْن البرنامج المُستدعِي للدالة. لنَفْترِض مثلًا مسالة حِسَاب الجذر التربيعي (square root)، تُوفِّر لغة الجافا دالة (function) لهذا الغرض اسمها هو Math.sqrt. تُعدّ تلك الدالة عضو برنامج فرعي ساكن (static member subroutine) بالصنف Math. إذا كانت x هي أي قيمة عددية، فإن الاستدعاء Math.sqrt(x) يَحسِب الجذر التربيعي (root) لتلك القيمة ثم يُعيدها. لمّا كانت الدالة Math.sqrt(x) تُمثِل قيمة، فليس هناك أي مغزى من استدعائها بمفردها بتَعْليمَة استدعاء برنامج فرعي (subroutine call statement) كالتالي: Math.sqrt(x); لا يَفعَل الحاسوب أي شيء بالقيمة المُعادة من الدالة (function) بالأعلى، أي أنه يَحسِبها ثم يتجاهلها، وهو أمر غَيْر منطقي، لذا يُمكِنك أن تُخبره مثلًا بأن عليه طباعتها على الأقل كالتالي: System.out.print( Math.sqrt(x) ); // اعرض الجذر التربيعي أو قد تَستخدِم تَعْليمَة إِسْناد (assignment statement) لتُخبره بأن عليه تَخْزِينها بمُتْغيِّر كالتالي: lengthOfSide = Math.sqrt(x); يُمثِل استدعاء الدالة Math.sqrt(x) قيمة من النوع double أي يُمكِنك كتابة ذلك الاستدعاء أينما أَمْكَن اِستخدَام قيمة عددية مُصنَّفة النوع (numeric literal) من النوع double. يُمثِل x مُعامِلًا (parameter) يُمرَّر إلى البرنامج الفرعي (subroutine) والذي قد يَكُون مُتْغيِّرًا (variable) اسمه x أو قد يَكُون أي تعبير آخر (expression) بشَّرط أن يُمثِل ذلك التعبير قيمة عددية. على سبيل المثال، يَحسِب الاستدعاء Math.sqrt(2) قيمة الجذر التربيعي للعَدَد ٢ أما الاستدعاء Math.sqrt(a*a+b*b) فهو صالح تمامًا طالما كانت قيم a و b مُتْغيِّرات من النوع العددي. يحتوي الصَنْف Math على العديد من الدوال الأعضاء الساكنة (static member functions) الأخرى. اُنظر القائمة التالية والتي تَعرِض بعضًا منها: Math.abs(x): تَحسِب القيمة المطلقة للمُعامِل x. الدوال المثلثية العادية (trigonometric functions) Math.sin(x) و Math.cos(x) و Math.tan(x): تُقاس جميع الزوايا بوحدة قياس راديان (radians) وليس بوحدة الدرجات (degrees). الدوال المثلثية العكسية (inverse trigonometric functions) Math.asin(x) و Math.acos(x) و Math.atan(x): تُقاس القيمة المُعادة (return value) من تلك الدوال بوحدة قياس راديان وليس بوحدة الدرجات. تَحسِب الدالة الأسية Math.exp(x) قيمة العدد e مرفوعة للأس x أما دالة اللوغاريتم الطبيعي Math.log(x) فتَحسِب لوغاريتم x بالنسبة للأساس e. Math.pow(x,y) تحسب قيمة x مرفوعة للأس y. Math.floor(x): تُقرِّب x لأكبر عدد صحيح أقل من أو يُساوِي x. تَحسِب تلك الدالة عددًا صحيحًا بالمفهوم الرياضي، ولكنها مع ذلك تُعيد قيمة من النوع double بدلًا من النوع int كما قد تَتَوقَّع. فمثلًا يُعيد استدعاء الدالة Math.floor(3.76) القيمة 3.0 بينما يُعيد استدعاء الدالة Math.floor(-4.2) القيمة -5. علاوة على ذلك، تَتَوفَّر أيضًا الدالة Math.round(x) والتي تُعيد أقرب عدد صحيح للمُعامِل x وكذلك الدالة Math.ceil(x) والتي تُقرِّب x لأصغر عدد صحيح أكبر من أو يُساوِي x. Math.random() تُعيد قيمة عشوائية من النوع double ضِمْن نطاق يتراوح من ٠ إلى ١. تَحسِب تلك الدالة أعدادًا شبه عشوائية (pseudorandom number) أي أنها ليست عشوائية تمامًا وإنما إلى درجة كافية لغالبية الأغراض. سنكتشف خلال الفصول القادمة أن تلك الدالة لها استخدامات أخرى كثيرة مفيدة. تَستقبِل الدوال (functions) بالأعلى مُعامِلات (parameters) -أي x أو y داخل الأقواس- بأي قيم طالما كانت من النوع العددي أما القيم المُعادة (return value) من غالبيتها فهي من النوع double بغض النظر عن نوع المُعامِل (parameter) باستثناء الدالة Math.abs(x) والتي تَكُون قيمتها المُعادة من نفس نوع المُعامِل x، فإذا كانت x من النوع int، تُعيد تلك الدالة قيمة من النوع int أيضًا وهكذا. على سبيل المثال، تُعيد الدالة Math.sqrt(9) قيمة من النوع double تُساوِي 3.0 بينما تُعيد الدالة Math.abs(9) قيمة من النوع int تُساوِي 9. لا تَستقبِل الدالة Math.random() أي مُعامِلات (parameter)، ومع ذلك لابُدّ من كتابة الأقواس حتى وإن كانت فارغة لأنها تَسمَح للحاسوب بمَعرِفة أنها تُمثِل برنامجًا فرعيًا (subroutine) لا مُتْغيِّرًا (variable). تُعدّ الدالة System.currentTimeMillis() من الصنف System مثالًا آخرًا على برنامج فرعي (subroutine) ليس له أي مُعامِلات (parameters). عندما تُنفَّذ تلك الدالة، فإنها تُعيد الوقت الحالي مُقاس بحساب الفارق بين الوقت الحالي ووقت آخر قياسي بالماضي (بداية عام ١٩٧٠) بوحدة المللي ثانية. تَكُون القيمة المعادة من System.currentTimeMillis() من النوع long (عدد صحيح ٦٤ بت). يُمكِنك اِستخدَام تلك الدالة لحِساب الوقت الذي يَستَغْرِقه الحاسوب لتّنْفيذ مُهِمّة معينة. كل ما عليك القيام به هو أن تُسجِّل كُلًا من الوقت الذي بدأ فيه التّنْفيذ وكذلك الوقت الذي انتهى به ثم تَحسِب الفرق بينهما. للحصول على توقيت أكثر دقة، يُمكِنك اِستخدَام الدالة System.nanoTime() والتي تُعيد الوقت الحالي مُقاس بحساب الفارق بين الوقت الحالي ووقت آخر عشوائي بالماضي بوحدة النانو ثانية. لكن لا تَتَوقَّع أن يَكُون الوقت دقيقًا بحق لدرجة النانوثانية. يُنفِّذ البرنامج بالمثال التالي مجموعة من المهام الحسابية ويَعرِض الوقت الذي يَستَغْرِقه البرنامج لتّنْفيذ كُلًا منها: /** * This program performs some mathematical computations and displays the * results. It also displays the value of the constant Math.PI. It then * reports the number of seconds that the computer spent on this task. */ public class TimedComputation { public static void main(String[] args) { long startTime; // وقت البدء بالنانو ثانية long endTime; // وقت الانتهاء بالنانو ثانية long compTime; // زمن التشغيل بالنانو ثانية double seconds; // فرق الوقت بالثواني startTime = System.nanoTime(); double width, height, hypotenuse; // جوانب المثلث width = 42.0; height = 17.0; hypotenuse = Math.sqrt( width*width + height*height ); System.out.print("A triangle with sides 42 and 17 has hypotenuse "); System.out.println(hypotenuse); System.out.println("\nMathematically, sin(x)*sin(x) + " + "cos(x)*cos(x) - 1 should be 0."); System.out.println("Let's check this for x = 100:"); System.out.print(" sin(100)*sin(100) + cos(100)*cos(100) - 1 is: "); System.out.println( Math.sin(100)*Math.sin(100) + Math.cos(100)*Math.cos(100) - 1 ); System.out.println("(There can be round-off errors when" + " computing with real numbers!)"); System.out.print("\nHere is a random number: "); System.out.println( Math.random() ); System.out.print("\nThe value of Math.PI is "); System.out.println( Math.PI ); endTime = System.nanoTime(); compTime = endTime - startTime; seconds = compTime / 1000000000.0; System.out.print("\nRun time in nanoseconds was: "); System.out.println(compTime); System.out.println("(This is probably not perfectly accurate!"); System.out.print("\nRun time in seconds was: "); System.out.println(seconds); } // نهاية main() } // نهاية الصنف TimedComputation الأصناف (classes) والكائنات (objects) بالإضافة إلى استخدام الأصناف (classes) كحاويات للمُتْغيِّرات والبرامج الفرعية الساكنة (static). فإنها قد تُستخدَم أيضًا لوصف الكائنات (objects). يُعدّ الصنف ضِمْن هذا السياق نوعًا (type) بنفس الطريقة التي تُعدّ بها كلًا من int و double أنواعًا أي يُمكِننا إذًا أن نَستخدِم اسم الصنف للتَصْرِيح (declare) عن مُتْغيِّر (variable). تَحمِل المُتْغيِّرات في العموم نوعًا واحدًا من القيم يَكُون في تلك الحالة عبارة عن كائن (object). ينتمي أي كائن إلى صنف (class) معين يُبلِّغه بنوعه (type)، ويُعدّ بمثابة تجميعة من المُتْغيِّرات والبرامج الفرعية (subroutines) يُحدِّدها الصنف الذي ينتمي إليه الكائن أي تتشابه الكائنات (objects) من نفس الصنف (class) وتحتوي على نفس تجميعة المُتْغيِّرات والبرامج الفرعية. على سبيل المثال، إذا كان لدينا سطح مستو، وأردنا أن نُمثِل نقطة عليه باِستخدَام كائن، فيُمكِن إذًا لذلك الكائن المُمثِل للنقطة أن يُعرِّف مُتْغيِّرين (variables) x و y لتمثيل إحداثيات النقطة. ستُعرِّف جميع الكائنات المُمثِلة لنقطة قيمًا لكُلًا من x و y والتي ستكون مختلفة لكل نقطة معينة. في هذا المثال، يُمكِننا أن نُعرِّف صَنْفًا (class) اسمه Point مثلًا ليُعرِّف (define) البنية المشتركة لجميع الكائنات المُمثِلة لنقطة بحيث تَكُون جميع تلك الكائنات (objects) قيمًا من النوع Point. لنَفْحَص الاستدعاء System.out.println مرة آخرى. أولًا، System هو عبارة عن صَنْف (class) أما out فهو مُتْغيِّر ساكن (static variable) مُعرَّف بذلك الصنف. ثانيًا، يشير المُتْغيِّر System.out إلى كائن (object) من الصَنْف القياسي PrintStream و System.out.println هو الاسم الكامل لبرنامج فرعي (subroutine) مُعرَّف بذلك الكائن. يُمثِل أي كائن من النوع PrintStream مقصدًا يُمكِن طباعة المعلومات من خلاله حيث يَتَضمَّن برنامجًا فرعيًا println يُستخدَم لإرسال المعلومات إلى ذلك المقصد. لاحِظ أن الكائن System.out هو مُجرد مقصد واحد محتمل، فقد تُرسِل كائنات (objects) آخرى من النوع PrintStream المعلومات إلى مقاصد أخرى مثل الملفات أو إلى حواسيب آخرى عبر شبكة معينة. يُمثَل ذلك ما يُعرَف باسم البرمجة كائنية التوجه (object-oriented programming): عندما يَتَوفَّر لدينا مجموعة من الأشياء المختلفة في العموم والتي لديها شيئًا مشتركًا مثل كَوْنها تَعمَل مقصدًا للخَرْج، فإنه يُمكِن اِستخدَامها بنفس الطريقة من خلال برنامج فرعي مثل println. في هذا المثال، يُعبِر الصَنْف PrintStream عن الأمور المُشتركة بين كل تلك الأصناف (objects). تلعب الأصناف (classes) دورًا مزدوجًا وهو ما قد يَكُون مُربِكًا للبعض، ولكن من الناحية العملية تُصمَّم غالبية الأصناف لكي تؤدي دورًا واحدًا منها بشكل رئيسي أو حصري. لا تقلق عمومًا بشأن ذلك حتى نبدأ في التعامل مع الكائنات (objects) بصورة أكثر جديّة بالفصل الخامس. لمّا كانت أسماء البرامج الفرعية (routines) دائمًا متبوعة بقوس أيسر، يَصعُب خَلْطها إذًا مع أسماء المُتْغيِّرات. في المقابل، تُستخدَم أسماء كُلًا من الأصناف (classes) والمُتْغيِّرات (variables) بنفس الطريقة، لذا قد يَكُون من الصعب أحيانًا التَمْييز بينها. في الواقع، تَتَّبِع جميع الأسماء المُعرَّفة مُسْبَقًا بالجافا نَمْط تسمية تبدأ فيه أسماء الأصناف بحروف كبيرة (upper case) بينما تبدأ أسماء المُتْغيِّرات والبرامج الفرعية (subroutines) بحروف صغيرة (lower case). لا يُمثِل ذلك قاعدة صيغة (syntax rule)، ومع ذلك من الأفضل أن تَتَّبِعها أيضًا. كملاحظة عامة أخيرة، يُستخدَم عادةً مصطلح "التوابع (methods)" للإشارة إلى البرامج الفرعية (subroutines) بالجافا. يَعنِي مصطلح "التابع (method)" أن برنامجًا فرعيًا (subroutine) مُعرَّف ضِمْن صنف (object) أو كائن (object). ولأن ذلك يُعدّ صحيحًا لأي برنامج فرعي بالجافا، فإن أيًا منها يُعدّ تابعًا. سنميل عمومًا إلى اِستخدَام المصطلح الأعم "البرنامج الفرعي (subroutine)"، ولكن كان لابُدّ من إعلامك بأن بعض الأشخاص يُفضِّلون اِستخدَام مصطلح "التابع (method)". العمليات على السلاسل النصية من النوع String يُمثِل String صنفًا (class)، وأي قيمة من النوع String هي عبارة عن كائن (object). يَحتوِي أي كائن في العموم على بيانات (data) وبرامج فرعية (subroutines). بالنسبة للنوع String، فإن البيانات مُكوَّنة من متتالية محارف السِلسِلة النصية (string) أما البرامج الفرعية فهي في الواقع مجموعة من الدوال (functions) منها مثلًا الدالة length والتي تَحسِب عدد المحارف (characters) بالسِلسِلة النصية. لنَفْترِض أن advice هو مُتْغيِّر يُشير إلى String، يُمكِننا إذًا أن نُصرِّح عنه ونُسنِد إليه قيمة كالتالي: String advice; advice = "Seize the day!"; الآن، يُمثِل التعبير advice.length() استدعاءً لدالة (function call)، والتي ستُعيد في تلك الحالة تحديدًا عدد محارف السِلسِلة النصية "Seize the day!" أي ستُعيد القيمة ١٤. في العموم، لأي مُتْغيِّر str من النوع String، يُمثِل الاستدعاء str.length() قيمة من النوع int تُساوِي عدد المحارف بالسِلسِلة النصية (string). لا تَستقبِل تلك الدالة (function) أي مُعامِلات (parameters) لأن السِلسِلة النصية المطلوب حِسَاب طولها هي بالفعل قيمة المُتْغيِّر str. يُعرِّف الصَنْف String البرنامج الفرعي length ويُمكِن اِستخدَامه مع أي قيمة من النوع String حتى مع أي سِلسِلة نصية مُجرّدة (string literals) فهي في النهاية ليست سوى قيمة ثابتة (constant value) من النوع String. يُمكِنك مثلًا كتابة برنامج يَحسِب عدد محارف السِلسِلة النصية "Hello World" كالتالي: System.out.print("The number of characters in "); System.out.print("the string \"Hello World\" is "); System.out.println( "Hello World".length() ); يُعرِّف الصَنْف String الكثير من الدوال (functions) نَستعرِض بعضًا منها خلال القائمة التالية: s1.equals(s2): تُوازن تلك الدالة بين السِلسِلتين s1 و s2، وتُعيد قيمة من النوع boolean تُساوِي true إذا كانت s1 و s2 مُكوّنتين من نفس متتالية المحارف، وتُساوِي false إن لم تَكُن كذلك. s1.equalsIgnoreCase(s2): هي دالة من النوع المنطقي (boolean-valued function). مثل الدالة السابقة، تَفْحَص تلك الدالة ما إذا كانت السِلسِلتان s1 و s2 مُكوّنتين من نفس السِلسِلة النصية (string)، ولكنها تختلف عن الدالة السابقة في أن الحروف الكبيرة (upper case) والصغيرة (lower case) تُعدّ متكافئة. فمثلًا، إذا كانت s1 تحتوي على السِلسِلة النصية "cat"، فستُعيد الدالة s1.equals("Cat") القيمة false أما الدالة s1.equalsIgnoreCase("Cat") فستُعيد القيمة true. s1.length(): هي دالة من النوع الصحيح (integer-valued function)، وتُعيد قيمة تُمثِل عدد محارف السِلسِلة النصية s1. s1.charAt(N): حيث N هو عدد صحيح. تُعيد تلك الدالة قيمة من النوع char تُساوي قيمة محرف السِلسِلة النصية برقم المَوضِع N. يبدأ الترقيم من الصفر، لذا يُمثِل استدعاء الدالة s1.charAt(0) المحرف الأول أما استدعاء الدالة s1.charAt(1) فيُمثِل المحرف الثاني، وهكذا حتى نَصِل إلى المَوضِع الأخير s1.length() - 1. مثلًا، يُعيد "cat".charAt(1) القيمة 'a'. لاحظ أنه إذا كانت قيمة المُعامِل المُمرَّرة أقل من صفر أو أكبر من أو تُساوِي s1.length، فسيَحدُث خطأ. s1.substring(N,M): حيث N و M هي أعداد صحيحة. تُعيد تلك الدالة قيمة من النوع String مُكوَّنة من محارف السلسلة النصية بالمواضع N و N+1 و .. حتى M-1 (المحرف بالمَوضِع M ليس مُضمَّنًا). تُعدّ القيمة المُعادة "سلسلة جزئية (substring)" من السِلسِلة الأصلية s1. يُمكِنك ألا تُمرِّر قيمة للمُعامِل M كالتالي s1.substring(N) وعندها ستُعيد تلك الدالة سِلسِلة جزئية من s1 مُكوَّنة من محارف السلسلة النصية بدايةً من N إلى النهاية. s1.indexOf(s2): تُعيد عددًا صحيحًا. إذا كانت s2 هى سِلسِلة جزئية (substring) من s1، تُعيد تلك الدالة مَوضِع المحرف الأول من السِلسِلة الجزئية s2 بالسِلسِلة الأصلية s1. أما إذا لم تَكُن جزءًا منها، فإنها تُعيد القيمة -١. تَتَوفَّر أيضًا الدالة s1.indexOf(ch) حيث ch عبارة عن محرف من النوع char، وتُستخدَم للبحث عنه بسِلسِلة نصية s1. تُستخدَم أيضًا الدالة s1.indexOf(x,N) لإيجاد أول حُدوث من x بعد موضع N بسِلسِلة نصية، وكذلك الدالة s1.lastIndexOf(x) لإيجاد آخر حُدوث من x بسِلسِلة نصية s1. s1.compareTo(s2): هي دالة من النوع العددي الصحيح (integer-valued function) تُوازن بين سلسلتين نصيتين s1 و s2، فإذا كانت السِلسِلتان متساويتين، تُعيد الدالة القيمة ٠ أما إذا كانت s1 أقل من s2، فإنها تُعيد عددًا أقل من ٠، وأخيرًا إذا كانت s1 أكبر من s2، فإنها تُعيد عددًا أكبر من ٠. إذا كانت السلسلتان s1 و s2 مُكوّنتين من حروف صغيرة (lower case) فقط أو حروف كبيرة (upper case) فقط، فإن الترتيب المُستخدَم بموازنات مثل "أقل من" أو "أكبر من" تُشير إلى الترتيب الأبجدي. أما إذا تَضمَّنتا محارف آخرى فسيَكُون الترتيب أكثر تعقيدًا. تَتَوفَّر دالة آخرى مشابهة هي s1.compareToIgnoreCase(s2). s1.toUpperCase(): هي دالة من النوع النصي (String-valued function) تُعيد سِلسِلة نصية جديدة تُساوِي s1 ولكن بَعْد تَحْوِيل أي حرف صغير (lower case) بها إلى حالته الكبيرة (upper case). فمثلًا، تُعيد الدالة "Cat".toUpperCase() السِلسِلة النصية "CAT". تَتَوفَّر دالة آخرى مشابهة هي s1.toLowerCase. s1.trim(): هي دالة من النوع النصي (String-valued function) تُعيد سِلسِلة نصية جديدة (string) تساوي s1 ولكن بَعْد حَذْف أي محارف غَيْر مطبوعة -كالمسافات الفارغة (spaces)- من بداية السِلسِلة النصية (string) ونهايتها. فمثلًا إذا كانت s1 هي السِلسِلة النصية "fred "، فستُعيد الدالة s1.trim() السِلسِلة "fred" بدون أي مسافات فارغة بالنهاية. لا تُغيِّر الدوال s1.toUpperCase() و s1.toLowerCase() و s1.trim() قيمة s1، وإنما تُنشِئ سِلسِلة نصية جديدة (string) تُعَاد كقيمة للدالة يُمكِن اِستخدَامها بتَعْليمَة إِسْناد (assignment statement) مثلًا كالتالي smallLetters = s1.toLowerCase();. إذا كنت تُريد تَعْدِيل قيمة s1 نفسها، فيُمكِنك ببساطة أن تَستخدِم تَعْليمَة الإِسْناد التالية s1 = s1.toLowerCase();. يُمكِنك أن تَستخدِم عَامِل الزيادة (plus operator) + لضم (concatenate) سِلسِلتين نصيتين (strings)، وينتج عنهما سِلسِلة نصية جديدة مُكوَّنة من كل محارف السِلسِلة النصية الأولى متبوعة بكل محارف السِلسِلة النصية الثانية. فمثلًا، يؤول التعبير "Hello" + "World" إلى السِلسِلة النصية "HelloWorld". إذا كنت تُريد مسافة فارغة (space) بين الكلمات، فعليك أن تُضيفها إلى أي من السِلسِلتين كالتالي Hello " + "World". لنَفْترِض أن name هو مُتْغيِّر من النوع String يُشير إلى اسم مُستخدِم البرنامج. يُمكِنك إذًا أن تُرحِّب به بتّنْفيذ التَعْليمَة التالية: System.out.println("Hello, " + name + ". Pleased to meet you!"); يُمكِنك حتى أن تَستخدِم نفس العَامِل + لكي تَضُمُّ (concatenate) قيم من أي نوع آخر إلى سِلسِلة نصية (string). ستَتَحوَّل تلك القيم إلى سِلسِلة نصية (string) أولًا كما يَحدُث عندما تَطبَعها إلى الخَرْج القياسي (standard output) ثم ستُضَمّ إلى السِلسِلة النصية الآخرى. فمثلًا، سيؤول التعبير "Number" + 42 إلى السِلسِلة النصية "Number42". اُنظر التعليمات التالية: System.out.print("After "); System.out.print(years); System.out.print(" years, the value is "); System.out.print(principal); يُمكِنك اِستخدَام التَعْليمَة المُفردة التالية بدلًا من التَعْليمَات بالأعلى: System.out.print("After " + years + " years, the value is " + principal); تُعدّ النسخة الثانية أفضل بكثير ويُمكِنها أن تَختصِر الكثير من الأمثلة التي عَرَضَناها خلال هذا الفصل. مقدمة إلى التعدادات (enums) تُوفِّر الجافا ثمانية أنواع أساسية مَبنية مُسْبَقًا (built-in primitive types) بالإضافة إلى مجموعة ضخمة من الأنواع المُعرَّفة باِستخدَام أصناف (classes) مثل النوع String، ولكنها ما تزال غَيْر كافية لتغطية جميع المواقف المُحتمَلة والتي قد يحتاج المُبرمج إلى التَعامُل معها. لهذا، وبالمثل من غالبية اللغات البرمجية الآخرى، تَمنَحك الجافا القدرة على إنشاء أنواع (types) جديدة، والتي غالبًا ما تَكُون بهيئة أصناف (classes)، وهو ما سنَتَعلَّمه بالفصل الخامس. تَتَوفَّر مع ذلك طريقة آخرى وهي التعدادات (enumerated types - enums) سنناقشها خلال هذا القسم. تقنيًا، يُعدّ أي تعداد (enum) نوعًا خاصًا من صَنْف (class)، ولكن هذا غَيْر مُهِمّ في الوقت الحالي. سنَفْحَص خلال هذا القسم التعدادات بصيغتها البسيطة (simplified form)، والمُستخدَمة عمليًا في غالبية الحالات. أي تعداد (enum) هو عبارة عن نوع (type) يَتَضمَّن قائمة ثابتة مُكوَّنة من قيمه المُحتمَلة تُخصَّص عند إِنشاء نوع التعداد. بشكل ما، تُشبه أنواع التعداد (enum) النوع boolean والتي قيمه المحتملة هي true و false فقط. لكن لاحِظ أن النوع boolean هو نوع أساسي (primitive type) أما أنواع التعداد (enums) فليست كذلك. يُكْتَب تعريف تعداد (enum definition) معين بالصيغة التالية: enum <enum-type-name> { <list-of-enum-values> } لا يُمكِنك كتابة ذلك التعريف (definition) داخل أي برنامج فرعي (subroutine)، لذا قد تَضَعه خارج البرنامج main() أو حتى بملف آخر مُنفصِل. تُشير إلى اسم نوع التعداد (enum) بنفس الطريقة التي تُشير بها كلمات مثل "boolean" و "String" إلى النوع boolean والنوع String على الترتيب. يُمكِننا أن نَستخدِم أي مُعرّف بسيط (simple identifier) كاسم لنوع التعداد. أما فتُشير إلى قيم التعداد المُحتمَلة وتَتَكوَّن من قائمة من المُعرّفات (identifiers) يَفصِل بينها فاصلة (comma). يُعرِّف المثال التالي نوع تعداد (enum) اسمه هو Season وقيمه المُحتمَلة هي أسماء فصول السنة الأربعة: enum Season { SPRING, SUMMER, FALL, WINTER } عادةً ما تُكْتَب القيم المُحتمَلة لنوع التعداد (enum) بحروف كبيرة (upper case)، لكنه ليس أمرًا ضروريًا فهو ليس ضِمْن قواعد الصيغة (syntax rules). تُعدّ القيم المُحتمَلة لتعداد معين ثوابتًا (constants) لا يُمكِن تعديلها، وعادةً ما يُطلَق عليها اسم ثوابت التعداد (enum constants). لأن ثوابت التعداد لنوع مثل Season مُعرَّفة داخله، لابُدّ من أن نُشير إليها باستخدام أسماء مركبة مُكوَّنة من اسم النوع المُتْضمِّن ثم نقطة ثم اسم ثابت التعداد كالتالي: Season.SPRING و Season.SUMMER و Season.FALL و Season.WINTER. بمُجرّد إنشاء نوع تعداد (enum)، تستطيع أن تَستخدِمه للتَصْرِيح (declare) عن مُتْغيِّرات بنفس الطريقة التي تُصرِّح بها عن مُتْغيِّرات من أي أنواع آخرى. يمكنك مثلًا أن تُصرِّح عن مُتْغيِّر اسمه vacation من النوع Season باستخدام التَعْليمَة التالية: Season vacation; بعد التَصْرِيح عن مُتْغيِّر، يُمكِنك أن تُسنِد (assign) إليه قيمة باستخدام تَعْليمَة إِسْناد (assignment statement). يُمكن لتلك القيمة -على يمين عامل الإِسْناد- أن تَكُون أي من ثوابت التعداد من النوع Season. تَذَكَّر أنه لابُدّ من اِستخدَام الاسم الكامل لثابت التعداد بما في ذلك اسم النوع Season. اُنظر المثال التالي: vacation = Season.SUMMER; يُمكِنك أن تَستخدِم تَعْليمَة طباعة عادية مثل System.out.print(vacation) لطباعة قيمة تعداد، وتَكُون القيمة المطبوعة عندها عبارة عن اسم ثابت التعداد (enum constant) بدون اسم نوع التعداد أي سيَكُون الخَرْج في هذه الحالة هو "SUMMER". لأن التعداد (enum) هو عبارة عن صَنْف (class) تقنيًا، فلابُدّ إذًا من أن تَكُون قيم التعداد (enum value) عبارة عن كائنات (objects) وبالتالي يُمكِنها أن تَحتوِي على برامج فرعية (subroutines). أحد البرامج الفرعية المُعرَّفة بأي قيمة تعداد من أي نوع هي ordinal() والتي تُعيد العدد الترتيبي (ordinal number) لتلك القيمة بقائمة القيم المُحتمَلة لذلك التعداد. يُشير العدد الترتيبي ببساطة إلى مَوضِع القيمة بالقائمة، فمثلًا، يُعيد Season.SPRING.ordinal() قيمة من النوع int تُساوِي صفر أما Season.SUMMER.ordinal() فيُعيد ١ أما Season.FALL.ordinal() فيُعيد ٢ وأخيرًا Season.WINTER.ordinal() يُعيد ٣. يمكنك بالطبع أن تَستخدِم التابع (method) ordinal() مع مُتْغيِّر من النوع Season مثل vacation.ordinal(). يُساعد اِستخدَام أنواع التعداد (enums) على كتابة شيفرة مقروءة؛ لأنك ستَستخدِم بطبيعة الحال أسماءً ذات مغزى لقيم التعداد. علاوة على ذلك، يَفْحَص المُصرِّف (compiler) ما إذا كانت القيم المُسنَدة لمُتْغيِّر تعداد معين قيمًا صالحة أم لا مما يُجنِّبك أنواعًا مُحدَّدة من الأخطاء. وفي العموم، ينبغي أن تُدرك أهمية التعدادات (enums) بعدّها الطريقة الأولى لإنشاء أنواع (types) جديدة. يُوضِح المثال التالي كيفية اِستخدَام أنواع التعداد ضِمْن برنامج كامل: public class EnumDemo { // عرف نوعين تعداد خارج البرنامج main enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY } enum Month { JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC } public static void main(String[] args) { Day tgif; // صرح عن متغير من النوع Day Month libra; // صرح عن متغير من النوع Month tgif = Day.FRIDAY; // أسند قيمة من النوع Day إلى tgif libra = Month.OCT; // أسند قيمة من النوع Month إلى libra System.out.print("My sign is libra, since I was born in "); // قيمة الخرج ستكون OCT System.out.println(libra); System.out.print("That's the "); System.out.print( libra.ordinal() ); System.out.println("-th month of the year."); System.out.println(" (Counting from 0, of course!)"); System.out.print("Isn't it nice to get to "); // قيمة الخرج ستكون: FRIDAY System.out.println(tgif); System.out.println( tgif + " is the " + tgif.ordinal() + "-th day of the week."); } } كما ذَكَرنا مُسْبَقًا، يُمكِنك أن تُعرِّف التعدادات (enum) بملفات مُنفصِلة. لاحظ أن البرنامج SeparateEnumDemo.java هو نفسه البرنامج EnumDemo.java باستثناء أن أنواع التعداد المُستخدَمة قد عُرِّفت بملفات مُنفصلة هي Month.java و Day.java. ترجمة -بتصرّف- للقسم Section 3: Strings, Classes, Objects, and Subroutines من فصل Chapter 2: Programming in the Small I: Names and Things من كتاب Introduction to Programming Using Java. اقرأ أيضًا كيف تتعلم البرمجة
-
تتكون تقنية Forms من عدة مكتبات تُسهِل من عملية تطوير التطبيقات. النموذج أو الاستمارة (form) هو ببساطة واجهة مُستخدِم مُكَوَّنة من عدة عناصر منفصلة (controls). تُستخدَم هذه العناصر لعَرْض معلومات للمُستخدِم أو استقبال مُدخَلات منه. تَعتمِد برمجة تطبيقات Forms بشكل أساسي على مفهوم الأحَدَاث (events)، والتي تُثار تلقائيًا عند ظرف معين أو نتيجة تَفاعُل المُستخدِم مع البرنامج. بالتالي، لابُد للبرنامج أن يُعالِج هذه الأحداث (event handlers) بتَّنْفيذ الإجراء المُتناسب. برنامج "أهلا بالعالم" على سبيل المثال، يُثار الحَدَث Load مرة وحيدة عند تَحمِيل النموذج (form) وعَرضه لأول مرة. في المقابل، يُثار الحَدَث Shown في كل مرة يُعرَض فيها النموذج. لعَرْض مربع رسالة (message box) في كل مرة يُعرَض فيها النموذج، اِستخدِم الشيفرة التالية: Public Class Form1 Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown MessageBox.Show("Hello, World!") End Sub End Class لِعَرْضِها مرة واحدة فقط عند تَحمِيل النموذج، اِستخدِم الشيفرة التالية: Public Class Form1 Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load MessageBox.Show("Hello, World!") End Sub End Class يُثار أيضًا الحَدَث Activate في كل مرة يقوم فيها المُستخدِم بتَفعيل النموذج. تنبيه: يُثار الحَدَث Load قبل الحَدَث Show. مع ذلك، إذا اِستدعَى مُعالِج الحَدَث Show التابع msgBox لِعَرْض مربع رسالة، فقد يؤدي ذلك إلى تَّنْفيذ التابع msgBox قبل انتهاء تنفيذ مُعالِج الحََدَث Load. لذلك، عامةً، لا يُنصَح بالاعتماد على ترتيب إثارة الحَدَثين Load و Show. المؤقت Timer لنفترض أن لدينا نموذج (form) يحتوي على العناصر التالية: زر، وعنوان (label)، ومؤقت (timer). يُهيئ المثال التالي مُعالِج حَدَث الضغط على الزر. يَعرِض ذلك المُعالِج التوقيت الحالي للمُستخدِم ثم يَضبُط مؤقت لمدة دقيقة، بعد انتهائها يُثار الحدث Tick تلقائيًا، مما يؤدي إلى تَّنْفيذ مُعالِج الحدث Tick الذي يُحدِّث قيمة الوقت المعروض: Public Class Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Button1.Enabled = False Timer1.Interval = 60 * 1000 'one minute intervals 'start timer Timer1.Start() Label1.Text = DateTime.Now.ToLongTimeString End Sub Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick Label1.Text = DateTime.Now.ToLongTimeString End Sub End Class مثال آخر هو محاولة اِستخدَام عنصر المؤقت Timer لمحاكاة مؤقت للعد التنازلي (countdown) لمدة معينة -مثلًا 3 دقائق. كالمثال السابق، يُضبَط المؤقت عند الضغط على الزر. لكن، في هذا المثال، ستُستخدََم أيضًا ساعة توقيت من النوع Stopwatch لموازنة مدة المؤقت المُحدَّدة مع الوقت الفعلي المُنصرِم منذ لحظة ضَبْط المؤقت وحتى تنْفيذ مُعالِج الحدث Tick. يَعرِض مُعالِج الحدث Tick الزمن المُنصرم بوحدة الثواني: Public Class Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Button1.Enabled = False ctSecs = 0 'clear count Timer1.Interval = 1000 'one second in ms. 'start timers stpw.Reset() stpw.Start() Timer1.Start() End Sub Dim stpw As New Stopwatch Dim ctSecs As Integer Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick ctSecs += 1 If ctSecs = 180 Then 'about 2.5 seconds off on my PC! 'stop timing stpw.Stop() Timer1.Stop() 'show actual elapsed time 'Is it near 180? Label1.Text = stpw.Elapsed.TotalSeconds.ToString("n1") End If End Sub End Class قد تَفترِض أنها لابُدّ وأن تكون مساوية للقيمة 180؟ في الواقع، ليس هذا ضروريًا. مثلًا قد عَرَضَ حاسوبي القيمة 182.5. السبب وراء هذا التفاوت هو أن عنصر المؤقت Windows.Forms.Timer أحادي الخيط (single-threaded)، ومحدود بدقة تصل إلى 55 ميللي ثانية، مما يعني أنه غير ملائم لتسجيل الزمن وتدقيقه، ولا ينبغي استخدامه لأغراض حساسة جدًا للزمن. يُمكن الحصول على نتائج أفضل باستخدام المؤقت وساعة الإيقاف بطريقة مختلفة، كالتالي: Public Class Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Button1.Enabled = False Timer1.Interval = 100 'one tenth of a second in ms. 'start timers stpw.Reset() stpw.Start() Timer1.Start() End Sub Dim stpw As New Stopwatch Dim threeMinutes As TimeSpan = TimeSpan.FromMinutes(3) Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick If stpw.Elapsed >= threeMinutes Then '0.1 off on my PC! 'stop timing stpw.Stop() Timer1.Stop() 'show actual elapsed time 'how close? Label1.Text = stpw.Elapsed.TotalSeconds.ToString("n1") End If End Sub End Class يوجد مؤقتات أخرى يمكن اِستخدَامها. قد تُساعِدك نتائج هذا البحث بهذا الخصوص. بعض النصائح للمبتدئين نَستعرِض بعض النصائح التي ينبغي أن يَتبِعهَا المبتدئون حتى يحصلوا على بداية موفقة مع VB.NET: اضبط الخيارات التالية: 'can be permanently set ' Tools / Options / Projects and Soluntions / VB Defaults Option Strict On Option Explicit On Option Infer Off Public Class Form1 End Class اِستخدِم العامل & وليس + لضم السَلاسَل النصية. عامةً ينبغي دراسة النوع String بشئ من التفصيل نظرًا للحاجة إلى استخدامه بكثرة. اقض بعض الوقت لاستيعاب الفرق بين أنواع القيمة وأنواع المَرجِع. لا تَستخدِم التابع Application.DoEvents أبدًا. ألق نظرة على الفقرة التنبيهية بالرابط. عندما تكون متأكدًا من حاجتك الماسة لاستخدامه، اسأل. التوثيق هو رفيق دربك. ترجمة -وبتصرف- للفصل VB Forms من كتاب .NET Framework Notes for Professionals
-
تُعدّ الأسماء واحدة من أساسيات البرمجة حيث تُستخدَم بالبرامج للإشارة إلى كثير من الأشياء المختلفة. لابُدّ من فهم قواعد تَسْمية الأشياء وطريقة اِستخدَام الأسماء لكي تَتَمكَّن من اِستخدَام الأشياء أي تحتاج في العموم إلى فهم كُلًا من صيغة (syntax) الأسماء ودلالتها (semantics). وفقًا لقواعد الصيغة (syntax rules) بالجافا، المُعرّفات (identifiers) واحدة من أبسط الأسماء، وتُستخدَم لتسمية كُلًا من الأصناف (classes) والمُتْغيِّرات (variables) والبرامج الفرعية (subroutines). يَتَكوَّن أي مُعرّف من متتالية تتألف من محرف (character) واحد أو أكثر بشَّرْط أن يَكُون أول محرف حرفًا أبجديًا (letter) أو شرطة سفلية (underscore) _، وأن تَكوُن المتتالية بالكامل مُكوَّنة من حروف أبجدية وأرقام (digits) وشرط سفلية فقط. اُنظر الأمثلة التالية لأسماء مُعرّفات (identifiers) صالحة: N n rate x15 quite_a_long_name HelloWorld لا يُسمَح للمسافات الفارغة (spaces) بأن تَكُون جزءًا من اسم أي مُعرّف، فمثلًا، في حين تستطيع اِستخدَام HelloWorld كاسم مُعرّف، لا يُمكِنك أن تَستخدِم "Hello World". تختلف كذلك الحروف الكبيرة عن الحروف الصغيرة فيما يَتَعلَّق بالتسمية أي أن الأسماء HelloWorld و helloworld و HELLOWORLD و hElloWorLD هي أسماء مُعرّفات مختلفة. بالإضافة إلى ذلك، لا يُمكِنك اِستخدَام الكلمات المحجوزة (reserved) المُخصَّصة لأغراض خاصة بالجافا كأسماء مُعرِّفات مثل الكلمات class و public و static و if و else و while وعشرات من الكلمات الآخرى. لاحِظ أن الكلمات المحجوزة ليست مُعرّفات فهي لا تُستخدَم كأسماء تُشير إلى أشياء. تَستخدِم الجافا محارف اليونيكود (Unicode character set) والتي تَتَضمَّن آلافًا من المحارف والحروف الأبجدية (alphabets) من مختلف اللغات. تُعدّ كثير من تلك المحارف بمثابة حروف أبجدية أو أرقام. سنَقْتصِر عمومًا على اِستخدَام المحارف المُتوفِّرة بأي لوحة مفاتيح إنجليزية عادية فقط. هنالك بعض الاصطلاحات والقواعد الإرشادية التي ينبغي اتباعها في العموم عند تَسْمية (naming) الأشياء. أولًا، تبدأ أسماء الأصناف (classes) بحروف كبيرة (upper case) بعكس أسماء كُلًا من المُتْغيِّرات (variables) والبرامج الفرعية (subroutines) والتي تبدأ عادةً بحروف صغيرة (lower case). يُساعِد الالتزام بذلك التَقْليد (convention) على تَجنُّب أي خلط مُحتمَل. ثانيًا، لا يَستخدِم غالبية مُبرمجي الجافا الشُرط السُفليّة (underscores) بالأسماء مع أن البعض يلجأ إلى اِستخدَامها ببداية أسماء أنواع مُحدَّدة من المُتْغيِّرات (variables). ثالثًا، إذا كان الاسم مُكوَّن من عدة كلمات مثل HelloWorld أو interestRate، فيُعتاد تَكْبير الحرف الأول (capitalize) من كل كلمة باستثناء الأولى فيما يُعرَف باسم نَمْط سنام الجمل (camelCase) حيث تُشبه الحروف الأبجدية الكبيرة (upper case) بمنتصف الاسم سنام الجمل. بالإضافة إلى الأسماء البسيطة (simple names)، قد تَكُون الأسماء أيضًا مُركَّبة من عدة أسماء بسيطة يَفصِل بين كُلًا منها نقطة (period) مثل الاسم System.out.println، وتُعرَف باسم "الأسماء المُؤهلة (qualified names)". لأن الجافا تَسمَح بتَضْمِين الأشياء ضِمْن بعضها البعض، فإن الاسم المُركَّب يَعمَل كمسار إلى شيء عبر واحد أو أكثر من مستويات الاحتواء (containment level)، فمثلًا يُشير الاسم System.out.println إلى شيء اسمه System يحتوي على شيء آخر اسمه out والذي بدوره يحتوي على شيء اسمه printn. المتغيرات (variables) تُستخدَم البرامج في العموم لمُعالجة البيانات المُخزَّنة بذاكرة الحاسوب. إذا كنا نُبرمج باِستخدَام لغة الآلة (machine language)، فإننا نَكُون مُضطرّين لاِستخدَام العنوان العددي (numerical memory address) لمَوضِع الذاكرة لكي نَتَمكَّن من الإشارة إلى البيانات المُخزَّنة به أما باللغات عالية المستوى (high-level language) مثل الجافا، فإننا في العموم نَستخدِم أسماءً وليس أعدادًا للإشارة إلى بيانات الذاكرة أي لا يحتاج المبرمج إلى ما هو أكثر من تَذكُّر تلك الأسماء، والتي يُطلَق عليها اسم "المُتْغيِّرات (variables)"، وفي المقابل، يَتعقَب الحاسوب مواضع الذاكرة الفعليّة لتلك البيانات. لا يُعدّ المُتْغيِّر (variable) اسمًا للبيانات نفسها وإنما لمَوضِع الذاكرة الحامل لتلك البيانات أي أنه يَعمَل كصندوق أو كحاوي يُمكِنك أن تُخزِّن به بعض البيانات التي قد تحتاج إليها لاحقًا. يُشير أي مُتْغيِّر إذًا إلى صندوق بصورة مباشرة وإلى البيانات الموجودة بذلك الصندوق بصورة غير مباشرة. لمّا كانت البيانات المُخزَّنة بالصندوق قابلة للتَعْديل، فقد يشير مُتْغيِّر معين إلى بيانات مختلفة بلحظات مختلفة من تّنْفيذ البرنامج، ولكنه دائمًا ما سيُشير إلى نفس الصندوق. عندما نَستخدِم مُتْغيِّرًا ضِمْن برنامج، فإنه -وبحسب الطريقة التي اُستخدِم بها ذلك المُتْغيِّر- إما يُشير إلى صندوق وإما إلى البيانات المُخزَّنة بذلك الصندوق، وهو ما قد يَكُون مُربِكًا لبعض المبرمجين المبتدئين. سنَفْحَص أمثلة لكلا الحالتين بالأسفل. تُستخدَم تَعْليمَات الإسناد (assignment statement) بالجافا لتَخْزين بيانات معينة بمُتْغيِّر أي بصندوق، وتُكْتَب على النحو التالي: <variable> = <expression>; يُمثِل التعبير -بالأعلى- أي شيء طالما كان يُشير إلى قيمة بيانات أو يَحسِبها. عندما يُنفِّذ الحاسوب تَعْليمَة إسناد معينة، فإنه يُحصِّل (evaluates) قيمة ذلك التعبير ثم يُخزِّنها بالمُتْغيِّر . اُنظر تَعْليمَة الإسناد (assignment statement) التالية على سبيل المثال: rate = 0.07; بالتَعْليمَة السابقة، يُمثِل rate المُتْغيِّر أما القيمة 0.07 فتُمثِل التعبير . تُخزِّن تَعْليمَة الإسناد السابقة القيمة 0.07 بالمُتْغيِّر rate بحيث تحلّ تلك القيمة محلّ قيمته السابقة. سنَفْحَص الآن مثالًا آخر لتَعْليمَة إِسناد أكثر تعقيدًا بقليل، والتي سنحتاج إليها لاحقا ضِمْن البرنامج: interest = rate * principal; تُسنِد تَعْليمَة الإسناد (assignment statement) بالأعلى قيمة التعبير rate * principal إلى المُتْغيِّر interest. يُشير المحرف * الموجود بالتعبير إلى "عامِل حاصل الضرب (multiplication operator)" المسئول عن حساب حاصل ضرب rate في principal. لمّا كانت الأسماء rate و principal ضِمْن التعبير هي نفسها مُتْغيِّرات، يُحسَب حاصل ضرب القيم المُخزَّنة بتلك المُتْغيِّرات أي يُحسَب حاصل ضرب قيمة rate في قيمة principal ثم تُخزَّن الإجابة بالصندوق الذي يُشير إليه interest. نستطيع أن نقول إذًا أنه وفي العموم عندما نَستخدِم مُتْغيِّرًا ضِمْن تعبير (expression)، فإن القيمة المُخزَّنة بذلك المُتْغيِّر هي كل ما يُهِمّ، ويبدو المُتْغيِّر في تلك الحالة كما لو كان يُشير إلى بيانات الصندوق وليس الصندوق ذاته. في المقابل، عندما نَستخدِم مُتْغيِّرًا على الجانب الأيسر من تَعْليمَة إسناد، فإنه في تلك الحالة يُشير إلى الصندوق ذاته المُمثِل لذلك المُتْغيِّر. تَعْليمَات الإِسناد ليست تَعْليمَات خبرية، وإنما هي بمثابة أوامر يُنفِّذها الحاسوب بأوقات محددة. لنَفْترِض مثلً أن برنامجًا معينًا يُنفِّذ التَعْليمَة rate = 0.07; ثم يُنفِّذ التَعْليمَة interest = rate * principal; بوقت لاحق ضِمْن البرنامج، هل يُمكِننا ببساطة أن نَدعِي أننا قد ضربنا principal بالقيمة 0.07؟ في الواقع لا! لأن قيمة rate قد تَتَغيَّر في أي لحظة بواسطة تَعْليمَة إِسناد آخرى. يختلف معنى تَعْليمَة الإِسناد تمامًا عن معنى أي معادلة رياضية على الرغم من أن كليهما يَستخدِم الرمز =. الأنواع (types) يُمكِن لأي مُتْغيِّر (variable) بالجافا أن يَحمِل نوعًا (type) واحدًا فقط من البيانات وليس أي نوع آخر. إذا حاولت أن تُسنِد (assigning) قيمة من نوع مختلف عن نوع مُتْغيِّر معين إلى ذلك المُتْغيِّر، فسيُعدّ ذلك انتهاكًا لتلك القاعدة، وسيُبلِّغ عنه المُصرِّف (compiler) على أساس كَوْنه خطأ في بناء الجملة (syntax error). نقول إذًا أن الجافا هي لغة صارمة في تَحْديد النوع (strongly typed language). تُوفِر الجافا 8 أنواع أساسية (primitive types) مَبنية مُسْبَقًا هي كالتالي: byte و short و int و long و float و double و char و boolean. تَحمِل الأنواع الأربعة الأولى أعدادًا صحيحة (integers) مثل 17 و -38477 و 0، ولكنها تختلف عن بعضها فيما يَتعَلَّق بنطاق الأعداد الصحيحة التي يُمكِن لكل نوع منها حَمْله. في المقابل، يَحمِل النوعان float و double أعدادًا حقيقية (real numbers) مثل 3.6 و -145.99، ويختلفان عن بعضهما بكُلًا من نطاق الأعداد المسموح به وبدقة العَدَد. تستطيع المُتْغيِّرات من النوع char أن تَحمِل إحدى محارف اليونيكود (Unicode character set) أما المُتْغيِّرات من النوع boolean فتَحمِل إما القيمة المنطقية true أو القيمة false. تُمثَل جميع قيم البيانات بذاكرة الحاسوب بهيئة عدد من النظام الثُنائي (binary number) عبارة عن سِلسِلة نصية (string) مُكوَّنة من الرقمين صفر وواحد. يُطلَق اسم "بت (bit)" على كُلًا منها بينما يُطلَق اسم "بايت (byte)" على أي سِلسِلة نصية مُكوَّنة من ٨ بتات، وتُقاس الذاكرة عادةً بتلك الوحدة. يُشير النوع byte إلى بايت واحد فقط بالذاكرة أي تَحمِل المُتْغيِّرات (variable) من النوع byte سِلسِلة نصية من ٨ بتات يُمكِنها أن تُمثِل ٢٥٦ عددًا صحيحًا (٢ مرفوعة للأس ٨) أي قد تُمثِل أي عدد صحيح بين -١٢٨ و ١٢٧. أما بالنسبة للأنواع الصحيحة (integer) الآخرى: short يُمثِل ٢ بايت (١٦ بت)، وتَحمِل المُتْغيِّرات من النوع short قيم ضِمْن نطاق يتراوح من -32768 إلى 32767. int يُمثِل ٤ بايت (٣٢ بت)، وتَحمِل المُتْغيِّرات من النوع int قيم تتراوح من -2147483648 إلى 2147483647. long يُمثِل ٨ بايت (٦٤ بت)، وتَحمِل المُتْغيِّرات من النوع long قيم بنطاق يتراوح من -9223372036854775808 إلى 9223372036854775807. لا تحتاج إلى تَذكُّر كل تلك الأعداد بالأعلى، فالغرض منها فقط هو إِعطائك فكرة عن حجم الأعداد الصحيحة التي يُمكِن لكل نوع أن يَحمِلها. وفي العموم عادةً ما يُستخدَم النوع int لتمثيل أي قيمة عددية صحيحة لأنه جيد كفاية لغالبية الأغراض. أما النوع float فيَستخدِم طريقة ترميز (encoding) قياسية لتمثيل الأعداد الحقيقية (real) بمساحة ٤ بايت من الذاكرة، ويُمكِنه أن يَحمِل قيمة قصوى تَصِل إلى ١٠ مرفوعة للأس ٣٨. علاوة على ذلك، يُمكِن للقيم من النوع float أن تَشتمِل على ٧ أرقام معنوية (significant digits) بحد أقصى. لابُدّ إذًا من تَقْرِيب كُلًا من العددين 32.3989231134 و 32.3989234399 إلى العَدَد 32.398923 حتى نَتَمكَّن من تَخْزِينه بمُتْغيِّر من النوع float. في المقابل، يُمثِل النوع double الأعداد الحقيقية بمساحة ٨ بايت، لذا يُمكِنه أن يَحمِل قيمة قصوى تَصِل إلى ١٠ مرفوعة للأس ٣٠٨، وقد يَشتمِل على ١٥ رقم معنوي (significant digit). في العموم عادةً ما يُستخدَم النوع double لتمثيل قيم الأعداد الحقيقية. أما المُتْغيِّرات من النوع char فتَحتلّ مساحة ٢ بايت من الذاكرة، وتَحمِل محرفًا وحيدًا مثل "A" أو "*" أو "x" أو مسافة فارغة (space) أو محرفًا خاصًا مثل المحرف "tab" أو محرف "العودة إلى بداية السطر (carriage return)" أو إحدى محارف اليونيكود (Unicode character) بلغات مختلفة. يَستخدِم النوع char ترميزًا عدديًا صحيحًا (integer code number) مُكوَّن من ١٦ بت لتمثيل أي محرف، لذلك فإنه قيمه تُعدّ مرتبطة نوعًا ما بقيم الأعداد الصحيحة (integer)، وسنرى لاحقًا أنه يُمكِننا حتى اِستخدَامها كما لو كانت أعدادًا صحيحة بمواقف معينة. لمّا كان الحاسوب يَستخدِم عددًا مُحدّدًا ومُتناهيًا (finite) من البتات لتمثيل قيمة أي نوع أساسي (primitive type)، فلا يُمكِن إذًا لقيم النوع int أن تُمثِل أي عدد صحيح عشوائي بل لابُدّ أن يَقَع العََدَد الصحيح ضِمْن نطاق مُحدّد ومُتناهي من القيم. بالمثل، يُمكِن للمُتْغيِّرات من النوعين float و double أن تَحمِل قيمًا محددة فقط أي أنها ليست أعدادًا حقيقية فعليًا بالمفهوم الرياضي. فمثلًا، لأن الثابت الرياضي π يَتَطلّب عددًا لامُتناهيًا من الأرقام العشرية لتمثيله، فلابُدّ من تقريبه أولًا قبل تَخْزِينه بمُتْغيِّر من النوع float أو النوع double. بالمثل، لابُدّ من تقريب الأعداد البسيطة مثل 1/3 قبل تَخْزِينها بمُتْغيِّرات من النوع float و double. القيم مصنَّفة النوع (literals) تُخزَّن أي بيانات بالحاسوب كمتتالية من البتات (bits). بالنسبة للقيم الثابتة (constant values)، فإنها تُمثَل باستخدَام ما يُعرَف باسم "القيم مُصنَّفة النوع (literals)"، والتي هي عبارة عن شيء يُمكِنك كتابته لتمثيل قيمة أي يُمكِن عدّه اسمًا لقيمة ثابتة. لكتابة قيمة من النوع char على سبيل المثال، لابُدّ من إحاطتها بعلامتي اقتباس مُفرّدتين مثل 'A' و '*' و 'x'. يُشكِّل المحرف وعلامتي الاقتباس معًا "قيمة مُصنَّفة النوع (literal)" من النوع char. بدون علامتي الاقتباس، سيُمثِل A مُعرّفًا (identifier) أما * فستُمثِل عامل حاصل الضرب (multiplication operator). إذا كنت تريد أن تُخزِّن المحرف A بمُتْغيِّر ch من النوع char، فيُمكِنك أن تَستخدِم تَعْليمَة الإِسناد (assignment statement) التالية. لا تُعدّ علامتي الاقتباس جزءًا من القيمة ولا تُخزَّن بالمُتْغيِّر (variable)، فهي فقط نَمْط لتسمية الثوابت المحرفية (character constant): ch = 'A'; تَستخدِم بعض المحارف الخاصة قيمًا خاصة مُصنَّفة النوع (literals) تَتَضمَّن خطًا مائلًا عكسيًا (backslash) يَعمَل كمحرف هروب (escape character)، فمثلًا، يُمثِل '\t' المحرف "tab"، ويُمثِل '\r' محرف العودة إلى بداية السطر (carriage return)، ويُمثِل '\n' محرف إضافة سطر جديد، ويُمثِل ''\' علامة اقتباس مُفردة، ويُمثِل '\\' محرف خط مائل عكسي (backslash). يَتَكوَّن كُلًا منها من محرفين إلى جانب علامتي الاقتباس، ومع ذلك فإنه يُشير إلى محرف واحد فقط، فمثلًا تُشير القيمة مُصنَّفة النوع '\t' إلى محرف "tab" واحد فقط. قد تَكُون القيم العَدَدية مُصنَّفة النوع (Numeric literals) أعقد قليلًا مما تَتَوقَّع. هنالك بالطبع أمثلة واضحة مثل 317 و 17.42، ولكن هنالك احتمالات وطرائق أخرى لتمثيل الأعداد بلغة الجافا. أولًا، قد تُستخدَم صيغة أُسية (exponential form) لتمثيل الأعداد الحقيقية مثل 1.3e12 أو 12.3737e-108 حيث "e12" و "e-108" هي أسس للعدد ١٠ أي يُكافئ العدد 1.3e12 حاصل ضرب ١,٣ في ١٢١٠ أما العدد 12.3737e-108 فيُكافئ حاصل ضرب ١٢,٣٧٣٧ في ١٠-١٠٨. عادةً ما تُستخدَم هذه الصيغة للتعبير عن الأعداد الصغيرة جدًا أو الكبيرة جدًا. ثانيًا، قد تحتوي بعض القيم العددية مُصنَّفة النوع (numeric literal) على علامة عشرية (decimal point) أو على أس (exponential)، وتُعدّ عندها قيمًا من النوع double أتوماتيكيًا أما إذا أردت أن تُنشِيء قيمة مُصنَّفة من النوع float، فينبغي أن تضيف الحرف "f" أو "F" إلى نهاية العدد، فتُمثِل "1.2F" مثلًا قيمة من النوع float تُساوِي 1.2. ولأن الجافا لا تَسمَح بإسناد (assign) قيمة من النوع double لمُتْغيِّر من النوع float، فستُواجهك رسالة خطأ سخيفة نوعًا ما إذا حاولت أن تقوم بشيء مثل x = 1.2; عندما يَكُون x مُتْغيِّر من النوع float، وعندها يَنبغي أن تُضيف الحرف "F" بالنهاية كالتالي x = 1.2F;. حاول عمومًا أن تَستخدِم النوع double لتمثيل الأعداد الحقيقية بالبرامج الخاصة بك. أما بالنسبة لقيم الأعداد الصحيحة مُصنَّفة النوع (integer literals)، فهنالك أعداد صحيحة عادية مثل 177777 و -32 والتي إما أن تَكُون من النوع byte أو النوع short أو النوع int بحسب حجمها. في المقابل، يُمكِنك أن تُنشِيء قيمة مُصنَّفة النوع (literal) من النوع long بإضافة الحرف "L" بنهاية العدد مثل 17L أو 728476874368L. تَسمَح الجافا أيضًا باستخدام قيم عددية ثنائية (binary) أو ثُمانيّة (octal) أو ست عشريّة (hexadecimal) مُصنَّفة النوع. لن نناقش أيًا منها على نحو تفصيلي وإنما سنَكتفِي بالنقاط التالية: أولًا، تَقْتصِر الأعداد الثُمانيّة (octal numbers) على الأرقام العشرية من ٠ إلى ٧ فقط، وتبدأ أي قيمة ثُمانيّة مُصنَّفة النوع (octal literal) بصفر مثل القيمة 045 والتي تُمثِل العدد 37 وليس العدد 45. نادرًا ما تُستخدَم الأعداد الثُمانيّة ومع ذلك تذكَّر على الأقل أنها تبدأ بصفر. ثانيًا، تَتَكوَّن الأعداد الست عشرية (Hexadecimal numbers) من ١٦ رقم هي الأرقام العشريّة العادية من ٠ إلى ٩ بالإضافة إلى الحروف A و B و C و D و E و F وبحيث تُمثِل تلك الحروف القيم من ١٠ إلى ١٥ على التوالي. لاحِظ أن الحروف الصغيرة (lower case letters) من a وحتى f لها نفس قيمة نظيراتها من الحروف الكبيرة (upper case). تبدأ القيم الست عشرية مُصنَّفة النوع (hexadecimal literal) بالجافا باستخدام 0x أو 0X مثل 0x45 أو 0xFF7A. وأخيرًا، تبدأ القيم الثنائية مُصنَّفة النوع (binary literals) باستخدام 0b أو 0B، وتَتَكوَّن من الرقمين ٠ و ١ فقط مثل 0b10110. قد تحتوي القيم العددية مُصنَّفة النوع (numeric literals) على محرف شرطة سفلية (underscores) "_" لفَصْل مجموعات الأرقام (digits) عن بعضها وبدون أي قاعدة فيما يَتَعلَّق بعَدَد الأرقام ضِمْن كل مجموعة. يُمكِننا مثلًا كتابة الثابت العددي ٢ بليون بهيئة 2_000_000_000 بدلًا من 2000000000 مما يُسهِل من قراءة العدد. تَبرُز أهمية الشُرَط السفليّة على نحو أكبر عند اِستخدَامها بالأعداد الثنائية (binary numbers) الطويلة مثل 0b1010_1100_1011. تُستخدَم الأعداد الست عشرية (hexadecimal numbers) أيضًا كقيم محرفية مُصنَّفة النوع (character literals) لتمثيل بعض محارف اليونيكود (unicode characters). تَتَكوَّن أي قيمة يونيكود مُصنَّفة النوع (unicode literal) من \u متبوعة بأربعة أرقام ست عشريّة. تُمثِل مثلًا القيمة المحرفية مُصنَّفة النوع '\u00E9' محرف يونيكود هو الحرف "é". أخيرًا، بالنسبة للنوع boolean، فتُستخدَم القيمتين true و false كقيم مُصنَّفة النوع (literal)، وتُكْتَب على نفس هذه الهيئة بدون علامتي اقتباس مع أنها تُمثِل قيم لا مُتْغيِّرات (variables). عادةً ما تُستخدَم القيم المنطقية كقيم للتعبيرات الشرطية (conditional expressions) كالمثال التالي: rate > 0.05 يُعيد التعبير المنطقي (boolean-valued expressions) السابق قيمة إما تُساوِي true إذا كانت قيمة المُتْغيِّر rate أكبر من 0.05 أو تُساوِي false إذا كنت قيمته أقل من أو تُساوِي 0.05. تُستخدَم التعبيرات المنطقية بكثرة ضِمْن بُنى التَحكُّم (control structure) كما سنرى بالفصل الثالث. إلى جانب ذلك، يُمكِننا أيضًا أن نُسنِد القيم المنطقية إلى مُتْغيِّرات من النوع boolean. فمثلًا إذا كان test مُتْغيِّرًا من النوع boolean فيُمكننا كتابة تَعْليمَتي الإِسْناد (assignment statement) التاليتين: test = true; test = rate > 0.05; السلاسل النصية (strings) والسلاسل النصية المجردة (string literals) إلى جانب الأنواع الأساسية (primitive types)، تُوفِّر الجافا أنواعًا آخرى قيمها عبارة عن كائنات (objects) وليس قيم أساسية (primitive). ليس هذا وقتًا مناسبًا بالطبع للحديث عن الكائنات (objects)، ولكننا مُضطرّون للحديث عن النوع String لأهميته. هو ببساطة نوع كائني (object type) مُعرَّف مُسْبَقًا تُوفِّره الجافا لتمثيل السَلاسِل النصية (strings). لا يَقَع ذلك النوع ضِمْن الأنواع الأساسية (primitive)، بل هو في الواقع اسم صَنْف (class)، وهو ما سنعود للحديث عنه بالقسم التالي. تَتَكوَّن القيم من النوع String من متتالية من المحارف مثل السِلسِلة النصية المُجرّدة "Hello World!" التي تَعرَضنا لها بقسم سابق. لابُدّ من إحاطة النص بعلامتي اقتباس مزدوجتين حيث تُعدّ كلتاهما جزءًا من السِلسِلة النصية المجرّدة (string literal)، ولكنهما مع ذلك ليسا جزءًا من قيمتها الفعليّة والتي تَقْتصِر على المحارف بين علامتي الاقتباس. قد تحتوي السِلسِلة النصية من النوع String على أي عدد من المحارف بما في ذلك الصفر، وعندها تَكُون بمثابة سِلسِلة نصية فارغة (empty string) تُمثَل بالسِلسِلة النصية المُجرّدة "" أي مُكوَّنة من علامتي اقتباس مزدوجتين فارغتين. انتبه جيدًا للفارق بين علامتي اقتباس فرديتين وآخريتين مزدوجتين حيث تُستخدَم الأولى مع القيم المحرفية مُصنَّفة النوع (char literals) أما الثانية فتُستخدَم مع السَلاسِل النصية المُجرّدة (String literals)، فمثلًا، لاحظ الفرق بين السِلسِلة النصية "A" والحرف 'A'. قد تَتَضمَّن السَلاسِل النصية المُجرّدة (string literal) محارفًا خاصة، ويُستخدم عندها خطًا مائلًا عكسيًا (backslash) لتمثيلها. ضِمْن هذا السياق، تُعدّ علامة الاقتباس المزدوجة محرفًا خاصًا، فإذا كان لدينا القيمة النصية التالية (string value): I said, "Are you listening!" يُمكِننا إذًا تمثيلها باستخدام سِلسِلة نصية مُجرّدة (string literal) كالتالي مع محرف "إضافة سطر جديد (linefeed)" بالنهاية: "I said, \"Are you listening!\"\n" يُمكِنك أيضًا أن تَستخدِم \t و \r و \\ وكذلك متتاليات اليونيكود مثل \u00E9 لتمثيل محارف خاصة آخرى ضمن السِلاسِل النصية المُجرّدة (string literals). المتغيرات بالبرامج ينبغي دائمًا أن تُصرِّح (declare) عن أي مُتْغيِّر (variable) قبل اِستخدَامه. تُستخدَم تَعْليمَة التَصْرِيح عن المُتْغيِّرات (variable declaration statement) للإعلان عن مُتْغيِّر واحد أو أكثر وكذلك تَخْصيص اسم لكُلًا منها. عندما يُنفِّذ الحاسوب تَعْليمَة تَصْرِيح عن مُتْغيِّر، فإنه يَحجِز له مساحة من الذاكرة كما يَربُط اسمه بتلك المساحة. تُكْتَب تَعْليمَة التَصْريح عن مُتْغيِّر بالصياغة التالية: <type-name> <variable-name-or-names>; تُمثِل اسم مُتْغيِّر واحد أو قائمة من أسماء المُتْغيِّرات يَفصِل بينها فاصلة (comma). يُمكِن لتَعْليمَات التَصْرِيح عن المُتْغيِّرات أن تَكُون أكثر تعقيدًا من ذلك كما سنرى لاحقًا. يُفضَّل عمومًا التَصْرِيح (declare) عن مُتْغيِّر واحد فقط بكل تَعْليمَة تَصْرِيح (declaration statement) إلا لو كانت تلك المُتْغيِّرات مُرتبطَة ببعضها. اُنظر الأمثلة التالية: int numberOfStudents; String name; double x, y; boolean isFinished; char firstInitial, middleInitial, lastInitial; يُفضَّل أيضًا إضافة بعض التعليقات (comment) لكل تَعْليمَة تَصْرِيح عن مُتْغيِّر (variable declaration)؛ لشرح غرضه بالبرنامج أو لتَوفِير بعض المعلومات التي يُمكِنها أن تُساعِد أي مبرمج آخر يَرغَب بقراءة البرنامج. اُنظر الأمثلة التالية: double principal; // مقدار النقود المُستثمَرَ double interestRate; // معدل الفائدة كقيمة وليس نسبة بهذا الفصل، سنَستخدِم المُتْغيِّرات (variables) المُصرَّح (declare) عنها داخل البرنامج الفرعي main() فقط. تُعرَف المُتغيرات المُصرَّح عنها داخل برنامج فرعي (subroutine) معين باسم "المُتْغيِّرات المحليّة (local)" وتَكُون مُتوفِّرة فقط داخل ذلك البرنامج الفرعي عند تَشْغِيله ولا يُمكِن الوصول إليها من خارجه نهائيًا. يُمكِنك أن تُصرِّح عن مُتْغيِّرات بأي مكان داخل البرنامج الفرعي بشَّرْط ألا تَستخدِم أبدًا مُتْغيِّر معين بدون أن تُصرِّح عنه أولًا. يُفضِّل بعض الأشخاص التَصْريح (declare) عن جميع المُتْغيِّرات ببداية البرنامج الفرعي بينما يُفضِّل آخرون إرجاء التَصْرِيح عن المُتْغيِّرات إلى حين الحاجة لاِستخدَامها مباشرةً. يُفضِّل الكاتب التَصْرِيح عن المُتْغيِّرات الهامة والمحورية ببداية البرنامج الفرعي مع إضافة تعليق (comment) على كل تَعْليمَة تَصْرِيح منها يشرح الغرض من المُتْغيِّر، وفي المقابل يرى الكاتب أنه من الممكن إرجاء التَصْرِيح عن المُتْغيِّرات غَيْر المُهِمّة إلى حين الحاجة لاِستخدَامها لأول مرة. يَستخدِم البرنامج التالي بعض المُتْغيِّرات (variables) وتَعْليمَات الإِسْناد (assignment statements): public class Interest { public static void main(String[] args) { /* صرح عن المتغيرات */ double principal; // قيمة الاستثمار double rate; // معدل الفائدة السنوي double interest; // قيمة معدل الفائدة بعام واحد /* إحسب القيم المطلوبة */ principal = 17000; rate = 0.027; interest = principal * rate; // احسب معدل الفائدة principal = principal + interest; // احسب قيمة الاستثمار بعد عام واحد مع الفائدة /* اعرض النتائج */ System.out.print("The interest earned is $"); System.out.println(interest); System.out.print("The value of the investment after one year is $"); System.out.println(principal); } // نهاية main() } // نهاية الصنف Interest يَستدعِي البرنامج بالأعلى مجموعة من البرامج الفرعية (subroutine call) مثل System.out.print و System.out.println لعَرْض بعض المعلومات للمُستخدِم. بخلاف البرنامج الفرعي الأول، يَطبَع البرنامج الثاني محرف "إضافة سطر جديد (linefeed)" بَعْد عَرْض المعلومة ذاتها، فمثلًا، تَطبَع تَعْليمَة الاستدعاء System.out.println(interest); قيمة المُتْغيِّر interest بنفس سطر السِلسِلة النصية المعروضة بواسطة تَعْليمَة الاستدعاء System.out.print السابقة. يُفْترَض للقيمة المطلوب طباعتها باِستخدَام System.out.print أو System.out.println أن تُكْتَب بَعْد اسم البرنامج الفرعي (subroutine) داخل أقواس ويُطلق عليها اسم المُعامِل (parameter). قد يَستقبِل أي برنامج فرعي معاملًا واحدًا أو أكثر لتوفير بعض المعلومات التي يَحتاج إليها ليُنفِّذ المُهِمّة المُوكَلة إليه. تُكْتَب تلك المُعامِلات (parameters) عند استدعاء البرنامج الفرعي (subroutine call) بَعْد اسمه داخل أقواس. في المقابل، قد لا يَستقبِل برنامج فرعي (subroutine) أي مُعامِلات، وفي تلك الحالة لابُدّ أن يُكْتَب زوج من الأقواس الفارغة بَعْد اسم البرنامج الفرعي بتَعْليمَة استدعائه. ملحوظة: جميع ملفات الشيفرة المصدرية (source code) للأمثلة التوضيحية بالكتاب متاحة بنسخة الكتاب المتوفرة عبر الانترنت كما أنها مُضمَّنة بمجلد اسمه "source" بالنسخة الأرشيفية لموقع الإنترنت. يمكنك مثلًا العثور على ملف الشيفرة المصدرية للبرنامج Interest بالملف Interest.java الموجود بمجلد فرعي اسمه "chapter2" داخل المجلد "source". ترجمة -بتصرّف- للقسم Section 2: Variables and the Primitive Types من فصل Chapter 2: Programming in the Small I: Names and Things من كتاب Introduction to Programming Using Java.
-
يُوفِّر فضاء الاسم System.Diagnostics العديد من الأصناف بهَدَف التَشخِيص، مثال أصناف للتَعامُل مع عمليات النظام (processes)، وأُخرى تَعمَل كعَدادات لقياس الأداء. أوامر الصدفة (shell commands) تنفيذ أوامر الصَّدَفَة يُستخدَم التابع Process.Start لتَّنْفيذ أمر صَّدَفَة (shell commands) تَّنْفيذًا برمجيًا. يتم ذلك من خلال إرساله للأمر المُمرَّر إليه إلى برنامج سطر الأوامر cmd.exe، كالتالي: using System.Diagnostics; string strCmdText = "/C copy /b Image1.jpg + Archive.rar Image2.jpg"; Process.Start("CMD.exe",strCmdText); تَتحَكَم الخاصية WindowStyle بحالة نافذة سطر الأوامر (cmd) عند تَّنْفيذ الأمر، فمثلًا يُمكِن إخفائها كالتالي: using System.Diagnostics; Process process = new Process(); ProcessStartInfo startInfo = new ProcessStartInfo(); startInfo.WindowStyle = ProcessWindowStyle.Hidden; startInfo.FileName = "cmd.exe"; startInfo.Arguments = "/C copy /b Image1.jpg + Archive.rar Image2.jpg"; process.StartInfo = startInfo; process.Start(); إرسال أوامر إلى سطر الأوامر واستقبال خرجها يُعيد التابع SendCommand -المُعرَّف بالمثال التالي- سِلسِلة نصية تتَضمَن مُحتوَى كلًا من مَجْرى الخَرْج القياسي (STDOUT) ومَجْرى الخَطأ القياسي (STDERR) بعد تَّنْفيذ أمر صدَفَة، بالاعتماد على الحَدَثين OutputDataReceived و ErrorDataReceived، كالتالي: private static string SendCommand(string command) { var cmdOut = string.Empty; var startInfo = new ProcessStartInfo("cmd", command) { WorkingDirectory = @"C:\Windows\System32", WindowStyle = ProcessWindowStyle.Hidden, // لإخفاء نافذة سطر الأوامر UseShellExecute = false, // لا تستخدم طرفية نظام التشغيل لبدء العملية CreateNoWindow = true, // ابدأ العملية بنافذة جديدة RedirectStandardOutput = true, // مطلوب لإتاحة مجرى الخرج القياسي RedirectStandardError = true // مطلوب لإتاحة مجرى الخطأ القياسي }; var p = new Process {StartInfo = startInfo}; p.Start(); p.OutputDataReceived += (x, y) => cmdOut += y.Data; p.ErrorDataReceived += (x, y) => cmdOut += y.Data; p.BeginOutputReadLine(); p.BeginErrorReadLine(); p.WaitForExit(); return cmdOut; } يُمكِن استدعاء التابع بالأعلى كالآتي: var servername = "SVR-01.domain.co.za"; var currentUsers = SendCommand($"/C QUERY USER /SERVER:{servername}") يَكُون الخَرْج كالتالي: string currentUsers = "USERNAME SESSIONNAME ID STATE IDLE TIME LOGON TIME Joe.Bloggs ica-cgp#0 2 Active 24692+13:29 25/07/2016 07:50 Jim.McFlannegan ica-cgp#1 3 Active . 25/07/2016 08:33 Andy.McAnderson ica-cgp#2 4 Active . 25/07/2016 08:54 John.Smith ica-cgp#4 5 Active 14 25/07/2016 08:57 Bob.Bobbington ica-cgp#5 6 Active 24692+13:29 25/07/2016 09:05 Tim.Tom ica-cgp#6 7 Active . 25/07/2016 09:08 Bob.Joges ica-cgp#7 8 Active 24692+13:29 25/07/2016 09:13" ملحوظة: سيُعيد التابع المُعرَّف بالأعلى مُحتوَى كلًا من مَجْرى الخَرْج القياسي (STDOUT) ومَجْرى الخٍَطأ القياسي (STDERR) مَضْمومين بسِلسِلة نصية واحدة؛ حيث يُلحِق كلًا من الحَدَثين OutputDataReceived و ErrorDataReceived البيانات المُستلَمة إلى نفس المُتَغيّر cmdOut. يَقتصِر أحيانًا الولوج إلى الخادم المَعنِّى على مُستخدِمين بعينهم. إذا كان لديك بيانات دخول مُستخدِم مُعين، فمن المُمكن أن تُرسِل اِستعلامات (queries) إلى ذلك الخادم كالتالي: private static string SendCommand(string command) { var cmdOut = string.Empty; var startInfo = new ProcessStartInfo("cmd", command) { WorkingDirectory = @"C:\Windows\System32", // This does not actually work in conjunction with "runas" - the console window will still appear! WindowStyle = ProcessWindowStyle.Hidden, UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, Verb = "runas", Domain = "doman1.co.za", UserName = "administrator", Password = GetPassword() }; var p = new Process {StartInfo = startInfo}; p.Start(); p.OutputDataReceived += (x, y) => cmdOut += y.Data; p.ErrorDataReceived += (x, y) => cmdOut += y.Data; p.BeginOutputReadLine(); p.BeginErrorReadLine(); p.WaitForExit(); return cmdOut; } التابع GetPassword لجلْب كلمة السر: static SecureString GetPassword() { var plainText = "password123"; var ss = new SecureString(); foreach (char c in plainText) { ss.AppendChar(c); } return ss; } ضبط الإعداد ProcessThread.ProcessorAffinity تُعبر الخاصية ProcessorAffinity من النوع IntPtr عن المُعالج (processor) الذي يُنفِّذ خيط العملية (process thread). يَعتمِد هذا الإعداد افتراضيًا على عدد مُعالِجات الحاسوب. اِستخدِم الشيفرة التالية لجَلْب خاصية ProcessorAffinity لعملية: public static int GetProcessAffinityMask(string processName = null) { Process myProcess = GetProcessByName(ref processName); int processorAffinity = (int)myProcess.ProcessorAffinity; Console.WriteLine("Process {0} Affinity Mask is : {1}", processName, FormatAffinity(processorAffinity)); return processorAffinity; } public static Process GetProcessByName(ref string processName) { Process myProcess; if (string.IsNullOrEmpty(processName)) { myProcess = Process.GetCurrentProcess(); processName = myProcess.ProcessName; } else { Process[] processList = Process.GetProcessesByName(processName); myProcess = processList[0]; } return myProcess; } private static string FormatAffinity(int affinity) { return Convert.ToString(affinity, 2) .PadLeft(Environment.ProcessorCount, '0'); } يُستخدَم كالآتي: private static void Main(string[] args) { GetProcessAffinityMask(); Console.ReadKey(); } الخَرْج: // Output: // Process Test.vshost Affinity Mask is : 11111111 اِستخدِم الشيفرة التالية لضبط خاصية ProcessorAffinity لعملية: public static void SetProcessAffinityMask(int affinity, string processName = null) { { Process myProcess = GetProcessByName(ref processName); Console.WriteLine("Process {0} Old Affinity Mask is : {1}", processName, FormatAffinity((int)myProcess.ProcessorAffinity)); myProcess.ProcessorAffinity = new IntPtr(affinity); Console.WriteLine("Process {0} New Affinity Mask is : {1}", processName, FormatAffinity((int)myProcess.ProcessorAffinity)); } يُستخدَم كالآتي: private static void Main(string[] args) { int newAffinity = Convert.ToInt32("10101010", 2); SetProcessAffinityMask(newAffinity); Console.ReadKey(); } الخَرْج: // Output : // Process Test.vshost Old Affinity Mask is : 11111111 // Process Test.vshost New Affinity Mask is : 10101010 قياس الأداء باستخدام النوع Stopwatch يُمكِن اِستخدَام النوع Stopwatch بفضاء الاسم System.Diagnostics لقياس أداء (benchmark) كُتلة من الشيفرة، كالتالي: using System; using System.Diagnostics; public class Benchmark : IDisposable { private Stopwatch sw; public Benchmark() { sw = Stopwatch.StartNew(); } public void Dispose() { sw.Stop(); Console.WriteLine(sw.Elapsed); } } public class Program { public static void Main() { using (var bench = new Benchmark()) { Console.WriteLine("Hello World"); } } } مواصفات اصطلاحية للشيفرة (code contracts) يُوفِّر فضاء الاسم System.Diagnostics.Contracts العديد من الأصناف لتَعزيز الشيفرة الخاصة بك بمزيد من الشروط (conditions) اللازم تَحقيقها إمّا خلال وقت التَصْريف أو التَّنْفيذ، مما يُحسِن من فَحْص الشيفرة واكتشاف الأخطاء. تثبيت المواصفات الاصطلاحية للشيفرة وتفعيلها يأتي فضاء الاسم System.Diagnostics.Contracts ضِمْن إطار عمل .NET. لكن ما زلت في حَاجة إلى تَثْبيت الإضافة Code Contracts Tools ببيئة التطوير المتكاملة فيجوال ستوديو (Visual Studio IDE) حتى تستطيع اِستخدَام المُواصَفَات الاصطلاحيّة للشيفرة (code contracts). يُمكنك البحث عن Code Contracts بنافذة الإضافات والتحديثات Extensions and Updates بفيجوال ستوديو. يجب أن تُفعِّل خاصية المُواصَفَات الاصطلاحيّة للشيفرة (code contracts) بحل المشروع بعد الإنتهاء من تَثْبيت الإضافة. تحتاج إلى تَّفْعِيل خاصية الفَحْص الساكن (Static Checking) -فَحْص ما بعد البناء (build)- على الأقل. قد تَرغَب أيضًا بتَّفْعِيل خاصية الفَحْص أثناء التشغيل (Runtime Checking) بالتحديد إذا كنت تُطَوِّر مكتبة (library) ستُستَعَمَل من قِبل حلول (solutions) أُخرى. الشروط المسبقة (Preconditions) يَضمَن استخدام الشروط المُسبَقة للتوابع (preconditions) الحد الأدنى من مُتطلَّبات قيم مُعامِلات الدَخْل لتلك التوابع. انظر المثال التالي: void DoWork(string input) { Contract.Requires(!string.IsNullOrEmpty(input)); //do work } نتائج تحليل الفَحْص الساكن: الشروط اللاحقة (Postconditions) يَضمَن اِستخدَام الشروط اللاحقة للتوابع (postconditions) تَوَافُق النتائج التي تُعيدها تلك التوابع مع التَعرِيف المُخصص للنتائج المتوقعة. يُساعد ذلك على تنْفيذ مُبسَط من خلال إمداد المُحلِّل الساكن (static analyizer) بالعديد من النتائج المُحتَملة. انظر المثال التالي: string GetValue() { Contract.Ensures(Contract.Result<string>() != null); return null; } نتائج تحليل الفَحْص الساكن: إضافة مواصفات اصطلاحيّة للشيفرة إلى الواجهات (interfaces) يُمكِن أيضًا فَرْض مُواصَفَات اصطلاحيّة للشيفرة (code contracts) على وَاجِهة (interface) عن طريق الإعلان عن صنف مُجرَّد (abstract class) يُنفِّذ هذه الواجهة بشرط أن تُزخرَف الواجهة والصنف المُجرَّد بالسمتين ContractClassAttribute و ContractClassForAttribute على الترتيب. انظر المثال التالي: [ContractClass(typeof(MyInterfaceContract))] public interface IMyInterface { string DoWork(string input); } [ContractClassFor(typeof(IMyInterface))] internal abstract class MyInterfaceContract : IMyInterface { private MyInterfaceContract() { } public string DoWork(string input) { Contract.Requires(!string.IsNullOrEmpty(input)); Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>())); throw new NotSupportedException(); } } public class MyInterfaceImplmentation : IMyInterface { public string DoWork(string input) { return input; } } في المثال بالأعلى، تُعلِّن الواجهة IMyInterface عن التابع DoWork الذي يَستقبِل مُعامِلًا من النوع string. في الحالة العادية، يُمكنك أن تُمرِّر القيمة الفارغة null إليه. لكن لا يُصبِح ذلك مُمكنًا بعد إضافة المُواصفة الإصطلاحيّة بالأعلى والتي تَستخدِم التابع Contract.Requires لفَرْض شرط مُسبَق (precondition) بألا يَكون المُعامِل المُمرّر فارغًا. نتائج تحليل الفَحْص الساكن: ترجمة -وبتصرف- للفصول System.Diagnostics - Code Contracts - Process and Thread Affinity setting من كتاب .NET Framework Notes for Professionals
-
تُستخدَم الشجرة التعبيرية (Expression Tree) عند الحاجة لإنشاء تعبيرات (expressions) خلال زمن التشغيل (runtime)، مما يجعلها مناسبة للأغراض التالية: مع الواجهات IEnumerable و IQueryable لفَحْص خَبَر (predicate) معين. مع Entity Framework أو LINQ to SQL لتنشئة عبارة Where لفَحْص خَبَر معين. يُمكِن إنشاء شجرة تعبيرية (Expression Tree) بطريقتين أساسيتين: الأولى: اِستخدَام واجهة برمجة التطبيقات (API) الخاصة بالنوع Expression (طريقة يدوية). الثانية: إِسْناد دالة مُجرَّدة (lambda expression) إلى مُتغيّر من النوع Expression (طريقة مُولدَّة آليًا). إنشاء النوع BinaryExpression باستخدام واجهة برمجة التطبيقات على سبيل المثال، إذا كان لديك كُلًا من الخَبَر _ => _.Field وسِلسِلة نصية تَحمِل القيمة "VALUE"، يُمكِنك إنشاء التعبير _ => _.Field == "VALUE" أثناء زمن التشغيل (runtime) لفَحْص قيمة الخَبَر. في الشيفرة التالية، عُرِّف التابع BuildEqualPredicate والذي يَستخدِم واجهة برمجة التطبيقات الخاصة بالصنف Expression، فيَستدعِي التابع Expression.Equal ليُنشِئ شجرة تعبيرية من النوع BinaryExpression تَفحَص ما إذا كانت قيمة المُتغيّر Field مُساوية للسِلسِلة النصية "VALUE": public static Expression<Func<T, bool>> BuildEqualPredicate<T>( Expression<Func<T, string>> memberAccessor, string term) { var toString = Expression.Convert(Expression.Constant(term), typeof(string)); Expression expression = Expression.Equal(memberAccessor.Body, toString); var predicate = Expression.Lambda<Func<T, bool>>( expression, memberAccessor.Parameters); return predicate; } يُمكن تَمرير الخَبَر المُنشَئ (predicate) كمُعامِل للتابع المُوسِع Where، كالتالي: var predicate = PredicateExtensions.BuildEqualPredicate<Entity>( _ => _.Field, "VALUE"); var results = context.Entity.Where(predicate).ToList(); تنشئة النوع LambdaExpression بإسناد دالة مجردة إلى متغير من النوع Expression عادةً ما تُسْنَد الدوال المُجرَّدة (lambda expressions) إلى مُتغيرات من النوع Delegate تَعمَل كمُفوِّض قابل للاستدعاء. في المُقابل، يُمكنك إِسنادها إلى مُتَغيّر من النوع Expression، وفي هذه الحالة، يُولِّد مُصرِّف C# شجرة تعبيرية (Expression Tree) مُكافئة، فمثلًا: Expression<Func<int, int>> expression = a => a + 1; يُولَّد عن المثال بالأعلى شجرة تعبيرية (Expression Tree) من النوع LambdaExpression مُكافِئة للشيفرة التالية: ParameterExpression parameterA = Expression.Parameter(typeof(int), "a"); var expression = (Expression<Func<int, int>>)Expression.Lambda( Expression.Add( parameterA, Expression.Constant(1)), parameterA); تُمثِل الشجرة التعبيرية من النوع LambdaExpression دالة مُجرَّدة تتكون من مَتْن الدالة body وقائمة المُتغيّرات. مثلًا، في المثال بالأعلى، تَستقبِل الدالة المُمثَّلة مُعامِلًا وحيدًا يُدعى a بينما يتكون المَتن من عبارة وحيدة من النوع BinaryExpression بخاصية NodeType من النوع Add. يُمثِل هذا التعبير بدوره عملية جَمع تتكون من تعبيرين فرعيين (sub-expressions) يُشار إليهما بالتعبيرين الأيمن والأيسر. التعبير الأيسر هو من النوع ParameterExpression يُمثِل المُعامِل a المُمرَّر، أما التعبير الأيمن فهو من النوع ConstantExpression بقيمة تساوي الواحد. أبسط ما يُمكنك القيام به هو طباعة قيمة التَعبير (expression)، والذي بدوره يَطبع شيفرة C# المكافئة كالتالي: Console.WriteLine(expression); //prints a => (a + 1) يُستخدَم التابع Compile لتَصرِّيف الشجرة التعبيرية (expression tree) إلى مُتغيّر من النوع Delegate، قابل للاستدعاء ببيئة التَّنفيذ المُشتركة (CLR)، كالتالي: Func<int, int> lambda = expression.Compile(); Console.WriteLine(lambda(2)); //prints 3 إنشاء النوع MemberExpression باستخدام واجهة برمجة التطبيقات عادةً ما تُترجَم التعبيرات (expressions) إلى لغات آخرى مثل SQL، لكن يُمكِن اِستخدَامها أيضًا لاستدعاء أعضاء الأصناف (members) سواء كانت هذه الأعضاء خاصة (private) أو (internal) أو (protected) وسواء ما كانت الأنواع عَلَّنية (public) أم لا، كطريقة بديلة للانعكاس (Reflection). بفرض أن لديك الصنف التالي: public TestClass { public static string StaticPublicField = "StaticPublicFieldValue"; } يمكن استرجاع قيمة الخاصية StaticPublicField الساكنة (static) كالتالي: var fieldExpr = Expression.Field(null, typeof(TestClass), "StaticPublicField"); var labmda = Expression.Lambda<Func<string>>(fieldExpr); يُمكن تَصرِيف الشجرة التعبيرية لمُفوِّض يُمكِن استدعائه للولوج لقيمة الخاصية: Func<string> retriever = lambda.Compile(); var fieldValue = retriever(); إنشاء النوع InvocationExpression باستخدام واجهة برمجة التطبيقات يُستخدَم التابع الساكن Expression.Invoke لإنشاء شجرة تعبيرية من النوع InvocationExpression. يُمَكِّنك هذا النوع من استدعاء دوال مُجرَّدة أُخرى (lambda expressions) مُضمنة بالشجرة التعبيرية ذاتها (Expression tree). المشكلة: نريد الوصول إلى العناصر التي تحتوي خاصية Description الخاصة بهم على السِلسِلة النصية "car". نحتاج إلى التأكد من أن تلك الخاصية ليست فارغة null قبل البحث فيها عن السِلسِلة النصية، لكن لا نريد أن نُفْرِط في استدعائها لأن الكلفة قد تكون عالية. using System; using System.Linq; using System.Linq.Expressions; public class Program { public static void Main() { var elements = new[] { new Element { Description = "car" }, new Element { Description = "cargo" }, new Element { Description = "wheel" }, new Element { Description = null }, new Element { Description = "Madagascar" }, }; var elementIsInterestingExpression = CreateSearchPredicate( searchTerm: "car", whereToSearch: (Element e) => e.Description); Console.WriteLine(elementIsInterestingExpression.ToString()); var elementIsInteresting = elementIsInterestingExpression.Compile(); var interestingElements = elements.Where(elementIsInteresting); foreach (var e in interestingElements) { Console.WriteLine(e.Description); } var countExpensiveComputations = 0; Action incCount = () => countExpensiveComputations++; elements .Where( CreateSearchPredicate( "car", (Element e) => ExpensivelyComputed( e, incCount ) ).Compile() ) .Count(); Console.WriteLine("Property extractor is called {0} times.", countExpensiveComputations); } private class Element { public string Description { get; set; } } private static string ExpensivelyComputed(Element source, Action count) { count(); return source.Description; } private static Expression<Func<T, bool>> CreateSearchPredicate<T> (string searchTerm, Expression<Func<T, string>> whereToSearch) { var extracted = Expression.Parameter(typeof(string), "extracted"); Expression<Func<string, bool>> coalesceNullCheckWithSearch = Expression.Lambda<Func<string, bool>>( Expression.AndAlso( Expression.Not( Expression.Call(typeof(string), "IsNullOrEmpty", null, extracted) ), Expression.Call(extracted, "Contains", null, Expression.Constant(searchTerm)) ), extracted); var elementParameter = Expression.Parameter(typeof(T), "element"); return Expression.Lambda<Func<T, bool>>( Expression.Invoke( coalesceNullCheckWithSearch, Expression.Invoke(whereToSearch, elementParameter) ), elementParameter ); } } الخَرْج: element => Invoke( extracted => (Not(IsNullOrEmpty(extracted)) AndAlso extracted.Contains("car")), Invoke(e => e.Description, element)) car cargo Madagascar Predicate is called 5 times. تم تَضمِين الولوج للخاصية Description بداخل التابع Invoke كالتالي: Invoke(e => e.Description, element) وهذا هو المكان الوحيد الذي يتم التعامل فيه مع الخاصية Description مباشرة، فقد تم استخراج متُغير آخر من النوع String وتمريره للتابع التالي. (Not(IsNullOrEmpty(extracted)) AndAlso extracted.Contains("car")) من المهم أن تكون على دراية بكيفية عمل العَامِل AndAlso. إذا آلت قيمة المُعامِل الأيسر للقيمة المنطقية false، فإن التابع AndAlso يُعيد نفس ذات القيمة false دون أن يحسِب قيمة المُعامِل الأيمن. يُعدّ استخدام العَامِل And أحد أكثر الأخطاء شيوعًا. فبالإضافة إلى حِسابه لقيمة كلا المُعامِلين دومًا بغض النظر عن قيمتهما، فإنه أيضًا قد يُبلِّغ عن اعتراض من النوع NullReferenceException إذا اُستخدِم بشكل مشابه للمثال بالأعلى. ترجمة -وبتصرف- للفصل Expression Trees من كتاب .NET Framework Notes for Professionals
-
خادم HTTP إنشاء خادم HTTP باستخدام الصنف HttpListener يُستخدَم النوع HttpListener لإنشاء مُستمِع (listener) مُبسط للرد على طلبات HTTP. نُنشِئ نسخة من هذا النوع كالتالي: listener = new HttpListener(); تُستخدَم الخاصية Prefixes لتخصيص الرابط (url) الذي يَستمِع إليه الخادم والذي ستُرسَل إليه طلبات الـ HTTP. listener.Prefixes.Add("http://*:" + port + "/"); listener.Start(); عندما يَستلِم الخادم طَلَبًا (http request) مُعينًا، فإنه بالضرورة يحتاج إلى معلومات عن الطلب حتى يقوم بمُعالجته. تَتوَفر تلك المعلومات من خلال التابع GetContext(). var context = listener.GetContext(); var request = context.Request; response = context.Response; نظرًا لأن الهدف من خادم الملفات هو إرسال الملفات عند طلبها، سيقوم الخادم أولًا بتحديد اسم الملف المطلوب: var fileName = request.RawUrl.Substring(1); ثم يُرسِل مُحتويات الملف إلى مَجْرى مَتْن الرد (response body) كالتالي: using (var fileStream = File.OpenRead(fullFilePath)) { response.ContentType = "application/octet-stream"; response.ContentLength64 = (new FileInfo(fullFilePath)).Length; response.AddHeader("Content-Disposition", "Attachment; filename=\"" + Path.GetFileName(fullFilePath) + "\""); fileStream.CopyTo(response.OutputStream); } response.OutputStream.Close(); المثال بالكامل: using System; using System.IO; using System.Net; class HttpFileServer { private static HttpListenerResponse response; private static HttpListener listener; private static string baseFilesystemPath; static void Main(string[] args) { if (!HttpListener.IsSupported) { Console.WriteLine( "*** HttpListener requires at least Windows XP SP2 or Windows Server 2003."); return; } if(args.Length < 2) { Console.WriteLine("Basic read-only HTTP file server"); Console.WriteLine(); Console.WriteLine("Usage: httpfileserver <base filesystem path> <port>"); Console.WriteLine("Request format: http://url:port/path/to/file.ext"); return; } baseFilesystemPath = Path.GetFullPath(args[0]); var port = int.Parse(args[1]); listener = new HttpListener(); listener.Prefixes.Add("http://*:" + port + "/"); listener.Start(); Console.WriteLine("--- Server stated, base path is: " + baseFilesystemPath); Console.WriteLine("--- Listening, exit with Ctrl-C"); try { ServerLoop(); } catch(Exception ex) { Console.WriteLine(ex); if(response != null) { SendErrorResponse(500, "Internal server error"); } } } static void ServerLoop() { while(true) { var context = listener.GetContext(); var request = context.Request; response = context.Response; var fileName = request.RawUrl.Substring(1); Console.WriteLine("--- Got {0} request for: {1}", request.HttpMethod, fileName); if (request.HttpMethod.ToUpper() != "GET") { SendErrorResponse(405, "Method must be GET"); continue; } var fullFilePath = Path.Combine(baseFilesystemPath, fileName); if(!File.Exists(fullFilePath)) { SendErrorResponse(404, "File not found"); continue; } Console.Write(" Sending file..."); using (var fileStream = File.OpenRead(fullFilePath)) { response.ContentType = "application/octet-stream"; response.ContentLength64 = (new FileInfo(fullFilePath)).Length; response.AddHeader("Content-Disposition", "Attachment; filename=\"" + Path.GetFileName(fullFilePath) + "\""); fileStream.CopyTo(response.OutputStream); } response.OutputStream.Close(); response = null; Console.WriteLine("Ok!"); } } static void SendErrorResponse(int statusCode, string statusResponse) { response.ContentLength64 = 0; response.StatusCode = statusCode; response.StatusDescription = statusResponse; response.OutputStream.Close(); Console.WriteLine("*** Sent error: {0} {1}", statusCode, statusResponse); } } إنشاء خادم HTTP باستخدام ASP.NET Core بالمثل، نُنشِئ خادم ببروتوكول HTTP لقراءة الملفات (file server) مُشابه للمثال بالأعلى لكن باستخدام بيئة عمل ASP.NET Core المتطورة. أولًا: اِنشِئ مجلد فارغ، سنُضيف إليه ملفات المشروع المُنشئة خلال الخطوات التالية. ثانيًا: اِنشِئ ملف باسم project.json وأَضِف إليه المحتويات التالية: { "dependencies": { "Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final", "Microsoft.AspNet.StaticFiles": "1.0.0-rc1-final" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel --server.urls http://localhost:60000" }, "frameworks": { "dnxcore50": { } }, "fileServer": { "rootDirectory": "c:\\users\\username\\Documents" } } ثالثًا: اِنشِئ ملف باسم Startup.cs وأضف إليه الشيفرة التالية: using System; using Microsoft.AspNet.Builder; using Microsoft.AspNet.FileProviders; using Microsoft.AspNet.Hosting; using Microsoft.AspNet.StaticFiles; using Microsoft.Extensions.Configuration; public class Startup { public void Configure(IApplicationBuilder app) { var builder = new ConfigurationBuilder(); builder.AddJsonFile("project.json"); var config = builder.Build(); var rootDirectory = config["fileServer:rootDirectory"]; Console.WriteLine("File server root directory: " + rootDirectory); var fileProvider = new PhysicalFileProvider(rootDirectory); var options = new StaticFileOptions(); options.ServeUnknownFileTypes = true; options.FileProvider = fileProvider; options.OnPrepareResponse = context => { context.Context.Response.ContentType = "application/octet-stream"; context.Context.Response.Headers.Add( "Content-Disposition", $"Attachment; filename=\"{context.File.Name}\""); }; app.UseStaticFiles(options); } } لاحظ اِستخدَام التابع UseStaticFiles لتزويد الخادم بخاصية قراءة الملفات الساكنة من مجلد معين. رابعًا: اِفتَح سطر الأوامر (command prompt) في المجلد الذي قُمت لتوك بإنشائه، ونفذ الأوامر التالية: dnvm use 1.0.0-rc1-final -r coreclr -p dnu restore تُنْفَّذ الأوامر بالأعلى مرة واحدة فقط. خامسًا: شَغِّل الخادم باستخدَام الأمر dnx web. كلمة web هنا هي مُجرد كلمة مُخصَّصة ضِمْن حَقْل الأوامر commands بالملف project.json وتُستخدَم اسمًا تعريفيًا لأمر مُخصَّص، وبالتالي ما يُنْفَّذ بالفعل هو قيمة هذا الحقل بالملف. `Microsoft.AspNet.Server.Kestrel --server.urls http://localhost:60000 والآن تستطيع إرسال طلبات (requests) إلى الخادم من خلال الرابط http://localhost:60000 وهو نفس الرابط (url) المُخصَّص بالأعلى. لاحظ أننا لغرض التبسيط قد افترضنا أن أسماء جميع الملفات ستكون بترميز ASCII، بالإضافة إلى أننا لم نُعالِج الأخطاء المُحتمَلة أثناء الولوج للملفات. عميل HTTP يَتوَفر الصنف HttpClient من خلال حزمة مكتبات مايكروسوفت Microsoft HTTP Client Libraries. إرسال طلب GET باستخدام HttpClient.GetAsync يُرسِل التابع GetAsync طلب GET إلى خَادِم (server) عن طريق رابط يُمرَّر إليه كمُعامِل، ويُعيد قيمة من النوع Task<HttpResponseMessage> تُمثِل رد الخادم (response). يُمكن قراءة الرد كـسِلسِلة نصية string باستخدام التابع response.Content.ReadAsStringAsync، كالمثال التالي: string requestUri = "http://www.example.com"; string responseData; using (var client = new HttpClient()) { using(var response = client.GetAsync(requestUri).Result) { response.EnsureSuccessStatusCode(); responseData = response.Content.ReadAsStringAsync().Result; } } إرسال طلب GET باستخدام HttpClient.GetStreamAsync يُرسِل التابع HttpClient.GetStreamAsync طلب GET إلى خَادِم (server) عن طريق رابط يُمرَّر إليه كمُعامِل، ولكنه يُعيد مَتْن الرد (response body) في صورة مَجْرى (stream). private static async Task DownloadAsync(string fromUrl, string toFile) { using (var fileStream = File.OpenWrite(toFile)) { using (var httpClient = new HttpClient()) { Console.WriteLine("Connecting..."); using (var networkStream = await httpClient.GetStreamAsync(fromUrl)) { Console.WriteLine("Downloading..."); await networkStream.CopyToAsync(fileStream); await fileStream.FlushAsync(); } } } } يُمكن استدعاء الدالة المُعرَّفة بالأعلى كالتالي: using System; using System.IO; using System.Linq; using System.Net.Http; using System.Threading.Tasks; class HttpGet { static void Main(string[] args) { try { Run(args).Wait(); } catch (Exception ex) { if (ex is AggregateException) ex = ((AggregateException)ex) .Flatten().InnerExceptions.First(); Console.WriteLine("--- Error: " + (ex.InnerException?.Message ?? ex.Message)); } } static async Task Run(string[] args) { if (args.Length < 2) { Console.WriteLine("Basic HTTP downloader"); Console.WriteLine(); Console.WriteLine("Usage: httpget <url>[<:port>] <file>"); return; } await DownloadAsync(fromUrl: args[0], toFile: args[1]); Console.WriteLine("Done!"); } } إرسال طلب POST باستخدام HttpClient.SendAsync يُهيَّّئ الطلب في صورة كائن من النوع HttpRequestMessage، فمثلًا تُسْنَد قيمة الرابط للخاصية RequestUri بينما تُسنَد قيمة المَتْن (request body) للخاصية Content. يُرسِل التابع SendAsync طلب POST إلى الخَادِم مع قيمة الطلب المُهيَّئ، ويُعيد قيمة من النوع Task<HttpResponseMessage> تُمثِل رد الخادم (response)، كالمثال التالي: string requestUri = "http://www.example.com"; string requestBodyString = "Request body string."; string contentType = "text/plain"; string requestMethod = "POST"; using (var client = new HttpClient()) { var request = new HttpRequestMessage { RequestUri = requestUri, Method = requestMethod, }; byte[] requestBodyBytes = Encoding.UTF8.GetBytes(requestBodyString); request.Content = new ByteArrayContent(requestBodyBytes); request.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); HttpResponseMessage result = client.SendAsync(request).Result; result.EnsureSuccessStatusCode(); } إرسال طلب GET باستخدام HttpWebRequest.GetResponse يُهيِّئ التابع WebRequest.Create طلب GET إلى خادم (server) بمُعامِل الرابط المُمرَّر إليه، ثم يتم الارسال الفعلي بواسطة التابع GetResponse وتُعاد قيمة من النوع WebResponse تُمثِل رد الخادم. في المثال التالي: string requestUri = "http://www.example.com"; string responseData; HttpWebRequest request =(HttpWebRequest)WebRequest.Create(parameters.Uri); WebResponse response = request.GetResponse(); يُمكن تَحويل رد الخَادِم إلى سِلسِلة نصية كالتالي: using (StreamReader responseReader = new StreamReader(response.GetResponseStream())) { responseData = responseReader.ReadToEnd(); } إرسال طلب POST باستخدام HttpWebRequest.GetResponse يُهيِّئ التابع WebRequest.Create طلب POST إلى خادم (server) بمُعامِل الرابط المُمرر إليه. قد تحتاج إلى تهيئة مَتْن الطلب (request body) أيضًا. للقيام بذلك، استخدم التابع GetRequestStream لاستعادة مَتْن الطلب بصورة مَجْرى (stream) يُكتَّب عليه المُحتوَى المطلوب. بعد انتهاء التهيئة، يتم الارسال الفعلي بواسطة التابع GetResponse وتُعاد قيمة من النوع WebResponse تُمثِل رد الخادم. في المثال التالي: string requestUri = "http://www.example.com"; string requestBodyString = "Request body string."; string contentType = "text/plain"; string requestMethod = "POST"; HttpWebRequest request = (HttpWebRequest)WebRequest.Create(requestUri) { Method = requestMethod, ContentType = contentType, }; byte[] bytes = Encoding.UTF8.GetBytes(requestBodyString); Stream stream = request.GetRequestStream(); stream.Write(bytes, 0, bytes.Length); stream.Close(); HttpWebResponse response = (HttpWebResponse)request.GetResponse(); رفع ملفات إلى خادم باستخدام HttpWebRequest.GetResponseAsync تُرفَّع الملفات إلى الخوادم من خلال إرسال طلب POST إلى الخادم مع إرفاق مُحتوَى الملف ضِمْن مَتْن الطلب. يُهيِّئ التابع WebRequest.CreateHttp طلب (request) إلى خادم (server) بمُعامِل الرابط المُمرَّر إليه. var request = WebRequest.CreateHttp(url); قد تحتاج إلى تهيئة إضافية للطلب مثل إضافة مَتْن إليه (request body). للقيام بذلك، اِستخدِم التابع GetRequestStream لاستعادة مَتْن الطلب بصورة مَجْرى (stream). تستطيع الكتابة على المَجْرى مباشرة أو تَضْمِينه داخل كائن من النوع StreamWriter ثم تَكتِب عليه المُحتوَى المطلوب. using (var requestStream = request.GetRequestStream()) using (var writer = new StreamWriter(requestStream)) { await writer.WriteAsync(""); } نَحتَاج لرَفع ملف إلى الخادم، مما يعني كتابة مُحتوَيات هذا الملف على مَجْرى مَتْن الطلب. يُفتَح الملف أولًا في صورة مَجْرى ثم تُنقَل محتوياته لمَجْرى الطلب، كالتالي: using (var fileStream = File.OpenRead(filename)) await fileStream.CopyToAsync(requestStream); بعد انتهاء التهيئة، يتم الارسال الفعلي للطلب بواسطة التابع GetResponseAsync وتُعاد قيمة من النوع Task<WebResponse> تُمثِل رد الخادم، كالتالي: var response = (HttpWebResponse) await request.GetResponseAsync(); تستعرض الشيفرة التالية المثال بالكامل: using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Threading.Tasks; public async Task<string> UploadFile(string url, string filename, Dictionary<string, object> postData) { var request = WebRequest.CreateHttp(url); var boundary = $"{Guid.NewGuid():N}"; request.ContentType = $"multipart/form-data; {nameof(boundary)}={boundary}"; request.Method = "POST"; using (var requestStream = request.GetRequestStream()) using (var writer = new StreamWriter(requestStream)) { foreach (var data in postData) await writer.WriteAsync( $"\r\n--{boundary}\r\nContent-Disposition: " + $"form-data; name=\"{data.Key}\"\r\n\r\n{data.Value}"); await writer.WriteAsync( // file header $"\r\n--{boundary}\r\nContent-Disposition: " + $"form-data; name=\"File\"; filename=\"{Path.GetFileName(filename)}\"\r\n" + "Content-Type: application/octet-stream\r\n\r\n"); await writer.FlushAsync(); using (var fileStream = File.OpenRead(filename)) await fileStream.CopyToAsync(requestStream); await writer.WriteAsync($"\r\n--{boundary}--\r\n"); } using (var response = (HttpWebResponse) await request.GetResponseAsync()) using (var responseStream = response.GetResponseStream()) { if (responseStream == null) return string.Empty; using (var reader = new StreamReader(responseStream)) return await reader.ReadToEndAsync(); } } إرسال طلب GET باستخدام WebClient.DownloadString يُرسِل التابع DownloadString طلب GET إلى خادم (server) عن طريق مُعامِل الرابط المُمرر إليه، ويُعيد قيمة من النوع string تُمثِل رد الخادم. string requestUri = "http://www.example.com"; string responseData; using (var client = new WebClient()) { responseData = client.DownloadString(requestUri); } إرسال طلب POST باستخدام WebClient.UploadData يُرسِل التابع UploadData طلب POST إلى خادم (server) مع مَتْن الطلب المُمرر له، كالتالي: string requestUri = "http://www.example.com"; string requestBodyString = "Request body string."; string contentType = "text/plain"; string requestMethod = "POST"; byte[] responseBody; byte[] requestBodyBytes = Encoding.UTF8.GetBytes(requestBodyString); using (var client = new WebClient()) { client.Headers[HttpRequestHeader.ContentType] = contentType; responseBody = client.UploadData(requestUri, requestMethod, requestBodyBytes); } عميل SMTP لإرسال بريد إلكتروني يُمكِنك بسهولة إنشاء كائن من النوع MailMessage بحيث يَحمِل معلومات البريد الإلكتروني المَطلوب إرساله، ثم مَرِّره إلى كائن من النوع SmtpClient حتى يقوم بالإرسال الفعلي. يَحتوِي النوع MailMessage على الخاصيات: To From ReplyToList CC Bcc Subject Body IsBodyHtml Attachments Priority والتي يُمكن ضَبْط قيمها كأيّ بريد الكتروني عادي. using(MailMessage MyMail = new MailMessage()) { MyMail.From = new MailAddress(mailfrom); MyMail.To.Add(mailto); MyMail.ReplyToList.Add(replyto); MyMail.CC.Add(mailcc); MyMail.Bcc.Add(mailbcc); MyMail.Subject = subject; MyMail.IsBodyHtml = true; MyMail.Body = body; MyMail.Priority = MailPriority.Normal; // } في المقابل، يَحتوِي النوع SmtpClient على الخاصيات Host و Port و Credentials لتخصيص بيانات خَادِم الـ SMTP المُستخدَم لإرسال البريد الإلكتروني. SmtpClient smtpMailObj = new SmtpClient(); smtpMailObj.Host = "your host"; smtpMailObj.Port = 25; smtpMailObj.Credentials = new System.Net.NetworkCredential("uid", "pwd"); الشيفرة بالكامل: public class clsMail { private static bool SendMail(string mailfrom, List<string>replytos, List<string> mailtos, List<string> mailccs, List<string> mailbccs, string body, string subject, List<string> Attachment) { try { using(MailMessage MyMail = new MailMessage()) { MyMail.From = new MailAddress(mailfrom); foreach (string mailto in mailtos) MyMail.To.Add(mailto); if (replytos != null && replytos.Any()) { foreach (string replyto in replytos) MyMail.ReplyToList.Add(replyto); } if (mailccs != null && mailccs.Any()) { foreach (string mailcc in mailccs) MyMail.CC.Add(mailcc); } if (mailbccs != null && mailbccs.Any()) { foreach (string mailbcc in mailbccs) MyMail.Bcc.Add(mailbcc); } MyMail.Subject = subject; MyMail.IsBodyHtml = true; MyMail.Body = body; MyMail.Priority = MailPriority.Normal; if (Attachment != null && Attachment.Any()) { System.Net.Mail.Attachment attachment; foreach (var item in Attachment) { attachment = new System.Net.Mail.Attachment(item); MyMail.Attachments.Add(attachment); } } SmtpClient smtpMailObj = new SmtpClient(); smtpMailObj.Host = "your host"; smtpMailObj.Port = 25; smtpMailObj.Credentials = new System.Net.NetworkCredential("uid", "pwd"); smtpMailObj.Send(MyMail); return true; } } catch { return false; } } } يُدعم النوع MailMessage إضافة المُرفَقات من خلال الخاصية Attachments كالتالي: using System.Net.Mail; using(MailMessage myMail = new MailMessage()) { Attachment attachment = new Attachment(path); myMail.Attachments.Add(attachment); } عميل UDP لمزامنة التوقيت باستخدام خادم SNTP يُمكِن لعميل إرسال طلبات لخادم SNTP لمُزامنة التوقيت مع ذلك الخادم. اطلع على RFC 2030 للمزيد من المعلومات عن بروتوكول SNTP. تجهيز طلب SNTP كالتالي: var sntpRequest = new byte[48]; sntpRequest[0] = 0x23; //LI=0 (no warning), VN=4, Mode=3 (client) إرسال الطلب من خلال الصنف UDPClient: var udpClient = new UdpClient(); udpClient.Client.ReceiveTimeout = 5000; udpClient.Send( dgram: sntpRequest, bytes: sntpRequest.Length, hostname: args[0], port: SntpPort); مُزامنة التوقيت: var date = BaseDate.AddSeconds(numberOfSeconds).AddHours(localTimeZoneInHours); الشيفرة بالكامل: using System; using System.Globalization; using System.Linq; using System.Net; using System.Net.Sockets; class SntpClient { const int SntpPort = 123; static DateTime BaseDate = new DateTime(1900, 1, 1); static void Main(string[] args) { if(args.Length == 0) { Console.WriteLine("Simple SNTP client"); Console.WriteLine(); Console.WriteLine("Usage: sntpclient <sntp server url> [<local timezone>]"); Console.WriteLine(); Console.WriteLine("<local timezone>: a number between -12 and 12 as hours from UTC"); Console.WriteLine("(append .5 for an extra half an hour)"); return; } double localTimeZoneInHours = 0; if(args.Length > 1) localTimeZoneInHours = double.Parse(args[1], CultureInfo.InvariantCulture); var udpClient = new UdpClient(); udpClient.Client.ReceiveTimeout = 5000; var sntpRequest = new byte[48]; sntpRequest[0] = 0x23; //LI=0 (no warning), VN=4, Mode=3 (client) udpClient.Send( dgram: sntpRequest, bytes: sntpRequest.Length, hostname: args[0], port: SntpPort); byte[] sntpResponse; try { IPEndPoint remoteEndpoint = null; sntpResponse = udpClient.Receive(ref remoteEndpoint); } catch(SocketException) { Console.WriteLine("*** No response received from the server"); return; } uint numberOfSeconds; if(BitConverter.IsLittleEndian) numberOfSeconds = BitConverter.ToUInt32( sntpResponse.Skip(40).Take(4).Reverse().ToArray(), 0); else numberOfSeconds = BitConverter.ToUInt32(sntpResponse, 40); var date = BaseDate.AddSeconds(numberOfSeconds).AddHours(localTimeZoneInHours); Console.WriteLine( $"Current date in server: {date:yyyy-MM-dd HH:mm:ss} UTC{localTimeZoneInHours:+0.#;-0.#;.}"); } } خادم وعميل TCP لتنشئة برنامج دردشة باستخدام الأنواع TcpListener و TcpClient و NetworkStream. using System; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; class TcpChat { static void Main(string[] args) { if(args.Length == 0) { Console.WriteLine("Basic TCP chat"); Console.WriteLine(); Console.WriteLine("Usage:"); Console.WriteLine("tcpchat server <port>"); Console.WriteLine("tcpchat client <url> <port>"); return; } try { Run(args); } catch(IOException) { Console.WriteLine("--- Connection lost"); } catch(SocketException ex) { Console.WriteLine("--- Can't connect: " + ex.Message); } } static void Run(string[] args) { TcpClient client; NetworkStream stream; byte[] buffer = new byte[256]; var encoding = Encoding.ASCII; if(args[0].StartsWith("s", StringComparison.InvariantCultureIgnoreCase)) { var port = int.Parse(args[1]); var listener = new TcpListener(IPAddress.Any, port); listener.Start(); Console.WriteLine("--- Waiting for a connection..."); client = listener.AcceptTcpClient(); } else { var hostName = args[1]; var port = int.Parse(args[2]); client = new TcpClient(); client.Connect(hostName, port); } stream = client.GetStream(); Console.WriteLine("--- Connected. Start typing! (exit with Ctrl-C)"); while(true) { if(Console.KeyAvailable) { var lineToSend = Console.ReadLine(); var bytesToSend = encoding.GetBytes(lineToSend + "\r\n"); stream.Write(bytesToSend, 0, bytesToSend.Length); stream.Flush(); } if (stream.DataAvailable) { var receivedBytesCount = stream.Read(buffer, 0, buffer.Length); var receivedString = encoding.GetString(buffer, 0, receivedBytesCount); Console.Write(receivedString); } } } } ترجمة -وبتصرف- للفصول: HTTP servers HTTP clients Upload file and POST data to webserver System.Net.Mail Networking من كتاب .NET Framework Notes for Professionals
-
تُعدّ .NET Core بيئة تطوير مُتَعدِّدة الأغراض، مُصانة بواسطة مايكروسوفت Microsoft ومجتمع الـ .NET على GitHub. تتميز بكْونها مُتَعدِّدة المنصات (cross-platform) حيث تُدعِّم أنظمة التشغيل: ويندوز Windows وماك macOS ولينكس Linux. ويُمكِن اِستخدَامها بسياقات مُتَعدِّدة: على الأجهزة أو على السحاب (cloud) أو على الأجهزة المُدْمَجة (embedded) أو بإنترنت الأشياء (IoT). عندما تُفكِر ببيئة التطوير .NET Core، فينبغي لمُصطلحات مثل سهولة النشر (flexible deployment) وتَعدُّد المنصات (cross-platform) وأدوات سطر الأوامر والمصدر المفتوح (open source) أن تَكون أول ما يطرأ بذهنك. على الرغم من كَوْنها مفتوحة المصدر، تُدعمها مايكروسوفت بشكل فعال. الاختيار ما بين .NET Framework و .NET Core تُعرِّف .NET Standard مُواصَفَات واجهة تطوير التطبيقات (.NET APIs) والتي تُمثِل مجموعة من المواصفات الاصطلاحية (contracts) تُصرَّف شيفرتك وفقًا لها. يَتوفَّر أكثر من مُنفِّذ لهذه المواصفات منها بيئتي العمل .NET Framework و .NET Core. يعني ذلك أنهما يشتركان في العديد من المُكوِنات ولكن توجد أيضًا بعض الاختلافات الجوهرية والتي يُساعدك الإطلاع عليها على الاختيار بينهما. اختيار بيئة عمل .NET Core يُفضَّل استخدام بيئة عمل .NET Core في الحالات التالية: الحاجة لتشغيل تطبيقك على منصات متعددة مثل ويندوز ولينكس وماك. تُدعِّم بيئة عمل .NET Core أيضًا إمكانية التطوير على الأنظمة المذكورة سلفًا. استهداف الخدمات المُصغرة (microservices). على الرغم من إمكانية استخدام بيئة عمل .NET Framework داخل حاويات ويندوز (Windows containers)، تُعدّ .NET Core أكثر مثالية للحاويات (containers) لما تتميز به من صغر الحجم وخفة الوزن. بالإضافة إلى كونها متعددة المنصات مما يُمكِنك من نشر تطبيقك على حاويات Docker بنظام لينكس على سبيل المثال. تُعدّ .NET Core خيارك الأفضل إذا كنت في حاجة إلى بناء أنظمة عالية المستوى قابلة للتوسع (scalable)، . اختيار بيئة عمل .NET Framework على الرغم من أن بيئة عمل .NET Core تُوفِّر الكثير من المميزات خاصة للتطبيقات الجديدة، مع ذلك تَظل بيئة عمل .NET Framework الخيار الطبيعي في كثير من الحالات. مثلًا: إذا كان تطبيقك يعمل بالفعل على بيئة عمل .NET Framework، فأنت عادة لست في حاجة إلى تحويله. على الرغم من توجه المكتبات السريع لتبَنّى مواصفات .NET Standard والذي سيؤدي إلى توافق المكتبة مع جميع المُنفِّذين (implementations)، فقد يَستخدِم تطبيقك مكتبات .NET من طرف ثالث (third-party) أو حزم NuGet غير مُتاحة ببيئة عمل .NET Core. يُمكنك في هذه الحالات استخدام بيئة عمل .NET Framework. اِستخدَام تطبيقك لبعض تقنيات .NET غير المُتوفِّرة ببيئة عمل .NET Core. اِستخدَام تطبيقك لمنصة (platform) غير مُدعَّمة ببيئة عمل .NET Core. التحويل من .NET Framework إلى .NET Core عملية تحويل الشيفرة المُطورة أساسًا لاستهداف بيئة عمل .NET Framework بحيث تُصبح متوافقة مع بيئة عمل .NET Core هي عملية بسيطة نسبيًا بغالبية المشروعات خاصة إذا كان نموذج المشروع (app model) متوفرًا بكلتا البيئتين كالمكتبات (libraries) وتطبيقات الطرفية (Console Applications)، أما المشروعات التي تتطلب إنشاء نموذج مشروع (app model) جديد كالانتقال من ASP.NET إلى ASP.NET Core فإن العملية قد تحتاج إلى مزيد من العمل ولكنها بالنهاية تَتَبع نمطًا ثابتًا. تحويل حل (solution) إذا كنت تعمل على حل (solution) به أكثر من مشروع، فقد تبدو العملية معقدة. لذلك فمن الأفضل أن تُحوِّل المشروعات تحويلًا تصاعديًا من أسفل لأعلى، بمعنى أن تبدأ أولًا بالمشروعات التي لا تَعتمِد على مشروعات اخرى، ثم تنتقل إلى المشروعات التي تَعتمِد على المشروعات المُحوَّلة وهكذا إلى أن تُحوِّل الحل (solution) بالكامل. يُمكِنك اِستخدَام احدى الطرائق التالية لتتعرف على الترتيب الذي ينبغي أن تُحوِّل به المشروعات: تُنشِئ أداة Dependency Diagrams بفيجوال ستوديو رسم بياني مُوجَّه (directed graph) لشيفرة الحل. يُولِد الأمر التالي ملف بصيغة تبادل البيانات (JSON) يتضَمَّن قائمة بمَراجِع (references) المشروعات: msbuild _SolutionPath_ /t:GenerateRestoreGraphFile /p:RestoreGraphOutputPath=graph.dg.json تُعيد أداة .NET Portability Analyzer مُخطط للتبعيات (dependency diagram) عند تفعيل الخاصية -r DGML . خطوات تحويل مشروع (project) يُنصَح باتباع الخطوات التالية عند تحويل مشروع بحيث يُصبِح متوافقًا مع .NET Core: غَيِّر طريقة تَضْمِين التبعيات من صيغة بيئة عمل .NET Framework والموجودة بملف packages.config إلى صيغة PackageReference المُستخدَمة ببيئة عمل .NET Core. لاحظ أن بيئة عمل .NET Core تُضمِن فقط التبعيات المُستخدمة فعليًا بالمشروع بغرض تقليل عدد التبعيات المُضافة. قد تَستخدِم أداة تحويل متوفرة بفيجوال ستوديو للقيام بذلك. حَوِل صيغة ملف المشروع (project file) إلى الصيغة المُستخدَمة ببيئة عمل .NET Core، والتي تُعدّ أبسط من تلك المُستخدَمة ببيئة عمل .NET Framework. تَسمَح الصيغة الجديدة بتخصيص بيئة العمل المُستهدَفة أثناء بناء المشروع مما يعني أنه من الممكن أن تَستمر باستهداف بيئة عمل .NET Framework حتى تنتهي من عملية التحويل. اِستهدف بيئة عمل .NET Framework إصدار 4.7.2 أو أحدث بجميع المشروعات. حَدِث جميع تَبعِيات المشروع إلى آخِر إصدار، فربما لا تُدعِّم الإصدارات القديمة من تلك التبعيات -والتي قد يكون مشروعك مُعتمِدًا عليها- مواصفات واجهة تطوير التطبيقات .NET Standard بينما قد تُدعِّمها الإصدارات الأحدث. قد تَضطرك بعض تلك التحديثات إلى إجراء تعديلات بالشيفرة. اِستخدِم أداة .NET Portability Analyzer لتحليل شيفرات التجميع (assemblies)، ومعرفة ما إذا كانت قابلة للنقل (portable) إلى بيئة عمل .NET Core، حيث تُولِد هذه الأداة تقرير يحتوي على ملخص لجميع واجهات تطوير التطبيقات (APIs) غير المُتوفرة بـ NET Core والمُستخدَمة بالمشروع. يَتوفَّر لغالبية تلك الواجهات نظير ببيئة عمل .NET Core ستحتاج إلى استخدامه كبديل. تُمكنك أداة .NET API analyzer من تحديد ما إذا كنت تَستخدِم أي من واجهات تطوير التطبيقات (APIs) بطريقة تَتسبب بحدوث اعتراض من النوع PlatformNotSupportedException أثناء زمن التشغيل. أخيرًا غَيّر بيئة العمل المستهدفة بملف المشروع من: <TargetFramework>net472</TargetFramework> إلى .NET Core أو .NET Standard: <TargetFramework>netcoreapp3.1</TargetFramework> يَعتمِد الاختيار ما بين .NET Core و .NET Standard على الغرض من المشروع. إذا كان المشروع هو مُجرد مكتبة (library) ستُستخدَم بواسطة تطبيقات اخرى، يُفضَّل عندها استهداف مواصفات واجهة تطوير التطبيقات .NET Standard. في المقابل، ستضطر إلى استهداف بيئة عمل .NET Core إذا كان المشروع يَعتمِد على بعض الواجهات المتوفرة فقط بتلك البيئة، وفي تلك الحالة يُمكِنك أيضًا توفير بناء ثانوي يَستهِدف .NET Standard لكن بخاصيات أقل. تطبيق طرفية تَستعرِض الشيفرة التالية تطبيق طرفية بسيط (Console App) باستخدام بيئة العمل .NET Core: public class Program { public static void Main(string[] args) { Console.WriteLine("\nWhat is your name? "); var name = Console.ReadLine(); var date = DateTime.Now; Console.WriteLine("\nHello, {0}, on {1:d} at {1:t}", name, date); Console.Write("\nPress any key to exit..."); Console.ReadKey(true); } } ترجمة -وبتصرف- للفصل 24 من كتاب .NET Framework Notes for Professionals
-
التشفير (Encryption) تشفير البيانات وفكها باستخدام النوع Aes تَستعرِض الشيفرة التالية مثال طرفية توضيحي من شبكة مطوري مايكروسوفت (MSDN). يَشرَح هذا المثال طريقة تشفير سِلسِلة نصية ثُم فَكّ التشفير باِستخدَام الخوارزمية القياسية "معيار التشفير المُتقدِم Advanced Encryption Standard"، وتُسمَى اختصارًا AES. يُوفِّر إطار عمل .NET النوع Aes، والذي يُنْفِّذ خوارزمية معيار التشفير المُتقدِم AES. تَتكون كُلًا من شيفرتي التشفير وفَكُه مِن عدة خطوات مُشتَركة. نحتاج عامةً لإِنشاء مَجْرى (stream) بيانات ستمُرّ عَبره البيانات المَطلوب تَشفيِرها أو فَكّ تَشفيِرها. using (MemoryStream msEncrypt = new MemoryStream()) كذلك سنحتاج إلى تنشئة عملية تَحوِيل مَجْرى (stream transform) -إِمّا مُشفِر أو مُفكِّك للتشفير- من النوع ICryptoTransform بحيث تُطبَّق على البيانات أثناء مرُّورَها بالمَجْرى. يُستخدَم التابعين CreateEncryptor وCreateDecryptor لإنشاء المُشفِر ومُفكِّك التشفير على الترتيب. ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); أخيرًا، نُنشِئ مَجْرى تَشفير من النوع CryptoStream يُوصِل عملية تَحوِيل المَجْرى (stream transform) بمَجْرى البيانات مع تحديد وَضْع التَوصِيل إِمّا للقراءة أو للكتابة. using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) مثلًا، في حالة التشفير، سنَكتُب البيانات المطلوب تشفيرها على مَجْرى التشفير CryptoStream. يتم تَّنْفيذ عملية تَحوِيل المَجْرى (stream transform) -مُشفِر في هذه الحالة- على البيانات، وتُكتَب النتيجة المُشفَّرة على المَجْرى المُمرَّر لمَجْرى التشفير بوَضْع الكتابة. using (MemoryStream msEncrypt = new MemoryStream()) using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) { using (StreamWriter swEncrypt = new StreamWriter(csEncrypt)) swEncrypt.Write(plainText); encrypted = msEncrypt.ToArray(); } في حالة فَكّ التشفير، سيَقرأ مَجْرى التشفير CryptoStream البيانات المطلوب فَكّ تشفيرها من المَجْرى المُمرَّر له بوَضْع القراءة. يتم تَّنْفيذ عملية تَحوِيل المَجْرى (stream transform) -مُفكِّك شَفرة في هذه الحالة- على البيانات. أخيرًا نقرأ البيانات بعد فَكّ التشفير من خلال مَجْرى التشفير. using (MemoryStream msDecrypt = new MemoryStream(cipherText)) using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) using (StreamReader srDecrypt = new StreamReader(csDecrypt)) plaintext = srDecrypt.ReadToEnd(); الشيفرة بالكامل: using System; using System.IO; using System.Security.Cryptography; namespace Aes_Example { class AesExample { public static void Main() { try { string original = "Here is some data to encrypt!"; // أنشئ كائن من النوع Aes المُستخدَم لتوليد كلا من المفتاح ومتجه التهيئة using (Aes myAes = Aes.Create()) { // قم بتشفير سلسلة نصية إلى مصفوفة بايتات byte[] encrypted = EncryptStringToBytes_Aes(original, myAes.Key, myAes.IV); // قم بفك تشفير مصفوفة بايتات إلى سلسلة نصية string roundtrip = DecryptStringFromBytes_Aes(encrypted, myAes.Key, myAes.IV); Console.WriteLine("Original: {0}", original); Console.WriteLine("Round Trip: {0}", roundtrip); } } catch (Exception e) { Console.WriteLine("Error: {0}", e.Message); } } } } static byte[] EncryptStringToBytes_Aes(string plainText, byte[] Key, byte[] IV) { // Check arguments. if (plainText == null || plainText.Length <= 0) throw new ArgumentNullException("plainText"); if (Key == null || Key.Length <= 0) throw new ArgumentNullException("Key"); if (IV == null || IV.Length <= 0) throw new ArgumentNullException("IV"); byte[] encrypted; // أنشئ كائن من النوع Aes باستخدام المفتاح ومتجه التهيئة المحددين using (Aes aesAlg = Aes.Create()) { aesAlg.Key = Key; aesAlg.IV = IV; // أنشئ مُشفر والذي سيستخدم كمحول للمجرى ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); // انشئ المجاري المستخدمة خلال عملية التشفير using (MemoryStream msEncrypt = new MemoryStream()) using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) { using (StreamWriter swEncrypt = new StreamWriter(csEncrypt)) swEncrypt.Write(plainText); encrypted = msEncrypt.ToArray(); } } // أعد مصفوفة البايتات المشفرة المُنشأة من مجرى الذاكرة return encrypted; } static string DecryptStringFromBytes_Aes(byte[] cipherText, byte[] Key, byte[] IV) { // تحقق من الوسائط if (cipherText == null || cipherText.Length <= 0) throw new ArgumentNullException("cipherText"); if (Key == null || Key.Length <= 0) throw new ArgumentNullException("Key"); if (IV == null || IV.Length <= 0) throw new ArgumentNullException("IV"); string plaintext = null; // أنشئ كائن من النوع Aes باستخدام المفتاح ومتجه التهيئة المحددين using (Aes aesAlg = Aes.Create()) { aesAlg.Key = Key; aesAlg.IV = IV; // أنشئ مفكك الشفرة الذي سيستخدم كمحول للمجرى ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); // انشئ المجاري المستخدمة خلال عملية فك التشفير using (MemoryStream msDecrypt = new MemoryStream(cipherText)) using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) using (StreamReader srDecrypt = new StreamReader(csDecrypt)) // قم بقراءة البايتات من مجرى فك التشفير وأسْندها إلى متغير من النوع string plaintext = srDecrypt.ReadToEnd(); } return plaintext; } تُحدِّد الخوارزمية AES طريقة لتشفير البيانات الالكترونية. أُسِست عام 2001 بواسطة المعهد الوطني للمعايير والتقنية NIST بالولايات المتحدة الامريكية، ومازالت تُعدّ الخوارزمية القياسية للتشفير التَماثلي (symmetric encryption). ملاحظات: تَتوفَّر عِدة أوضاع تَشفير cipher mode ضِمْن خوارزمية AES. تَستطيع تَحديد وَضْع التشفير بإِسْناد إِحدى قيم التعداد CipherMode إلى الخاصية Mode. لا تَستخدِم أبدًا وَضْع التشفير Electronic codebook - ECB - مما يَعنِي عدم إِختيار CipherMode.ECB-؛ لأنه يُنتِج عملية تَحوِيل مَجْرى ضعيفة. يجب أن تَستخدِم مُولِّد تَشفير عشوائي -أو اِستخدِم الشيفرة بالأسفل (إنشاء مفتاح من كلمة سرّ / سلسلة نصية إضافية (Salt) عشوائية)- لتنشئة مفتاح (Key) جيد غيْر ضعيف. يُفضَّل أيضًا أن يَكُون حجم المفتاح 256 بت. تستطيع تخصيص حَجم المفتاح مِن خلال الخاصية KeySize كما تُوفِّر الخاصية LegalKeySizes قائمة بالأحجام المُدعَّمة. يُمكِنك اِستخدَام سِلسِلة نصية إضافية (salt) -كالمثال بالأسفل (إنشاء مفتاح من كلمة سرّ / سلسلة نصية إضافية (Salt) عشوائية)- لتهيئة مُتّجَه التهيئة (initialization vector - IV). مثال آخر باستخدام خوارزمية AES شيفرة التشفير: public static string Encrypt(string cipherText) { if (cipherText == null) return null; byte[] clearBytes = Encoding.Unicode.GetBytes(cipherText); using (Aes encryptor = Aes.Create()) { Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(CryptKey, new byte[] { 0x49, 0x76, 0x61, 0x6e, 0x20, 0x4d, 0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76 }); encryptor.Key = pdb.GetBytes(32); encryptor.IV = pdb.GetBytes(16); using (MemoryStream ms = new MemoryStream()) { using (CryptoStream cs = new CryptoStream(ms, encryptor.CreateEncryptor(), CryptoStreamMode.Write)) { cs.Write(clearBytes, 0, clearBytes.Length); cs.Close(); } cipherText = Convert.ToBase64String(ms.ToArray()); } } return cipherText; } شيفرة فَكّ التشفير: public static string Decrypt(string cipherText) { if (cipherText == null) return null; byte[] cipherBytes = Convert.FromBase64String(cipherText); using (Aes encryptor = Aes.Create()) { Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(CryptKey, new byte[] { 0x49, 0x76, 0x61, 0x6e, 0x20, 0x4d, 0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76 }); encryptor.Key = pdb.GetBytes(32); encryptor.IV = pdb.GetBytes(16); using (MemoryStream ms = new MemoryStream()) { using (CryptoStream cs = new CryptoStream(ms, encryptor.CreateDecryptor(), CryptoStreamMode.Write)) { cs.Write(cipherBytes, 0, cipherBytes.Length); cs.Close(); } cipherText = Encoding.Unicode.GetString(ms.ToArray()); } } return cipherText; } تُستخدَم كالتالي: var textToEncrypt = "TestEncrypt"; var encrypted = Encrypt(textToEncrypt); var decrypted = Decrypt(encrypted); تشفير البيانات وفكها باستخدام النوع RijndaelManaged يَتطلَّب فضاء الاسم System.Security.Cryptography private class Encryption { private const string SecretKey = "topSecretKeyusedforEncryptions"; private const string SecretIv = "secretVectorHere"; public string Encrypt(string data) { return string.IsNullOrEmpty(data) ? data : Convert.ToBase64String( this.EncryptStringToBytesAes(data, this.GetCryptographyKey(), this.GetCryptographyIv()) ); } public string Decrypt(string data) { return string.IsNullOrEmpty(data) ? data : this.DecryptStringFromBytesAes(Convert.FromBase64String(data), this.GetCryptographyKey(), this.GetCryptographyIv()); } private byte[] GetCryptographyKey() { return Encoding.ASCII.GetBytes(SecretKey.Replace('e', '!')); } private byte[] GetCryptographyIv() { return Encoding.ASCII.GetBytes(SecretIv.Replace('r', '!')); } } private byte[] EncryptStringToBytesAes(string plainText, byte[] key, byte[] iv) { MemoryStream encrypt; RijndaelManaged aesAlg = null; try { aesAlg = new RijndaelManaged { Key = key, IV = iv }; var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); encrypt = new MemoryStream(); using (var csEncrypt = new CryptoStream(encrypt, encryptor, CryptoStreamMode.Write)) using (var swEncrypt = new StreamWriter(csEncrypt)) swEncrypt.Write(plainText); } finally { aesAlg?.Clear(); } return encrypt.ToArray(); } private string DecryptStringFromBytesAes(byte[] cipherText, byte[] key, byte[] iv) { RijndaelManaged aesAlg = null; string plaintext; try { aesAlg = new RijndaelManaged { Key = key, IV = iv }; var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); using (var msDecrypt = new MemoryStream(cipherText)) using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) using (var srDecrypt = new StreamReader(csDecrypt)) plaintext = srDecrypt.ReadToEnd(); } finally { aesAlg?.Clear(); } return plaintext; } يُستخدَم كالتالي: var textToEncrypt = "hello World"; //-> zBmW+FUxOvdbpOGm9Ss/vQ== var encrypted = new Encryption().Encrypt(textToEncrypt); //-> hello World var decrypted = new Encryption().Decrypt(encrypted); تنبيه: يُنفِّذ النوع Rijndael النسخة الأقدم من خوارزمية AES، ولذلك ينبغي استعمال النوع Aes الذي يُنفِّذ النسخة الحديثة. إنشاء مفتاح من كلمة سر / سلسلة نصية إضافية (Salt) عشوائية تَستعرِض الشيفرة التالية مثال طرفية توضيحي من شبكة مطوري مايكروسوفت (MSDN). يَشرح هذا المثال كيفية تنشئة مفتاح (key) آمِن اعتمادًا على كلمة سرّ مُحدَّدة من قِبَل المُستخدِم، بالإضافة إلى طريقة تنشئة سِلسِلة نصية إضافية (salt أو يدعى غفل باللغة العربية، انظر كتاب «علم التعمية واستخراج المعمى عند العرب») عشوائية باِستخدَام مُولِّد تشفير عشوائي. using System; using System.Security.Cryptography; using System.Text; public class PasswordDerivedBytesExample { public static void Main(String[] args) { // اجلب كلمة السر من المستخدم Console.WriteLine("Enter a password to produce a key:"); byte[] pwd = Encoding.Unicode.GetBytes(Console.ReadLine()); byte[] salt = CreateRandomSalt(7); // TripleDESCryptoServiceProvider أنشئ كائنًا من النوع TripleDESCryptoServiceProvider tdes = new TripleDESCryptoServiceProvider(); try { Console.WriteLine("Creating a key with PasswordDeriveBytes..."); // أنشئ كائنًا من النوع PasswordDeriveBytes ثم أنشئ مفتاح لخوارزمية TripleDES // من كلمة سر وسلسلة نصية إضافية PasswordDeriveBytes pdb = new PasswordDeriveBytes(pwd, salt); // أنشئ المفتاح وأسنده إلى الخاصية Key الموجودة بكائن موفر خدمة التشفير tdes.Key = pdb.CryptDeriveKey("TripleDES", "SHA1", 192, tdes.IV); Console.WriteLine("Operation complete."); } catch (Exception e) { Console.WriteLine(e.Message); } finally { ClearBytes(pwd); ClearBytes(salt); tdes.Clear(); } Console.ReadLine(); } } /// توليد غفل (سلسلة نصية إضافية) بالطول المحدد public static byte[] CreateRandomSalt(int length) { byte[] randBytes; if (length >= 1) randBytes = new byte[length]; else randBytes = new byte[1]; // RNGCryptoServiceProvider إنشاء كائن من النوع RNGCryptoServiceProvider rand = new RNGCryptoServiceProvider(); // املأ المخزن بالبايتات العشوائية rand.GetBytes(randBytes); return randBytes; } /// امسح البايتات من المخزن لكي لا تُقرَأ مستقبلًا من الذاكرة public static void ClearBytes(byte[] buffer) { if (buffer == null) throw new ArgumentNullException("buffer"); // اضبط قيمة كل بايت إلى القيمة 0 for (int x = 0; x < buffer.Length; x++) { buffer[x] = 0; } } ملاحظات: تَستقبِل الدالة PasswordDeriveBytes المَبنية مُسبَقًا (built-in) كلمة سرّ، وتَستخدِم خوارزمية PBKDF1 القياسية لتولِّيد المفتاح. تَقوم هذه الدالة بشكل افتراضي بمائة تكرار أثناء تولِّيد المفتاح؛ وذلك لتُبطئ من فاعلية هَجمَات القوى الغاشمة (brute force attacks). بالإضافة إلى ذلك، يُعزز اِستخدَام السِلسِلة النصية الإضافية (salt) المُولَّدة عشوائيًا من قوة المفتاح. تَستخدِم الدالة CryptDeriveKey خوارزمية التَقطيع (hashing) المُمرَّرة إليها -تم اختيار SHA1 بالمثال- لتَحوِيل المفتاح المُولَّد من الدالة PasswordDeriveBytes إلى مفتاح مُتوافِق مع خوارزمية التشفير المُمرَّرة إليها -تم اختيار TripleDES بالمثال-، يمكنك أيضًا تخصيص كلًا من حجم المفتاح والقيمة المبدئية لمُتٍّجَه التهيئة (initialization vector - IV) بتمريرهما كمُعامِلين للدالة. في المثال بالأعلى، مُرِّرت القيمة 192 بايت كما اُستخدِم مُوفِّر خدمة التشفير من النوع TripleDESCryptoServiceProvider لتهيئة مُتّجَه التهيئة. عندما تحتاج إلى مفتاح قوي مُولَّد عشوائيًا لاِستخدَامُه لتشفير كمية ضخمة من البيانات، اِستخدِم هذه الطريقة لتولِّيده مِن مجرد كلمة سرّ. يُمكِن أيضًا اِستخدَام هذه الطريقة لتوليد كلمات سر لعِدّة مُستخدِمين للولوج إلى نفس البيانات. للأسف، لا تُدعِّم الدالة CryptDeriveKey خوارزمية AES حاليًا (تَحقَّق هنا). يُمكِن التحايل على ذلك بالاعتماد على حَاوِي خوارزمية TripleDES، مع أنه سيُؤدِي إلى تَبَعيّة لهذه الخوارزمية وسيُقصِرك على مُستَوَى حمايتها، فمثلًا لن تستطيع تَوليد مفاتيح بأحجام أكبر من تلك المُدعَّمة بخوارزمية TripleDES على الرغم من دَعََّم تلك الأحجام بخوارزمية AES. دوال التعمية (Hashing) توليد تدقيق المجموع (checksum) لملف باستخدام خوارزمية SHA1 تَتطلَّب فضاء الاسم System.Security.Cryptography public string GetSha1Hash(string filePath) { using (FileStream fs = File.OpenRead(filePath)) { SHA1 sha = new SHA1Managed(); return BitConverter.ToString(sha.ComputeHash(fs)); } } توليد القيمة المعماة (hash أو المقطعة) لسِلسِلة نصية public static string TextToHash(string text) { var sh = SHA1.Create(); var hash = new StringBuilder(); byte[] bytes = Encoding.UTF8.GetBytes(text); byte[] b = sh.ComputeHash(bytes); foreach (byte a in b) { var h = a.ToString("x2"); hash.Append(h); } return hash.ToString(); } ملحوظة: الشيفرة بالكامل موجودة بـمستودع mahdiabasi/SHA1Tool في GitHub. ترجمة -وبتصرف- للفصلين Encryption / Cryptography و Work with SHA1 in C# من كتاب .NET Framework Notes for Professionals
-
السلسلة إلى JSON استخدام Json.NET Newtonsoft.Json هي حزمة قوية، وسريعة، وسهلة الاستخدام مما جعلها الأداة الأكثر شيوعًا عند التعامل مع "السَلاسِل النصية بصيغة تبادل البيانات (JSON)" بإطار عمل .NET. يُعدّ اِستخدامها سهلًا نوعًا ما؛ لكونها تعتمد على التوابع الساكنة (static) سواء للسَلسَلة (serialize)، أو لإلغاء السَلسَلة (de-serialize). بالتحديد يُسَلسِل التابع JsonConvert.SerializeObject الكائن المُمرَّر له ويُعيد سِلسِلة نصية، بينما يُلغي التابع JsonConvert.DeserializeObject<T> سَلسَلة المُعامِل المُمرَّر له ويُحاول تحليله إلى نوع يُحدَّد من خلال مُعامِل النوع (type parameter) كالمثالين التاليين: using Newtonsoft.Json; var rawJSON = "{\"Name\":\"Fibonacci Sequence\",\"Numbers\":[0, 1, 1, 2, 3, 5, 8, 13]}"; var fibo = JsonConvert.DeserializeObject<Dictionary<string, object>>(rawJSON); var rawJSON2 = JsonConvert.SerializeObject(fibo); internal class Sequence{ public string Name; public List<int> Numbers; } string rawJSON = "{\"Name\":\"Fibonacci Sequence\",\"Numbers\":[0, 1, 1, 2, 3, 5, 8, 13]}"; Sequence sequence = JsonConvert.DeserializeObject<Sequence>(rawJSON); اطلع على مزيد من المعلومات عن JSON.NET من خلال موقعها الرسمي. يُدعِّم اطار عمل .NET النوع JSON.NET منذ إصدار 2. استخدام السمات (attributes) مع Json.NET تتوفَّر بعض السمات (attributes) لزخرفة (decoration) كلًا من الأصناف وخاصياتها؛ مما يُمكنك من التحكم بشكل "السِلسِلة النصية بصيغة JSON" النهائية الناتجة عن السَلسَلة. مثلًا، تُخصِص السمة JsonProperty اسمًا للخاصية المُزخَرفة بدلًا من اسمها الأصلي، في حين تُهمَل الخاصيات المزخرفة بالسمة JsonIgnore. في المثال التالي: لا يحتوي ناتج السَلسَلة {"name":"Andrius","age":99} على الخاصية Address؛ لأنها مُزخرفة بالسمة JsonIgnore، كما اُستُخدِمت الأسماء name و age بدلًا من الأسماء الأصلية للخاصيات لأنها زُخرِفت بالسمة JsonProperty. [JsonObject("person")] public class Person { [JsonProperty("name")] public string PersonName { get; set; } [JsonProperty("age")] public int PersonAge { get; set; } [JsonIgnore] public string Address { get; set; } } Person person = new Person { PersonName = "Andrius", PersonAge = 99, Address = "Some address" }; string rawJson = JsonConvert.SerializeObject(person); Console.WriteLine(rawJson); // {"name":"Andrius","age":99} اطلع على مزيد من المعلومات عن سمات السَلسَلة التي يُمكنك استخدامها. استخدام المُعامِل JsonSerializerSettings مع Json.NET تتوفَّر بصمة أُخْرَى من التابع JsonConvert.SerializeObject والتي تَستقبِل -بالإضافة إلى الكائن المراد سَلسَلته- مُعامِلًا آخَر من النوع JsonSerializerSettings للتحكم بعملية السَلسَلة. يحتوي النوع JsonSerializerSettings على العديد من الخاصيات المُصممة خصيصًا لحل بعض أكثر المشاكل شيوعًا. مثلًا، الخاصية ContractResolver من نوع الواجهة IContractResolver، والتي يتوفَّر لها أكثر من مُنفِّذ (implementation). إحداها هو النوع DefaultContractResolver المُستخدَم افتراضيًا، بالإضافة إلى النوع CamelCasePropertyNamesContractResolver الذي قد يُفيدك عند التحويل من كائنات c# إلى صيغة JSON والعكس؛ نظرًا لشيوع استخدَام النمط PascalCase لدى مبرمجي c# بينما في الغالب ما تكون صيغة تبادل البيانات JSON بنمط سنام الجمل CamelCase. using Newtonsoft.Json; using Newtonsoft.Json.Serialization; public class Person { public string Name { get; set; } public int Age { get; set; } [JsonIgnore] public string Address { get; set; } } public void ToJson() { Person person = new Person { Name = "Andrius", Age = 99, Address = "Some address" }; var resolver = new CamelCasePropertyNamesContractResolver(); var settings = new JsonSerializerSettings { ContractResolver = resolver }; string json = JsonConvert.SerializeObject(person, settings); Console.WriteLine(json); // {"name":"Andrius","age":99} } مثال آخر هو الخاصية NullValueHandling المُستخدَمة لضَبط طريقة التعامُل مع القيم الفارغة null، كالمثال التالي: public static string Serialize(T obj) { string result = JsonConvert.SerializeObject(obj, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore}); return result; } مثال أخير هو الخاصية ReferenceLoopHandling التي تُستخدَم لضَبط طريقة التعامل مع تكرار الإشارة الذاتية (self referencing loop). مثلًا، إذا أردت تَمثيل برنامج دراسي مُلحق به عدد من الطلبة، سيكون لديك النوع Student يحوي خاصية من النوع Course والذي بدوره يحمل خاصية Students من النوع List<Student>. public static string Serialize(T obj) { string result = JsonConvert.SerializeObject( obj, new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore}); return result; } يُمكنك تخصيص أكثر من خاصية سَلسَلة كالتالي: public static string Serialize(T obj) { string result = JsonConvert.SerializeObject( obj, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ReferenceLoopHandling = ReferenceLoopHandling.Ignore }); return result; } الربط الديناميكي (Dynamic Binding) تُوفِّر Json.NET خاصية الربط الديناميكي، مما يَسمَح بإلغاء سَلسَلة "سِلسِلة نصية بصيغة JSON" وتحويلها إلى نوع ديناميكي لم يُعلَّن عن خاصياته صراحةً. يتم ذلك إِما باستخدام النوع DynamicObject أو النوع ExpandoObject. السَلسَلة: dynamic jsonObject = new ExpandoObject(); jsonObject.Title = "Merchent of Venice"; jsonObject.Author = "William Shakespeare"; Console.WriteLine(JsonConvert.SerializeObject(jsonObject)); في المثال التالي، حُُوِّلَت المفاتيح (keys) الموجودة "بالسِلسِلة النصية بصيغة JSON" إلى مُتَغيّرات أعضاء (member variables) بالنوع الديناميكي: var rawJson = "{\"Name\":\"Fibonacci Sequence\",\"Numbers\":[0, 1, 1, 2, 3, 5, 8, 13]}"; dynamic parsedJson = JObject.Parse(rawJson); Console.WriteLine("Name: " + parsedJson.Name); Console.WriteLine("Name: " + parsedJson.Numbers.Length); قد يكون الربط الديناميكي مفيدًا خاصةً إذا كان البرنامج يَستقبِل ويُنتج العديد من "السَلاسِل النصية بصيغة JSON"، ولكن من الأفضل التحقق (validate) من تلك السَلاسِل النصية الناتجة عن السَلسَلة وكذلك من الكائنات الديناميكية الناتجة عن إلغائها. استخدام JavascriptSerializer يتوفَّر أيضًا النوع JavascriptSerializer -الموجود بفضاء الاسم System.Web.Script.Serialization- والذي يُعرِّف التابع Deserialize<T>(input). يُلِّغي هذا التابع سَلسَلة "سِلسِلة نصية بصيغة تبادل البيانات (JSON)"، ويُحَوِّلها لكائن نوعه مُحدَّد بمُعامِل النوع (type parameter). using System.Collections; using System.Web.Script.Serialization; string rawJSON = "{\"Name\":\"Fibonacci Sequence\",\"Numbers\":[0, 1, 1, 2, 3, 5, 8, 13]}"; JavaScriptSerializer JSS = new JavaScriptSerializer(); Dictionary<string, object> parsedObj = JSS.Deserialize<Dictionary<string, object>>(rawJSON); string name = parsedObj["Name"].toString(); ArrayList numbers = (ArrayList)parsedObj["Numbers"] يُدعِّم اطار عمل .NET النوع JavaScriptSerializer منذ إصدار 3.5. السلسلة إلى XML استخدام XmlSerializer يُستخدَم النوع XmlSerializer -الموجود بفضاء الاسم System.Xml.Serialization - لسَلسَلة كائن إلى ملف (document) نصي بصيغة لغة الترميز القابلة للامتداد Extensible Markup Language - XML. public void SerializeFoo(string fileName, Foo foo) { var serializer = new XmlSerializer(typeof(Foo)); using (var stream = File.Open(fileName, FileMode.Create)) { serializer.Serialize(stream, foo); } } يُمكن إلغاء السَلسَلة كالتالي: public Foo DeserializeFoo(string fileName) { var serializer = new XmlSerializer(typeof(Foo)); using (var stream = File.OpenRead(fileName)) { return (Foo)serializer.Deserialize(stream); } } استخدام السمات مع XmlSerializer تتوفَّر بعض السمات (attributes) لزخرفة (decoration) خاصيات الأنواع؛ مما يُمكنك من التحكم بالصورة النهائية لملف XML الناتج عن السَلسَلة. مثلًا، تُخصِص السمة XmlArray اسمًا للخاصيات من النوع Array -أو أي نوع قد يُعيد Array- بدلًا من اسمها الأصلي. في المثال التالي، اُستخدِم الاسم Articles بدلًا من الاسم Products نظرًا لأن الخاصية المُناظِرة زُخرِفت بالسمة XmlArray. public class Store { [XmlArray("Articles")] public List<Product> Products {get; set; } } <Store> <Articles> <Product/> <Product/> </Articles> </Store> في المثال التالي، تُخصِص السمة XmlElement اسمًا للخاصية المُزخَرفة بدلًا من اسمها الأصلي. public class Foo { [XmlElement(ElementName="Dog")] public Animal Cat { get; set; } } <Foo> <Dog/> </Foo> في المثال التالي، اُستخدِم جَالْب الخاصية لتحديد صيغة مُخصَّصة للقيمة العائدة بدلًا من الافتراضية. public class Dog { private const string _birthStringFormat = "yyyy-MM-dd"; [XmlIgnore] public DateTime Birth {get; set;} [XmlElement(ElementName="Birth")] public string BirthString { get { return Birth.ToString(_birthStringFormat); } set { Birth = DateTime.ParseExact(value, _birthStringFormat, CultureInfo.InvariantCulture); } } } أخيرًا، تُهمَل الخاصيات المُزخَرفة بالسمة XmlIgnore. سلسلة أصناف فرعية بشكل ديناميكي المشكلة: أحيانًا قد لا نستطيع استخدَام السِمات (attributes) لإمداد اطار عمل XmlSerializer بكل البيانات الوصفية (metadata) التي يحتاجها لإجراء السَلسَلة. مثلًا، بفرض أن لدينا صنف أساسي (base class) مطلوب سَلسَلة كائناته. على الرغم من تَوفّر بعض السمات التي قد تُستخدَم للإشارة إلى أنواعه الفرعية، أحيانًا، قد لا تكون جميع الأصناف الفرعية مَعلومَة أثناء تصميم الصنف الأساسي؛ فمن المُحتمل وجود فريق آخر يَعمَل على تطوير بَعضًا من تلك الأنواع الفرعية. كما أنها حتى وإن تَوفَّرت فمِن المُفترض ألا يعلم الصنف الأساسي شيئًا عن أصنافه الفرعية. حَل مُقْترَح: نحتاج لإمداد المُسَلسِلات (serializers) بالأنواع المَعلومَة بطريقة أُخْرى، مثلًا، باستخدَام بَصمَة أُخْرى من باني الكائنات XmlSerializer(type, knownTypes). تَستقبِل هذه البَصمة مصفوفة تحتوي على الأنواع المَعلومَة كمُعامِل ثان. قد تَفي هذه الطريقة بالغرض ولكنها بتعقيد زمني O(N^2) على الأقل -إذا كان لدينا عدد N من المُسَلسِلات-، فقط لاكتشاف جميع الأنواع المُمرَّرة للمُعامِل. var allSerializers = allTypes.Select(t => new XmlSerializer(t, allTypes)); var serializerDictionary = Enumerable.Range(0, allTypes.Length) .ToDictionary(i => allTypes[i], i => allSerializers[i]); لاحظ أنه في المثال بالأعلى، لا علم للصنف الأساسي بالأصناف المشتقة منه، وهو ما يُعدّ أمرًا عاديًا -بل ومطلوبًا- بالبرمجة كائنية التوجه OOP. حل أكثر كفاءة: هناك لحسن الحظ طريقة تَعني بحل هذه المشكلة حلًا أكثر كفاءة عن طريق تَوفير مصفوفة بالأصناف المَعلومَة لعِدة مُسَلسِلات (serializers). تَستخدِم هذه الطريقة التابع FromTypes(Type[])، والذي يَسمَح بإنشاء مصفوفة من المُسَلسِلات من النوع XmlSerializer لمُعالجة مصفوفة من الأنواع (Type objects) بكفاءة. var allSerializers = XmlSerializer.FromTypes(allTypes); var serializerDictionary = Enumerable.Range(0, allTypes.Length) .ToDictionary(i => allTypes[i], i => allSerializers[i]); بفرض وجود الأصناف التالية: public class Container { public Base Base { get; set; } } public class Base { public int JustSomePropInBase { get; set; } } public class Derived : Base { public int JustSomePropInDerived { get; set; } } نُنشئ كائن من النوع Container: var sampleObject = new Container { Base = new Derived() }; نحاول أولًا سَلسَلته دون إمداد المُسَلسِل (serializer) بمعلومات عن النوع Derived: var allTypes = new[] { typeof(Container), typeof(Base), typeof(Derived) }; SetupSerializers(allTypes.Except(new[] { typeof(Derived) }).ToArray()); Serialize(sampleObject); سيُنتَج عن ذلك رسالة خطأ. نحاول مُجددًا سَلسَلته مع إمداد المُسَلسِل بجميع الأنواع: var allTypes = new[] { typeof(Container), typeof(Base), typeof(Derived) }; SetupSerializers(allTypes); Serialize(sampleObject); ستتم العملية بنجاح. الشيفرة بالكامل: using System; using System.Collections.Generic; using System.Xml.Serialization; using System.Linq; using System.Linq; public class Program { public class Container { public Base Base { get; set; } } public class Base { public int JustSomePropInBase { get; set; } } public class Derived : Base { public int JustSomePropInDerived { get; set; } } public void Main() { var sampleObject = new Container { Base = new Derived() }; var allTypes = new[] { typeof(Container), typeof(Base), typeof(Derived) }; Console.WriteLine("Trying to serialize without a derived class metadata:"); SetupSerializers(allTypes.Except(new[] { typeof(Derived) }).ToArray()); try { Serialize(sampleObject); } catch (InvalidOperationException e) { Console.WriteLine(); Console.WriteLine("This error was anticipated,"); Console.WriteLine("we have not supplied a derived class."); Console.WriteLine(e); } Console.WriteLine("Now trying to serialize with all of the type information:"); SetupSerializers(allTypes); Serialize(sampleObject); Console.WriteLine(); Console.WriteLine("Slides down well this time!"); } static void Serialize<T>(T o) { serializerDictionary[typeof(T)].Serialize(Console.Out, o); } private static Dictionary<Type, XmlSerializer> serializerDictionary; static void SetupSerializers(Type[] allTypes) { var allSerializers = XmlSerializer.FromTypes(allTypes); serializerDictionary = Enumerable.Range(0, allTypes.Length) .ToDictionary(i => allTypes[i], i => allSerializers[i]); } } الخْرج: Trying to serialize without a derived class metadata: <?xml version="1.0" encoding="utf-16"?> <Container xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" This error was anticipated, we have not supplied a derived class. System.InvalidOperationException: There was an error generating the XML document. ---> System.InvalidOperationException: The type Program+Derived was not expected. Use the XmlInclude or SoapInclude attribute to specify types that are not known statically. at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriter1.Write2_Base(String n, String ns, Base o, Boolean isNullable, Boolean needType) at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriter1.Write3_Container(String n, String ns, Container o, Boolean isNullable, Boolean needType) at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriter1.Write4_Container(Object o) at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces, String encodingStyle, String id) --- End of inner exception stack trace --- at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces, String encodingStyle, String id) at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces, String encodingStyle) at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces) at Program.Serialize[T](T o) at Program.Main() Now trying to serialize with all of the type information: <?xml version="1.0" encoding="utf-16"?> <Container xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Base xsi:type="Derived"> <JustSomePropInBase>0</JustSomePropInBase> <JustSomePropInDerived>0</JustSomePropInDerived> </Base> </Container> Slides down well this time! تقترح رسالة الخطأ الآتي: "اِستخدِم السمة XmlInclude أو السمة SoapInclude لتخصيص الأنواع غير المَعلومَة بشكل ثابت (statically)" في الواقع، لا تستطيع دائمًا القيام بذلك -كما أشرنا من قبل-، وحتى إن كان ذلك مُمكنا، لا يُنصح بالقيام بذلك؛ لأن ليس من المُفترَض أن يعلم الصنف الأساسي شيئًا عن الأصناف المُشتقة منه فضلًا عن الإشارة إليها. هذه هي الطريقة التي يَظهر بها النوع المُشتق بملف XML: <Base xsi:type="Derived"> في المثال بالأعلى، تُشير كلمة Base إلى النوع المُصرَّح عنه للخاصية داخل الصنف Container، بينما كلمة Derived فهي تُشير إلى نوع النسخة المُسْنَدة للخاصية. (إليك مثال حي) ترجمة -وبتصرف- للفصل Serialization من كتاب .NET Framework Notes for Professionals
-
التجميعات (Assemblies) تُصرَّف (compile) الأصناف التي تُعرِّفها، مَصحُوبة بتوابعها وخواصها وملفات البايتكود (bytecode) الخاصة بها، وتُحزَّم بداخل تجميعة (Assembly) تكون في صورة ملف يتضمن شيفرة مُصرَّفة جزئيًا بامتداد .dll او .exe. هذه التجميعات (Assemblies) هي المُكوِّن الأساسي لأي برنامج يتم تشغيله من خلال بيئة التنفيذ المشتركة (CLR). تُعدّ التجميعات ذاتية التَوْثيق، فهي لا تَحتوِي على اﻷنواع وتوابعها وملفات اللغة الوسيطة (IL code) الخاصة بها فقط، بل أيضًا تَضُمّ البيانات الوَصفيّة (metadata) الضرورية للفَحْص والاستهلاك خلال زمني التَصرِّيف (compile time) والتشغيل (runtime). تَملُك كل تجميعة (Assembly) اسم يَصِف هويتها المتفردة توصيفًا كاملًا: // Will print: "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" تُعدّ أسماء التجميعات التي تحتوي على PublicKeyToken أسماء صارمة (strong). لِمَنح تجميعة اسمًا صارمًا (strong-naming)، لابّد أن يكون لديك زوجًا من المفاتيح، أحدهما عام (public key) والآخر سري (private key). يُوزَّع المفتاح العام مع التجميعة أما المفتاح السري فيُستخدَم لانشاء بصمة (signature) تُضاف لبيان التجميعة (Assembly manifest)، والذي يحتوي على أسماء جميع ملفات التجميعة وقيمها المقطّعة (hashes)، كما تُصبِح قيمة PublicKeyToken جزءًا من اسمها. التجميعات التي تَملُك نفس الاسم الصارم هي بالضرورة مُتطابقة، ومِنْ ثَمَّ يُمكِن الاعتماد على ذلك لتَجنُب تضارب أسماء التجميعات (assembly conflicts) وكذلك للإصدارة (versioning). تنشئة تجميعة (assembly) ديناميكيًا يُوفِّر إطار عمل .NET عددًا من الأصناف والتوابع بفضاء الاسم System.Reflection.Emit، والتي يُمكِن اِستخدَامِها لإنشاء تجميعة (assembly) بشكل ديناميكي. عامةً، تَضمّ أي تجميعة (assembly) وَحدة (module) واحدة أو أكثر، كلًا منها قد يتَضمَن صنف واحد أو أكثر. مثلًا، يَتوفَّر التابع ModuleBuilder.DefineType الذي يُمكِن اِستخدَامه لإضافة نوع جديد، ويُعيد قيمة من النوع TypeBuilder. يُوفِّر هذا النوع بدوره العديد من التوابع لإضافة أعضاء (members) بالنوع المُنشَئ. فمثلًا، يُستخدَم التابع TypeBuilder.DefineField لإضافة حَقْل، بينما يُستخدَم التابع TypeBuilder.DefineProperty لإضافة خاصية. يَتوفَّر أيضًا التابعين TypeBuilder.DefineMethod و TypeBuilder.DefineConstructor لإضافة التوابع وبواني الكائن على الترتيب. في المثال التالي، نَستعرِض طريقة تَنشئة تجميعة تَضُمّ وَحدة (module) وَاحدة تَشتمِل على تَعرِيف لنوع واحد يَحمِل الاسم MyDynamicType. يتكون هذا النوع من: حَقْل وحيد يُسمَى m_number من النوع العددي int. خاصية مُناظِرة لهذا الحقل تَحمِل الاسم Number لها ضَابِط (setter) وجَالِب (getter). بانيين للكائن (constructor) أحدهما بدون مُعامِلات والآخر يَستقبِل مُعامِل وحيد لتهيئة قيمة الحَقْل المبدئية. التابع MyMethod والذي يَستقبِل مُعامِل من النوع العددي int، ويُعيد حاصل ضرب قيمة المُعامِل في قيمة الحَقْل m_number. وبالتالي، يَكُون النوع المُراد إنشائه كالتالي: public class MyDynamicType { private int m_number; public MyDynamicType() : this(42) {} public MyDynamicType(int initNumber) { m_number = initNumber; } public int Number { get { return m_number; } set { m_number = value; } } public int MyMethod(int multiplier) { return m_number * multiplier; } } سنحتاج أولًا لاستدعاء التابعين DefineDynamicAssembly و DefineDynamicModule لتنشئة كائنين من النوع AssemblyBuilder و ModuleBuilder على الترتيب، يُمثل هذين التابعين كُلًا من التجميعة (assembly) والوَحدة (module) المُراد إنشائها، كالتالي: AssemblyName aName = new AssemblyName("DynamicAssemblyExample"); AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly( aName, AssemblyBuilderAccess.RunAndSave ); // عادةً ما يكون اسم الوحدة هو نفسه اسم ملف التجميع عند تنشئة ملف تجميع من وحدة واحدة ModuleBuilder mb = ab.DefineDynamicModule(aName.Name, aName.Name + ".dll"); للإعلان عن النوع MyDynamicType داخل الوَحدة المُنشَأة، نَستخدِم الشيفرة التالية: TypeBuilder tb = mb.DefineType("MyDynamicType", TypeAttributes.Public); لإضافة الحَقْل m_number بالنوع الجديد، نَستخدِم الشيفرة التالية: FieldBuilder fbNumber = tb.DefineField( "m_number", typeof(int), FieldAttributes.Private); لإضافة الخاصية Number المُناظِرة للحَقْل، نَستخدِم الشيفرة التالية: PropertyBuilder pbNumber = tb.DefineProperty( "Number", // اسم الخاصية PropertyAttributes.None, typeof(int), // نوع الخاصية new Type[0]); لإضافة ضَابِط (setter) للخاصية المُنشَئة للتو، نَستخدِم الشيفرة التالية: MethodBuilder mbSetNumber = tb.DefineMethod( "set_Number", // لعدم السماح باستدعاء الضابط لأنه تابع من نوع خاص MethodAttributes.PrivateScope | MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.SpecialName, typeof(void), // لا يُعيد الضابط قيمة // يستقبل الضابط قيمة من النوع العددي new[] { typeof(int) }); // سنستخدم مولد الشيفرة الوسيطة IL generator للحصول على متن التابع il = mbSetNumber.GetILGenerator(); // لابد من تحميل this لأنه المُعامِل الأول لجميع التوابع il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); // حمل المعامل الثاني الذي يمثل القيمة المراد إسنادها للحقل il.Emit(OpCodes.Stfld, fbNumber); // خزن القيمة الجديدة المحملة للتو بالحقل il.Emit(OpCodes.Ret); // عُد // وأخيرًا، اربط التابع بضابط الخاصية pbNumber.SetSetMethod(mbSetNumber); عادةً ما يَكُون اسم الضَابِط هو set_Property. بالمثل، لإضافة جَالِب (getter) لنفس الخاصية، نَستخدِم الشيفرة التالية: MethodBuilder mbGetNumber = tb.DefineMethod( "get_Number", MethodAttributes.PrivateScope | MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.SpecialName, typeof(int), new Type[0]); il = mbGetNumber.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, fbNumber); // حمل قيمة الحقل il.Emit(OpCodes.Ret); // أعد القيمة المحملة // وأخيرًا، اربط التابع بجالب الخاصية pbNumber.SetGetMethod(mbGetNumber); لإضافة بواني الكائن بالنوع الجديد، نَضيِف الشيفرة التالية: ConstructorBuilder intConstructor = tb.DefineConstructor( MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { typeof(int) }); il = intConstructor.GetILGenerator(); // حمل this il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, typeof(object).GetConstructor(new Type[0])); // اِستدعي باني الأب // حمل this il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); // حمل قيمة المعامل الثاني الذي يمثل القيمة الممررة لباني الكائن il.Emit(OpCodes.Stfld, fbNumber); // خزن القيمة المحملة بالحقل il.Emit(OpCodes.Ret); var parameterlessConstructor = tb.DefineConstructor( MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new Type[0]); il = parameterlessConstructor.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldc_I4_S, (byte)42); // حمل القيمة 42 // استدعي this(42) il.Emit(OpCodes.Call, intConstructor); il.Emit(OpCodes.Ret); لاحِظ أنه لابُدّ للبواني من استدعاء باني الصنف الأساسي أو بَانِي آخر بنفس الصنف. لإضافة التابع MyMethod، نَستخدِم الشيفرة التالية: MethodBuilder mbMyMethod = tb.DefineMethod( "MyMethod", MethodAttributes.Public, typeof(int), new[] { typeof(int) }); ILGenerator il = mbMyMethod.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, fbNumber); // حمل قيمة الحقل il.Emit(OpCodes.Ldarg_1); // حمل قيمة المعامل الممرر il.Emit(OpCodes.Mul); // احسب حاصل ضرب قيمة الحقل بقيمة المعامل il.Emit(OpCodes.Ret); // عُد وأخيرًا نقوم بالتَنشئِة الفِعلّية للنوع الجديد عن طريق التابع CreateType: Type ourType = tb.CreateType(); الشيفرة بالكامل: using System; using System.Reflection; using System.Reflection.Emit; class DemoAssemblyBuilder { public static void Main() { AssemblyName aName = new AssemblyName("DynamicAssemblyExample"); AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly( aName, AssemblyBuilderAccess.RunAndSave ); ModuleBuilder mb = ab.DefineDynamicModule(aName.Name, aName.Name + ".dll"); TypeBuilder tb = mb.DefineType( "MyDynamicType", TypeAttributes.Public); FieldBuilder fbNumber = tb.DefineField( "m_number", typeof(int), FieldAttributes.Private); MethodBuilder mbMyMethod = tb.DefineMethod( "MyMethod", MethodAttributes.Public, typeof(int), new[] { typeof(int) }); ILGenerator il = mbMyMethod.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, fbNumber); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Mul); il.Emit(OpCodes.Ret); PropertyBuilder pbNumber = tb.DefineProperty( "Number", PropertyAttributes.None, typeof(int), new Type[0]); MethodBuilder mbSetNumber = tb.DefineMethod( "set_Number", MethodAttributes.PrivateScope | MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.SpecialName, typeof(void), new[] { typeof(int) }); il = mbSetNumber.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Stfld, fbNumber); il.Emit(OpCodes.Ret); pbNumber.SetSetMethod(mbSetNumber); MethodBuilder mbGetNumber = tb.DefineMethod( "get_Number", MethodAttributes.PrivateScope | MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.SpecialName, typeof(int), new Type[0]); il = mbGetNumber.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, fbNumber); il.Emit(OpCodes.Ret); pbNumber.SetGetMethod(mbGetNumber); ConstructorBuilder intConstructor = tb.DefineConstructor( MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { typeof(int) }); il = intConstructor.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, typeof(object).GetConstructor(new Type[0])); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Stfld, fbNumber); il.Emit(OpCodes.Ret); var parameterlessConstructor = tb.DefineConstructor( MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new Type[0]); il = parameterlessConstructor.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldc_I4_S, (byte)42); il.Emit(OpCodes.Call, intConstructor); il.Emit(OpCodes.Ret); Type ourType = tb.CreateType(); object ourInstance = Activator.CreateInstance(ourType); Console.WriteLine(ourType.GetProperty("Number") .GetValue(ourInstance)); ab.Save(@"DynamicAssemblyExample.dll"); var myDynamicType = tb.CreateType(); var myDynamicTypeInstance = Activator.CreateInstance(myDynamicType); Console.WriteLine(myDynamicTypeInstance.GetType()); var numberField = myDynamicType.GetField("m_number", BindingFlags.NonPublic | BindingFlags.Instance); numberField.SetValue (myDynamicTypeInstance, 10); Console.WriteLine(numberField.GetValue(myDynamicTypeInstance)); } } الانعكاس (Reflection) يُوفِّر الانعكاس العديد من الأصناف -منها الصنف Assembly- والذي يَعمَل كمُغلِّف للتجميعة (assemblies). يُمكِن لبعض هذه الأصناف تَوفِير معلومات عن التجميعات المُحمَّلة من خلال البيانات الوَصفيّة (metadata) ببيان تلك التجميعات (Assembly manifest). يُمكِن استخدام بعض الأصناف الأُخرى لتَحمِيل ديناميكي للتجميعات، بل ولإنشاء أنواع جديدة واستدعائها ديناميكيا أثناء وقت التشغيل. جَلْب بيانات عن تجميعة باستخدام الانعكاس اِستخدِم الشيفرة التالية لجلب كائن Assembly الخاص بصنف معين: using System.Reflection; Assembly assembly = this.GetType().Assembly; اِستخدِم الشيفرة التالية لجَلْب كائن Assembly الخاص بالشيفرة قيد التنفيذ: Assembly assembly = Assembly.GetExecutingAssembly(); يُوفِّر الصنف Assembly التابع GetTypes المُستخدَم لجَلْب قائمة بجميع الأصناف المُعرَّفة ضِمْن التجميعة: foreach (var type in assembly.GetTypes()) { Console.WriteLine(type.FullName); } موازنة كائنين باستخدام الانعكاس في المثال التالي، يُستخَدم الانعكاس لموازنة كائنين. بالتحديد، يُستخدم التابع GetType لجلْب قيمة من الصنف Type تُحدد نوع الكائن، والتي بدورها تُستخدَم لجلْب قائمة بحقول الكائن باستدعاء التابع GetFields من خلالها، ثم يتم موازنة قيم تلك الحقول مع نظيراتها بالكائن الآخر. public class Equatable { public string field1; public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; var type = obj.GetType(); if (GetType() != type) return false; var fields = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); foreach (var field in fields) if (field.GetValue(this) != field.GetValue(obj)) return false; return true; } public override int GetHashCode() { var accumulator = 0; var fields = GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); foreach (var field in fields) accumulator = unchecked ((accumulator * 937) ^ field.GetValue(this).GetHashCode()); return accumulator; } } لاحظ أنه بغرض التبسيط، فإن المثال باﻷعلى يُجري موازنة معتمدة على الحقول فقط (يتجاهل الحقول الساكنة [static fields]، والخاصيات [properties]). جَلْب سِمة تعداد (enum) باستخدام الانعكاس (وتخزينها بصورة مؤقتة caching) تُعدّ السمات (attributes) مفيدة للإشارة إلى بعض البيانات الوَصفيّة (metadata) بالتعدادات (enums). يُستخدَم عادة التابع GetCustomAttributes لجَلْب قيم تلك السمات والذي قد يكون بطيئًا، لذلك من المهم الاستعانة بالذاكرة المخبئية لتخزين تلك القيم (caching)، كالتالي: private static Dictionary<object, object> attributeCache = new Dictionary<object, object>(); public static T GetAttribute<T, V>(this V value) where T : Attribute where V : struct { object temp; // حاول جلب قيمة السمة من الذاكرة المخبئية أولًا if (attributeCache.TryGetValue(value, out temp)) { return (T) temp; } else { // اجلب النوع Type type = value.GetType(); FieldInfo fieldInfo = type.GetField(value.ToString()); // اجلب سمات هذا النوع T[] attribs = (T[])fieldInfo.GetCustomAttributes(typeof(T), false); // أعد أول سمة تجدها var result = attribs.Length > 0 ? attribs[0] : null; // خزن النتيجة بالذاكرة المخبئية attributeCache.Add(value, result); return result; } } ضَبْط خواص الكائنات باستخدام الانعكاس بفرض أن لدينا الصنف التالي Classy الذي يَملك الخاصية Propertua: public class Classy { public string Propertua {get; set;} } لضبْط الخاصية Propertua الموجودة بكائن من النوع Classy باستخدام الانعكاس، يمكن استخدام التابع SetValue: var typeOfClassy = typeof (Classy); var classy = new Classy(); var prop = typeOfClassy.GetProperty("Propertua"); prop.SetValue(classy, "Value"); تنشئة كائن من النوع T باستخدام الانعكاس باستخدام باني الكائنات الافتراضي (default constructor): T variable = Activator.CreateInstance(typeof(T)); باستخدام بَانِي ذات معاملات غير محدَّدة النوع (parameterized constructor): T variable = Activator.CreateInstance(typeof(T), arg1, arg2); الإطار المُدار القابل للتوسيع (MEF) الإطار المُدار القَابِل للتوسيع Managed Extensibility Framework - MEF هو مكتبة لإنشاء برامج صغيرة الحجم وقابلة للتوسيع. عادةً ما تُسجَّل التَبَعيّات (dependencies) داخِل ملفات إعداد بالشيفرة المصدرية (hardcoding). يَترتَب على ذلك أنه لا يُصبِح بالإمكان تَغيير تلك التَبَعيّات إلا عن طريق تعديل الشيفرة وإعادة تَجمِيعها. على العكس من ذلك، يَسمَح الإطار المُدار القابل للتوسيع MEF باكتشاف التَبَعيّات أثناء زمن التشغيل (runtime) ضِمْنِيًّا، واستخدامها دون إعداد مُسبَق. يَسمَح MEF لعِدة مُكَوِّنات (components) بالتواصل معًا بانسيابية وسهولة. يَستخدِم كل مُكَوِّن سِمات مُعينة (attributes) للإعلان عن تَبَعيّاته وقُدراته، أي ما يَحتاج إلى اِستيراده (imports) وما يقوم بتَصديره (exports)-إن وُجِدَ- على الترتيب. يُعلَّن عن كُلًا من الاستيرادات والتصديرات بصورة مُواصَفَة اِصطلاحيّة (contract). يَنبغي لمُصَدِّر ومُستورِد مُعينين الإعلان عن نفس المُواصَفَة الاصطلاحيّة لعَدِّهما نَظيرين. لاحظ أنه لمّا كانت كل هذه المَعلومات مُتوفِّرة بالبيانات الوصفية (metadata) للمُكَوِّن، أَصبَح مِن المُمكن اكتشافها أثناء زمن التشغيل (runtime). يُزوِّد مُحرِك MEF المُكَوِّنات باستيراداتها (imports) المُعلَّن عنها اعتمادًا على حَاوِي التركيب (composition container) الذي يَضُمّ كتالوجات (catalogs) تَشتمِل على معلومات عن جميع المُكَوِّنات المُصدَّرة والمُتاحة للتركيب. تصدير صنف (Exporting) يُمكن لأي مُكَوِّن استخدام السمة ExportAttribute للاعلان عن تَصدير (export). في المثال التالي، صُدِّرَ النوع UserProvider كمُحقِّق للمُواصَفَة الاصطلاحيّة IUserProvider: using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel.Composition; namespace Demo { [Export(typeof(IUserProvider))] public sealed class UserProvider : IUserProvider { public ReadOnlyCollection<User> GetAllUsers() { return new List<User> { new User(0, "admin"), new User(1, "Dennis"), new User(2, "Samantha"), }.AsReadOnly(); } } } في المثال بالأعلى، يُمكن تَعرِيف الصنف UserProvider بأيّ مكان؛ فالمهم هو تَزْوِيد الكتالوج (ComposablePartCatalogs) -الذي يُنشئه البرنامج- بطريقة يَستطيع مِن خلالها اكتشاف هذا الصنف. استيراد صنف (Importing) يُمكن لأي مُكَوِّن استخدَام السمة ImportAttribute للاعلان عن استيراد (import) أو تَبَعيّة. انظر المثال التالي: using System; using System.ComponentModel.Composition; namespace Demo { public sealed class UserWriter { [Import(typeof(IUserProvider))] private IUserProvider userProvider; public void PrintAllUsers() { foreach (User user in this.userProvider.GetAllUsers()) { Console.WriteLine(user); } } } } في المثال بالأعلى، يُعلِّن الصنف UserWriter عن استيراد لصنف يُحقِّق المُواصَفَة الاصطلاحيّة IUserProvider كقيمة للحَقْل userProvider. لاحِظ أنه ليس مُهمًا أين تقوم بتَعرِيف الصنف المُناظِر؛ فالمهم هو تَزْوِيد الكتالوج (ComposablePartCatalogs) -الذي يُنشئه البرنامج- بطريقة يستطيع من خلالها اكتشاف هذا الصنف. الرَبطْ (مثال بسيط) using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; namespace Demo { public static class Program { public static void Main() { using (var catalog = new ApplicationCatalog()) using (var exportProvider = new CatalogExportProvider(catalog)) using (var container = new CompositionContainer(exportProvider)) { exportProvider.SourceProvider = container; UserWriter writer = new UserWriter(); // at this point, writer's userProvider field is null container.ComposeParts(writer); // now, it should be non-null (or an exception will be thrown). writer.PrintAllUsers(); } } } } في المثال بالأعلى، تم تَزْوِيد حَاوِي التركيب (composition container) بكتالوج من النوع ApplicationCatalog، والذي يَعتمِد -في بَحْثه عن التَصديرات المُناظِرة- على ملفات التجميعات بامتداد .exe و DLL الموجودة بمجلد البرنامج. لذلك طالما تَوَفَّرت السِمة [Export(typeof(IUserProvider))] باحدى ملفات التجميعات بالمجلد، ستَنجَح عملية استيراد المُواصَفَة الاصطلاحيّة IUserProvider المُعلَّن عنها بداخل الصنف UserWriter. تَتوفَّر أنواع أُخرى من الكتالوجات مثل DirectoryCatalog، والتي يُمكِن استخدَامِها كبديل أو كإضافة للنوع ApplicationCatalog لتَوسِيع دائرة البَحْث عن تَصدِيرات مُناظِرة للاستيرادات المطلوبة. ترجمة -وبتصرف- للفصول Reflection - Managed Extensibility Framework - System.Reflection.Emit namespace من كتاب .NET Framework Notes for Professionals
-
يُنصح بتعزيز المشروع الخاص بك بتقنية اختبار الوَحْدَات (unit testing)، حيث يُوفِّر ذلك العديد من المزايا منها: سهولة إضافة خاصيات جديدة مع ضمان استمرارية عمل الشيفرة القديمة بطريقة سليمة. توفير توثيق برمجي لخاصيات المشروع. كتابة شيفرة أفضل من خلال تعزيز اِستخدَام الواجهات. نصائح لكتابة اختبار الوحدات ينبغي أن يتكون اسم الاختبار من ثلاثة أجزاء: اسم التابع تحت الاختبار. وصف للموقف المُراد اختباره. وصف التصرف المُتوقع عند حدوث هذا الموقف. مثال لاسم سئ: public void Test_Single() { var stringCalculator = new StringCalculator(); var actual = stringCalculator.Add("0"); Assert.Equal(0, actual); } مثال لاسم جيد: public void Add_SingleNumber_ReturnsSameNumber() { // } يَتكون أي اختبار من ثلاث خطوات: خطوة الإِعداد (arrange): يتم خلالها الإِعداد للاختبار بتجهيز الكائنات والنُسخ المُزيَّفة (mocks) وتوابعها بحيث يقتصِر الاختبار على ما نُريد فَحْصه فقط بمَعزَل عن بقية التَبَعيّات (dependencies) وغيره مما لا يَشمَله الاختبار. خطوة التَّنْفيذ (act): يتم خلالها الاستدعاء الفعلّي للتابع تحت الاختبار. خطوة الفَحْص (assert): يتم خلالها إِجراء الفُحوصات المَطلوبة. انظر المثال التالي: public void Add_EmptyString_ReturnsZero() { // Arrange var stringCalculator = new StringCalculator(); // Act var actual = stringCalculator.Add(""); // Assert Assert.Equal(0, actual); } تجنب كتابة اختبارات تحتوي على شروط منطقية مثل if و for و while ..إلخ؛ لأن ذلك سيزيد من احتمالية وجود أخطاء برمجية بالاختبار، وهذا آخر ما تود أن يحدث. لابُدّ لاختبارات الوحدة أن تكون خالية تمامًا من الأخطاء حتى تكون محلًا للثقة. مثال سئ: [Fact] public void Add_MultipleNumbers_ReturnsCorrectResults() { var stringCalculator = new StringCalculator(); var expected = 0; var testCases = new[] { "0,0,0", "0,1,2", "1,2,3" }; foreach (var test in testCases) { Assert.Equal(expected, stringCalculator.Add(test)); expected += 3; } } مثال أفضل: [Theory] [InlineData("0,0,0", 0)] [InlineData("0,1,2", 3)] [InlineData("1,2,3", 6)] public void Add_MultipleNumbers_ReturnsSumOfNumbers(string input, int expected) { var stringCalculator = new StringCalculator(); var actual = stringCalculator.Add(input); Assert.Equal(expected, actual); } لابُدّ أن يحتوي كل اختبار على جملة فحص وحيدة. إذا كنت تريد إجراء أكثر من فحص لموقف معين، يُمكنك إنشاء اختبار منفصل لكل جملة فحص، مما سيُعطيك تصور أوضح لسبب فشل الاختبار. مثال سئ: [Fact] public void Add_EdgeCases_ThrowsArgumentExceptions() { Assert.Throws<ArgumentException>(() => stringCalculator.Add(null)); Assert.Throws<ArgumentException>(() => stringCalculator.Add("a")); } مثال أفضل: [Theory] [InlineData(null)] [InlineData("a")] public void Add_InputNullOrAlphabetic_ThrowsArgumentException(string input) { var stringCalculator = new StringCalculator(); Action actual = () => stringCalculator.Add(input); Assert.Throws<ArgumentException>(actual); } افحص التوابع العامة (public) فقط، وتجنب فحص التوابع الخاصة (private) فهي بالنهاية من المُفترض أن تُنفَّذ أثناء تَّنْفيذ احدى التوابع العامة. علاوة على ذلك فإنها تُعدّ مجرد تفصيلة صغيرة ضمن تَّنفيذ معين (implementation). قد يُغيّر المُبرمج في الواقع من طريقة التنفيذ بدون أن يؤثر على الوظيفة الفعلية الواقعة تحت الاختبار. لذا لا تُقيد اختباراتك على تَّنْفيذ بعينه. عند الحاجة لتهيئة نفس الكائنات لجميع الاختبارات، استخدم توابع مُساعدة (helper methods) بدلًا من اِستخدَام التابعين setup و teardown. يلجأ الكثير من المُبرمجين إلى اضافة شيفرات التهيئة بهما نظرًا لأنهما يُستدعيان تلقائيا قبل بدء تنفيذ أي اختبار وبعد انتهاء تنفيذه على الترتيب، لكن يؤدي ذلك إلى صعوبة قراءة الاختبار كما قد يؤدي أحيانًا إلى تهيئة كائنات غير مطلوبة لبعض الاختبارات. مثال للطريقة غير السليمة: private readonly StringCalculator stringCalculator; public StringCalculatorTests() { stringCalculator = new StringCalculator(); } // more tests... [Fact] public void Add_TwoNumbers_ReturnsSumOfNumbers() { var result = stringCalculator.Add("0,1"); Assert.Equal(1, result); } مثال للطريقة الأفضل: [Fact] public void Add_TwoNumbers_ReturnsSumOfNumbers() { var stringCalculator = CreateDefaultStringCalculator(); var actual = stringCalculator.Add("0,1"); Assert.Equal(1, actual); } // more tests... private StringCalculator CreateDefaultStringCalculator() { return new StringCalculator(); } إضافة مشروع اختبار الوحدات إلى حل موجود مسبقًا انقر بزر الفأرة الأيمن على الحل (solution) واختر أَضِف مشروع جديد. اختر نموذج مشروع اختبار الوَحْدَات (Unit Test Project) من جزء الاختبارات. اختر اسم للمكتبة (assembly). مثلًا إذا كان اسم المشروع المُختبَر Foo، فرُبما تَستخدِم الاسم Foo.Tests. أَضِف مَرجِع (reference) المشروع المُختبَر ضِمْن مَراجِع مشروع اختبار الوَحْدَات. إضافة تابع بغرض الاختبار تَتطلَّب MSTest (بيئة العمل الافتراضية للفَحْص) زَخرفة (decoration) الأصناف بغرض الاختبار (test classes) باستخدام السمة TestClass، وكذلك أن تَكون التوابع بغرض الاختبار (test methods) مُزخرَفة باستخدَام السمة TestMethod وأن تَكون عَلنية (public). في المثال التالي، يَختبِر التابع Test1 كَوْن قيمة المُتغيّر result مُساوِية للقيمة 1. [TestClass] public class FizzBuzzFixture { [TestMethod] public void Test1() { //arrange var solver = new FizzBuzzSolver(); //act var result = solver.FizzBuzz(1); //assert Assert.AreEqual("1",result); } } تتوفَّر الكثير من التوابع لإجراء الفحوصات مثل التابع AreEqual و AreNotEqual و IsTrue وغيرها. مثال آخر: [TestClass] public class UnitTest1 { private const string Expected = "Hello World!"; [TestMethod] public void TestMethod1() { using (var sw = new StringWriter()) { Console.SetOut(sw); HelloWorldCore.Program.Main(); var result = sw.ToString().Trim(); Assert.AreEqual(Expected, result); } } } مثال آخر باستخدام بيئة عمل NUnit للفَحْص (لاحظ اختلاف السمات المُستخدَمة): using NUnit.Framework; using System.IO; using System; namespace HelloWorldTests { public class Tests { private const string Expected = "Hello World!"; [SetUp] public void Setup() { } [Test] public void TestMethod1() { using (var sw = new StringWriter()) { Console.SetOut(sw); HelloWorldCore.Program.Main(); var result = sw.ToString().Trim(); Assert.AreEqual(Expected, result); } } } } تشغيل اختبار الوحدات افتح نافذة Test Explorer من قائمة Test بفيجوال ستوديو، ثم اُنقر على زر "تشغيل الكل (Run All)" مما سيؤدي إلى بدء تَّنْفيذ جميع الاختبارات. بعد انتهاء تَّنْفيذ أي اختبار، ستجد علامة بجانب اسمه إما باللون الأخضر أو الأحمر. يُشير اللون الأخضر إلى نجاح الاختبار أما اللون الأحمر فيُشير إلى فشله. نتائج حية لاختبار الوحدات (Live Unit Testing) يُوفِّر فيجوال ستوديو بدايةً من الإصدار 2017 خاصية عَرْض نتائج اختبار الوَحْدَات بصورة حيّة. يُمكنك تفعيل هذه الخاصية من خلال فتح قائمة Test ثم النقر على زر "اختبار حيّ للوَحْدَات Live Unit Testing" ثم على زر "ابدأ Start" بالقائمة الفرعية. ستجد -في حالة وجود اختبار مُناظِر- علامة صح أو خطأ إلى جانب كل تابع بنافذة مُحرِّر الشيفرة (code editor) لتُشير إلى نجاح أو فشل الاختبارات الخاصة به، كما أنها تُحدَّث تلقائيًا بينما تقوم بتعديل الشيفرة. ترجمة -وبتصرف- للفصل Unit Testing من كتاب .NET Framework Notes for Professionals
-
يُوفِّر إطار عمل .NET طرائق مُبسطة لتَدْويِل (Internationalization) وتَوْطِين (localization) مشروعك. التَدْويِل هو بناء مشروعك بطريقة تُسهِل من عملية تَهيئته للعَمَل بلغات مُختلفة دون إجراء تَغْييرات جوهرية عليه. يتم ذلك بفَصْل ملفات الترجمة عن ملفات شيفرة المشروع. في المقابل، فإن التَوْطِين هو عملية تهيئة المشروع ذاتها ليعَمَل مع لغة بعينها من خلال ترجمة النصوص إلى تلك اللغة. يُطلَق مُصطلح العَولمة (Globalization) ليشمل كُلًا من التَدْويِل والتَوْطِين. مثال بمشروع ASP.NET MVC أولًا: أَضِف حزمة I18N إلى مشروع الـ MVC الخاص بك. ثانيًا: أَضِف i18n.LocalizingModule لقسم <httpModules> أو لقسم <modules> بملف web.config، كالتالي: <!-- IIS 6 --> <httpModules> <add name="i18n.LocalizingModule" type="i18n.LocalizingModule, i18n" /> </httpModules> <!-- IIS 7 --> <system.webServer> <modules> <add name="i18n.LocalizingModule" type="i18n.LocalizingModule, i18n" /> </modules> </system.webServer> ثالثًا: أَضِف مجلد باسم locale للمجلد الرئيسي الخاص بموقعك، ثم انشِئ بداخله مجلد فرعي لكل لغة ترغب بدَعْمها، مثلًا /locale/ar/. رابعًا: انشِئ ملفًا نصيًا باسم messages.po بداخل كل مجلد من المجلدات الفرعية من الخطوة السابقة. خامسًا: يُمكِنك كتابة السطور التالية بملف messages.po، فقط بغرض الاختبار: #: Translation test msgid "Hello, world!" msgstr "أهلًا بالعالم!" سادسًا: أَضِف مُتحكِمًا (controller) إلى المشروع، يُعيد نصًا ما بغرض تجربة الترجمة. في المثال التالي، يُعيد المُتحكِم القيمة "[[[Hello, world!]]]". لاحظ أنه لابُد للنص الموجود داخل الأقواس الثلاثة أن يتطابق مع قيمة msgid ضِمْن ملف الترجمة .po using System.Web.Mvc; namespace I18nDemo.Controllers { public class DefaultController : Controller { public ActionResult Index() { return Content("[[[Hello, world!]]]"); } } } سابعًا: شغِّل المشروع، ثم اِفتَح الرابط المُقابِل للإجراء (action) الذي عَرَّفناه للتو، مثلًًا http://localhost:[yourportnumber]/default. ستُلاحِظ أنه قد تم تَغْيير الرابط تلقائيًا ليَعكِس اللغة الافتراضية الخاصة بك كالتالي http://localhost:[yourportnumber]/en/default -بفرض أن الانجليزية هي اللغة الافتراضية بمُتصفحك. ثامنًا: إذا استبدلت كلمة ar (أو اللغة التي أَعددت ملف ترجمة خاص بها) بكلمة en الموجودة بالرابط، فستظهر النُسخة المُترجمة من النص. تاسعًا: إذا غَيّرت إعدادات اللغة بمُتصفحك لتفضِيل لغة أُخرى، ثم فتحت الرابط /default مُجددًا دون تخصيص اللغة، ستجد أنه مثل المرة السابقة قد تم تَغْيير الرابط تلقائيًا ليَعكِس لغتك الافتراضية التي قمت بضَبطِها في الخطوة السابقة، وسيَظهر النص مُترجمًا بتلك اللغة. عاشرًا: أَضِف مُعالِجَات (handlers) بملف web.config لمنع مُستخدِمي موقعك من فَتح مجلد locale. <!-- IIS 6 --> <system.web> <httpHandlers> <add path="*" verb="*" type="System.Web.HttpNotFoundHandler"/> </httpHandlers> </system.web> <!-- IIS 7 --> <system.webServer> <handlers> <remove name="BlockViewHandler"/> <add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler"/> </handlers> </system.webServer> ترجمة -وبتصرف- للفصل Globalization من كتاب .NET Framework Notes for Professionals