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

صياغة أنماط التصميم الصحيحة Pattern Syntax في لغة رست


Naser Dakhel

سنجمع في هذا المقال الصياغة الصالحة في الأنماط وسنتحدث عن مكان استخدام كل واحد منها.

مطابقة القيم المجردة Literals

يمكننا مطابقة الأنماط مباشرةً مع القيم المجرّدة كما رأينا سابقًا في الفصل التعدادات enums في لغة رست. تمنحنا الشيفرة البرمجية التالية بعض الأمثلة:

    let x = 1;

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

تطبع هذه الشيفرة one لأن القيمة في x هي 1. تُعد هذه الصياغة مفيدة عندما تريد من الشيفرة أن تنفّذ عملًا ما عندما تحصل على قيمة معينة واحدة.

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

المتغيرات المُسمّاة هي أنماط غير قابلة للجدل تطابق أي قيمة، وقد استخدمناها مرات عديدة سابقًا، ولكن هناك تعقيدات عند استخدامها في تعابير match. ينشئ تعبير match نطاقًا scope جديدًا، وبالتالي ستُخفي المتغيرات المُصرّح عنها على أنها جزء من النمط داخل match المتغيرات التي تحمل الاسم ذاته خارج هيكل match كما هو الحال في جميع المتغيرات. صرّحنا ضمن الشيفرة 11 عن متغير مُسمى x قيمته Some(5)‎ ومتغير y قيمته 10، ثم أنشأنا تعبير match على القيمة x. ألقِ نظرةً على الأنماط في أذرع المطابقة و !println وحاول اكتشاف ماذا ستطبع الشيفرة قبل تنفيذ هذه الشيفرة أو متابعة القراءة.

اسم الملف: src/main.rs

    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 = {:?}, y = {y}", x);

[الشيفرة 11: تعبير match مع ذراع يتسبب بظهور متغير خفي y]

لنرى ما سيحصل عند تنفيذ تعبير match؛ إذ لا تتطابق القيمة المُعرّفة x مع ذراع المطابقة الأول في النمط لذا يستمر تنفيذ الشيفرة.

يتسبب النمط الموجود في ذراع المطابقة الثاني بإنشاء متغير جديد باسم y وهو يطابق أي قيمة داخل قيمة Some، وذلك لأننا في نطاق جديد داخل تعبير match، هذا المتغير الجديد yهو ليس المتغير y ذاته الذي صرّحنا عنه في البداية بقيمة 10. يطابق الإسناد الجديد للمتغير y أي قيمة داخل Some وهي القيمة الموجودة في x، وبالتالي ترتبط y الجديدة بقيمة Some الداخلية في x، وتبلغ تلك القيمة 5، لذلك يُنَفَذ تعبير الذراع ويطبع Matched, y = 5.

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

عندما ينتهي تعبير match ينتهي نطاقه أيضًا، وينتهي أيضًا نطاق المتغير y الداخلي. تطبع آخر تعليمة println!‎ ما يلي:

at the end: x = Some(5), y = 10

يجب علينا استخدام درع مطابقة شرطي match guard conditional لإنشاء تعبير match يقارن قيم x و y الخارجية عوضًا عن تقديم متغير خفي، وسنتحدث عن ذلك لاحقّا في قسم "الشرطيات الإضافية مع دروع المطابقة".

الأنماط المتعددة Multiple Patterns

يمكننا مطابقة عدة أنماط في تعبير match باستخدام الصياغة | التي يمكن أن تكون النمط أو المعامل. نطابق في المثال التالي قيمة x مع أذرع المطابقة، بحيث يحتوي أول ذراع على الخَيَار "أو or"، بمعنى أنه إذا طابقت القيمة x أحد القيمتين في الذراع تُنفَّذ شيفرة الذراع كما يلي:

    let x = 1;

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

تطبع الشيفرة السابقة ما يلي:

one or two

مطابقة مجالات القيم باستخدام الصيغة =..

