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

بنية match للتحكم بسير برامج لغة رست Rust


Naser Dakhel

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

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

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

[الشيفرة 3: تعداد enum وتعبير match يحتوي على جميع متغايرات التعداد مثل أنماط]

دعنا نشرح التعبير match بالتفصيل في الدالة value_in_cents؛ إذ يبدأ أولًا بكتابة الكلمة المفتاحية match متبوعة بتعبير وهو قيمة القطعة النقدية coin في هذه الحالة، ويبدو هذا الأمر مشابهًا لاستخدام تعابير if إلا أن هناك فارقًا كبيرًا؛ إذ يحتاج تعبير if أن يُعيد قيمة بوليانية boolean إلا أن التعبير هنا يمكن أن يُعيد قيمةً من أي نوع، فعلى سبيل المثال نحصل هنا على القيمة coin من نوع التعداد Coin الذي عرفناه في السطر الأول.

ننتقل الآن إلى أذرع arms البنية match، إذ يكون للذراع جزءان: نمط وشيفرة برمجية. يحتوي الذراع الأول هنا على نمط من النوع Coin::Penny ومن ثم العامل <= الذي يفصل ما بين النمط والشيفرة البرمجية الواجب تنفيذها، والشيفرة البرمجية في هذه الحالة هي فقط القيمة "1"، ومن ثم يُفصل كل ذراع من الذراع الذي يليه باستخدام الفاصلة.

تُقارن نتيجة التعبير match عند تنفيذه مع نمط كل ذراع بالترتيب؛ فإذا كان النمط يوافق القيمة تُنفّذ الشيفرة البرمجية المرتبطة بذلك النمط؛ وإذا لم يوافق القيمة يستمر تنفيذ البرنامج ليفحص الذراع التالية كما هو الحال في آلة عد القطع النقدية. يمكننا إضافة أذرع أخرى بقدر ما نريد، إذ تحتوي match في الشيفرة 3 على أربعة أذرع.

الشيفرة البرمجية المرتبطة بكل ذراع هي تعبير، ونتيجة ذلك التعبير في الذراع الموافقة للقيمة هي القيمة التي تُعاد من تعبير match الكامل.

لا تُستخدم عادةً الأقواس المعقوصة curly brackets، إذا كانت الشيفرة البرمجية في الذراع قصيرة كما هو الحال في الشيفرة 3، إذ تُعيد الشيفرة قيمة واحدة فقط مباشرةً، إلا أنه يجب علينا استخدام الأقواس المعقوصة إذا أردنا تنفيذ عدة أسطر برمجية داخل ذراع البنية match، ويكون استخدام الفاصلة بعد الذراع عندئذٍ اختياريًا. على سبيل المثال، تطبع الشيفرة البرمجية في المثال التالي "Lucky penny!‎" كل مرة يُستدعى فيها التابع باستخدام القيمة Coin::Penny إلا أنها ما زالت تُعيد أيضًا القيمة "1" في نهاية الكتلة البرمجية:

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

الأنماط المرتبطة مع القيم

لذراع البنية match ميزةٌ مفيدةٌ أخرى، وهي قدرة ربط أجزاء من القيمة لتوافق النمط، وهذه هي الطريقة المتبعة عندما نريد الحصول على قيم من متغايرات التعدادات. دعنا نعدل مثلًا متغايرات التعداد السابق بحيث نستطيع تخزين بعض البيانات داخله. سكّت الولايات المتحدة من العام 1999 إلى 2008 قطعًا نقدية بقيمة ربع دولار بتصاميم مختلفة لكلٍ من الولايات الخمسين على أحد الجوانب، ولا يوجد أي قطع نقدية أخرى تحتوي على تصاميم خاصة بالولايات. إذًا، نحتاج وضع القيمة الإضافية فقط في الأرباع، ويمكننا إضافة هذه المعلومة داخل التعداد enum بتغيير المتغاير Quarter بحيث يحتوي على القيمة UsState مخزنةً بداخله وتلك العملية موضحة في الشيفرة 4.

#[derive(Debug)] // حتى نستطيع معاينة الولاية لاحقًا
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

[الشيفرة 4: تعداد Coin يحتوي فيه المتغاير Quarter على القيمة UsState]

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

نُضيف في تعبير match ضمن هذه الشيفرة البرمجية متغيرًا يدعى state إلى النمط الذي يطابق قيمة المتغاير Coin::Quarter وعندما تتطابق القيمة مع النوع Coin::Quarter يُربَط المتغير state مع قيمة ولاية الربع، ثم يصبح بإمكاننا استخدام state في الشيفرة البرمجية ضمن الذراع الخاصة بها كما يلي:

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        }
    }
}

