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

بناء جملة الأنماط (Pattern Syntax)

في هذا القسم، نجمع كل بناء الجملة (syntax) الصالح في الأنماط (patterns) ونناقش سبب وموعد رغبتك في استخدام كل منها.

مطابقة القيم الحرفية (Matching Literals)

كما رأيت في الفصل السادس، يمكنك مطابقة الأنماط مع القيم الحرفية (literals) مباشرة. يوفر الكود التالي بعض الأمثلة:

fn main() {
    let x = 1;

    match x {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}

يطبع هذا الكود one لأن القيمة في x هي 1. هذا syntax مفيد عندما تريد أن يتخذ الكود الخاص بك إجراءً إذا حصل على قيمة ملموسة (concrete value) معينة.

مطابقة المتغيرات المسماة (Matching Named Variables)

المتغيرات المسماة (Named variables) هي أنماط غير قابلة للدحض (irrefutable patterns) تطابق أي قيمة، وقد استخدمناها عدة مرات في هذا الكتاب. ومع ذلك، هناك تعقيد عند استخدام المتغيرات المسماة في تعبيرات match أو if let أو while let. نظرًا لأن كل نوع من هذه التعبيرات يبدأ نطاقاً (scope) جديداً، فإن المتغيرات المعلن عنها كجزء من نمط داخل هذه التعبيرات ستظلل (shadow) تلك التي لها نفس الاسم خارج هذه البنيات، كما هو الحال مع جميع المتغيرات. في القائمة 19-11، نعلن عن متغير باسم x بقيمة Some(5) ومتغير y بقيمة 10. ثم ننشئ تعبير match على القيمة x. انظر إلى الأنماط في أذرع المطابقة (match arms) و println! في النهاية، وحاول معرفة ما سيطبعه الكود قبل تشغيل هذا الكود أو القراءة أكثر.

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(y) => println!("Matched, y = {y}"),
        _ => println!("Default case, x = {x:?}"),
    }

    println!("at the end: x = {x:?}, y = {y}");
}

دعونا نستعرض ما يحدث عند تشغيل تعبير match. النمط في ذراع المطابقة الأول لا يطابق القيمة المحددة لـ x ، لذا يستمر الكود.

يقدم النمط في ذراع المطابقة الثاني متغيراً جديداً باسم y سيطابق أي قيمة داخل قيمة Some. نظرًا لأننا في scope جديد داخل تعبير match ، فهذا متغير y جديد، وليس y الذي أعلنا عنه في البداية بالقيمة 10. سيطابق ربط (binding) y الجديد هذا أي قيمة داخل Some ، وهو ما لدينا في x. لذلك، يرتبط y الجديد هذا بالقيمة الداخلية لـ Some في x. تلك القيمة هي 5 ، لذا يتم تنفيذ التعبير لهذا الذراع ويطبع Matched, y = 5.

إذا كانت x قيمة None بدلاً من Some(5) ، فلن تتطابق الأنماط في أول ذراعين، لذا كانت القيمة ستتطابق مع الشرطة السفلية (underscore). لم نقدم المتغير x في نمط ذراع underscore، لذا فإن x في التعبير لا يزال هو x الخارجي الذي لم يتم تظليله. في هذه الحالة الافتراضية، سيطبع match عبارة Default case, x = None.

عندما ينتهي تعبير match ، ينتهي نطاقه، وكذلك ينتهي نطاق y الداخلي. ينتج println! الأخير at the end: x = Some(5), y = 10.

لإنشاء تعبير match يقارن قيم x و y الخارجية، بدلاً من تقديم متغير جديد يظلل متغير y الموجود، سنحتاج إلى استخدام حارس مطابقة (match guard) شرطي بدلاً من ذلك. سنتحدث عن match guards لاحقاً في قسم “إضافة شروط باستخدام حراس المطابقة”.

مطابقة أنماط متعددة (Matching Multiple Patterns)

