السابقالفهرسالتالي

الملحق D

التنقيح

 

يعتمد الأسلوب الأفضل في التنقيح (debugging) على نوع الخطأ الذي تواجهه:

تم ترتيب الأقسام التالية وفق أنواع الأخطاء، توجد بعض التقنيات المفيدة لأكثر من نوع واحد.

D.1 الأخطاء النحوية

أفضل نوع من التنقيح هو الذي لا تحتاج إلى القيام به لأنك تتفادى عمل الأخطاء من البداية. في القسم السابق، اقترحت أسليب تطوير تقلل من الأخطاء وتجعل العثور عليها أسهل في حال وجودها.

الفكرة هي أن تبدأ مع برنامج يعمل وتضيف كميات صغيرة من الشفرة في كل مرة. عند وجود خطأ، ستكون عندك كرة جيدة عن مكان وقوعه.

ومع ذلك، فقد تجد نفسك في أحد المواقف التالية. في كل موقف، سأقدم بعض المقترحات عن كيفية التعامل معه.

المجمّع يمطر رسائل الأخطاء

إذا أعطى المجمع 100 رسالة خطأ، فهذا لا يعني وجود 100 خطأ في برنامجك. عندما يواجه المجمع خطأ، فسيخرج عن المسار لفترة. سيحاول استرجاع المسار والمتابعة بعد الخطأ الأول، لكنه أحياناً يعطي أخطاء زائفة.

يمكن الاعتماد على رسالة الخطأ الأولى فقط. أنا أقترح أن تصحح خطأ واحداً فقط في كل مرة، ثم أعد تجميع البرنامج. قد تجد أن فاصلة منقوطة واحدة "تصحح" 100 خطأ.

أتلقى رسالة غريبة من المجمع لا ترضى بأن تذهب عني

أولاً، اقرأ رسالة الخطأ بدقة. إنها مكتوبة بكلمات مكثفة، لكن غالباً ما توجد فيها معلومات مفيدة مخبأة بعناية.

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

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

إذا لم تعثر على الخطأ بسرعة، خذ نفساً وانظر على البرنامج ككل. تأكد من أن البرنامج مرتب (indented) بشكل مناسب؛ ما يجعل اكتشاف الأخطاء النحوية أسهل.

الآن ابدأ بالبحث عن الأخطاء الشائعة:

  1. تحقق من تناظر جميع الأقواس وأنها متداخلة بشكل صحيح. يجب أن تكون تعاريف العمليات كلها داخل تعريف صنف. كافة تعليمات البرنامج يجب أن تكون داخل تعريف عملية.
  2. تذكر أن الأحرف الكبيرة لا تكافئ الأحرف الصغيرة.
  3. تحقق من وجود الفواصل المنقوطة عند نهاية كل تعليمة (ومن عدم وجودها بعد الأقواس المنحنية).
  4. تأكد أن جميع السلاسل المحرفية في الشفرة لها علامتي اقتباس عند البداية والنهاية. تأكد من استعمال علامات الاقتباس المزدوجة للسلاسل المحرفية وعلامات الاقتباس المفردة للمحارف.
  5. بالنسبة لتعليمات الإسناد، تأكد أن نوع الطرف الأيسر من نفس نوع الطرف الأيمن. تأكد أن الطرف الأيسر هو اسم متغير أو شيء آخر يمكنك إسناد قيمة له (مثل عنصر من مصفوفة).
  6. بالنسبة لكل استدعاء لعملية، تأكد أن المتحولات التي تعطيها لها مكتوبة بالترتيب الصحيح، وأنها من نوع مناسب، وأن يكون الكائن الذي تستدعي العملية عليه من النوع المناسب.
  7. إذا كنت تستدعي عملية مثمرة (عملية تعيد قيمة)، تأكد من عمل شيء ما بالنتيجة. إذا كنت تستدعي عملية عقيمة (void method)، تأكد من أنك لا تحاول استخدام نتيجتها لعمل شيء ما.
  8. إذا كنت تستدعي عملية كائنية (object method)، تأكد من أنك تستدعيها على كائن من النوع الملائم. إذا كنت تستدعي عملية صنف من مكان خارج الصنف الذي عرِّفت فيه، تأكد من ذكر اسم الصنف.
  9. يمكنك الولوج إلى متغيرات الحالة بدون تحديد الكائن داخل عمليات الكائنات. إذا حاولت عمل ذلك في عملية صنف، ستحصل على رسالة مثل، "Static reference to non-static variable".