تسمح صيغة =.. بمطابقة مجال شامل من القيم، إذ تُنفَّذ الشيفرة البرمجية الخاصة بالذراع عندما يطابق النمط أي قيمة في مجال ما كما في الشيفرة التالية:

    let x = 5;

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

تتطابق الذراع الأولى إذا كانت قيمة x هي 1 أو 2 أو 3 أو 4 أو 5، هذه الصياغة ملائمة لمطابقة قيم متعددة بدلًا من استخدام المعامل | للتعبير عن الفكرة ذاتها؛ إذا أردنا استخدام | فيجب تحديد 5 | 4 | 3 | 2 | 1، وستكون طريقة تحديد المجال أقصر خاصةً إذا أردنا مطابقة أي رقم بين 1 و 1,000.

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

إليك مثالًا يستخدم مجال من قيم char:

    let x = 'c';

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

تميز رست أن 'c' تقع داخل مجال النمط الأول وتطبع early ASCII letter.

التفكيك Restructuring لتجزئة القيم

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

تفكيك الهياكل

تظهر الشيفرة 12 هيكل Point بحقلين x و y يُمكن تجزئتهما باستخدام نمط مع التعليمة let.

اسم الملف:src/main.rs

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

[الشيفرة 12: تفكيك حقل هيكل إلى متغيرات منفصلة]

تُنشئ الشيفرة السابقة المتغيرين a و b اللذين يطابقان قيمتي الحقلين x و y في الهيكل p، ويوضح هذا المثال أنه ليس من الضروري لأسماء المتغيرات في النمط أن تُطابق أسماء الحقول في الهيكل، ولكن من الشائع مطابقة أسماء المتغيرات مع أسماء الحقول لتذكر ارتباط المتغير بحقل معين. بما أن كتابة ما يلي مثلًا:

let Point { x: x, y: y } = p;‎

تحتوي على الكثير من التكرار، لدى رست طريقةً مختصرة للأنماط التي تطابق حقول الهيكل؛ إذ عليك فقط أن تُضيف اسم حقل الهيكل مما يجعل المتغيرات المُنشأة من هذا النمط تحمل الاسم ذاته. تعمل الشيفرة 13 بطريقة عمل الشيفرة 12 ذاتها إلا أن المتغيرات المُنشأة في النمط let هي x و y بدلًا من a و b.

اسم الملف: src/main.rs

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

[الشيفرة 13: تفكيك حقول هيكل باستخدام طريقة حقل الهيكل المختصرة]

تُنشئ الشيفرة المتغيرين x و y اللذين يطابقان الحقلين x و y الخاصين بالهيكل p، والنتيجة هي احتواء المتغيرين x و y على القيم الموجودة في الهيكل p.

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

لدينا في الشيفرة 14 تعبير match يقسم قيم Point إلى ثلاث حالات: نقاط تقع مباشرةً على محور x (الذي يُعدّ محققًا عندما y = 0)، ونقاط تقع على المحور y (أي x = 0)، ونقاط لا تقع على أي من المحورين.

اسم الملف: src/main.rs

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})");
        }
    }
}

[ الشيفرة 14: تفكيك ومطابقة القيم المجردة في نمط واحد]

ستتطابق الذراع الأولى مع أي نقطة تقع على المحور 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.

تفكيك المعددات

فكّكنا سابقًا المعدّدات (الشيفرة 5 في الفصل بنية match للتحكم بسير برامج لغة رست Rust) إلا أننا لم نتحدث صراحةً أن النمط لتفكيك المعدّد يوافق طريقة تخزين البيانات داخله. تستخدم الشيفرة 15 معدّدًا يدعى Message من الشيفرة 2 من الفصل التعدادات enums في لغة رست وتُكتب match مع أنماط تفكك كل من القيم الداخلية الخاصة بذلك المعدّد.

اسم الملف: src/main.rs

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 the color to red {r}, green {g}, and blue {b}",)
        }
    }
}

[الشيفرة 15: تفكيك متغايرات المعدّد التي تحتوي على أنواع مختلفة من القيم]

