تظهر الأنماط في العديد من الأماكن في رست، وقد استخدمتها سابقًا دون ملاحظتها غالبًا. سنتحدّث في هذه المقالة عن جميع الحالات التي يكون فيها استخدام الأنماط صالحًا، إضافةً إلى الحالات التي تكون فيها قابلة للدحض Refutable.
أذرع تعبير match
كما تحدثنا سابقًا في الفصل التعدادات enums في لغة رست، يمكننا استخدام الأنماط في أذرع arms تعبير match
، ويُعرَّف التعبير match
بالكلمة المفتاحية match
، تليها قيمة للتطابق معها وواحد أو أكثر من أذرع المطابقة التي تتألف من نمط وتعبير يُنفذ إذا طابقت القيمة نمط الذراع على النحو التالي:
match VALUE { PATTERN => EXPRESSION, PATTERN => EXPRESSION, PATTERN => EXPRESSION, }
على سبيل المثال إليك تعبير match
من الشيفرة 5 (من الفصل المذكور آنفًا) الذي يطابق القيمة Option<i32>
في المتغير x
:
match x { None => None, Some(i) => Some(i + 1), }
الأنماط في تعبير match
السابق، هما: None
و Some(i)
على يسار كل سهم.
أحد مُتطلبات تعبير match
هو ضرورة كونه شاملًا لجميع الحالات، أي ينبغي أخذ جميع القيم المُحتملة بالحسبان في تعبير match
. أحد الطرق لضمان تغطية شاملة الاحتمالات هي وجود نمط مطابقة مع الكل Catchall pattern للذراع الأخير، فلا يمكن مثلًا فشل تطابق اسم متغير لأي قيمة إطلاقًا وبذلك يغطي كل حالة متبقية.
يطابق النمط المحدد _
أي قيمة، لكن لا يرتبط بمتغير، لذلك يُستخدم غالبًا في ذراع المطابقة الأخير. يمكن أن يكون النمط _
مفيدًا عندما نريد تجاهل أي قيمة غير محددة مثلًا، وسنتحدث لاحقًا بتفصيل أكثر عن النمط _
في قسم "تجاهل القيم في النمط".
تعابير if let الشرطية
تحدثنا في الفصل التعدادات enums في لغة رست عن كيفية استخدام تعابير if let
بصورةٍ أساسية مثل طريقة مختصرة لكتابة مكافئ التعبير match
، بحيث يطابق حالةً واحدةً فقط، ويمكن أن يكون التعبير if let
مترافقًا مع else
اختياريًا، بحيث يحتوي على شيفرة برمجية تُنفّذ في حال لم يتطابق النمط في if let
.
تبيّن الشيفرة 1 أنه من الممكن مزج ومطابقة تعابير if let
و else if
و else if let
، لإعطاء مرونة أكثر من استخدام التعبير match
الذي يقارن قيمةً واحدة مع الأنماط. إضافةً إلى ذلك، لا تتطلب رست أن تكون الأذرع في سلسلة if let
أو else if
أو else if let
متعلقة ببعضها بعضًا.
تحدد الشيفرة 1 لون الخلفية اعتمادّا على تحقّق عدد من الشروط، إذ أنشأنا في مثالنا هذا متغيرات مع قيم مضمّنة في الشيفرة بحيث يمكن لبرنامج حقيقي استقبالها مثل مدخلات من المستخدم.
اسم الملف: src/main.rs
fn main() { let favorite_color: Option<&str> = None; let is_tuesday = false; let age: Result<u8, _> = "34".parse(); if let Some(color) = favorite_color { println!("Using your favorite color, {color}, as the background"); } else if is_tuesday { println!("Tuesday is green day!"); } else if let Ok(age) = age { if age > 30 { println!("Using purple as the background color"); } else { println!("Using orange as the background color"); } } else { println!("Using blue as the background color"); } }
[الشيفرة 1: استخدام if let
و else if
و else if let
و else
بنفس الوقت]
يُستخدم اللون المفضّل للمستخدم لونًا للخلفية إذا حدده المستخدم، وفي حال لم يُحدد لونه المفضل وكان اليوم خميس فسيكون لون الخلفية أخضر؛ وإذا حدّد المستخدم عمره في سلسلة نصية string، يمكننا تحليلها إلى رقم بنجاح، ويكون اللون إما برتقاليًا أو بنفسجيًا اعتمادًا على قيمة هذا الرقم؛ وأخيرًا إذا لم تنطبق أي من هذه الشروط سيكون لون الخلفية أزرق.
يسمح لنا هذا الهيكل الشرطي بدعم المتطلبات المعقدة، إذ نطبع في هذا المثال باستخدام القيم المضمّنة في الشيفرة ما يلي:
Using purple as the background color
يمكنك ملاحظة أن التعبير if let
تسبب بحصولنا على متغير مخفي shadowed variable بالطريقة ذاتها التي تسببت بها أذرع التعبير match
؛ إذ يُنشئ السطر if let Ok(age) = age
متغيرًا مخفيًا جديد يدعى age
يحتوي على القيمة داخل المتغاير Ok
، وهذا يعني أنه يجب إضافة الشرط if age > 30
داخل الكتلة؛ إذ لا يمكننا جمع الشَرطين في if let ok(age) = age && age > 30
، والقيمة الخفية age
التي نريد مقارنتها مع 30 غير صالحة حتى يبدأ النطاق scope الجديد بالقوس المعقوص curly bracket.
الجانب السلبي في استخدامنا لتعبير if let
هو أن المصرّف لا يتحقق من شمولية الحالات مثلما يفعل تعبير match
. إذا تخلّينا عن كتلة else
الأخيرة وبالتالي فوّتنا معالجة بعض الحالات، لن ينبهنا المصرّف على احتمالية وجود خطأ منطقي.
حلقات while let الشرطية
تسمح الحلقة الشرطية while let
بتنفيذ حلقة while
طالما لا يزال النمط مُطابقًا على نحوٍ مشابه لبُنية if let
. كتبنا في الشيفرة 2 حلقة while let
تستخدم شعاعًا مثل مكدّس stack وتطبع القيم في الشعاع بالترتيب العكسي لإدخالها.
fn main() { let mut stack = Vec::new(); stack.push(1); stack.push(2); stack.push(3); while let Some(top) = stack.pop() { println!("{}", top); } }
[الشيفرة 2: استخدام حلقة while let
لطباعة القيم طالما يُعيد استدعاء stack.pop()
المتغاير Some
]
يطبع المثال السابق القيمة 3 ثم 2 ثم 1، إذ يأخذ التابع pop
آخر عنصر في الشعاع vector ويُعيد Some(value)
، ويعيد القيمة None
إذا كان الشعاع فارغًا. يستمر تنفيذ الحلقة while
والشيفرة البرمجية داخل كتلتها طالما يُعيد استدعاء pop
القيمة Some
، وعندما يعيد استدعاء pop
القيمة None
تتوقف الحلقة، ويمكننا استخدام while let
لإزالة pop off كل عنصر خارج المكدّس.
حلقات for التكرارية
القيمة التي تتبع الكلمة المفتاحية for
في حلقة for
هي النمط، فعلى سبيل المثال النمط في for x in y
هو x
. توضح الشيفرة 3 كيفية استخدام النمط في حلقة for
لتفكيك destructure أو تجزئة الصف tuple إلى جزء من حلقة for
.
fn main() { let v = vec!['a', 'b', 'c']; for (index, value) in v.iter().enumerate() { println!("{} is at index {}", value, index); } }
[الشيفرة 3: استخدام نمط في حلقة for
لتفكيك صف]
تطبع الشيفرة 3 ما يلي:
$ cargo run Compiling patterns v0.1.0 (file:///projects/patterns) Finished dev [unoptimized + debuginfo] target(s) in 0.52s Running `target/debug/patterns` a is at index 0 b is at index 1 c is at index 2
نُعدل مكرّرًا iterator باستخدام التابع enumerate
بحيث يعطينا قيمةً ودليلًا index لهذه القيمة ضمن صف tuple، بحيث تكون القيمة الأولى هي الصف ('0, 'a)
. عندما تطابق هذه القيمة النمط (index, value)
ستكون index
هي 0
وستكون value
هي a
، مما سيتسبب بطباعة السطر الأول من الخرج.
تعليمات let
تحدثنا سابقًا عن استخدام الأنماط مع match
و if let
فقط، إلا أننا استخدمنا الأنماط في أماكن أُخرى أيضًا مثل تعليمة let
. ألقِ نظرةً على عملية إسناد المتغير التالية باستخدام let
:
#![allow(unused)] fn main() { let x = 5; }
اِستخدمت الأنماط في كلّ مرة استخدمت فيها تعليمة let
بالشكل السابق دون معرفتك لذلك. تكون تعليمة let
بالشكل التالي:
let PATTERN = EXPRESSION;
يشكّل اسم المتغير في التعليمات المشابهة لتعليمة let x = 5;
-مع اسم المتغير في فتحة PATTERN
- نوعًا بسيطًا من الأنماط. تقارن رست التعابير مع الأنماط وتُسند أي اسم تجده، أي تتألف التعليمة let x = 5;
من نمط هو x
يعني "اربط ما يتطابق هنا مع المتغير x
"، ولأن الاسم x
يمثّل كامل النمط، فإن هذا النمط يعني "اربط كل شيء مع المتغير x
مهما تكُن القيمة".
لملاحظة مطابقة النمط في let
بوضوح أكبر، جرّب الشيفرة 4 التي تستخدم نمطًا مع let
لتفكيك الصف.
let (x, y, z) = (1, 2, 3);
[الشيفرة 4: استخدام نمط لتفكيك الصف وإنشاء ثلاثة متغيرات بخطوة واحدة]
طابقنا الصف مع النمط هنا، إذ تُقارن رست القيمة (3, 2, 1)
مع النمط (x, y, z)
وترى أن القيمة تطابق النمط فعلًا، لذلك تربط رست 1
إلى x
و 2
إلى y
و 3
إلى z
. يمكن عدّ نمط الصف هذا بمثابة تضمين ثلاثة أنماط متغيرات مفردة داخله.
إذا كان عدد العناصر في النمط لا يطابق عدد العناصر في الصف، لن يُطابَق النوع بأكمله وسنحصل على خطأ تصريفي. تبيّن الشيفرة 5 على سبيل المثال محاولة تفكيك صف بثلاثة عناصر إلى متغيرين وهي محاولة فاشلة.
let (x, y) = (1, 2, 3);
[الشيفرة 5: بناء خاطئ لنمط لا تطابق متغيراته عدد العناصر الموجودة في الصف]
ستتسبب محاولة تصريف الشيفرة السابقة بالخطأ التالي:
$ cargo run Compiling patterns v0.1.0 (file:///projects/patterns) error[E0308]: mismatched types --> src/main.rs:2:9 | 2 | let (x, y) = (1, 2, 3); | ^^^^^^ --------- this expression has type `({integer}, {integer}, {integer})` | | | expected a tuple with 3 elements, found one with 2 elements | = note: expected tuple `({integer}, {integer}, {integer})` found tuple `(_, _)` For more information about this error, try `rustc --explain E0308`. error: could not compile `patterns` due to previous error
يمكننا تجاهل قيمة أو أكثر في الصف لتصحيح الخطأ وذلك باستخدام _
أو ..
كما سنرى لاحقًا في قسم "تجاهل القيم في النمط". إذا كانت المشكلة هي وجود عدة متغيرات في النمط فإن الحل يكمن بجعل الأنواع تتطابق عن طريق إزالة المتغيرات حتى يتساوى عدد المتغيرات وعدد العناصر في الصف.
معاملات الدالة Function Parameters
يمكن لمعاملات الدالة أن تمثّل أنماطًا. ينبغي أن تكون الشيفرة 6 مألوفةً بالنسبة لك، إذ نصرّح فيها عن دالة اسمها foo
تقبل معاملًا واحدًا اسمه x
من النوع i32
.
fn foo(x: i32) { // تُكتب الشيفرة البرمجية هنا }
[الشيفرة 6: بصمة دالة function signature تستخدم الأنماط في معاملاتها]
يشكّل الجزء x
نمطًا. يمكننا مطابقة الصف في وسيط الدالة مع النمط كما فعلنا مع let
. تجزِّء الشيفرة 7 القيم الموجودة في الصف عندما نمررها إلى الدالة.
اسم الملف: src/main.rs
fn print_coordinates(&(x, y): &(i32, i32)) { println!("Current location: ({}, {})", x, y); } fn main() { let point = (3, 5); print_coordinates(&point); }
[الشيفرة 7: دالة تفكك الصف مع معاملاتها]
تطبع الشيفرة السابقة: Current location: (3, 5)
، إذ تطابق القيم &(3, 5)
النمط &(x, y)
، لذا فإن قيمة x
هي 3
وقيمة y
هي 5
.
يمكننا أيضًا استخدام الأنماط في قوائم معاملات التغليف closure parameter lists بطريقة قوائم معاملات الدالة function parameter lists ذاتها، لأن المغلفات مشابهة للدالات كما رأينا سابقًا في الفصل المغلفات closures في لغة رست.
رأينا بحلول هذه النقطة عدة طرق لاستخدام الأنماط، إلا أن الأنماط لا تعمل بالطريقة ذاتها في كل مكان تُستخدم فيه، إذ يجب أن تكون الأنماط غير قابلة للدحض في بعض الأماكن وفي بعضها الآخر كذلك، وسنتحدث عن هذين المفهومين تاليًا.
قابلية الدحض refutability واحتمالية فشل مطابقة النمط
تأتي الأنماط بشكلين: قابلة للدحض أو النقض refutable وغير قابلة للدحض irrefutable، إذ تُدعى الأنماط التي تطابق أي قيمة تمرر خلالها بالأنماط القابلة للدحض، ومثال على ذلك هو x
في التعليمة let x = 5;
وذلك لأن المتغير x
سيطابق أي شيء وبالتالي لا تفشل المطابقة؛ بينما تُدعى الأنماط التي تفشل في بعض القيم بالأنماط القابلة للدحض، ومثال على ذلك هو Some(x)
في التعبير if let Some(x) = a_value
لأن النمط Some(x)
لن يُطابق إذا كانت القيمة في المتغير a_value
هي None
عوضًا عن Some
.
تقبل معاملات الدالة وتعليمات let
وحلقات for
فقط الأنماط غير القابلة للدحض لأن البرنامج لا يستطيع عمل أي شيء مفيد عندما لا تتطابق القيم. يقبل التعبيران if let
و while let
الأنماط القابلة للدحض وغير القابلة للدحض، إلا أنّ المصرّف يحذّر من استخدام الأنماط غير القابلة للدحض، لأنها -بحسب تعريفها- ليست معدّة لتتعامل مع فشل محتمل، إذ تتمثّل الوظيفة الشرطية بقدرتها على التصرف بصورةٍ مختلفة اعتمادًا على النجاح أو الفشل.
عمومًا، لا يهم كثيرّا التمييز بين الأنماط القابلة للدحض وغير القابلة للدحض، ولكن يجب أن يكون مفهوم قابلية الدحض مألوفًا، وذلك لحل الأخطاء التي قد تحصل، إذ يجب في تلك الحالات تغيير إما النمط أو البنية construct المستخدمة مع النمط حسب السلوك المُراد من الشيفرة.
لنتابع مثالّا لما قد يحصل عندما نجرب استخدام نمط قابل للدحض عندما تتطلب رست نمطّا غير قابل للدحض -والعكس صحيح- إذ تبيّن الشيفرة 8 تعليمة let
إلا أن النمط الذي حددناه هو Some(x)
وهو نمط قابل للدحض، ولن تُصرَّف الشيفرة كما هو متوقع.
let Some(x) = some_option_value;
[الشيفرة 8: محاولة استخدام نمط قابل للدحض مع let
]
ستفشل مطابقة النمط في Some(x)
إذا كانت القيمة في some_option_value
هي None
أي أن النمط هو قابل للدحض، ولكن تقبل تعليمة let
فقط الأنماط غير القابلة للدحض لأنه لا توجد قيمة صالحة تستطيع الشيفرة استخدامها مع قيمة None
. تنبّهنا رست عند استخدام قيمة قابلة للدحض عندما يتطلب الأمر وجود قيمة غير قابلة للدحض وقت التصريف:
$ cargo run Compiling patterns v0.1.0 (file:///projects/patterns) error[E0005]: refutable pattern in local binding: `None` not covered --> src/main.rs:3:9 | 3 | let Some(x) = some_option_value; | ^^^^^^^ pattern `None` not covered | = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant = note: for more information, visit https://doc.rust-lang.org/book/ch18-02-refutability.html note: `Option<i32>` defined here --> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:518:1 | = note: /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:522:5: not covered = note: the matched value is of type `Option<i32>` help: you might want to use `if let` to ignore the variant that isn't matched | 3 | let x = if let Some(x) = some_option_value { x } else { todo!() }; | ++++++++++ ++++++++++++++++++++++ help: alternatively, you might want to use let else to handle the variant that isn't matched | 3 | let Some(x) = some_option_value else { todo!() }; | ++++++++++++++++ For more information about this error, try `rustc --explain E0005`. error: could not compile `patterns` due to previous error
تُعطينا رست الخطأ التصريفي السابق، وذلك بسبب عدم تغطيتنا لكل القيم الممكنة مع النمط Some(x)
، ولن نستطيع فعل ذلك حتى لو أردنا.
يمكننا إصلاح المشكلة في حال وجود نمط قابل للدحض يحل مكان نمط غير قابل للدحض عن طريق تغيير الشيفرة التي تستخدم النمط؛ فبدلًا من استخدام let
نستخدم if let
، وهكذا إذا لم يُطابق النمط تتخطى الشيفرة تلك الشيفرة الموجودة في القوسين المعقوصين وتستمر بذلك صلاحية الشيفرة. تبيّن الشيفرة 9 كيفية إصلاح الخطأ في الشيفرة 8.
if let Some(x) = some_option_value { println!("{}", x); }
[الشيفرة 9: استخدام if let
وكتلة تحتوي على أنماط قابلة للدحض بدلًا من let
]
سُمح للشيفرة السابقة بالتصريف، فهذه الشيفرة صالحة، على الرغم من أنه لا يمكن استخدام نمط غير قابل للدحض دون رسالة خطأ. إذا أعطينا if let
نمطًا يطابق دومًا مثل x
كما في الشيفرة 10، سيمنحنا المصرّف تنبيهًا.
fn main() { if let x = 5 { println!("{}", x); }; }
[الشيفرة 10: محاولة استخدام نمط غير قابل للدحض مع if let
]
تشتكي رست من عدم منطقيّة استخدام if let
مع نمط غير قابل للدحض:
$ cargo run Compiling patterns v0.1.0 (file:///projects/patterns) warning: irrefutable `if let` pattern --> src/main.rs:2:8 | 2 | if let x = 5 { | ^^^^^^^^^ | = note: this pattern will always match, so the `if let` is useless = help: consider replacing the `if let` with a `let` = note: `#[warn(irrefutable_let_patterns)]` on by default warning: `patterns` (bin "patterns") generated 1 warning Finished dev [unoptimized + debuginfo] target(s) in 0.39s Running `target/debug/patterns` 5
يجب أن تستخدم مطابقة الأذرع match arms الأنماط القابلة للدحض لهذا السبب ما عدا الذراع الأخير، الذي يجب أن يطابق أي قيمة متبقية من النمط غير القابل للدحض. تسمح رست باستخدام نمط غير قابل للدحض في match
باستخدام ذراع واحد فقط، ولكن الصياغة هذه ليست مفيدة ويمكن استبدالها بتعليمة let
أبسط.
بعدما عرفنا أماكن استخدام الأنماط والفرق بين الأنماط القابلة للدحض وغير القابلة للدحض، دعنا نكمل طريقة الصياغة syntax التي يمكن استخدامها لإنشاء الأنماط.
ترجمة -وبتصرف- لقسم من الفصل Patterns and Matching من كتاب The Rust Programming Language.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.