في تعبيرات match ، يمكنك مطابقة أنماط متعددة باستخدام syntax المسمى | ، وهو عامل “أو” (or operator) للأنماط. على سبيل المثال، في الكود التالي، نطابق قيمة x مع match arms، أولها يحتوي على خيار أو ، مما يعني أنه إذا كانت قيمة x تطابق أيًا من القيمتين في ذلك الذراع، فسيتم تشغيل كود ذلك الذراع:

fn main() {
    let x = 1;

    match x {
        1 | 2 => println!("one or two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}

يطبع هذا الكود one or two.

مطابقة نطاقات من القيم باستخدام ..= (Matching Ranges of Values with ..=)

يسمح لنا syntax المسمى ..= بالمطابقة مع نطاق شامل (inclusive range) من القيم. في الكود التالي، عندما يطابق نمط أيًا من القيم ضمن النطاق المحدد، سيتم تنفيذ ذلك الذراع:

fn main() {
    let x = 5;

    match x {
        1..=5 => println!("one through five"),
        _ => println!("something else"),
    }
}

إذا كانت x هي 1 أو 2 أو 3 أو 4 أو 5 ، فسيتم مطابقة الذراع الأول. هذا syntax أكثر ملاءمة لقيم مطابقة متعددة من استخدام عامل | للتعبير عن نفس الفكرة؛ إذا أردنا استخدام | ، فسنضطر إلى تحديد 1 | 2 | 3 | 4 | 5. تحديد النطاق (range) أقصر بكثير، خاصة إذا أردنا مطابقة، على سبيل المثال، أي رقم بين 1 و 1000!

يتحقق compiler من أن النطاق ليس فارغاً في وقت التصريف (compile time)، ولأن الأنواع الوحيدة التي يمكن لـ Rust معرفة ما إذا كان النطاق فارغاً أم لا هي char والقيم الرقمية، فإن النطاقات مسموح بها فقط مع القيم الرقمية أو char.

إليك مثال باستخدام نطاقات من قيم char:

fn main() {
    let x = 'c';

    match x {
        'a'..='j' => println!("early ASCII letter"),
        'k'..='z' => println!("late ASCII letter"),
        _ => println!("something else"),
    }
}

يمكن لـ Rust معرفة أن 'c' تقع ضمن نطاق النمط الأول وتطبع early ASCII letter.

التفكيك لتقسيم القيم (Destructuring to Break Apart Values)

يمكننا أيضاً استخدام الأنماط لتفكيك (destructure) الهياكل (structs) والتعدادات (enums) والصفوف (tuples) لاستخدام أجزاء مختلفة من هذه القيم. دعونا نستعرض كل قيمة.

الهياكل (Structs)

توضح القائمة 19-12 هيكلاً يسمى Point مع حقلين، x و y ، يمكننا تفكيكهما باستخدام نمط مع عبارة let.

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x: a, y: b } = p;
    assert_eq!(0, a);
    assert_eq!(7, b);
}

ينشئ هذا الكود المتغيرين a و b اللذين يطابقان قيم الحقلين x و y للهيكل p. يوضح هذا المثال أن أسماء المتغيرات في النمط لا يجب أن تطابق أسماء الحقول في struct. ومع ذلك، فمن الشائع مطابقة أسماء المتغيرات مع أسماء الحقول لتسهيل تذكر المتغيرات التي جاءت من أي حقول. وبسبب هذا الاستخدام الشائع، ولأن كتابة let Point { x: x, y: y } = p; تحتوي على الكثير من التكرار، فإن Rust لديها اختصار للأنماط التي تطابق حقول struct: ما عليك سوى إدراج اسم حقل struct، وستكون للمتغيرات التي تم إنشاؤها من النمط نفس الأسماء. تتصرف القائمة 19-13 بنفس طريقة الكود في القائمة 19-12، ولكن المتغيرات التي تم إنشاؤها في نمط let هي x و y بدلاً من a و b.

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);
}

