لدى رست بنية 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.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.