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

لا تُكتب البرامج بصورةٍ صحيحةٍ من قبيل المصادفة، وإنما تتطلّب تخطيطًا وانتباهًا للتفاصيل لتجنُّب أي أخطاءٍ محتمَلةٍ، ولحسن الحظ، تتوفّر بعض التقنيات التي عادةً ما يستعين بها المبرمجون لزيادة فرصة صحة برامجهم.

برامج يمكن إثبات صحتها

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

يُقصد بالأفكار الأساسية كلًا من الحالة state والعملية process؛ وتتكون الحالة من كلّ المعلومات المتعلّقة بتنفيذ البرنامج خلال لحظةٍ معينةٍ، فقد تتضمن قيم المتغيّرات مثلًا، الخرْج الناتج (أي دخْلٍ يُنتظر قراءته) من سطْر البرنامج المنفَّذ حاليًا؛ بينما تتكون العملية من متتالية الحالات التي يمر بها البرنامج أثناء تنفيذه، ومن وجهة النظر تلك، يمكننا أن نفسّر معنى أي تعليمةٍ statement ضِمن برنامجٍ بالكيفية التي تؤثّر بها على حالته، فمثلًا، تعني التعليمة x = 7 أن قيمة المتغيّر x ستساوي 7، في الواقع نحن على يقينٍ من تلك النتيجة تمامًا، ولهذا يمكننا أن نبني على أساسها إثباتًا رياضيًا.

قد ندرك أحيانًا أن حقيقةً معينةً هي تصِح خلال لحظةٍ معينةٍ في البرنامج بمجرد أن ننظر إليه؛ انظر إلى حلقة التكرار loop التالية:

do {
   System.out.print("Enter a positive integer: ");
   N = TextIO.getlnInt();
} while (N <= 0);

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

الشروط اللاحقة Postconditions والشروط المسبقة Pretconditions

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

انظر شيفرة المثال التالي حيث كل المتغيّرات من النوع double:

disc = B*B - 4*A*C;
x = (-B + Math.sqrt(disc)) / (2*A);

نفرض أن قيمة disc أكبر من أو تساوي الصفر وأن قيمة A غير صفريةٍ، إذ تؤكد لنا المعادلة التربيعية في الشيفرة السابقة أن القيمة المسنَدة للمتغيّر x هي حلٌ للمعادلة A*x2 + B*x + C = 0، وإذا كنا سنضمن صحة كلّ من الشرطين B*B-4*A*C >= 0 وA != 0، ستكون عندها حقيقةً "لأن x يمثّل حلًا للمعادلة" شَرطًا لاحقًا لذلك الجزء من البرنامج، كما سيكون عندها الشرْط B*B-4*A*C >= 0 شرطًا مُسبقًا، بالإضافة إلى الشرْط A != 0 لذلك الجزء من البرنامج، ويمكننا تعريف الشرْط المُسبق بأنه شرطٌ يجب أن يتحقّق ضِمن نقطةٍ معينةٍ في البرنامج ليستمر على نحوٍ صحيحٍ.

إذا ما أردت لبرنامجك أن يكون صحيحًا، tيجب أن تفْحص الشرْط المُسبق إذا ما كان متحقِّقًا أم لا، أو أن تُجبره على ذلك.

لقد تعرّضنا لمفهوميْ الشروط اللاحقة والشروط المسبَقة من قَبل في القِسم 4.7.1 لكونهما طريقةٌ لتخصيص المواصفة الاصطلاحية contract لبرنامجٍ فرعيٍ subroutine، حيث عرّفنا الشرْط المسبَق لبرنامجٍ فرعيٍ بأنه شرْطٌ مُسبَقٌ لشيفرة ذلك البرنامج الفرعي، بينما عرّفنا الشرْط اللاحق بأنه شرطٌ يتحقّق بتنفيذ تلك الشيفرة؛ وبهذا القِسم، عمّمنا المقصود بهذين المصطلحين ليكونا أكثر فائدةً بينما نتحدث عن صحة البرامج عمومًا.

افحص المثال التالي:

do {
   System.out.println("Enter A, B, and C.");
   System.out.println("A must be non-zero and B*B-4*A*C must be >= 0.");
   System.out.print("A = ");
   A = TextIO.getlnDouble();
   System.out.print("B = ");
   B = TextIO.getlnDouble();
   System.out.print("C = ");
   C = TextIO.getlnDouble();
   if (A == 0 || B*B - 4*A*C < 0)
      System.out.println("Your input is illegal.  Try again.");
} while (A == 0 || B*B - 4*A*C < 0);