إذا لم ينفع أي من هذه الأشياء، انتقل إلى القسم التالي...

لا أستطيع تجميع برنامجي مهما فعلت

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

إذا فحصت الشفرة بصورة شاملة، وكنت متأكداً أن المجمع يترجم الشفرة الصحيحة، فقد آن الأوان لاتخاذ تدابير اليأس: التنقيح المجزأ (debugging by bisection).

  1. انسخ الملف الذي تعمل عليه. إذا كنت تعمل على Bob.java، اصنع نسخة منه باسم Bob.java.old.
  2. احذف ما يعادل نصف الشفرة تقريباً من Bob.java. جرب تجميعه ثانيةً.
  3. إذا تمكنت من تجميع البرنامج الآن، ستعرف أن الخطأ في النصف الثاني. أرجع ذاك النصف الذي حذفته وأعد الكرة.
  4. إذا لم تتمكن من تجميع البرنامج، فلا بد أن الخطأ في هذا النصف. احذف نصف هذا النصف وأعد الكرة.
  5. بمجرد العثور على الخطأ وتصحيحه، ابدأ بإعادة الشفرة التي حذفتها، قطعة صغيرة في كل مرة.
هذه العملية بشعة، لكنها أسرع مما تتصور، وهي موثوقة تماماً.

لقد قمت بما طلبه المجمع مني، ولا يزال البرنامج لا يعمل

تحتوي بعض رسائل المجمع على نصائح تبشر بالخير، مثل

"class Golfer must be declared abstract. It does not define int compareTo(java.lang.Object) from interface java.lang.Comparable"

يبدو أن المجمع يخبرك بوجوب التصريح عن الصنف Golfer كصنف مجرد، وإذا كنت تقرأ هذا الكتاب، فعلى الأغلب أنك لا تعرف ما هذا أو كيفية عمله. لحسن الحظ، المجمع على خطأ. الحل في هذه الحالة هو التأكد أن الصنف Golfer يملك عملية باسم compareTo تأخذ Object كمعامل.

لا تدع المجمّـع يسحبك من أنفك. رسائل الخطأ التي يعطيك إياها دليل على حدوث خطأ، لكن لا يمكنك الاعتماد على الحلول التي يقترحها عليك.

D.2 أخطاء التشغيل

برنامجي يعلق

إذا توقف البرنامج وبدا انه لا يفعل أي شيء، نقول أنه يعلق (أو يجمد hang). ذلك يكون معناه غالباً أن البرنامج دخل في حلقة لا نهائية أو عملية عودية لا نهائية.

الحلقات اللانهائية

إذا كنت تعتقد أنك تملك حلقة لا نهائية وكنت تعرف أية حلقة هي، أضف تعليمة طباعة في نهاية الحلقة تطبع قيم المتغيرات الموجودة في الجملة الشرطية، وقيمة الشرط. مثلاً،

while (x > 0 && y < 0) {
   // do something to x
   // do something to y

   System.out.println("x: " + x);
   System.out.println("y: " + y);
   System.out.println("condition: " + (x > 0 && y < 0));
}
الآن عندما تشغل البرنامج سترى ثلاثة أسطر من الخرج في كل مرة تدور فيها الحلقة. في آخر مرة يتم تنفيذ الحلقة فيها، يجب أن تكون قيمة الشرط false. إذا استمرت الحلقة بالعمل، انظر إلى قيم x وy فقد تعرف سبب عدم تحديثهما بشكل صحيح.

التعاود اللانهائي

في أغلب الأحيان يسبب التعاود اللانهائي توليد استثناء StackOverflowException. لكن إذا كان البرنامج بطيئاً فقد يستغرق وقتاً طويلاً قبل أن يملأ المكدس.

إذا كنت تعرف أية عملية هي سبب المشكلة، تحقق من وجود حالة قاعدية فيها. يجب وجود شرط يجعل العملية تنتهي بدون عمل استدعاء تعاودي آخر. إذا لم توجد حالة قاعدية للعملية، عليك إعادة التفكير بالخوارزمية وتعريف حالة قاعدية لها.