سيأخذ المتغير coin القيمة Coin::Quarter(UsState::Alaska)‎ إذا استدعينا value_in_cents(Coin::Quarter(UsState::Alaska))‎ وعندما نقارن تلك القيمة مع كل من أذرع البنية match لن يُطابق أي منهم القيمة وسنصل إلى Coin::Quarter(state)‎ وبحلول تلك النقطة سيكون ربط القيمة state مع النوع UsState::Alaska ومن ثم يمكننا استخدام ذلك الربط في تعبير println!‎، مما سيسمح لنا بالحصول على قيمة الولاية الداخلية من متغاير Quarter ضمن التعداد Coin.

المطابقة مع Option

أردنا سابقًا الحصول على القيمة T الداخلية من الحالة Some عند استخدام Option<T>‎، إلا أنه يمكننا أيضًا التعامل مع Option<T>‎ باستخدام match كما فعلنا في معدد Coin؛ إذ سنقارن متغايرات Option<T>‎ عوضًا عن مقارنة العملات، إلا أن طريقة عمل تعبير match ستبقى كما هي.

دعنا نقول أننا نريد كتابة دالة تأخذ Option<i32>‎ مثل وسيط وتُضيف إلى قيمته "1" إذا كان هناك قيمةٌ داخله، وإذا لم يحتوي الوسيط على قيمة يجب أن تُعيد الدالة القيمة None وألّا تُجري أي عمليات أخرى.

الدالة سهلة الكتابة جدًا وذلك بفضل match وستبدو كما هو موضح في الشيفرة 5.

    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);

[الشيفرة 5: دالة تستخدم تعبير match على المتغير Option‎]

دعنا نفحص التنفيذ الأول للدالة plus_one بالتفصيل، إذ يأخذ المتغير x الموجود داخل دالة plus_one القيمة Some(5)‎ عند الاستدعاء plus_one(five)‎ ومن ثم نقارن تلك القيمة مع كل ذراع ضمن match.

            None => None,

لا تُطابق القيمة Some(5)‎ النمط الأول None التالي، لذا نستمر بمحاولة النمط الذي يليه.

            Some(i) => Some(i + 1),

هل يُطابق Some(5)‎ النمط Some(i)‎؟ نعم. لدينا المتغاير ذاته وترتبط i بالقيمة التي تحتويها Some وبالتالي يأخذ i القيمة 5. تُنفّذ الشيفرة البرمجية الموجودة في ذراع match الموافقة، وبالتالي نُضيف "1" إلى قيمة i ونُنشأ قيمة Some جديدة باستخدام الناتج "6" الإجمالي داخله.

دعنا ننظر إلى الاستدعاء الثاني للدالة plus_one في الشيفرة 5، إذ يأخذ المتغير x القيمة None. ندخل البنية match ونُقارن مع الذراع الأول.

            None => None,

حصلنا على مطابقة. ليس هناك أي قيمة لنضيفها لذا يتوقف البرنامج ويُعيد القيمة None على يمين المعامل <= ولا تحدث أي مقارنة أخرى لأننا حصلنا على مطابقة مع الذراع الأولى.

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

يجب أن تكون بنى match شاملة

هناك جانب آخر من البنية match لم نناقشه بعد. ألقِ نظرةً على الإصدار التالي من دالة plus_one الذي يحتوي على خطأ برمجي ولن يُصرّف:

    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            Some(i) => Some(i + 1),
        }
    }

لم نشمل حالة None في بنية match السابقة وبالتالي ستتسبب شيفرتنا السابقة بخطأ برمجي، إلا أن رست تنبهنا على هذا الخطأ لحسن الحظ، إذ أنك ستحصل على رسالة الخطأ التالية إذا حاولت تصريف الشيفرة البرمجية:

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
   --> src/main.rs:3:15
    |