disc = B*B - 4*A*C;
x = (-B + Math.sqrt(disc)) / (2*A);

بعدما تنتهي الحلقة، نتأكد من تحقُّق الشرطين B*B-4*A*C >= 0 وA != 0، ونظرًا لتحقُّق الشروط المسبقَة للسطرين الأخيرين، فإن الشرْط اللاحق (أي كَوْن x حلًا للمعادلة A*x2 + B*x + C = 0) يكون صالحٌ كذلك، إذًا تَحسِب الشيفرة السابقة حلّ المعادلة بصورةٍ صحيحةٍ ومُثبَتةٍ.

اقتباس

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

الآن سنفحص مثالًا آخر، تختبِر تعليمة if في الشيفرة التالية شرطًا مسبَقًا، إذ يتحقّق بكلّ تأكيدٍ أثناء حساب قيمة الحل وطباعته ضِمن الجزء الأول من تعليمة if، بينما لا يتحقّق الشرط ضِمن الأجزاء الأخرى من التعليمة، إلا أن البرنامج صحيحٌ في جميع الأحوال.

System.out.println("Enter your values for A, B, and C.");
System.out.print("A = ");
A = TextIO.getlnDouble();
System.out.print("B = ");
B = TextIO.getlnDouble();
System.out.print("C = ");
C = TextIO.getlnDouble();

if (A != 0 && B*B - 4*A*C >= 0) {
   disc = B*B - 4*A*C;
   x = (-B + Math.sqrt(disc)) / (2*A);
   System.out.println("A solution of A*X*X + B*X + C = 0 is " + x);
}
else if (A == 0) {
   System.out.println("The value of A cannot be zero.");
}
else {
   System.out.println("Since B*B - 4*A*C is less than zero, the");
   System.out.println("equation A*X*X + B*X + C = 0 has no solution.");
}

قبل تكتب برنامجًا، فكِّر بشروطه المسبَقة وبالكيفية التي سيتعامل بها برنامجك معها؛ فغالبًا ما سيعطيك ذلك الشرْط تلميحًا لما ينبغي أن تفعله.

فمثلًا، يملِك كل عنصر مصفوفة مثل A[‎i] شرطًا مسبَقًا بضرورة أن يقع الموضع المُشار إليه ضِمن مجال تلك المصفوفة؛ إذًا سيفحص الحاسوب الشرْط المسبَق للعنصر A[‎i]< وهو 0 <= i < A.length عندما يُحصّل قيمة A[‎i]، فإذا لم يتحقّق الشرْط فسينتهي البرنامج، وينبغي عليك أن تتجنّب وقوع ذلك.

تبحث الشيفرة التالية عن عدد x ضِمن مصفوفة A، حيث تُضبَط قيمة i إلى موضع عنصر المصفوفة الذي يحتوي على ذلك العدد إذا وُجد:


i = 0;
while (A[i] != x) {
   i++;
}

عندما نُشغّل الشيفرة السابقة، يكون لدينا شرطٌ مسبَقٌ بأن العدد يوجد فعليًا ضِمن نطاق المصفوفة، فإذا تحقّق ذلك الشرط، فستنتهي الحلقة عندما تكون A[‎i] == x، وذلك يعني أن قيمة i ستحمل موضع العدد x في المصفوفة، بينما إذا لم يكن x ضِمن المصفوفة، فإن قيمة i ستزداد إلى أن تصل قيمتها إلى A.length، بحيث سيشير A[‎i] إلى عنصرٍ غير صالحٍ، ثم بعد ذلك سينتهي البرنامج؛ ولذلك يَلزم عليك أن تضيف اختبارًا يتأكد من تحقُّق الشرْط المسبَق، كالتالي:


i = 0;
while (i < A.length && A[i] != x) {
   i++;
}

هنا ستنتهي الحلقة، ونستطيع أن نحدّد أي الشرطين i == A.length أوA[‎i] == x قد تسبّب في انتهاء الحلقة، وهو ما تختبره تعليمة if في نهاية الشيفرة التالية:


i = 0;
while (i < A.length && A[i] != x) {
   i++;
}

if (i == A.length)
   System.out.println("x is not in the array");
else
   System.out.println("x is in position " + i);

اللامتغايرات Invariants

