مشاكل شبه المسند
في برمجة الحاسوب ، تحدث مشكلة شبه المسند عندما تفشل دالة فرعية مصممة لإرجاع قيمة مفيدة، ولكن الإشارة إلى الفشل تستخدم قيمة إرجاع صالحة في الأصل . [ 1 ] تكمن المشكلة في أن مستدعي الدالة الفرعية لا يستطيع تحديد معنى النتيجة في هذه الحالة.
مثال
تُنتج عملية القسمة عددًا حقيقيًا ، لكنها تفشل عندما يكون المقسوم عليه صفرًا . إذا أردنا كتابة دالة تُجري عملية القسمة، فقد نختار إرجاع صفر عند هذا الإدخال غير الصحيح. مع ذلك، إذا كان المقسوم صفرًا، فإن النتيجة ستكون صفرًا أيضًا. هذا يعني أنه لا يوجد عدد يمكننا إرجاعه للإشارة بشكل فريد إلى محاولة القسمة على صفر، لأن جميع الأعداد الحقيقية تقع ضمن نطاق القسمة .
الآثار العملية
تعامل المبرمجون الأوائل مع الحالات الاستثنائية المحتملة، مثل القسمة، باستخدام اصطلاحٍ يُلزم الروتين المُستدعي بالتحقق من المُدخلات قبل استدعاء دالة القسمة. وقد انطوى هذا على مشكلتين: أولاً، أنه أثقل كاهل جميع التعليمات البرمجية التي تُجري القسمة (وهي عملية شائعة جدًا)؛ ثانيًا، أنه يُخالف مبدأي " عدم تكرار نفسك" و "التغليف" ، حيث يُشير الأول إلى ضرورة حذف التعليمات البرمجية المُكررة، بينما يُشير الثاني إلى ضرورة احتواء التعليمات البرمجية المُتعلقة بالبيانات في مكان واحد (في مثال القسمة هذا، تم التحقق من المُدخلات بشكل منفصل). بالنسبة للحسابات الأكثر تعقيدًا من القسمة، قد يصعب على المُستدعي التعرّف على المُدخلات غير الصالحة؛ في بعض الحالات، قد يكون تحديد صحة المُدخلات مُكلفًا مثل إجراء الحساب بأكمله. كما يُمكن تعديل الدالة المستهدفة، وبالتالي ستتوقع شروطًا مُسبقة مُختلفة عن تلك التي يتوقعها المُستدعي؛ وسيتطلب هذا التعديل تغييرات في كل مكان يتم فيه استدعاء الدالة.
الحلول
مشكلة شبه المسند ليست مشكلة عامة بين الدوال التي يمكن أن تفشل.
استخدام اصطلاح مخصص لتفسير القيم المُعادة
إذا لم يُغطِّ نطاق دالة ما كامل المساحة المُطابقة لنوع بيانات القيمة المُعادة منها، يُمكن استخدام قيمة معروفة باستحالة الحصول عليها في ظل العمليات الحسابية العادية. على سبيل المثال، لنفترض الدالة indexالتي تأخذ سلسلة نصية وسلسلة فرعية، وتُعيد فهرس السلسلة الفرعية في السلسلة الرئيسية. إذا فشل البحث، يُمكن برمجة الدالة لإعادة القيمة -1 (أو أي قيمة سالبة أخرى)، لأن هذا لا يُمكن أن يُشير إلى نتيجة ناجحة.
لكن هذا الحل لا يخلو من المشاكل، إذ أنه يحمّل المعنى الطبيعي للدالة باتفاقية اعتباطية:
- يجب على المبرمج أن يتذكر قيم الفشل المحددة للعديد من الوظائف، والتي بالطبع لا يمكن أن تكون متطابقة إذا كانت للوظائف نطاقات مختلفة.
- قد يختار تطبيق مختلف لنفس الوظيفة استخدام قيمة فشل مختلفة، مما يؤدي إلى أخطاء محتملة عندما ينتقل المبرمجون من بيئة إلى أخرى.
- إذا كانت الوظيفة الفاشلة ترغب في إيصال معلومات مفيدة حول سبب فشلها، فإن قيمة فشل واحدة غير كافية.
- يؤدي استخدام عدد صحيح ذي إشارة إلى تقليل نطاق الفهرس الممكن إلى النصف حتى يتمكن من تخزين بت الإشارة .
- على الرغم من أن القيمة المختارة تُعد نتيجة غير صالحة لهذه العملية، إلا أنها قد تكون مدخلاً صالحاً لعمليات لاحقة. على سبيل المثال، في بايثون،
str.findتُرجع الدالة -1 إذا لم يتم العثور على السلسلة الفرعية، [ 2 ] ولكن -1 هو فهرس صالح (تبدأ الفهارس السالبة عموماً من النهاية [ 3 ] ).
إرجاع متعدد القيم
تسمح العديد من لغات البرمجة، عبر آليات مختلفة، للدالة بإرجاع قيم متعددة. إذا كان هذا متاحًا، يمكن إعادة تصميم الدالة لإرجاع قيمة منطقية تشير إلى النجاح أو الفشل، بالإضافة إلى قيمتها الأساسية. أما إذا كانت هناك احتمالات لظهور عدة أخطاء، فيمكن للدالة إرجاع رمز خطأ مُعدّد (رمز الخطأ) مع قيمتها الأساسية.
تتضمن التقنيات المختلفة لإرجاع قيم متعددة ما يلي:
- إرجاع مجموعة من القيم. هذا أمر شائع في اللغات (مثل بايثون ) التي تحتوي على نوع بيانات tuple مدمج وبنية خاصة للتعامل معها: في بايثون،
x, y = f()يتم استدعاء الدالةfالتي تُرجع زوجًا من القيم وتُسند عناصر الزوج إلى متغيرين. - كما هو الحال في لغة Common Lisp ، تُرجع الدوال قيمًا ثانوية . لكل تعبير قيمة أساسية، ولكن يمكن إرجاع قيم ثانوية للمستدعين المهتمين. على سبيل المثال،
GETHASHتُرجع الدالة قيمة المفتاح المُعطى في خريطة ترابطية ، أو قيمة افتراضية في غير ذلك. ومع ذلك، تُرجع أيضًا قيمة منطقية ثانوية تُشير إلى ما إذا تم العثور على القيمة، مما يُتيح التمييز بين حالتي "لم يتم العثور على قيمة" و"القيمة التي تم العثور عليها تُساوي القيمة الافتراضية". يختلف هذا عن إرجاع مجموعة، حيث أن القيم الثانوية اختيارية - إذا لم يهتم بها المستدعي، فيمكنه تجاهلها تمامًا، بينما تُعد القيم المُرجعة من نوع مجموعة مجرد اختصار برمجي لإرجاع قائمة وفكها، ويجب على كل مستدعي معرفة جميع العناصر المُرجعة واستخدامها. - تتيح اللغات التي تعتمد على الاستدعاء بالمرجع - أو ما يعادله، مثل الاستدعاء بالعنوان باستخدام المؤشرات - إمكانية إرجاع قيم متعددة عن طريق تحديد بعض المعاملات كمعاملات إخراج . في هذه الحالة، يمكن للدالة إرجاع قيمة الخطأ فقط، مع وجود متغير مخصص لتخزين النتيجة الفعلية التي يتم تمريرها إلى الدالة. وهذا يُشابه استخدام حالة الخروج لتخزين رمز الخطأ ، واستخدام التدفقات لإرجاع المحتوى.
- يتم استخدام نوع مختلف من معلمات الإخراج في لغات البرمجة الكائنية التي تستخدم الاستدعاء عن طريق المشاركة ، حيث يتم تمرير كائن قابل للتغيير إلى دالة، ويتم تغيير الكائن لإرجاع القيم.
- لا تحتوي لغات البرمجة المنطقية مثل برولوج على قيم إرجاع. بدلاً من ذلك، تُستخدم المتغيرات المنطقية غير المرتبطة كمعاملات إخراج، ليتم توحيدها مع القيم التي يتم إنشاؤها في استدعاء المسند.
متغير عام لحالة الإرجاع
على غرار وسيط "out"، يمكن لمتغير عام تخزين الخطأ الذي حدث (أو ببساطة ما إذا كان قد حدث خطأ).
على سبيل المثال، إذا حدث خطأ وتم الإشارة إليه (عادةً كما سبق، بقيمة غير صالحة مثل -1)، errnoيتم تعيين متغير يونكس للإشارة إلى القيمة التي حدثت. استخدام متغير عام له عيوبه المعتادة: يصبح أمان الخيوط مصدر قلق (تستخدم أنظمة التشغيل الحديثة نسخة آمنة للخيوط من errno)، وإذا تم استخدام متغير عام واحد فقط للأخطاء، فيجب أن يكون نوعه واسعًا بما يكفي لاحتواء جميع المعلومات المهمة حول جميع الأخطاء المحتملة في النظام.
الاستثناءات
تُعدّ الاستثناءات إحدى الطرق الشائعة لحل هذه المشكلة. لا تُعتبر حالة الخطأ قيمةً مُعادةً من الدالة على الإطلاق؛ إذ يتعطل مسار التحكم الطبيعي ، ويتم التعامل مع الخطأ تلقائيًا. وهي مثال على الإشارات خارج النطاق .
توسيع نوع القيمة المُعادة
أنواع هجينة تم إنشاؤها يدويًا
في لغة C ، يتمثل أحد الأساليب الشائعة، كلما أمكن، في استخدام نوع بيانات أوسع نطاقًا من النطاق المطلوب للدالة. على سبيل المثال، getchar()تُعرَّف الدالة القياسية بنوع إرجاع intوتُرجع قيمة ضمن النطاق(نطاق unsigned char) عند النجاح أو القيمة EOF( المحددة بواسطة التنفيذ ، ولكن خارج نطاق unsigned char) في نهاية الإدخال أو خطأ في القراءة.
أنواع المراجع القابلة للتصفير
في اللغات التي تستخدم المؤشرات أو المراجع، يتمثل أحد الحلول في إرجاع مؤشر إلى قيمة، بدلاً من القيمة نفسها. يمكن بعد ذلك ضبط هذا المؤشر المُعاد nullللإشارة إلى وجود خطأ. يُناسب هذا الأسلوب عادةً الدوال التي تُرجع مؤشرًا على أي حال. يتميز هذا الأسلوب بأداء أفضل من أسلوب معالجة الاستثناءات في البرمجة الكائنية التوجه [ 4 ]، مع عيب يتمثل في أن المبرمجين المهملين قد لا يتحققون من القيمة المُعادة، مما يؤدي إلى تعطل البرنامج عند استخدام مؤشر غير صالح. يُعدّ كون المؤشر فارغًا أم لا مثالاً آخر على مشكلة الشرط؛ فقد تكون القيمة الفارغة علامة تشير إلى فشل أو قيمة مؤشر تم إرجاعه بنجاح. من الأنماط الشائعة في بيئة يونكس تعيين متغير منفصل للإشارة إلى سبب الخطأ. ومن الأمثلة على ذلك دالة المكتبة القياسية للغة سيfopen() .
أنواع هجينة ضمنية
في اللغات ذات الكتابة الديناميكية ، مثل PHP و Lisp ، يتمثل النهج المعتاد في إرجاع قيمة فارغة false(null none) أو قيمة nullغير فارغة (null) عند فشل استدعاء الدالة. يعمل هذا عن طريق إرجاع نوع مختلف عن نوع الإرجاع المعتاد (وبالتالي توسيع النوع). وهو مكافئ ديناميكيًا لإرجاع مؤشر فارغ (null pointer).
على سبيل المثال، تُرجع الدالة العددية عادةً عددًا (صحيحًا أو عشريًا)، وبينما قد يكون الصفر استجابةً صحيحة، فإن القيمة false غير صحيحة. وبالمثل، قد تُرجع دالة تُرجع عادةً سلسلة نصية أحيانًا سلسلة نصية فارغة كاستجابة صحيحة، ولكنها تُرجع false في حالة الفشل. تتطلب عملية تغيير أنواع البيانات هذه عنايةً في اختبار القيمة المُرجعة: على سبيل المثال، في لغة PHP، استخدم ===(أي، متساويتان ومن نفس النوع) بدلاً من ==(أي، متساويتان بعد التحويل التلقائي للنوع). يعمل هذا فقط عندما لا يكون من المفترض أن تُرجع الدالة الأصلية قيمة منطقية، ومع ذلك يتطلب الأمر نقل معلومات حول الخطأ عبر وسائل أخرى.
أنواع هجينة صريحة
في لغة هاسكل وغيرها من لغات البرمجة الوظيفية ، من الشائع استخدام نوع بيانات بحجم مناسب للتعبير عن أي نتيجة محتملة. على سبيل المثال، يمكن كتابة دالة قسمة تُرجع النوع `A` Maybe Real، ودالة getcharأخرى تُرجع `B` Either String Char. النوع الأول هو نوع اختياري ، وله قيمة فشل واحدة فقط، وهي `A` Nothing. أما النوع الثاني فهو اتحاد مُوسَم : تكون النتيجة إما سلسلة نصية تحتوي على رسالة خطأ وصفية، أو حرفًا تمت قراءته بنجاح. يساعد نظام استنتاج الأنواع في هاسكل على ضمان تعامل الدوال المُستدعِية مع الأخطاء المحتملة. بما أن شروط الخطأ تُصبح واضحة في نوع الدالة، فإن النظر إلى توقيعها يُخبر المبرمج فورًا بكيفية معالجة الأخطاء. علاوة على ذلك، تُشكّل الاتحادات المُوسَمة وأنواع الخيارات وحدات أحادية (monads) عند تزويدها بالدوال المناسبة: يُمكن استخدام ذلك للحفاظ على ترتيب الكود من خلال نشر حالات الخطأ غير المُعالجة تلقائيًا.
مثال
يحتوي Rust على أنواع بيانات جبرية ويأتي مع الأنواع المدمجة std::result::Result<T, E>[ 5 ] و [ 6 ] .std::option::Option<T>
fn find ( key : String ) -> Option < String > { if key == "hello" { Some ( key ) } else { None } }انظر أيضاً
مراجع
- ↑ نورفيج، بيتر (1992). "حل المشكلات العامة". نماذج برمجة الذكاء الاصطناعي: دراسات حالة في لغة ليسب الشائعة . مورغان كوفمان . ص 127. ISBN 1-55860-191-0.
- ↑ "الأنواع المدمجة" . وثائق بايثون 3.10.4 .
- ↑ "إذا
iأوjسالبة، فإن الفهرس يكون نسبيًا إلى نهاية التسلسلs:يتم استبدالlen(s) + iأو ." ملاحظة حول عمليات التسلسل الشائعة (3) .len(s) + j - ↑ لماذا يجب أن تكون الاستثناءات استثنائية - مثال على مقارنة الأداء .
- ↑ فريق Rust (25 مايو 2026). "نتيجة التعداد" . doc.rust-lang.org . فريق Rust.
- ↑ فريق Rust (25 مايو 2026). "خيار التعداد" . doc.rust-lang.org . فريق Rust.
- مواضيع لغات البرمجة