ينشئ هذا الكود المتغيرين x و y اللذين يطابقان الحقلين x و y للمتغير p. والنتيجة هي أن المتغيرين x و y يحتويان على القيم من الهيكل p.

يمكننا أيضاً التفكيك باستخدام قيم حرفية كجزء من نمط struct بدلاً من إنشاء متغيرات لجميع الحقول. القيام بذلك يسمح لنا باختبار بعض الحقول لقيم معينة مع إنشاء متغيرات لتفكيك الحقول الأخرى.

في القائمة 19-14، لدينا تعبير match يفصل قيم Point إلى ثلاث حالات: النقاط التي تقع مباشرة على محور x (وهو ما يكون صحيحاً عندما يكون y = 0) ، أو على محور y (x = 0) ، أو لا تقع على أي من المحورين.

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    match p {
        Point { x, y: 0 } => println!("On the x axis at {x}"),
        Point { x: 0, y } => println!("On the y axis at {y}"),
        Point { x, y } => {
            println!("On neither axis: ({x}, {y})");
        }
    }
}

سيطابق الذراع الأول أي نقطة تقع على محور x من خلال تحديد أن حقل y يطابق إذا كانت قيمته تطابق القيمة الحرفية 0. لا يزال النمط ينشئ متغيراً x يمكننا استخدامه في الكود لهذا الذراع.

وبالمثل، يطابق الذراع الثاني أي نقطة على محور y من خلال تحديد أن حقل x يطابق إذا كانت قيمته 0 وينشئ متغيراً y لقيمة حقل y. لا يحدد الذراع الثالث أي قيم حرفية، لذا فهو يطابق أي Point أخرى وينشئ متغيرات لكل من الحقلين x و y.

في هذا المثال، تطابق القيمة p الذراع الثاني بحكم احتواء x على 0 ، لذا سيطبع هذا الكود On the y axis at 7.

تذكر أن تعبير match يتوقف عن فحص الأذرع بمجرد العثور على أول نمط مطابق، لذا على الرغم من أن Point { x: 0, y: 0 } تقع على محور x ومحور y ، فإن هذا الكود سيطبع فقط On the x axis at 0.

التعدادات (Enums)

لقد قمنا بتفكيك التعدادات (enums) في هذا الكتاب (على سبيل المثال، القائمة 6-5 في الفصل السادس)، لكننا لم نناقش صراحة بعد أن النمط لتفكيك enum يتوافق مع الطريقة التي يتم بها تعريف البيانات المخزنة داخل enum. كمثال، في القائمة 19-15، نستخدم التعداد Message من القائمة 6-2 ونكتب match مع أنماط تفكك كل قيمة داخلية.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
    let msg = Message::ChangeColor(0, 160, 255);

    match msg {
        Message::Quit => {
            println!("The Quit variant has no data to destructure.");
        }
        Message::Move { x, y } => {
            println!("Move in the x direction {x} and in the y direction {y}");
        }
        Message::Write(text) => {
            println!("Text message: {text}");
        }
        Message::ChangeColor(r, g, b) => {
            println!("Change color to red {r}, green {g}, and blue {b}");
        }
    }
}

سيطبع هذا الكود Change color to red 0, green 160, and blue 255. حاول تغيير قيمة msg لرؤية الكود من الأذرع الأخرى يعمل.

بالنسبة لمتغيرات التعداد (enum variants) التي لا تحتوي على أي بيانات، مثل Message::Quit ، لا يمكننا تفكيك القيمة أكثر من ذلك. يمكننا فقط المطابقة على قيمة Message::Quit الحرفية، ولا توجد متغيرات في ذلك النمط.

بالنسبة لمتغيرات التعداد الشبيهة بالهياكل (struct-like enum variants)، مثل Message::Move ، يمكننا استخدام نمط مشابه للنمط الذي نحدده لمطابقة structs. بعد اسم variant، نضع أقواساً متعرجة ثم ندرج الحقول مع المتغيرات بحيث نقسم الأجزاء لاستخدامها في الكود لهذا الذراع. هنا نستخدم الصيغة المختصرة كما فعلنا في القائمة 19-13.