سنفحص الآن طريقة عمَل حلقات التكرار من قُربٍ، حيث يحسِب البرنامج الفرعي التالي حاصل مجموع عناصر مصفوفةٍ من الأعداد الصحيحة:


static int arraySum( int[] A ) {
    int total = 0;
    int i = 0;
    while ( i < A.length ) {
        total = total + A[i];
        i = i + 1;
    }
    return total;
}

سيفترِض البرنامج الفرعي السابق شرطًا مسبَقًا بأن A لا تحتوي على القيمة الفارغة، فإذا لم يتحقّق ذلك الشرْط، سيبلِّغ البرنامج الفرعي عن اعتراضٍ من النوع NullPointerException.

إذًا كيف نتأكد من أن البرنامج في السابق يعمل بطريقةٍ سليمةٍ؟ ينبغي أن نُثبِت أن قيمة total تُساوي حاصل مجموع العناصر ضِمن المصفوفة A قبل تنفيذ تعليمة return؛ ولهذا نستخدم حلقات تكرار لامتغايرة loop invariants.

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

ملاحظة: اخترنا المصطلح العربي "لامتغاير" لترجمة المصطلح الأجنبي invariant، إذ معنى مُتغَاير variant بالعربية مختلف ومتباين ومتبدِّل وحتى تكون مختلفة عن "مُتغيِّر" التي هي ترجمة variable.

فمثلًا، تُعَد تعليمة total (حاصل مجموع أول عددٍ i من عناصر المصفوفة A) لامتغاير لحلقة التكرار السابقة، وسنفترض أن ذلك كان متحققًا في بداية حلقة التكرار while، أي قبل تنفيذ التعليمة total = total + A[‎i]؛ لذا فبعْد إضافة A[‎i] إلى total، فستساوي total حاصل مجموع أول عددٍ i+1 من عناصر تلك المصفوفة، في هذه اللحظة تحديدًا لا يَصِح لامتغاير الحلقة، بينما يعود لامتغاير الحلقة ليُصبِح صحيحًا مرةً أخرى بعد أن ننفّذ التعليمة التالية i = i + 1، أي زيادة i بمقدار الواحد؛ وبذلك نكون قد تأكدنا من صحة لامتغاير الحلقة أثناء بداية الحلقة ونهايتها.

اقتباس

لاحظ أنه لا ينبغي للامتغاير حلقةٍ معينةٍ أن يَصِح ضِمن كلّ نقاط تنفيذ الحلقة، إذ قد تتسبّب تعليمةٍ معينةٍ ضِمن الحلقة إلى عدم تحققُّه لبعض الوقت، بشرْط أن تؤدي تعليمةٌ أخرى ضِمن نفس الحلقة إلى تحققُّه مرةً أخرى.

هل نكون بذلك قد أثبتنا أن البرنامج الفرعي arraySum()‎ صحيحٌ تمامًا؟ لا بكلّ تأكيد، فما تزال هناك بعض الأشياء التي يجب أن تُفحَص، حيث يجب أولًا أن نتأكد من صحة لامتغاير حلقة التكرار قبل أول تنفيذٍ للحلقة، ففي تلك اللحظة، كانت قيمة كلّ من i وtotal تساوي 0، وهو حاصل المجموع الصحيح لمصفوفةٍ فارغةٍ، ويعني ذلك أن لامتغاير الحلقة كان صحيحًا قبل بدايتها، كما أنه سيبقى صحيحًا بعد كلّ تنفيذٍ لها وكذلك حتى بعد انتهائها.

تبقّى لنا أن نتأكد من قابلية حلقة التكرار للانتهاء، إذ تزداد قيمة i بمقدار الواحد مع كلّ تنفيذٍ للحلقة مما يعني أنها ستصل في النهاية إلى A.length، وهنا لن يتحقَّق شرْط حلقة while وستنتهي الحلقة.

بعد انتهاء الحلقة، ستتساوي قيمة كلّ من i وA.length، كما سيتحقّق لامتغاير الحلقة، ونظرًا لتساوي قيمة كلّ من i وA.length، فسنجد أن total تعبّر عن حاصل مجموع أول عددٍ مقداره A.length من عناصر المصفوفة A، كما تعبّر عن حاصل جمع جميع عناصر المصفوفة A، وبهذا سيعطينا لامتغاير الحلقة ما نريده تمامًا؛ وعندما يعيد البرنامج الفرعي قيمة total، فإنها ستساوي حاصل مجموع عناصر المصفوفة.