3   |         match x {
    |               ^ pattern `None` not covered
    |
note: `Option<i32>` defined here
    = note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
    |
4   ~             Some(i) => Some(i + 1),
5   ~             None => todo!(),
    |

For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` due to previous error

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

التعامل مع جميع الأنماط والمحرف _ المؤقت

يمكننا أن نحدد تنفيذ بعض الأمور المميزة لبعض القيم في التعدادات بينما تُنفَّذ شيفرةٌ برمجيةٌ محددة لجميع القيم الممكنة الأخرى. تخيل أننا نبرمج لعبة نرمي فيها النرد وعندما نحصل على القيمة 3 لا يتحرك اللاعب بل يحصل على قبعة جميلة جديدة، بينما إذا حصلت على 7 فإن اللاعب يفقد تلك القبعة الجميلة، ويتحرك اللاعب بباقي الحالات وفقًا للقيمة التي حصلنا عليها من النرد على الرقعة. نحاول تطبيق منطق اللعبة السابقة باستخدام البنية match بحيث يكون نتيجة النرد مكتوبة صراحةً عوضًا عن الحصول عليها عشوائيًا، ونمثل جميع الدوال الأخرى دون متن لها، وذلك لأن التطبيق الفعلي لكل منها خارج نطاق نقاشنا هنا:

    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        other => move_player(other),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn move_player(num_spaces: u8) {}

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

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

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

دعنا نغيّر من قوانين اللعبة بحيث يجب عليك إعادة رمي النرد مجددًا إذا حصلت على نتيجة مغايرة عن 3 أو 7، ولا نحتاج هنا لاستخدام القيم صراحةً وكل ما علينا هو وضع المحرف المميز _ بدلًا من اسم المتغيّر other:

    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => reroll(),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn reroll() {}

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

يمكننا استخدام قيمة الواحدة unit value (نوع الصف الفارغ empty tuple type الذي ذكرناه سابقًا) مع ذراع المحرف المميز _، إذا أردنا تغيير قوانين اللعبة مجددًا بحيث لا يحصل أي شيء عندما تحصل على قيمة مغايرة عن القيمة 3 أو 7 :

    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => (),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}

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

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

التحكم بسير البرنامج باستخدام if let

تسمح لك if let بجمع كل من if و let بطريقة أكثر اختصارًا مقارنةً باستخدام الأنماط و match مع تجاهل القيم الأخرى التي لا تهمنا. ألقِ نظرةً على البرنامج الموجود في الشيفرة 6 الذي يُطابق قيمة <Option<u8 في المتغير config_max بحثًا عن قيمة واحدة ألا وهي متغاير Some.

    let config_max = Some(3u8);
    match config_max {
        Some(max) => println!("The maximum is configured to be {}", max),
        _ => (),
    }

[الشيفرة 6: بنية match تنفذ شيفرة برمجية فقط في حالة الحصول على القيمة Some]

إذا كانت القيمة هي Some، نطبع القيمة داخل المتغاير Some من خلال ربطها مع المتغير max في النمط، إلا أننا لا نريد فعل أي شيء إذا حصلنا على القيمة None ولتحقيق شرط البنية match بكونها تشمل كل الاحتمالات نُضيف () <= _ بعد معالجة متغاير واحد، وهذا الأسلوب في الكتابة مزعج وهناك بديل أفضل.

بدلًا مما سبق، نستطيع اختصار الكتابة عن طريق استخدام if let، وتوضح الشيفرة البرمجية التالية استخدامها، إذ تؤدي الغرض ذاته مقارنةً بالشيفرة السابقة الموجودة في الشيفرة 6 باستخدام match:

    let config_max = Some(3u8);
    if let Some(max) = config_max {
        println!("The maximum is configured to be {}", max);
    }

نستخدم مع if let نمطًا وتعبيرًا مفصولَين بإشارة مساواة، ويعمل هذا بطريقة مماثلة للبنية match، إذ يُعطى التعبير إلى match ويمثّل النمط الذراع الأولى له، وفي هذه الحالة هو Some(max)‎، وبالتالي تُربط قيمة المتغير max إلى القيمة الموجودة داخل Some، ويمكننا بعد ذلك استخدام max داخل كتلة if let بطريقة مماثلة لاستخدامنا max في ذراع البنية match الموافقة، ولن تُنفَّذ كتلة if let إذا لم تطابق القيمة النمط الخاص بها.

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

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

يمكننا تضمين else مع if let، إذ أن كتلة else مطابقة لحالة _ في تعبير match المساوي لكل من if let و else. تذكّر مثال تعريف التعداد Coin في الشيفرة 4، إذ كان متغاير Quarter يحمل قيمةً من النوع UsState، ونستطيع استخدام تعبير match عندها إذا أردنا عدّ جميع الأرباع التي رأيناها بينما نُعلن عن ولاية كل من الأرباع كما يلي:

    let mut count = 0;
    match coin {
        Coin::Quarter(state) => println!("State quarter from {:?}!", state),
        _ => count += 1,
    }

أو يمكننا استخدام تعبير if let و else بدلًا من ذلك كما يلي:

    let mut count = 0;
    if let Coin::Quarter(state) = coin {
        println!("State quarter from {:?}!", state);
    } else {
        count += 1;
    }

إذًا، إذا صادفت موقفًا احتاج فيه برنامجك إلى المنطق الخاص بالبنية match، وكانت كتابة البنية طويلة ورتيبة تذكّر وجود if let في مخزونك أيضًا.

ترجمة -وبتصرف- لقسم من الفصل Enums and Pattern Matching من كتاب 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.


×
×
  • أضف...