Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

بنية التحكم في التدفق match (The match Control Flow Construct)

تمتلك Rust بنية تحكم في التدفق (control flow construct) قوية للغاية تسمى match تسمح لك بمقارنة قيمة مقابل سلسلة من الأنماط (patterns) ثم تنفيذ الكود بناءً على النمط الذي يتطابق. يمكن أن تتكون الأنماط من قيم حرفية (literal values)، وأسماء متغيرات، وعلامات بديلة (wildcards)، وأشياء أخرى كثيرة؛ يغطي الفصل 19 جميع الأنواع المختلفة من الأنماط وما تفعله. تأتي قوة match من التعبيرية في الأنماط وحقيقة أن المترجم (compiler) يؤكد أن جميع الحالات الممكنة قد تمت معالجتها.

فكر في تعبير match كما لو كان آلة لفرز العملات المعدنية: تنزلق العملات المعدنية على مسار به ثقوب ذات أحجام مختلفة، وتسقط كل عملة في أول ثقب تصادفه وتناسب حجمه. وبنفس الطريقة، تمر القيم عبر كل نمط في match وعند أول نمط “تناسبه” القيمة، تسقط القيمة في كتلة الكود المرتبطة بها لاستخدامها أثناء التنفيذ.

وبالحديث عن العملات المعدنية، دعنا نستخدمها كمثال باستخدام match! يمكننا كتابة دالة تأخذ عملة أمريكية غير معروفة، وبطريقة مماثلة لآلة العد، تحدد نوع العملة وترجع قيمتها بالسنتات، كما هو موضح في القائمة 6-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,
    }
}

fn main() {}

دعنا نحلل match في دالة value_in_cents. أولاً، ندرج الكلمة المفتاحية match متبوعة بتعبير، وهو في هذه الحالة القيمة coin. يبدو هذا مشابهًا جدًا للتعبير الشرطي المستخدم مع if ولكن هناك فرق كبير: مع if يجب أن يتم تقييم الشرط إلى قيمة منطقية (Boolean)، ولكن هنا يمكن أن يكون من أي نوع. نوع coin في هذا المثال هو التعداد Coin (enum) الذي عرفناه في السطر الأول.

بعد ذلك تأتي أذرع الـ match (match arms). يتكون الذراع (arm) من جزأين: نمط وكود. الذراع الأول هنا يحتوي على نمط هو القيمة Coin::Penny ثم عامل التشغيل => الذي يفصل بين النمط والكود المراد تشغيله. الكود في هذه الحالة هو مجرد القيمة 1. يتم فصل كل ذراع عن التالي بفاصلة.

عندما يتم تنفيذ تعبير match فإنه يقارن القيمة الناتجة مقابل نمط كل ذراع، بالترتيب. إذا تطابق النمط مع القيمة، يتم تنفيذ الكود المرتبط بهذا النمط. إذا لم يتطابق هذا النمط مع القيمة، يستمر التنفيذ إلى الذراع التالي، تمامًا كما هو الحال في آلة فرز العملات. يمكننا الحصول على عدد الأذرع الذي نحتاجه: في القائمة 6-3، يحتوي الـ match الخاص بنا على أربعة أذرع.

الكود المرتبط بكل ذراع هو تعبير (expression)، والقيمة الناتجة عن التعبير في الذراع المطابق هي القيمة التي يتم إرجاعها لتعبير match بالكامل.

عادة لا نستخدم الأقواس المتعرجة (curly brackets) إذا كان كود ذراع المطابقة قصيرًا، كما هو الحال في القائمة 6-3 حيث يرجع كل ذراع قيمة فقط. إذا كنت تريد تشغيل عدة أسطر من الكود في ذراع مطابقة، فيجب عليك استخدام الأقواس المتعرجة، وتكون الفاصلة التي تلي الذراع اختيارية حينها. على سبيل المثال، يقوم الكود التالي بطباعة “Lucky penny!” في كل مرة يتم فيها استدعاء الدالة بـ Coin::Penny ولكنه لا يزال يرجع القيمة الأخيرة في الكتلة، وهي 1:

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

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

fn main() {}

الأنماط التي ترتبط بالقيم (Patterns That Bind to Values)

ميزة أخرى مفيدة لأذرع المطابقة هي أنها يمكن أن ترتبط بأجزاء القيم التي تطابق النمط. هذه هي الطريقة التي يمكننا بها استخراج القيم من متغيرات التعداد (enum variants).

كمثال، دعنا نغير أحد متغيرات التعداد لدينا ليحمل بيانات بداخله. من عام 1999 إلى عام 2008، سكّت الولايات المتحدة أرباع دولارات (quarters) بتصميمات مختلفة لكل ولاية من الولايات الخمسين على أحد الجانبين. لم تحصل أي عملات معدنية أخرى على تصميمات الولايات، لذا فإن أرباع الدولارات فقط هي التي تمتلك هذه القيمة الإضافية. يمكننا إضافة هذه المعلومات إلى الـ enum الخاص بنا عن طريق تغيير متغير Quarter ليشمل قيمة UsState مخزنة بداخله، وهو ما فعلناه في القائمة 6-4.

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

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

fn main() {}

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