في حال وجود حالة قاعدية، لكن لا يبدو أن البرنامج يصل إليها، أضف تعليمة طباعة إلى بداية العملية تطبع معاملاتها. يجب أن ترى الآن بضعة أسطر من الخرج عند كل استدعاء للعملية، ترى فيه قيم معاملاتها. إذا لم تتحرك المعاملات نحو الحالة القاعدية، فقد تتمكن من رؤية السبب.

مجرى التنفيذ

إذا لم تكن متأكداً من حركة مجرى التنفيذ في البرنامج، أضف تعليمات طباعة إلى بداية كل عملية تطبع رسالة مثل
"entering method foo"، حيث foo اسم العملية.

عندما تشغل البرنامج الآن فسوف يترك أثراً لكل عملية يتم استدعائها.

يمكن طباعة المتحولات التي تستقبلها كل عملية أيضاً. عندما تشغل البرنامج، تأكد أن القيم منطقية، وتحقق من عدم وقوعك في أكثر الأخطاء شيوعاً – توفير المتحولات بترتيب خاطئ.

عندما أشغل البرنامج يولد استثناء

عند حدوث استثناء، تطبع Java رسالة تتضمن اسم الاستثناء، والسطر الذي حدثت فيه المشكلة، وسجل المكدس.

يحتوي سجل المكدس (stack trace) على العملية التي كانت تعمل أثناء حدوث الخطأ، العملية التي استدعتها، والعملية التي استدعت العملية التي سبقتها، وهكذا.

الخطوة الأولى هي فحص المكان الذي حدث فيه الخطأ ومحاولة اكتشاف ما الذي جرى.

NullPointerException: لقد حاولت الولوج إلى متغير حالة أو استدعاء عملية على كائن معدوم (null object). عليك معرفة أي متغير هو المعدوم ثم معرفة السبب وراء وصوله إلى تلك الحالة. تذكر أن التصريح عن متغيرات من نوع كائني (object type)، يكون معدوماً في الحالة الابتدائية، ويظل هكذا حتى تسند له قيمة. مثلاً، هذه الشفرة تسبب NullPointerException:

Point blank;
System.out.println(blank.x);

ArrayIndexOutOfBoundsException: الدليل الذي تستخدمه للوصول إلى عناصر مصفوفة إما أن يكون سالباً أو أكبر من array.length-1. إذا عثرت على الموقع الذي حدثت تلك المشكلة فيه، أضف تعليمة طباعة قبله تماماً تطبع قيمة الدليل (index) وطول المصفوفة. هل حجم المصفوفة صحيح؟ هل قيمة الدليل صحيحة؟ الآن أعمِل طريقك رجوعاً في البرنامج وابحث عن المكان الذي جاءت منه المصفوفة وجاء منه الدليل. اعثر على أقرب تعليمة إسناد وتحقق من أنها تعمل بشكل الصحيح. إذا كان أحدهما معاملاً، اذهب إلى مكان استدعاء العملية وانظر إلى المكان الذي أتت منه القيم.

StackOverflowException: انظر "التعاود اللانهائي".

FileNotFoundException: هذا يعني أن Java لم تعثر على الملف الذي كانت تبحث عنه. إذا كنت تستخدم بيئة برمجة تعتمد المشاريع كأساس للعمل، مثل Eclipse، فقد تحتاج إلى استيراد الملف إلى المشروع. وإلا عليك التأكد أن الملف موجود فعلاً وأن مساره صحيح. هذه المشكلة تعتمد على نظام الملفات لديك.

ArithmeticException: يحدث عند حدوث خطأ أثناء عملية رياضية، أغلب الأحيان بسبب القسمة على صفر.

لقد أضفت تعليمات طباعة كثيرة لدرجة أنني غرقت بمخرجات البرنامج

إحدى المشاكل التي تحدث عند استعمال تعليمات الطباعة لتنقيح البرامج هي احتمال أن تنتهي مدفوناً بالمخرجات. يوجد طريقتين للمتابعة: إما أن تبسط الخرج، أو أن تبسط البرنامج.

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

لتبسيط البرنامج، صغر حجم المشكلة التي يعالجها البرنامج. مثلاً، إذا كنت تعمل على ترتيب مصفوفة، رتب مصفوفة صغيرة. إذا كان البرنامج يستقبل مدخلات من المستخدم، أعطه أبسط مقدار من المعطيات التي تسبب الخطأ.

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

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