قد يُعَد هذا جهدًا كبيرًا لإثبات صحة شيءٍ واضحٍ، ولكنك إذا حاولت أن تشرح لماذا يعمل arraySum()‎ على نحوٍ صحيحٍ، فغالبًا ستستخدِم نفس المنطق الذي يُبنى عليه لامتغاير حلقة التكرار حتى إذا لم تستخدِم نفس المصطلح.

مثالٌ آخر، يبحث البرنامج الفرعي التالي عن أكبر قيمةٍ ضِمن مصفوفةٍ من الأعداد الصحيحة على فرْض أنها تحتوي على عنصرٍ واحدٍ على الأقل، اُنظر الشيفرة التالية:


static int maxInArray( int[] A ) {
    int max = A[0];
    int i = 1;
    while ( i < A.length ) {
        if ( A[i] > max )
            max = A[i];
        i = i + 1;
    }
    return max;
}

يُعَد لامتغاير الحلقة في تلك الحالة أن max هو أكبر قيمةٍ ضِمن أول عناصر i من المصفوفة A، وسنفترض صحة ذلك قبل تعليمة if،إذًا بعد انتهاء الحلقة، سيكون max أكبر من أو يساوي A[‎i] لأنه شرْطٌ لاحقٌ لتعليمة if، كما سيكون أكبر من أو يساوي القيم التي تتراوح بين A[0]‎ وA[i-1]‎ نتيجةً للامتغاير الحلقة. وعندما نضع تلك الحقيقتين معًا، فنستطيع أن تُثبِت أن max هو أكبر قيمةٍ بين أول عددٍ i+1من عناصر المصفوفة A؛ وعندما نستبدل i+1 بـ i في التعليمة التالية، فسيتحقّق لامتغاير الحلقة مرةً أخرى، كما سنجد بعد انتهاء الحلقة أن i يساوي A.length، وسيُثبِت لامتغاير الحلقة أن max هو أكبر عنصرٍ في المصفوفة.

اقتباس

لاحظ أن لامتغايرات حلقات التكرار لا تفيد في إثبات صحة البرامج فقط، وإنما تفيد كذلك في تطوير الخوارزميات.

مثالٌ آخر، افحَص خوارزمية الترتيب بالإدراج insertion sort التالية التي سبق وأن ناقشناها في القِسم الفرعي 7.4.3، وسنفترِض أننا سنرتّب مصفوفة A، ولذا ينبغي أن يتحقّق ما يلي في نهاية الخوارزمية:


A[0] <= A[1] <= ... <= A[A.length-1]

سيَطرح السؤال التالي نفْسه، ما هو الأسلوب الذي نتبعه لنتأكّد من صحة ذلك خطوةٍ بخطوةٍ؟ وهل يمكننا أن نوجد لامتغاير حلقةٍ يُمثّل العِبارة التي نريدها أن تتحقَّق في النهاية؟ فمثلًا، إذا أردنا لجميع عناصر مصفوفة أن تكون مرتبة في النهاية، فماذا عن لامتغاير حلقةٍ يشير إلى أن بعض عناصر المصفوفة مرتّبةٌ، وبصورةٍ أكثر تحديدًا يشير إلى أن العناصر الأولى من المصفوفة وحتى الفهرس i مرتبةٌ؟! ويقودنا ذلك إلى الخوارزمية التالية: 


i = 0;
while (i < A.length ) {
   // ‫لامتغاير الحلقة [A[0] <= A[1] <= ... <= A[i-1
      .
      . // شيفرة إضافة عنصر إلى الجزء المرتب من المصفوفة
      .
   i = i + 1;
}
// بهذه النقطة، تكون i = A.length, وA[0] <= A[1] <= ... <= A[A.length-1]
اقتباس

لاحظ أن لا متغاير الحلقة سيتحقّق قبل حلقة التكرار while، كما سيُمثِل اللامتغاير في نهاية الحلقة العِبارة التي أردناها أن تتحقّق في نهاية الخوارزمية، ولنكمِل الخوارزمية، سنكتب الشيفرة داخل حلقة التكرار بطريقةٍ تَضمن صحة لامتغاير الحلقة، وإذا تمكنا من ذلك، سيَضمن لامتغاير الحلقة صحة الخوارزمية. لاحظ تتطلّب خوارزمية إضافة عنصر A[‎i] إلى الجزء المرتّب من المصفوفة حلقة تكرارٍ أخرى تحتوي على لامتغاير آخرٍ، بإمكانك أن فكّر في الأمر.