تطبع الشيفرة السابقة ما يلي:

 Change the color to red 0, green 160, and blue 255

حاول تغيير قيمة msg لملاحظة تنفيذ الشيفرة البرمجية للأذرع الأُخرى.

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

يمكن استخدام أنماط مشابهة للنمط الذي نحدده لمطابقة الهياكل من أجل متغايرات المعدّدات التي تشبه الهيكل مثل Message::Move، إذ نضع أقواس معقوصة بعد اسم المتغير وبعدها نضع الحقول مع المتغيرات لتجزئتها واستخدامها في شيفرة الذراع، واستخدمنا هنا الطريقة المُختصرة كما فعلنا في الشيفرة 13.

يشابه النمط الذي نستخدمه لمتغيرات المعدّدات التي تشبه الصفوف مثل Message::Write الذي يحتوي على صف مع عنصر واحد و Message::ChangeColor الذي يحتوي صف مع ثلاثة عناصر للنمط الذي نحدده لمطابقة الصفوف، ويجب أن يطابق عدد المتغيرات في النمط عدد العناصر في المتغير المُراد مطابقته.

تفكيك الهياكل والمعددات المتداخلة

كانت أمثلتنا حتى الآن مقتصرةً على مطابقة الهياكل structs والمعدّدات enums بعمق طبقة واحدة، إلا أن المطابقة تعمل على العناصر المتداخلة أيضًا، فعلى سبيل المثال يمكن إعادة بناء الشيفرة 15 لتدعم ألوان RGB و HSV في رسالة ChangeColor كما تبين الشيفرة 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}")
        }
        _ => (),
    }
}

[الشيفرة 16: مطابقة معدّدات متداخلة]

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

تفكيك الهياكل والصفوف

يمكننا مزج ومطابقة وتضمين الأنماط المفككة بطرق معقدة أكثر، ويبين المثال التالي تفكيك معقد، إذ نضمّن هياكل وصفوف داخل صف ونفكك جميع القيم الأولية primitive:

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

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

تُعد التجزئة مع الأنماط طريقةً مناسبة لاستخدام أجزاء من القيم مثل قيمة من كل حقل في هيكل منفصلين عن بعضهم.

تجاهل القيم في نمط

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

تجاهل قيمة كاملة باستخدام _

استخدمنا الشرطة السفلية مثل نمط محرف بدل wildcard يُطابق أي قيمة ولكن لا يرتبط بها، هذا مفيد لذراع أخير في تعبير match ولكن يمكن استخدامه أيضًا في أي نمط مثل معاملات الدالة كما تبين الشيفرة 17.

اسم الملف: src/main.rs

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

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

[الشيفرة 17: استخدام _ في بصمة الدالة]

تتجاهل الشيفرة السابقة القيمة 3 المُمَررة مثل معامل أول تمامًا وتطبع:

This code only uses the y parameter: 4

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

تجاهل أجزاء من القيمة باستخدام _ متداخلة

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

    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);

[الشيفرة 18: استخدام شرطة سفلية داخل الأنماط التي تطابق متغايرات Some عندما لا توجد حاجة لاستخدام القيمة داخل Some]

تطبع الشيفرة ما يلي:

Can't overwrite an existing customized value

وبعدها:

setting is Some(5)‎

لا نحتاج لمطابقة أو استخدام القيمة في كِلا متغايري Some في ذراع المطابقة الأولى، لكننا بحاجة لاختبار الحالة عندما يكون متغايرا Some هما setting_value و new_setting_value، وفي تلك الحالة نطبع سبب عدم تغيير setting_value ولا تتغيّر؛ ومن أجل باقي الحالات (إذا كانت setting_value أو new_setting_value هي None) مُعبرة بالنمط _ في الذراع الثانية، سنسمح للقيمة new_setting_value بأن تصبح setting_value.

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

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

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

[الشيفرة 19: تجاهل قيم متعددة في صف]

تطبع الشيفرة Some numbers: 2, 8, 32 متجاهلةً القيمتين 4 و16.