يمكن أن تساعدك إعادة ترتيب البرنامج للعثور على الأخطاء الخبيثة. مثلاً، لو غيرت شيئاً في البرنامج وكنت تعتقد أن هذا التغيير لن يؤثر على سلوك البرنامج، ثم أثر عليه، فذلك قد يدلك على المشكلة.

D.3 الأخطاء المنطقية

برنامجي لا يعمل

يصعب العثور على الأخطاء المنطقية أكثر لأن المجمع ونظام التشغيل (run-time system) لا يوفران أي معلومات عن الخطأ. أنت فقط تعرف ما يفترض بالبرنامج أن يفعله، وتعرف أنه لا يفعل ذلك.

الخطوة الأولى هي إيجاد صلة بين الشفرة وبين السلوك الذي تراه. تحتاج إلى فرضية عما يحدث في البرنامج فعلاً.

إليك بعض الأسئلة لتطرحها على نفسك:

حتى تبرمج، تحتاج إلى نموذج عقلي (mental model) لما تجريه الشفرة التي تكتبها. إذا لم تعمل الشفرة كما هو متوقع، فقد لا تكون المشكلة في البرنامج؛ يمكن أن تكون المشكلة عندك.

أفضل طريقة لتقويم نموذجك العقلي تكون بتقسيم البرنامج إلى مكونات (عادة الأصناف والعمليات) واختبارها على انفراد.

إليك أكثر الأخطاء المنطقية شيوعاً لتتحقق منها:

لدي عبارة كبيرة غليظة ولا تنفذ ما أريده

كتابة العبارات المعقدة أمر محمود طالما أنها مقروءة، لكنها قد تكون صعبة التنقيح. من الجيد عادة تقسيم العبارات المعقدة إلى سلسلة من الجمل بالاستعانة بمتغيرات مؤقتة. مثلاً:

rect.setLocation(rect.getLocation().translate(
		-rect.getWidth(), -rect.getHeight()));
يمكن إعادة كتابتها كما يلي
int dx = -rect.getWidth();
int dy = -rect.getHeight();
Point location = rect.getLocation();
Point newLocation = location.translate(dx, dy);
rect.setLocation(newLocation);
النسخة المفصلة أسهل للقراءة، لأن أسماء المتغيرات توفر معلومات إضافية، وأسهل عند تصحيح الأخطاء، بسبب القدرة على التحقق من أنواع المتغيرات المؤقتة وطباعة قيمها.

من مشاكل العبارات الكبيرة الأخرى هي أن ترتيب الحساب قد لا يكون موافقاً لما تتوقعه. مثلاً، لحساب x/2π، قد تكتب

double y = x / 2 * Math.PI;
هذا ليس صحيحاً، لأن أولوية الضرب والقسمة متساوية، وسيتم تنفيذ العمليتين بحسب ترتيب ورودها من اليسار إلى اليمين. هذه العبارة تحسب xπ/2. إذا لم تكن واثقاً من ترتيب العمليات، استعمل الأقواس لتوضيحه أكثر.
double y = x / (2 * Math.PI);
هذه النسخة صحيحة، وأسهل للقراءة لمن لم يحفظ ترتيب العمليات.

عمليتي لا تعيد النتيجة المطلوبة

إذا كانت تعليمة العودة ذات عبارة معقدة، فلن تجد فرصة لطباعة قيمة ما للتحقق منها قبل إنهاء العملية. يمكنك استخدام المتغيرات المؤقتة هنا أيضاً. مثلاً، بدلاً من

public Rectangle intersection(Rectangle a, Rectangle b) {
	return new Rectangle(
		Math.min(a.x, b.x),
		Math.min(a.y, b.y),
		Math.max(a.x+a.width, b.x+b.width)-Math.min(a.x, b.x)
		Math.max(a.y+a.height, b.y+b.height)-Math.min(a.y, b.y) );
}
يمكنك كتابة
public Rectangle intersection(Rectangle a, Rectangle b) {
	int x1 = Math.min(a.x, b.x);
	int y2 = Math.min(a.y, b.y);
	int x2 = Math.max(a.x+a.width, b.x+b.width);
	int y2 = Math.max(a.y+a.height, b.y+b.height);
	Rectangle rect = new Rectangle(x1, y1, x2-x1, y2-y1);
	return rect;
}
الآن لديك فرصة لعرض أي من المتغيرات الوسيطة قبل الخروج من العملية. وبإعادة استخدام x1 وy1، أصبحت الشفرة أصغر أيضاً.