بالنسبة لمتغيرات التعداد الشبيهة بالصفوف (tuple-like enum variants)، مثل Message::Write التي تحمل صفاً (tuple) بعنصر واحد و Message::ChangeColor التي تحمل tuple بثلاثة عناصر، فإن النمط مشابه للنمط الذي نحدده لمطابقة tuples. يجب أن يتطابق عدد المتغيرات في النمط مع عدد العناصر في variant الذي نطابقه.

الهياكل والتعدادات المتداخلة (Nested Structs and Enums)

حتى الآن، كانت جميع أمثلتنا تطابق structs أو enums بعمق مستوى واحد، ولكن المطابقة يمكن أن تعمل على العناصر المتداخلة (nested items) أيضاً! على سبيل المثال، يمكننا إعادة بناء الرسالة، كما هو موضح في القائمة 19-16.

enum Color {
    Rgb(i32, i32, i32),
    Hsv(i32, i32, i32),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(Color),
}

fn main() {
    let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) => {
            println!("Change color to red {r}, green {g}, and blue {b}");
        }
        Message::ChangeColor(Color::Hsv(h, s, v)) => {
            println!("Change color to hue {h}, saturation {s}, value {v}");
        }
        _ => (),
    }
}

يطابق نمط الذراع الأول في تعبير match متغير التعداد Message::ChangeColor الذي يحتوي على متغير Color::Rgb ؛ ثم يرتبط النمط بقيم i32 الثلاث الداخلية. يطابق نمط الذراع الثاني أيضاً متغير التعداد Message::ChangeColor ، لكن التعداد الداخلي يطابق Color::Hsv بدلاً من ذلك. يمكننا تحديد هذه الشروط المعقدة في تعبير match واحد، على الرغم من مشاركة تعدادين.

الهياكل والصفوف (Structs and Tuples)

يمكننا خلط ومطابقة وتداخل أنماط التفكيك بطرق أكثر تعقيداً. يوضح المثال التالي تفكيكاً معقداً حيث نقوم بتداخل structs و tuples داخل tuple ونقوم بتفكيك جميع القيم الأولية (primitive values) منها:

fn main() {
    struct Point {
        x: i32,
        y: i32,
    }

    let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}

يتيح لنا هذا الكود تقسيم الأنواع المعقدة إلى أجزائها المكونة حتى نتمكن من استخدام القيم التي نهتم بها بشكل منفصل.

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

تجاهل القيم في النمط (Ignoring Values in a Pattern)

لقد رأيت أنه من المفيد أحياناً تجاهل القيم في النمط، كما هو الحال في الذراع الأخير من match ، للحصول على حالة شاملة (catch-all) لا تفعل شيئاً في الواقع ولكنها تأخذ في الاعتبار جميع القيم الممكنة المتبقية. هناك عدة طرق لتجاهل قيم كاملة أو أجزاء من القيم في النمط: استخدام نمط _ (الذي رأيته)، أو استخدام نمط _ داخل نمط آخر، أو استخدام اسم يبدأ بشرطة سفلية، أو استخدام .. لتجاهل الأجزاء المتبقية من القيمة. دعونا نستكشف كيفية وسبب استخدام كل من هذه الأنماط.

قيمة كاملة باستخدام _ (An Entire Value with _)

لقد استخدمنا underscore كنمط بدل (wildcard pattern) يطابق أي قيمة ولكنه لا يرتبط بالقيمة. هذا مفيد بشكل خاص كذراع أخير في تعبير match ، ولكن يمكننا أيضاً استخدامه في أي نمط، بما في ذلك معاملات الدوال (function parameters)، كما هو موضح في القائمة 19-17.

fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {y}");
}

fn main() {
    foo(3, 4);
}

سيتجاهل هذا الكود تماماً القيمة 3 الممرة كمعامل أول، وسيطبع This code only uses the y parameter: 4.