في تعبير المطابقة لهذا الكود، نضيف متغيرًا يسمى state إلى النمط الذي يطابق قيم المتغير Coin::Quarter. عندما يتطابق Coin::Quarter سيرتبط متغير state بقيمة ولاية ذلك الربع. بعد ذلك، يمكننا استخدام state في الكود الخاص بهذا الذراع، هكذا:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

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

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
        }
    }
}

fn main() {
    value_in_cents(Coin::Quarter(UsState::Alaska));
}

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

نمط match مع Option<T> (The Option<T> match Pattern)

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

لنفترض أننا نريد كتابة دالة تأخذ Option<i32> وإذا كانت هناك قيمة بداخلها، تضيف 1 إلى تلك القيمة. إذا لم تكن هناك قيمة بداخلها، يجب أن ترجع الدالة القيمة None ولا تحاول إجراء أي عمليات.

هذه الدالة سهلة الكتابة للغاية بفضل match وستبدو مثل القائمة 6-5.

fn main() {
    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);
}

دعنا نفحص التنفيذ الأول لـ plus_one بمزيد من التفصيل. عندما نستدعي plus_one(five) سيكون للمتغير x في جسم plus_one القيمة Some(5). ثم نقارن ذلك مقابل كل ذراع مطابقة:

fn main() {
    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);
}

القيمة Some(5) لا تطابق النمط None لذا نستمر إلى الذراع التالي:

fn main() {
    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);
}

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

الآن دعنا نفكر في الاستدعاء الثاني لـ plus_one في القائمة 6-5، حيث تكون x هي None. ندخل الـ match ونقارن بالذراع الأول:

fn main() {
    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);
}

إنه يتطابق! لا توجد قيمة للإضافة إليها، لذا يتوقف البرنامج ويرجع القيمة None على الجانب الأيمن من =>. ولأن الذراع الأول تطابق، لا يتم مقارنة أي أذرع أخرى.

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

المطابقات شاملة (Matches Are Exhaustive)

هناك جانب آخر لـ match نحتاج إلى مناقشته: يجب أن تغطي أنماط الأذرع جميع الاحتمالات. فكر في هذا الإصدار من دالة plus_one الذي يحتوي على خطأ (bug) ولن يترجم:

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

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

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

$ 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
 --> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:593:1
 ::: /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:597:5
  |
  = note: not covered
  = 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` (bin "enums") due to 1 previous error

تعرف Rust أننا لم نغطِ كل حالة ممكنة بل وتعرف النمط الذي نسيناه! المطابقات في Rust شاملة (exhaustive): يجب أن نستنفد كل الاحتمالات الأخيرة لكي يكون الكود صالحًا. خاصة في حالة Option<T> عندما تمنعنا Rust من نسيان التعامل صراحة مع حالة None فإنها تحمينا من افتراض أن لدينا قيمة بينما قد يكون لدينا null، مما يجعل “خطأ المليار دولار” الذي نوقش سابقًا مستحيلاً.

أنماط الالتقاط الشامل والعلامة البديلة _ (Catch-All Patterns and the _ Placeholder)

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

fn main() {
    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) {}
}

بالنسبة لأول ذراعين، الأنماط هي القيم الحرفية 3 و 7. بالنسبة للذراع الأخير الذي يغطي كل قيمة ممكنة أخرى، النمط هو المتغير الذي اخترنا تسميته other. الكود الذي يعمل لذراع other يستخدم المتغير عن طريق تمريره إلى دالة move_player.

يترجم هذا الكود، على الرغم من أننا لم ندرج جميع القيم الممكنة التي يمكن أن يمتلكها u8 لأن النمط الأخير سيطابق جميع القيم غير المدرجة بشكل محدد. يلبي نمط الالتقاط الشامل (catch-all pattern) هذا متطلب أن يكون match شاملاً. لاحظ أنه يجب علينا وضع ذراع الالتقاط الشامل في النهاية لأن الأنماط يتم تقييمها بالترتيب. إذا وضعنا ذراع الالتقاط الشامل في وقت سابق، فلن تعمل الأذرع الأخرى أبدًا، لذا ستحذرنا Rust إذا أضفنا أذرعًا بعد الالتقاط الشامل!

تمتلك Rust أيضًا نمطًا يمكننا استخدامه عندما نريد التقاطًا شاملاً ولكن لا نريد استخدام القيمة في نمط الالتقاط الشامل: _ هو نمط خاص يطابق أي قيمة ولا يرتبط بتلك القيمة. هذا يخبر Rust أننا لن نستخدم القيمة، لذا لن تحذرنا Rust بشأن متغير غير مستخدم.

دعنا نغير قواعد اللعبة: الآن، إذا حصلت على أي شيء آخر غير 3 أو 7، يجب عليك الرمي مرة أخرى. لم نعد بحاجة إلى استخدام قيمة الالتقاط الشامل، لذا يمكننا تغيير كودنا لاستخدام _ بدلاً من المتغير المسمى other:

fn main() {
    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() {}
}

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

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

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

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

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

هناك المزيد حول الأنماط والمطابقة سنغطيه في الفصل 19. في الوقت الحالي، سننتقل إلى بناء جملة if let والذي يمكن أن يكون مفيدًا في المواقف التي يكون فيها تعبير match مطولاً قليلاً.