تجاهل المتغيرات غير المستخدمة بكتابة _ بداية اسمها

تنبّهك رست إذا أنشأت متغيرًا ولم تستخدمه في أي مكان، إذ يمكن أن يكون عدم استخدام متغير خطأ برمجي، ولكن من المفيد إنشاء متغير لا تريد استخدامه حاليًا مثل عندما نريد كتابة نموذج أولي prototype أو عند بداية مشروع جديد، ففي هذه الحالات يمكن إخبار رست بعدم التنبيه عن المتغيرات غير المستخدمة بكتابة شرطة سفلية "_" قبل اسم المتغير. أنشأنا في الشيفرة 20 متغيرين غير مستخدمين ولكن عندما نصرّف الشيفرة هذه يجب أن نحصل على تنبيه بخصوص واحد منهما فقط.

اسم الملف: src/main.rs

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

[الشيفرة 20: كتابة اسم المتغير مسبوقًا بشرطة سفلية لتجنب الحصول على تنبيه متغير غير مُستخدم]

نحصل على تنبيه عن عدم استخدام المتغير y ولكن لن نحصل على تنبيه لعدم استخدام المتغير ‎_x.

لاحظ أن هناك اختلاف بسيط بين استخدام _ فقط أو استخدام اسم مسبوقًا بشرطة سفلية، إذ تُسنِد الصيغة ‎_x القيمة بالمتغير ولكن لا تُسند الصيغة _ أي قيمة إطلاقًا، ولتوضيح أهمية الفرق إليك الشيفرة 21 التي تعطينا الخطأ التالي.

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

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

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

[الشيفرة 21: يُسند متغير غير مستخدم يبدأ بشرطة سفلية إلى قيمة، مما قد يمنحه ملكية القيمة]

سنحصل على خطأ لأن قيمة s ستنتقل إلى s_ التي تمنع استخدام s مجددًا، بينما لا يُسند استخدام الشرطة السفلية لوحدها القيمة أبدًا. تُصرّف الشيفرة 22 التالية دون أي أخطاء لأن s لا تنتقل إلى _.

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

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

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

[ الشيفرة 22: استخدام الشرطة السفلية لا يُسند القيمة]

تعمل الشيفرة بصورةٍ صحيحة لأن s لا تُسند لأي قيمة ولا يتغير مكانها.

تجاهل الأجزاء المتبقية من القيمة باستخدام ..

يمكننا استخدام الصيغة .. لاستخدام أجزاء معينة من القيمة وتجاهل الباقي وذلك مع القيم التي تحتوي على أجزاء متعددة، ودون الحاجة لاستخدام الشرطة السفلية لكل قيمة مُتَجَاهلة؛ إذ يتجاهل النمط .. أي جزء لم يُطابق من القيمة صراحةً في باقي النمط. لدينا في الشيفرة 23 هيكل Point يحتوي إحداثيات في الفضاء ثلاثي الأبعاد، ونريد في تعبير match أن نعمل فقط على إحداثيات x وتتجاهل القيم الموجودة في الحقلين y و z.

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

[الشيفرة 23: تجاهل كل الحقول في Point عدا الحقل x باستخدام ..]

نضع القيمة x في قائمة وبعدها نضيف النمط ..، وتُعد هذه الطريقة أسرع من كتابة كل من y: _‎ و z: _‎ وتحديدًا عند العمل مع هياكل تحتوي على العديد من الحقول وتريد الحصول على حقل واحد أو اثنين.

يمكن زيادة الصيغة .. إلى عدد كبير من القيم حسب الحاجة، وتظهر الشيفرة 24 كيفية استخدام .. مع صف.

اسم الملف: src/main.rs

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

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

[الشيفرة 24: مطابقة القيمتين الأولى والأخيرة في الصف وتجاهل باقي القيم]

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

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

اسم الملف: src/main.rs

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

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

[الشيفرة 25: محاولة استخدام .. بطريقة غير واضحة]

