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

الاختيار ما بين الماكرو panic!‎ والنوع Result للتعامل مع الأخطاء في لغة Rust


Naser Dakhel

كيف يمكننا الاختيار ما بين استدعاء الماكرو 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.

اقرأ أيضًا


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...