في معظم الحالات عندما لا تعود بحاجة إلى معامل دالة معين، فإنك ستقوم بتغيير التوقيع (signature) بحيث لا يتضمن المعامل غير المستخدم. يمكن أن يكون تجاهل معامل الدالة مفيداً بشكل خاص في حالات مثل تنفيذ trait عندما تحتاج إلى signature معين ولكن جسم الدالة في تنفيذك لا يحتاج إلى أحد المعاملات. عندها تتجنب الحصول على تحذير من compiler حول معاملات الدوال غير المستخدمة، كما سيحدث إذا استخدمت اسماً بدلاً من ذلك.

أجزاء من القيمة باستخدام _ المتداخلة (Parts of a Value with a Nested _)

يمكننا أيضاً استخدام _ داخل نمط آخر لتجاهل جزء فقط من القيمة، على سبيل المثال، عندما نريد اختبار جزء فقط من القيمة ولكن ليس لدينا استخدام للأجزاء الأخرى في الكود المقابل الذي نريد تشغيله. توضح القائمة 19-18 كوداً مسؤولاً عن إدارة قيمة إعداد ما. متطلبات العمل هي أنه لا ينبغي السماح للمستخدم بإلغاء تخصيص موجود لإعداد ما، ولكن يمكنه إلغاء ضبط الإعداد وإعطاؤه قيمة إذا كان غير مضبوط حالياً.

fn main() {
    let mut setting_value = Some(5);
    let new_setting_value = Some(10);

    match (setting_value, new_setting_value) {
        (Some(_), Some(_)) => {
            println!("Can't overwrite an existing customized value");
        }
        _ => {
            setting_value = new_setting_value;
        }
    }

    println!("setting is {setting_value:?}");
}

سيطبع هذا الكود Can't overwrite an existing customized value ثم setting is Some(5). في ذراع المطابقة الأول، لا نحتاج إلى المطابقة على القيم داخل أي من متغيري Some أو استخدامها، ولكننا نحتاج إلى اختبار الحالة عندما يكون setting_value و new_setting_value هما متغير Some. في هذه الحالة، نطبع سبب عدم تغيير setting_value ، ولا يتم تغييرها.

في جميع الحالات الأخرى (إذا كان أي من setting_value أو new_setting_value هو None) المعبر عنها بنمط _ في الذراع الثاني، نريد السماح لـ new_setting_value بأن تصبح setting_value.

يمكننا أيضاً استخدام underscores في أماكن متعددة داخل نمط واحد لتجاهل قيم معينة. توضح القائمة 19-19 مثالاً لتجاهل القيمتين الثانية والرابعة في tuple مكون من خمسة عناصر.

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, _, third, _, fifth) => {
            println!("Some numbers: {first}, {third}, {fifth}");
        }
    }
}

سيطبع هذا الكود Some numbers: 2, 8, 32 ، وسيتم تجاهل القيمتين 4 و 16.

متغير غير مستخدم ببدء اسمه بـ _ (An Unused Variable by Starting Its Name with _)

إذا أنشأت متغيراً ولكنك لم تستخدمه في أي مكان، فسيصدر Rust عادةً تحذيراً لأن المتغير غير المستخدم قد يكون خطأً برمجياً (bug). ومع ذلك، أحياناً يكون من المفيد أن تكون قادراً على إنشاء متغير لن تستخدمه بعد، كما هو الحال عند عمل نموذج أولي (prototyping) أو مجرد بدء مشروع. في هذه الحالة، يمكنك إخبار Rust بعدم تحذيرك بشأن المتغير غير المستخدم عن طريق بدء اسم المتغير بشرطة سفلية. في القائمة 19-20، ننشئ متغيرين غير مستخدمين، ولكن عندما نقوم بتصريف هذا الكود، يجب أن نحصل فقط على تحذير بشأن أحدهما.

fn main() {
    let _x = 5;
    let y = 10;
}