تُعَد لامتغايرات الصنف class invariants (أو يطلق عليها أصناف لامتغايرة في بعض المواضع) نوعًا آخرًا من اللامتغايرات التي تفيد أثناء كتابة البرامج، حيث يمثّل لامتغاير الصنف عبارةً صحيحةً دائمًا عن حالة الصنف أو حالة الكائنات التي أُنشئت من ذلك الصنف؛ سنفترض أن لدينا صنف PairOfDice يُخزّن القيم الظاهرة على حجَريْ نرْدٍ في متغيّرين هما die1 وdie2 لأننا نرغب بوجود لامتغاير صنف يُخبِر أن قيَم حجَريْ النرْد تتراوح بين 1 و6؛ وهذه العِبارة صحيحةٌ دائمًا لأي حَجريْ نْرد.

ينبغي أن تَصِح العبارة السابقة في جميع الأحوال إذا أردنا تمثيلها بأحد لامتغايرات الأصناف، وإذا كان die1 وdie2 عبارة عن متغيّرات نُسخٍ عامةٍ instance variables، فلا توجد أي ضمانةٍ ممكنةٍ لتحقيق ذلك؛ لأننا لا نتحكم بالقيم التي يُسنِدها أي برنامج (يستخدم هذا الصنف) إلى تلك المتغيّرات، ولذلك نَعُدهما مثل المتغيرات الخاصة private variables، وبعد ذلك، نتأكد من تطبيق الشيفرة الموجودة داخل الصنف لقاعدة لامتغاير الصنف.

بدايةً، عندما نُنشِئ كائنًا من الصنف PairOfDice، فإن قيم die1 وdie2 ينبغي أن تُهيّئ إلى قيم تتراوح بين 1 و6، كما يجب أن يحافِظ كل تابعٍ method معرَّف داخل الصنف على تلك الحقيقة، بمعني أن يتأكد أيّ تابعٍ يُسنِد قيمةً إلى die1 أو die2 من وقوع تلك القيمة في نطاقٍ يتراوح من 1 إلى 6؛ فمثلًا، قد يَفحص تابع ضبْطٍ setter method ما إذا كانت القيمة المسنَدة صالحةٌ أم لا.

عمومًا يُمثّل لامتغاير الصنف شرطًا لاحقًا للمُنشئ constructor، كما أنه يُمثّل شرطًا لاحقًا ومسبقًا لأيّ تابعٍ آخرٍ ضِمن الصنف، فعندما تكتب صنفًا، فيجب أن تتأكد من صحة لامتغاير الصنف في كل الأوقات؛ بينما عندما تكتب تابعًا، فيجب أن تتأكد من أن شيفرة ذلك التابع تطبّق قواعد لامتغاير الصنف. سنفترض مثلًا تحقُّق لامتغاير الصنف عند استدعاء تابع، إذًا ينبغي أن نتأكد من صحته حتى بعد انتهاء تنفيذ شيفرة ذلك التابع، وستساعدك طريقة التفكير السابقة بقوةٍ أثناء تصميم الأصناف.

مثالٌ آخرٌ، سبَق وأن عرّفنا صنفًا ممثِّلًا لمصفوفةٍ ديناميكيةٍ في القِسم الفرعي 7.2.4، حيث يخزّن ذلك الصنف القيم في مصفوفةٍ عاديةٍ، بالإضافة إلى عدّادٍ لمعرفة عدد العناصر المخزَّنة فعليًا ضِمن المصفوفة:


private int[] items = new int[8];
private int itemCount = 0;

تحتوي لامتغايرات الصنف على عباراتٍ مِثل "تمثيلٍ itemCount لعدد العناصر" و"تراوح itemCount بين صفر وعدد عناصر المصفوفة" و"وجود قيم العناصر بمواضع المصفوفة من items[0‎]‎ إلى items[itemCount-1]‎"؛ ولذلك يجب أن تتذكر دائمًا لامتغايرات الصنف أثناء تعريفه، فعندما تكتب تابعًا ليضيف عنصرًا مثلًا، فسيذّكرك اللامتغاير الأول بضرورة زيادة itemCount بمقدار الواحد ليتحقّق اللامتغاير دائمًا، بينما سيُخبرك اللامتغاير الثاني بالمكان الذي ينبغي أن تخزِّن فيه العنصر الجديد، كما سيخبرك اللامتغاير الأخير أنه في حالة زيادة itemCount إلى items.length، فستحتاج إلى فعل شئٍ لتتجنّب مخالفة ما تنص عليه قواعد اللامتغاير.

