كيف يمكننا الاختيار ما بين استدعاء الماكرو panic!
وإعادة القيمة Result
عند حدوث الأخطاء؟ عندما تهلع الشيفرة البرمجية (أي عند استدعاء الماكرو panic!
)، فليس هناك أي طريقة لحل ذلك الخطأ، ويمكنك استدعاء panic!
لأي خطأ كان.
وسواءٌ كان خطأ يُمكن حلّه أو لا، فأنت من يتخذ القرار بجعل الخطأ هذا قابلًا للحل في شيفرتك البرمجية أم لا؛ فعندما تختار إعادة القيمة Result
، فأنت تحاول منح الشيفرة البرمجية التي استدعت ذلك الفعل الذي تسبب بالخطأ (دالةٌ ما) بعض الخيارات للتعامل مع ذلك الخطأ بحيث تحاول الشيفرة البرمجية حله بطريقة ملائمة لكل حالة، أو أن تحدد أن قيمة الخطأ Err
غير قابلة للحل مما يتسبب باستدعاء الماكرو panic!
وبالتالي جعل الخطأ القابل للحل خطأ غير قابل للحل. إذًا، إعادة القيمة Result
هي خيار افتراضي جيّد عندما تعرّف دالة ما قد تفشل في بعض الأحيان.
من المحبّذ في حالات كتابة الأمثلة والشيفرات البرمجية التجريبية كتابة شيفرة برمجية تهلع بدلًا من إعادة القيمة Result
، دعنا ننظر إلى السبب ومن ثم نناقش الحالات التي قد لا يستطيع فيها المصرّف إخبارنا بإمكانية حدوث خطأ ما، إلا أنك تستطيع فعل ذلك. سنختتم أيضًا هذا المقال بتوجيهات عامة تساعدك في تحديد الحالات التي يكون فيها جعل الشيفرة البرمجية تهلع بصورةٍ أفضل.
أمثلة وشيفرات برمجية تجريبية واختبارات
استخدام شيفرة برمجية تتعامل مع الأخطاء بصورةٍ جيدة عندما تكتب مثالًا ما لتوضيح مفهوم معين يجعل من المثال أقل وضوحًا، ونستدعي عادةً في الأمثلة تابعًا مثل unwrap
يمكن أن يهلع مثل موضع مؤقت placeholder من أجل الطريقة التي تريد التعامل مع الأخطاء في تطبيقك، والتي قد تختلف بناءً على ما تفعله بقية الشيفرة البرمجية.
التابعان unwrap
و expect
مفيدان جدًا أيضًا عند كتابة الشيفرات البرمجية التجريبية قبل أن تتخذ قرار بخصوص التعامل مع الأخطاء، إذ يساعدك التابعان بترك علامات واضحة في شيفرتك البرمجية انتظارًا للوقت الذي تريد فيه جعل برنامجك أكثر متانةً بالتعامل مع الأخطاء.
إذا فشل استدعاء تابع ما ضمن اختبار فمن الأفضل جعل كل الاختبار يفشل حتى لو كان التابع ذلك غير مضمّنٍ في الاختبار الأساسي، ولأن الماكرو panic!
هو بمثابة إشارة إلى أن الاختبار سيفشل، فإن استدعاء unwrap
أو expect
هو ما يجب حدوثه.
الحالات التي تعرف فيها معلومات أكثر من المصرف
من الملائم أيضًا استدعاء unwrap
أو expect
عند تواجد منطق ما يضمن أن Result
ستحتوي على قيمة Ok
إلا أن هذا المنطق لا يمكن فهمه من قبل المصرّف، إذ ستتواجد قيمة Result
بحاجة للتعامل معها وفحصها، فأي عملية تستدعيها قد تفشل عمومًا على الرغم من أن الأمر مستحيل منطقيًا في هذه الحالة. من المقبول استدعاء unwrap
إذا استطعت أن تتأكد بفحص الشيفرة البرمجية يدويًا أنها لن تحتوي على متغاير Err
أبدًا، والأفضل في هذه الحالة أن توثّق السبب الذي تعتقد أنك لن تحصل فيه على متغاير Err
في نص expect
. إليك مثالًا:
fn main() { use std::net::IpAddr; let home: IpAddr = "127.0.0.1" .parse() .expect("Hardcoded IP address should be valid"); }
نُنشئ هنا نسخةً من IpAddr
بالمرور على السلسلة النصية المكتوبة في الشيفرة البرمجية، ويمكن ملاحظة أن "127.0.0.1" هو عنوان IP صالح وبالتالي من المقبول استخدام expect
هنا، إلا أن وجود سلسلة نصية صالحة مكتوبة في الشيفرة البرمجية لا يغير من النوع المعاد للتابع parse
، إذ أننا ما زلنا نحصل على قيمة Result
وسيجبرنا المصرف على التعامل مع Result
لأنه يفترض أن وجود المتغاير Err
بداخل Result
أمر ممكن الحدوث وذلك لأن المصرف ليس ذكي بالقدر الكافي ليرى أن السلسلة النصية تمثل عنوان IP صالح دومًا.
يوجد احتمال بوجود المتغاير Err
إذا أتى عنوان IP مثل دخل من المستخدم بدلًا من كتابته مباشرةً في الشيفرة البرمجية وعندها علينا أن نتعامل مع Result
بطريقة شاملة بدلًا من الطريقة الحالية. ذكر الافتراض أن عنوان IP هذا مكتوب في الشيفرة البرمجية وبالتالي سيُطلب منّا تغيير expect
للحصول على شيفرة برمجية تتعامل مع الأخطاء بصورةٍ أفضل في المستقبل، وبالتالي فنحن بحاجة للحصول على عنوان IP من مصادر أخرى.
توجيهات للتعامل مع الأخطاء
يُنصح بجعل الشيفرة البرمجية تهلع عندما يمكن أن يؤدي الخطأ إلى حالة سيئة bad state للشيفرة البرمجية، ونقصد هنا بالحالة السيئة الحالة التي يتغير فيها افتراض assumption، أو ضمان guarantee، أو عقد contract، أو ثابت invariant، مثل الحصول على قيم غير صحيحة أو متناقضة أو مفقودة، إضافةً إلى واحدة أو أكثر من الحالات التالية:
- الحالة السيئة هي حالة غير متوقعة لا تحدث عادةً، مثل إدخال المستخدم بياناته بتنسيق خاطئ.
- يجب أن تعتمد شيفرتك البرمجية بعد حدوث هذه الحالة على فحص المشكلة بعد كل خطوة بدلًا من اتخاذ معطيات الحالة السيئة هذه.
- لا توجد طريقة مثالية لتمثيل هذه البيانات في الأنواع التي تستخدمها، وسننظر إلى مثال على ذلك في المقالات القادمة.
إذا استدعى أحدهم شيفرتك البرمجية ومرّر لها القيم التي تؤدي إلى حالة سيئة، فمن الأفضل إعادة الخطأ إذا استطعت، حتى يقرّر مستخدم المكتبة الإجراء الذي يريد اتخاذه في هذه الحالة، إلا أن الاستمرار بتنفيذ الشيفرة البرمجية في بعض الأحيان قد يكون غير آمن أو ضار، وفي هذه الحالة يكون استدعاء الماكرو panic!
أفضل خيار لتنبيه المستخدم الذي يستخدم المكتبة بالخطأ الموجود في شيفرته البرمجية وكيف يستطيع إصلاحه خلال عملية التطوير. استخدام panic!
أيضًا مناسب في حال استدعاء الشيفرة البرمجية الخارجية التي لا تستطيع التحكم بها وإعادة حالة غير صالحة لا يمكنك إصلاحها.
من الملائم عند الحالات التي تتوقع فيها حدوث فشل أن تُعيد القيمة Result
بدلًا من استدعاء panic!
، مثل تمرير بيانات مشوّهة وتحليلها، أو طلب HTTP يُعيد حالة تُشير إلى وصول حدٍ معدّل ما rate limit، وفي هذه الحالة تُشير إعادة القيمة Result
إلى أن الخطأ الذي حصل هو خطأ متوقع حدوثه عند استدعاء الشيفرة البرمجية، التي ستحدّد كيفية التعامل مع الخطأ.
يجب أن تتأكد شيفرتك البرمجية من القيم الصالحة أولًا وتهلع إذا لم تكن القيم صالحةً عندما تُجري الشيفرة عمليةً يمكن أن تضع المستخدم في خطرٍ ما عند استدعائها باستخدام قيم غير صالحة، وذلك لأسباب تتعلق بالأمان، إذ أن محاولة إجراء عمليةٍ على بيانات غير صالحة قد تعرّض شيفرتك البرمجية لثغرات أمنية، وهذا هو السبب الرئيس لاستدعاء المكتبة القياسية للماكرو panic!
إذا حاولت الوصول إلى عنصر يقع خارج حدود الذاكرة out-of-bound memory access، مثل محاولة الوصول إلى حيز ذاكرة لا ينتمي إلى هيكل البيانات الحالي، وهي مشكلة أمنية شائعة.
تحتوي الدوال عادةً على ما يُدعى بالعقود contracts، إذ أن تصرُّف الدوال مضمونٌ فقط في حال كان الدخل يوافق بعض المتطلبات، فإذا أُخلّ بالعقد، سنحصل على حالة هلع وهذا الأمر منطقي، لأن الإخلال بالعقد يعني أن هناك خطأ من طرف مستدعي الشيفرة البرمجية، وهو نوع من الأخطاء لا تجد للشيفرة البرمجية المستدعية ضرورةً لمعالجته، وفي الحقيقة لا توجد هناك أي طريقة منطقية حتى تتعافى الشيفرة البرمجية المُستدعية، إذ يقع إصلاح الخطأ على عاتق المبرمج الذي استدعى هذه الشيفرة البرمجية. تسبب عقود الدوال عند خرقها حالة هلع، وتُشرح العقود عادةً في توثيق الواجهة البرمجية API الخاص بالدالة.
الحصول على الكثير من الأخطاء في جميع الدوال أمرٌ رتيب ومزعج، ولحس الحظ يمكننا استخدام نظام أنواع لغة رست (وبالتالي التحقق من الأنواع من المصرّف) لإجراء العديد من الفحوصات نيابةً عنك.
إذا كانت الدالة تقبل نوعًا محددًا للمعامل، فيمكنك متابعة منطق شيفرتك البرمجية على أساس هذا النوع وأنت مطمئن، لأن المصرف سيتأكد من استخدام النوع الصحيح دومًا، فعلى سبيل المثال سيتوقع برنامجك قيمةً ما بدلًا من عدم وجود قيمة إذا كان لديك نوع معيّن بدلًا من Option
، وعندها لن يتوجب على شيفرتك البرمجية أن تتعامل مع متغاير Some
و None
على حدى، بل ستتحقق من حالة واحدة فقط وهو وجود قيمة، وبالتالي لن تستطيع تصريف أي شيفرة برمجية لا تمرر قيمة إلى الدالة ولن يتوجب عندها التحقق من أن الدالة تحصل على قيمة خلال وقت التشغيل.
وفي مثال على ما سبق، يمكن الإشارة إلى استخدام نوع العدد الصحيح عديم الإشارة unsigned integer مثل u32
، وهذا يضمن لك أن تكون قيمة المعامل موجبةً دومًا.
إنشاء أنواع مخصصة بهدف التحقق
دعنا نتطرق إلى فكرة استخدام نظام أنواع رست للتأكد من أن القيمة صالحة، وذلك بإنشاء نوع مخصص للتحقق.
تذكّر لعبة تخمين الأرقام التي أنشأناها سابقًا، إذ طلبنا من المستخدم حينها تخمين رقم يقع بين 1 و100، ولم نتحقق حينها من أن تخمين المستخدم كان ضمن هذا المجال قبل أن نتحقق من أن التخمين يطابق الرقم السري (الإجابة الصحيحة)، إذ أننا تأكدنا وقتها من أن القيمة موجبة فقط، وفي هذه الحالة فإن البرنامج لن يكون خاطئًا، لأن طباعة "تخمينك كبير" أو "تخمينك صغير" سيعمل كم هو مطلوب، إلا أنه من الأفضل تحسين البرنامج بحيث يرشد المستخدم نحو تخمين صحيح في حال كان تخمينه ضمن المجال ويختلف سلوك البرنامج في حال كان التخمين خارج المجال المطلوب، كما هو الحال عندما يكتب المستخدم أحرفًا بدل أرقام.
نستطيع تحقيق ذلك عن طريق النظر إلى تخمين المستخدم على أنه نوع i32
بدلًا من u32
للسماح بقيم سالبة ومن ثم التأكد من أن الرقم متواجد ضمن المجال كما يلي:
use rand::Rng; use std::cmp::Ordering; use std::io; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1..=100); loop { // --snip-- println!("Please input your guess."); let mut guess = String::new(); io::stdin() .read_line(&mut guess) .expect("Failed to read line"); let guess: i32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; if guess < 1 || guess > 100 { println!("The secret number will be between 1 and 100."); continue; } match guess.cmp(&secret_number) { // --snip-- Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { println!("You win!"); break; } } } }
يتحقق تعبير if
فيما إذا كانت القيمة خارج المجال ويخبر المستخدم بالمشكلة، ثم يستدعي continue
للبدء بالتكرار التالي من الحلقة لسؤال المستخدم عن التخمين مرةً أخرى. يمكننا الاستمرار بتنفيذ المقارنات بعد تعبير if
وذلك بين guess
والرقم السري بمعرفة أن guess
الآن هي ضمن المجال من 1 إلى 100، إلا أن الحل السابق ليس بالحل المثالي، فوجود عملية تحقق مثل هذه في برنامج يعمل فقط على القيم التي تقع ما بين 1 و100 عملية رتيبة، إذ علينا تكرار عملية التحقق داخل كل دالة في هذا البرنامج، كما قد يؤثر ذلك على أداء البرنامج.
يمكننا إنشاء نوع جديد عوضًا عمّا سبق وأن نضع عملية التحقق في دالة، ومن ثم يمكننا إنشاء نسخة عن النوع بدلًا من تكرار عملية التحقق في كل مكان، وبهذه الطريقة يصبح استخدام الدوال للنوع الجديد أكثر أمانًا في بصمتها signature، إذ أنها ستستخدم القيم المسموحة في البرنامج فقط. توضح الشيفرة 13 طريقةً لتعريف النوع Guess
الذي يُنشئ نسخةً من Guess
إذا استقبلت الدالة new
قيمةً ما بين 1 و100.
#![allow(unused)] fn main() { pub struct Guess { value: i32, } impl Guess { pub fn new(value: i32) -> Guess { if value < 1 || value > 100 { panic!("Guess value must be between 1 and 100, got {}.", value); } Guess { value } } pub fn value(&self) -> i32 { self.value } } }
الشيفرة 13: نوع Guess
سيستمر فقط في حال كانت القيم بين 1 و100
نعرّف هيكلًا أولًا باسم Guess
يحتوي على الحقل field value
الذي يخزن بداخله قيمة من النوع i32
، وهو الحقل الذي سنخزّن فيه تخمين المستخدم. نطبّق بعدها دالةً مرتبطة بالهيكل Guess
تدعى new
، إذ تُنشئ هذه الدالة نسخةً من قيم Guess
وهي معرفة بحيث تتلقى معاملًا واحدًا يُدعى value
نوعه i32
وأن تعيد هيكلًا من النوع Guess
.
تفحص الشيفرة البرمجية داخل الدالة new
القيمة value
لتتأكد من أنها تقع ما بين 1 و100، وإذا لم تحقق value
هذا الشرط، نستدعي الماكرو panic!
الذي سينبّه المبرمج الذي يكتب الشيفرة البرمجية المُستدعية للدالة أن شيفرته البرمجية تحتوي على خطأ يجب إصلاحه لأن إنشاء Guess
بقيمة value
خارج النطاق المحدد سيخرق الاتفاق الذي تعتمد عليه الدالة Guess::new
.
يجب أن تُناقش الحالات التي قد تهلع فيها الدالة Guess::new
في توثيق الواجهة البرمجية العلني الخاص بالدالة، وسنغطّي اصطلاحات التوثيق التي تُشير فيها لاحتمال حصول حالة هلع panic!
في توثيق الواجهة البرمجية API لاحقًا.
نُنشئ هيكل Guess
جديد بحقل value
قيمته مساوية إلى المعامل value
ونُعيد Guess
إذا لم تتخطى value
الاختبار، ثم نطبّق تابعًا يدعى value
يستعير self
ولا يمتلك أي معاملات أخرى، ويعيد قيمةً من نوع i32
، ويُدعى هذا النوع من التوابع بالجالب getter لأن الهدف منه يكمن في الحصول على بيانات من حقوله وإعادتها.
هذا التابع العام public method مهم لأن الحقل value
في الهيكل Guess
هو هيكل خاص private، ومن المهم لحقل value
أن يكون خاصًا بحيث لا يُسمح للشيفرة البرمجية التي تستخدم الهيكل Guess
بأن تضبط قيمة value
مباشرةً؛ إذ يجب على الشيفرة البرمجية التي تقع خارج الوحدة module أن تستخدم الدالة Guess::new
لإنشاء نسخة من Guess
للتأكد من أنه لا توجد أي طريقة أن يحتوي الهيكل Guess
على قيمة حقل value
دون فحصها ومطابقتها للشروط ضمن الدالة Guess::new
.
يمكن لدالة تحتوي معاملًا أو تعيد أرقام ضمن المجال 1 إلى 100 التصريح ضمن بصمتها بأنها أنها تأخذ أو تعيد Guess
بدلًا عن i32
وعندها لن تحتاج الدالة إلى إجراء أي عمليات تحقّق إضافية ضمنها.
ترجمة -وبتصرف- لقسم من الفصل Error Handling من كتاب The Rust Programming Language.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.