هنا، نحصل على تحذير بشأن عدم استخدام المتغير y ، لكننا لا نحصل على تحذير بشأن عدم استخدام _x.

لاحظ أن هناك فرقاً دقيقاً بين استخدام _ فقط واستخدام اسم يبدأ بشرطة سفلية. syntax المسمى _x لا يزال يربط القيمة بالمتغير، بينما _ لا يربط على الإطلاق. لإظهار حالة يكون فيها هذا التمييز مهماً، ستوفر لنا القائمة 19-21 خطأً.

fn main() {
    let s = Some(String::from("Hello!"));

    if let Some(_s) = s {
        println!("found a string");
    }

    println!("{s:?}");
}

سنتلقى خطأً لأن قيمة s ستظل تُنقل إلى _s ، مما يمنعنا من استخدام s مرة أخرى. ومع ذلك، فإن استخدام underscore بمفرده لا يرتبط أبداً بالقيمة. سيتم تصريف القائمة 19-22 دون أي أخطاء لأن s لا يتم نقلها إلى _.

fn main() {
    let s = Some(String::from("Hello!"));

    if let Some(_) = s {
        println!("found a string");
    }

    println!("{s:?}");
}

يعمل هذا الكود بشكل جيد لأننا لا نربط s بأي شيء؛ فهي لا تُنقل.

الأجزاء المتبقية من القيمة باستخدام .. (Remaining Parts of a Value with ..)

مع القيم التي تحتوي على أجزاء كثيرة، يمكننا استخدام syntax المسمى .. لاستخدام أجزاء محددة وتجاهل الباقي، وتجنب الحاجة إلى إدراج underscores لكل قيمة متجاهلة. يتجاهل النمط .. أي أجزاء من القيمة لم نقم بمطابقتها صراحة في بقية النمط. في القائمة 19-23، لدينا struct يسمى Point يحمل إحداثيات في فضاء ثلاثي الأبعاد. في تعبير match ، نريد العمل فقط على إحداثي x وتجاهل القيم في الحقلين y و z.

fn main() {
    struct Point {
        x: i32,
        y: i32,
        z: i32,
    }

    let origin = Point { x: 0, y: 0, z: 0 };

    match origin {
        Point { x, .. } => println!("x is {x}"),
    }
}

ندرج قيمة x ثم نضمن فقط النمط ... هذا أسرع من الاضطرار إلى إدراج y: _ و z: _ ، خاصة عندما نعمل مع structs تحتوي على الكثير من الحقول في المواقف التي يكون فيها حقل واحد أو حقلان فقط هما المهمان.

سيتوسع syntax المسمى .. إلى أكبر عدد يحتاجه من القيم. توضح القائمة 19-24 كيفية استخدام .. مع tuple.

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, .., last) => {
            println!("Some numbers: {first}, {last}");
        }
    }
}

في هذا الكود، يتم مطابقة القيمتين الأولى والأخيرة مع first و last. سيطابق .. ويتجاهل كل شيء في المنتصف.

ومع ذلك، يجب أن يكون استخدام .. غير غامض (unambiguous). إذا كان من غير الواضح أي القيم مخصصة للمطابقة وأيها يجب تجاهلها، فسيقوم Rust بإعطائنا خطأً. توضح القائمة 19-25 مثالاً لاستخدام .. بشكل غامض، لذا لن يتم تصريفه.

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (.., second, ..) => {
            println!("Some numbers: {second}")
        },
    }
}

عندما نقوم بتصريف هذا المثال، نحصل على هذا الخطأ:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
 --> src/main.rs:5:22
  |