سنشير أحيانًا خلال الفصول القادمة إلى فائدة التفكير بالشروط اللاحقة والمسبَقة واللامتغايرات.

يتعقّد تحليل البرامج المتوازية parallel programs باستخدام اللامتغايرات، إذ تنفِّذ هذه البرامج عِدّة سلاسل threads من التعليمات تتعامل مع نفْس البيانات في نفْس الوقت. وسنناقش تلك المشكلة أثناء حديثنا عن السلاسل بالفصل 12.

معالجة المدخلات

تُعَد معالجة البيانات المُدخَلة من المستخدِم أو المقروءة من ملفٍ أو المستقبَلة عبْر الشبكة واحدةٌ من أهم الأشياء التي نتأكد من خلالها من صحة البرنامج ومتانته، وسنتناول الملفات والشبكات بالفصل 11 بصورةٍ تعتمد على ما سنُناقشه بالقِسم التالي، سنفحص الآن مثالًا على معالجة مدخَلات المستخدِم.

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

قد يفيد أحيانًا أن تطلّع على ما سيُقرَأ لاحقًا مثل مُدخَلٍ دون أن تقرأه بصورةٍ فعليةٍ، فقد يحتاج برنامجٍ معينٍ مثلًا إلى معرفة إذا ما كانت قيمة المُدخَل التالي عبارةٌ عن عددٍ أم كلمةٍ، ولهذا يتضمّن الصنف TextIO على الدالة TextIO.peek()‎، حيث تعيد تلك الدالة قيمةً من النوع char، وتعرّف المحرّف التالي بمُدخَلات المستخدِم، رغم أنها لا تقرؤها بصورةٍ فعلية، فإذا كان المُدخَل التالي عبارةٌ عن محرّف نهاية السطر، فإن الدالة TextIO.peek()‎ ستعيد المحرّف إلى بداية السطر '‎\n'.

نحتاج عادةً إلى معرفة المحرّف التالي غير الفارغ بمُدخَلات المستخدِم، ولكننا نحتاج إلى تخطّي أيّ مسافاتٍ أو فراغاتٍ قبل أن نختبر ذلك؛ إذ تستخدِم الشيفرة المعرَّفة بالأسفل الدالة TextIO.peek()‎ لتتطلّع على الأحرف التالية إلى أن تُقابِل محرّف نهاية السطر أو محرّفًا غير فارغٍ، كما تقرأ الدالة TextIO.getAnyChar()‎ المحرّف التالي وتعيده ضِمن مُدخَلات المستخدِم حتى إذا كان مجرّد مسافةٍ فارغةٍ، في المقابل، تتخطّى الدالة TextIO.getChar()‎ الأكثر شيوعًا أي مسافاتٍ فارغةٍ ثم تعيد المحرّف التالي غير الفارغ بعد قراءته؛ ولا يمكننا أن نستخدم TextIO.getChar()‎ هنا، لأن الغرض هو تخطّي المسافات الفارغة دون قراءة المحرّف غير الفارغ التالي.


//[1]
static void skipBlanks() {
   char ch;
   ch = TextIO.peek();
   while (ch == ' ' || ch == '\t') {
       // الحرف التالي عبارة عن مسافة فارغة، اقرأه //
وافحص الحرف الذي يليه
      ch = TextIO.getAnyChar();
      ch = TextIO.peek();
   }
} // end skipBlanks()

[1] في الشيفرة السابقة تعني "اقرأ أي مسافاتٍ فارغةٍ بمُدخَلات المستخدم وسيكون الشرط المسبق، أما الحرف التالي بمُدخَلات المستخدم الذي هو عبارةٌ عن محرّف نهاية السطر أو حرفٍ غير فارغٍ".

تَشيع هذه العملية جدًا لدرجة أننا أضفناها إلى الصنف TextIO، ونستخدمها باستدعاء TextIO.skipBlanks()‎ المُماثِل تمامًا للتابع skipBlanks()‎.

يَسمح المثال بالقِسم الفرعي 3.5.3 للمستخدِم بأن يدخِل قياسات الطول مثل "‎3 miles" أو "‎1 ft"، ثم يحوِّلها إلى وِحداتٍ مثل البوصة inches والقدم feet والياردة yards والميل miles؛ وعادةً ما يدخِل المستخدِم عدّة قياساتٍ بأكثر من وحدةٍ مثل "‎3 feet 7 inches"، وسنحسِّن البرنامج ليسمَح بمُدخَلاتٍ من هذه النوعية.