عندما نصرّف الشيفرة في هذا المثال نحصل على الخطأ التالي:

$ 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` due to previous error

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

تعابير شرطية إضافية مع دروع المطابقة

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

يمكن أن يستخدم الشرط متغيرات مُنشأة في النمط، وتبين الشيفرة 26 تعليمة match تحتوي الذراع الأولى فيها على النمط Some(x)‎ وأيضًا درع مطابقة if x % 2 == 0 (الذي يكون صحيحًا إذا كان العدد زوجي).

    let num = Some(4);

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

[الشيفرة 26: إضافة درع مطابقة إلى نمط]

ستطبع الشيفرة السابقة The number 4 is even، وتتطابق عندما تُقارن num مع النمط الموجود في الذراع الأولى، لأن Some(4)‎ تطابق Some(x)‎. بعد ذلك، يتحقق درع المطابقة إذا كان الباقي من عملية قسمة x على 2 يساوي 0 ولأن هذه الحالة محقّقة يقع الاختيار على الذراع الأولى.

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

لا توجد طريقة للتعبير عن شرط if x % 2 == 0 داخل النمط، لذا يسمح لنا درع المطابقة بالتعبير عن هذا المنطق. سلبية هذا التعبير الإضافي هي أن المصرّف لا يتحقق من الشمولية عند تواجد تعابير درع مطابقة.

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

اسم الملف: src/main.rs

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 = {:?}, y = {y}", x);
}

[الشيفرة 27: استخدام درع المطابقة لاختبار المساواة مع متغير خارجي]

ستطبع الشيفرة الآن:

Default case, x = Some(5)‎

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

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

يمكن استخدام المعامل "أو" | في درع المطابقة لتحديد الأنماط المتعددة، وسيُطبق شرط درع المطابقة على كل الأنماط. تبين الشيفرة 28 الأسبقية عند جمع نمط يستخدم | مع درع مطابقة، والقسم الأهم من هذا المثال هو درع المطابقة if y الذي يطبق على 4 و 5 و 6 على الرغم من أن if y تبدو أنها مطبقةٌ فقط على 6.

    let x = 4;
    let y = false;

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

[ الشيفرة 28: جمع عدة أنماط مع درع مطابقة]

ينصّ شرط المطابقة على أن الذراع تتطابق فقط إذا كانت قيمة x تساوي 4 أو 5 أو 6 وإذا كانت قيمة y هي true، وعندما تُنفذ الشيفرة يُطَابَق نمط الذراع الأول لأن x هي 4 ولكن درع المطابقة if y خاطئ، لذا لا يقع الاختيار على الذراع الأولى وتنتقل الشيفرة إلى الذراع الثانية التي تُطابَق ويطبع البرنامج no، والسبب وراء ذلك هو تطبيق شرط if لكل النمط 6 | 5 | 4، وليس فقط للقيمة الأخيرة 6، بمعنى أخر تتصرف أسبقية درع المطابقة مع النمط على النحو التالي:

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

عوضًا عن:

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

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

ارتباطات @

يسمح لنا معامل at @ بإنشاء متغيرات تحتوي قيمة واختبارها من أجل مطابقة نمط بنفس الوقت. نريد في الشيفرة 29 اختبار حقل id في Message::Hello إذا كان ضمن المجال 3‎..=7، ونريد أيضًا ربط القيمة إلى المتغير id_variable لكي نستخدمها في الشيفرة المرتبطة مع الذراع. يمكن تسمية هذا المتغير باسم الحقل id، لكننا استخدمنا اسمًا مختلفًا.

enum Message {
        Hello { id: i32 },
    }

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

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

[الشيفرة 29: استخدام @ لربط القيمة في النمط مع اختبارها أيضًا]

سيطبع المثال السابق ما يلي:

Found an id in range: 5

نلتقط أي قيمة تطابق المجال ‎3..=7 بتحديد id_variable @‎ قبله، إضافةً إلى اختبار نمط تلك القيمة المطابقة.

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

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

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

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


×
×
  • أضف...