5 |         (.., second, ..) => {
  |          --          ^^ can only be used once per tuple pattern
  |          |
  |          previously used here

error: could not compile `patterns` (bin "patterns") due to 1 previous error

من المستحيل على Rust تحديد عدد القيم في tuple التي يجب تجاهلها قبل مطابقة قيمة مع second ثم عدد القيم الإضافية التي يجب تجاهلها بعد ذلك. قد يعني هذا الكود أننا نريد تجاهل 2 ، وربط second بـ 4 ، ثم تجاهل 8 و 16 و 32 ؛ أو أننا نريد تجاهل 2 و 4 ، وربط second بـ 8 ، ثم تجاهل 16 و 32 ؛ وهكذا دواليك. اسم المتغير second لا يعني أي شيء خاص لـ Rust، لذا نحصل على خطأ من compiler لأن استخدام .. في مكانين مثل هذا أمر غامض.

إضافة شروط إضافية باستخدام حراس المطابقة (Adding Conditionals with Match Guards)

حارس المطابقة (match guard) هو شرط if إضافي، يتم تحديده بعد النمط في ذراع match ، والذي يجب أن يتطابق أيضاً ليتم اختيار ذلك الذراع. تعد match guards مفيدة للتعبير عن أفكار أكثر تعقيداً مما يسمح به النمط وحده. لاحظ، مع ذلك، أنها متاحة فقط في تعبيرات match ، وليس في تعبيرات if let أو while let.

يمكن للشرط استخدام المتغيرات التي تم إنشاؤها في النمط. توضح القائمة 19-26 تعبير match حيث يحتوي الذراع الأول على النمط Some(x) ويحتوي أيضاً على match guard وهو if x % 2 == 0 (والذي سيكون true إذا كان الرقم زوجياً).

fn main() {
    let num = Some(4);

    match num {
        Some(x) if x % 2 == 0 => println!("The number {x} is even"),
        Some(x) => println!("The number {x} is odd"),
        None => (),
    }
}

سيطبع هذا المثال The number 4 is even. عندما تتم مقارنة num بالنمط في الذراع الأول، فإنه يتطابق لأن Some(4) تطابق Some(x). ثم لأنها كذلك، يتم اختيار الذراع الأول.

إذا كانت num هي Some(5) بدلاً من ذلك، فسيكون match guard في الذراع الأول false لأن باقي قسمة 5 على 2 هو 1، وهو لا يساوي 0. سينتقل Rust بعد ذلك إلى الذراع الثاني، والذي سيطابق لأن الذراع الثاني لا يحتوي على match guard وبالتالي يطابق أي متغير Some.

لا توجد طريقة للتعبير عن شرط if x % 2 == 0 داخل نمط، لذا فإن match guard يمنحنا القدرة على التعبير عن هذا المنطق. الجانب السلبي لهذه القدرة التعبيرية الإضافية هو أن compiler لا يحاول التحقق من الشمولية (exhaustiveness) عند استخدام تعبيرات match guard.

عند مناقشة القائمة 19-11، ذكرنا أنه يمكننا استخدام match guards لحل مشكلة تظليل الأنماط (pattern-shadowing). تذكر أننا أنشأنا متغيراً جديداً داخل النمط في تعبير match بدلاً من استخدام المتغير خارج match. كان ذلك المتغير الجديد يعني أننا لا نستطيع الاختبار مقابل قيمة المتغير الخارجي. توضح القائمة 19-27 كيف يمكننا استخدام match guard لإصلاح هذه المشكلة.

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(n) if n == y => println!("Matched, n = {n}"),
        _ => println!("Default case, x = {x:?}"),
    }

    println!("at the end: x = {x:?}, y = {y}");
}

سيطبع هذا الكود الآن Default case, x = Some(5). النمط في ذراع المطابقة الثاني لا يقدم متغيراً جديداً y من شأنه أن يظلل y الخارجي، مما يعني أنه يمكننا استخدام y الخارجي في match guard. بدلاً من تحديد النمط كـ Some(y) ، والذي كان سيظلل y الخارجي، نحدد Some(n). يؤدي هذا إلى إنشاء متغير جديد n لا يظلل أي شيء لأنه لا يوجد متغير n خارج match.