بصورةٍ أكثر تحديدًا، سيدخِل المستخدِم أسطرًا تحتوي على واحدةٍ أو أكثر من قياسات الطول مثل "‎1 foot" أو "‎3 miles 20 yards 2 feet" بحيث تكون وِحدات القياس المسموح بها هي inch وfoot وyard وmile، وكذلك يجب أن يُميّز البرنامج صيغ الجمع منها أي inches وfeet وyards وmiles، واختصاراتها مثل in وft وyd وmi.

سنكتب الآن برنامجًا فرعيًا يقرأ سطرًا واحدًا بتلك الصيغة ثم يحسِب عدد البوصات المكافِئة، ثم سيَستخدم البرنامج ذلك العدد لحساب القيمة المكافئة بالوحدات الأخرى feet وyards وmiles، وإذا احتوت مُدخَلات المستخدِم على خطأٍ، فسيطبَع البرنامج الفرعي رسالة خطأٍ ثم يُعيد القيمة -1؛ حيث سيَفترض البرنامج الفرعي أن السطر المُدخَل غير فارغٍ، ولذلك سيَفحص البرنامج main ذلك قبل استدعائه للبرنامج الفرعي، حيث سيَستخدم سطرًا فارغًا ليُشير إلى انتهاء البرنامج، وبفرض تجاهُل احتمالية المُدخَلات غير الصالحة، سنكتب خوارزمية الشيفرة الوهمية pseudocode للبرنامج الفرعي كالتالي:


inches = 0    // This will be the total number of inches
// طالما ما يزال هناك مُدخَلات بالسطر
while there is more input on the line:
    // اقرأ قيمة القياس الغددية
    read the numerical measurement
    // اقرأ وحدة القياس
    read the units of measure
    // ‫أضف القيمة المقروءة إلى inches
    add the measurement to inches
return inches

الآن، سنختبر إذا ما كانت هناك أي مُدخَلاتٍ أخرى ضِمن نفْس السطر، وسنفحَص إذا ما كان المحرّف التالي غير الفارغ هو محرّف نهاية السطر أم لا، إلا أن هذا الاختبار سيتطلّب هنا شرطًا مُسبَقًا للتأكد من أن المحرّف التالي إما محرّف نهاية السطر أو محرّفٌ غير فارغٍ، ولهذا يجب أن نتخطّى أولًا أي محارف فارغةٍ، وسنعيد كتابة الخوارزمية كالتالي:


inches = 0
skipBlanks()
while TextIO.peek() is not '\n':
    // اقرأ قيمة القياس الغددية
    read the numerical measurement
    // اقرأ وحدة القياس
    read the unit of measure
    // ‫أضف القيمة المقروءة إلى inches
    add the measurement to inches
    skipBlanks()
return inches

يتأكد التابع skipBlanks()‎ في نهاية حلقة التكرار while من تحقُّق الشرط المسبَق، وعمومًا إذا كان هناك شرْطٌ مسبَقٌ لحلقة تكرارٍ معينةٍ، فيجب أن نتأكد من تحقُّقه في نهاية الحلقة قبل أن يحاول الحاسوب أن يحصّل قيمته مرةً أخرى.

ماذا عن فحْص الأخطاء؟ ينبغي أن نتأكد من وجود قيمةٍ عدديةٍ قبل أن نقرأ قيمة القياس، كما يجب أن نتأكد من وجود رمزٍ أو كلمةٍ قبل أن نقرأ وحدة القياس، إلا أن وجود العدد آخر شيءٍ ضِمن السطر مثل "3" بدون وحدة قياسٍ يُعَد أمرًا غير مقبولٍ؛ في النهاية يجب أن نفحص إذا ما كانت وِحدة القياس المُدخَلة هي إحدى القيم التالية: inches و feet وyards و miles . ستكون الخوارزمية التي تتضمن شيفرة فحْص الأخطاء كالتالي:


inches = 0
skipBlanks()