تعليمة الطباعة لا تفعل شيئاً

إذا استخدمت عملية println، سيتم عرض المخرجات فوراً، لكن إذا استخدمت print (على الأقل في بعض بيئات البرمجة) تخزن المخرجات بدون عرضها حتى الوصول إلى السطر الجديد التالي. إذا انتهى البرنامج بدون طباعة سطر جديد، فقد لا ترى الخرج المخزن أبداً.

إذا كنت تشك بأن هذا ما يحصل، غير بعض تعليمات print أو كلها إلى println.

أنا عالق فعلاً، وأحتاج إلى المساعدة حقاً

أولاً ابتعد عن الحاسوب لعدة دقائق. تبث الحواسيب موجات تؤثر على الدماغ، وتسبب الأعراض التالية:

إذا عانيت أحد هذه الأعراض، انهض وتمشى. عندما تهدأ، فكر بالبرنامج. ماذا يفعل؟ ما هي الأسباب المحتملة لهذا السلوك؟ متى كانت آخر مرة اشتغل فيها البرنامج، وما الذي فعلته بعدها؟

أحياناً يحتاج العثور على الخطأ البرمجي بعض الوقت وحسب. غالباً ما أعثر على الأخطاء عندما أترك عقلي هائماً على غير هدى. من الأماكن المناسبة للعثور على الأخطاء البرمجية القطارات، والحمامات، والسرير.

لا، أنا أحتاج المساعدة بحق

ذلك يحصل. حتى أمهر المبرمجين يعلقون. أحياناً تحتاج إلى عينين جديدتين.

قبل أن تدعو أحداً آخر لمساعدتك، تأكد من تجريب كل الأساليب الواردة أعلاه. يجب أن يكون برنامجك بسيطاً بقدر المستطاع، وعليك أن تعمل مع أبسط الحالات التي تسبب الخطأ. يجب أن تكتب تعليمات طباعة في الأماكن المناسبة (ويجب أن يكون الخرج الذي تولده مفهوماً). عليك أن تفهم المشكلة بشكل جيد حتى تصفها بدقة.

عندما تطلب أحداً ليساعدك، أعطه المعلومات التي يحتاجها.

قد ترى الحل أثناء شرحك للمشكلة إلى شخص آخر. هذه الظاهرة شائعة لدرجة أن بعض الناس ينصح بأسلوب لتصحيح الأخطاء يتم بالاستعانة ببطة مطاطية وهو يدعى بالإنكليزية ("rubber ducking"). وهذه الطريقة تتم كما يلي:

  1. اشتر بطة مطاطية من النوع العادي.
  2. عندما تواجه مشكلة حقيقية فعلاً، ضع البطة على المكتب أمامك وقل لها، "أيتها البطة، أنا عالق فعلاً في مشكلة. إليكِ ما يحدث معي..."
  3. اشرح المشكلة للبطة.
  4. استنتج الحل.
  5. اشكر البطة المطاطية.

أنا لا أمزح. انظر http://en.wikipedia.org/wiki/Rubber_duck_debugging.

لقد عثرت على الحشرة!

عندما تعثر على الخطأ البرمجي، عادة ما يكون إصلاحه واضحاً. لكن ليس دائماً. أحياناً يكون الشيء الذي تعتقد أنه خطأ برمجي عادي إشارة لعدم فهمك للبرنامج، أو لوجود خطأ في خوارزمية الحل. في هذه الحالات، قد يتوجب عليك إعادة النظر في الخوارزمية،

أو تعديل النموذج العقلي للبرنامج. خذ بعض الوقت بعيداً عن الحاسب للتفكير، نفذ حالات الاختبار يدوياً، أو ارسم مخططات تمثل الحسابات.

بعد إصلاح الخطأ البرمجي، لا تنطلق مباشرة لصنع المزيد من الأخطاء. خذ دقيقة لتفكر: ما كان نوع الخطأ، لم حدث هذا الخطأ، كيف خبّـأ الخطأ نفسه، ما الذي أمكنك فعله حتى تجده أسرع. عندما ترى شيئاً مشابهاً في المرة القادمة، ستتمكن من العثور على الخطأ بسرعة أكبر.

السابقالفهرسالتالي