حارس المطابقة if n == y ليس نمطاً وبالتالي لا يقدم متغيرات جديدة. هذا الـ y هو y الخارجي بدلاً من y جديد يظلله، ويمكننا البحث عن قيمة لها نفس قيمة y الخارجي من خلال مقارنة n بـ y.

يمكنك أيضاً استخدام عامل أو | في match guard لتحديد أنماط متعددة؛ سيتم تطبيق شرط match guard على جميع الأنماط. توضح القائمة 19-28 الأسبقية (precedence) عند الجمع بين نمط يستخدم | مع match guard. الجزء المهم من هذا المثال هو أن match guard المسمى if y ينطبق على 4 و 5 و 6 ، على الرغم من أنه قد يبدو أن if y ينطبق فقط على 6.

fn main() {
    let x = 4;
    let y = false;

    match x {
        4 | 5 | 6 if y => println!("yes"),
        _ => println!("no"),
    }
}

ينص شرط المطابقة على أن الذراع يطابق فقط إذا كانت قيمة x تساوي 4 أو 5 أو 6 و إذا كانت y هي true. عندما يتم تشغيل هذا الكود، يطابق نمط الذراع الأول لأن x هي 4 ، ولكن match guard المسمى if y هو false ، لذا لا يتم اختيار الذراع الأول. ينتقل الكود إلى الذراع الثاني، الذي يطابق، ويطبع هذا البرنامج no. والسبب هو أن شرط if ينطبق على النمط بالكامل 4 | 5 | 6 ، وليس فقط على القيمة الأخيرة 6. بعبارة أخرى، تتصرف أسبقية match guard فيما يتعلق بالنمط على النحو التالي:

(4 | 5 | 6) if y => ...

بدلاً من هذا:

4 | 5 | (6 if y) => ...

بعد تشغيل الكود، يظهر سلوك الأسبقية بوضوح: إذا تم تطبيق match guard فقط على القيمة النهائية في قائمة القيم المحددة باستخدام عامل | ، لكان الذراع قد تطابق، ولكان البرنامج قد طبع yes.

استخدام روابط @ (Using @ Bindings)

يسمح لنا عامل “عند” (at operator) المسمى @ بإنشاء متغير يحمل قيمة في نفس الوقت الذي نختبر فيه تلك القيمة لمطابقة نمط ما. في القائمة 19-29، نريد اختبار أن حقل id في Message::Hello يقع ضمن النطاق 3..=7. نريد أيضاً ربط القيمة بالمتغير id حتى نتمكن من استخدامها في الكود المرتبط بالذراع.

fn main() {
    enum Message {
        Hello { id: i32 },
    }

    let msg = Message::Hello { id: 5 };

    match msg {
        Message::Hello { id: id @ 3..=7 } => {
            println!("Found an id in range: {id}")
        }
        Message::Hello { id: 10..=12 } => {
            println!("Found an id in another range")
        }
        Message::Hello { id } => println!("Found some other id: {id}"),
    }
}

سيطبع هذا المثال Found an id in range: 5. من خلال تحديد id @ قبل النطاق 3..=7 ، فإننا نلتقط أي قيمة طابقت النطاق في متغير باسم id مع اختبار أن القيمة طابقت نمط النطاق أيضاً.

في الذراع الثاني، حيث لدينا نطاق محدد فقط في النمط، لا يحتوي الكود المرتبط بالذراع على متغير يحتوي على القيمة الفعلية لحقل id. كان من الممكن أن تكون قيمة حقل id هي 10 أو 11 أو 12، لكن الكود الذي يتماشى مع هذا النمط لا يعرف أياً منها. كود النمط غير قادر على استخدام القيمة من حقل id لأننا لم نحفظ قيمة id في متغير.

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

يسمح لنا استخدام @ باختبار قيمة وحفظها في متغير داخل نمط واحد.

ملخص (Summary)

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

بعد ذلك، وفي الفصل قبل الأخير من الكتاب، سننظر في بعض الجوانب المتقدمة لمجموعة متنوعة من ميزات Rust.