while TextIO.peek() is not '\n':

    // إذا لم يكن الحرف التالي عبارة عن رقم
    if the next character is not a digit:
           // بلغ عن خطأ وأعد -1
        report an error and return -1

    Let measurement = TextIO.getDouble();

    skipBlanks()    // Precondition for the next test!!
    if the next character is end-of-line:
       report an error and return -1                   
    Let units = TextIO.getWord()

    if the units are inches: // ‫إذا كانت وحدة لقياس inches
        // ‫أضف قيمة القياس إلى inches
        add measurement to inches
    else if the units are feet: // ‫إذا كانت قدم feet
        // ‫أضف حاصل ضرب 12*قيمة القياس إلى inches
        add 12*measurement to inches
    else if the units are yards: // ‫إذا كانت ياردة
        // ‫أضف حاصل ضرب 36*قيمة القياس إلى inches
        add 36*measurement to inches
    else if the units are miles:
        // ‫أضف حاصل ضرب 5280*قيمة القياس إلى inches
        add 12*5280*measurement to inches
    else
        // بلغ عن الخطأ وأعد -1
        report an error and return -1

    skipBlanks()

return inches

كما ترى، عقّد اختبار الأخطاء الكثير من الخوارزمية بصورةٍ كبيرةٍ مع أنها مجرّد مثالٍ بسيطٍ؛ رغم أننا لم نعالِج جميع الأخطاء الممكِنة مثل إدخال قيمة قياسٍ من خارج النطاق المسموح به للنوع double مثل "1e400"، ولذلك سيعود البرنامج في تلك الحالة للاعتماد على الصنف TextIO ليعالِج الأخطاء، باإضافة إلى ذلك، قد يُدخِل المستخدِم قيمةً مثل "1e308 miles"، وفي حين أن العدد 1e308 يصلح للإدخال، إلا أن قيمته المكافِئة بوحدة البوصة تقع خارج النطاق المسموح به للنوع double، وعندها سيَحصل الحاسوب (كما ذكرنا في القِسم السابق) على القيمة Double.POSITIVE_INFINITY، وبإمكانك أن تُشغّل البرنامج وتجرّبه بنفْسك.

سنكتب الخوارزمية بلغة جافا كالتالي:


// [2]
static double readMeasurement() {

   double inches;  // حاصل مجموع البوصات الكلية

   double measurement;  // قيمة القياس المُدخَلة
    String units;        // وحدة القياس المُدخَلة

   char ch;  // لفحْص الحرف التالي من مُدخَل المستخدم

   inches = 0;  // No inches have yet been read.

   skipBlanks();
   ch = TextIO.peek();

    // [1]

   while (ch != '\n') {

       // اقرأ قيمة القياس التالية ووحدة القياس المستخدمة

       if ( ! Character.isDigit(ch) ) {
           System.out.println(
                 "Error:  Expected to find a number, but found " + ch);
           return -1;
       }
       measurement = TextIO.getDouble();

       skipBlanks();
       if (TextIO.peek() == '\n') {
           System.out.println(
                 "Error:  Missing unit of measure at end of line.");
           return -1;
       }
       units = TextIO.getWord();
       units = units.toLowerCase();

       /*
       ‫حوّل قيمة القياس إلى وحدة البوصة وأضفها إلى inches 
       */

       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("Error: \"" + units 
                             + "\" is not a legal unit of measure.");
           return -1;
       }

       // اختبر إذا ما كان المحرّف التالي هو محرّف نهاية السطر

       skipBlanks();
       ch = TextIO.peek();

   }  // end while

   return inches;

} // end readMeasurement()

حيث تعني [1] أنه طالما ما تزال هناك مُدخَلات أخرى بالسطر، اقرأ قيمة القياس وأضف مكافئها من البوصات إلى المتغيّر inches، وإذا وقع خطأٌ أثناء تنفيذ الحلقة، انهي البرنامج الفرعي فورًا وأعِد القيمة -1.

في حين [2] تعني "اقرأ سطرًا واحدًا من مُدخَلات المستخدم، بحيث يكون الشرط المسبَق هو السطر المُدخَل غير الفارغ، ويكون الشرط اللاحق هو تحويل قيمة القياس إلى وحدة البوصة ثم إعادتها مثل قيمةٍ للدالة إذا كان المُدخَل صالحًا، بينما إذا كانت قيمة المُدخَل غير صالحةٍ، تعيد الدالة قيمة "-1".

توجد شيفرة البرنامج بالكامل في الملف LengthConverter2.java.

ترجمة -بتصرّف- للقسم Section 2: Writing Correct Programs من فصل Chapter 8: Correctness, Robustness, Efficiency من كتاب Introduction to Programming Using Java.

اقرأ أيضًا


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

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

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



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

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

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

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

  Only 75 emoji are allowed.

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

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

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


×
×
  • أضف...