سازه جریان کنترلی match
زبان Rust دارای یک سازه جریان کنترلی بسیار قدرتمند به نام match
است که به شما اجازه میدهد تا یک مقدار را با یک سری الگوها مقایسه کنید و سپس بر اساس الگویی که مطابقت دارد، کد مربوطه را اجرا کنید. الگوها میتوانند شامل مقادیر ثابت، نام متغیرها، wildcardها و چیزهای دیگر باشند. فصل 19 انواع مختلف الگوها و عملکرد آنها را پوشش میدهد. قدرت 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
که حالتهای enum را به عنوان الگوهای خود داردبازبینی تابع value_in_cents
ابتدا کلمه کلیدی match
و سپس یک عبارت را فهرست میکنیم که در این مورد مقدار coin
است. این کار بسیار مشابه یک عبارت شرطی که با if
استفاده میشود به نظر میرسد، اما تفاوت بزرگی دارد: با if
، شرط باید به یک مقدار بولین ارزیابی شود، اما اینجا میتواند هر نوعی باشد. نوع coin
در این مثال enum Coin
است که در اولین خط تعریف کردیم.
بازوهای match
دو قسمت دارند: یک الگو و مقداری کد. اولین بازو در اینجا دارای الگویی است که مقدار Coin::Penny
است و سپس اپراتور =>
که الگو و کد اجرایی را از هم جدا میکند. کد در اینجا فقط مقدار 1
است. هر بازو با یک کاما از بازوی بعدی جدا میشود.
هنگامی که عبارت match
اجرا میشود، مقدار حاصل را با الگوی هر بازو به ترتیب مقایسه میکند. اگر الگویی با مقدار مطابقت داشته باشد، کدی که با آن الگو مرتبط است اجرا میشود. اگر آن الگو با مقدار مطابقت نداشته باشد، اجرا به بازوی بعدی ادامه مییابد، همانطور که در یک دستگاه مرتبکننده سکهها عمل میکند. ما میتوانیم به هر تعداد بازو که نیاز داریم داشته باشیم: در فهرست 6-3، match
ما چهار بازو دارد.
کد مرتبط با هر بازو یک عبارت است و مقدار حاصل از عبارت در بازوی منطبق شده، مقداری است که برای کل عبارت match
بازگردانده میشود.
معمولاً اگر کد بازوی match
کوتاه باشد، از آکولاد استفاده نمیکنیم، همانطور که در فهرست 6-3 که هر بازو فقط یک مقدار را برمیگرداند. اگر بخواهید چندین خط کد را در یک بازو اجرا کنید، باید از آکولاد استفاده کنید، و در این صورت کاما پس از بازو اختیاری است. به عنوان مثال، کد زیر هر بار که متد با یک Coin::Penny
فراخوانی میشود، “Lucky 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() {}
الگوهایی که به مقادیر متصل میشوند
یکی دیگر از ویژگیهای مفید بازوهای match
این است که میتوانند به بخشهایی از مقادیر که با الگو مطابقت دارند متصل شوند. این همان روشی است که میتوانیم مقادیر را از حالتهای enum استخراج کنیم.
به عنوان مثال، بیایید یکی از حالتهای enum خود را تغییر دهیم تا دادههایی را درون خود نگه دارد. از سال 1999 تا 2008، ایالات متحده ربعهایی با طرحهای مختلف برای هر یک از 50 ایالت در یک طرف ضرب کرد. هیچ سکه دیگری طرح ایالتی نداشت، بنابراین فقط ربعها این مقدار اضافی را دارند. میتوانیم این اطلاعات را به 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() {}
Coin
که حالت Quarter
آن همچنین یک مقدار UsState
را نگه میداردبیایید تصور کنیم که یک دوست ما سعی دارد تمام 50 ربع ایالتی را جمعآوری کند. در حالی که ما پولهای خود را بر اساس نوع سکه مرتب میکنیم، همچنین نام ایالتی که با هر ربع مرتبط است را اعلام میکنیم تا اگر این یکی از آنهایی باشد که دوست ما ندارد، بتوانند آن را به مجموعه خود اضافه کنند.
در عبارت match
برای این کد، یک متغیر به نام 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)
خواهد بود. هنگامی که آن مقدار را با هر بازوی match
مقایسه میکنیم، هیچکدام از آنها مطابقت ندارند تا اینکه به Coin::Quarter(state)
برسیم. در این نقطه، اتصال برای state
مقدار UsState::Alaska
خواهد بود. سپس میتوانیم از آن اتصال در عبارت println!
استفاده کنیم و به این ترتیب مقدار داخلی ایالت را از حالت Quarter
enum Coin
استخراج کنیم.
تطبیق با Option<T>
در بخش قبلی، ما میخواستیم مقدار داخلی T
را از حالت Some
استخراج کنیم زمانی که از Option<T>
استفاده میکردیم؛ همچنین میتوانیم با استفاده از match
حالتهای Option<T>
را مدیریت کنیم، همانطور که با enum Coin
انجام دادیم! به جای مقایسه سکهها، حالتهای Option<T>
را مقایسه میکنیم، اما روش کار عبارت match
همان باقی میماند.
بیایید فرض کنیم که میخواهیم تابعی بنویسیم که یک Option<i32>
بگیرد و اگر یک مقدار درون آن باشد، مقدار 1 را به آن اضافه کند. اگر هیچ مقداری درون آن نباشد، تابع باید مقدار 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); }
اجازه دهید اولین اجرای plus_one
را با جزئیات بیشتری بررسی کنیم. وقتی که plus_one(five)
را فراخوانی میکنیم، متغیر x
در بدنه plus_one
مقدار Some(5)
خواهد داشت. سپس آن را با هر بازوی 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);
}
مقدار 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
میگیرد. سپس کد موجود در بازوی match
اجرا میشود، بنابراین مقدار 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
و enumها در بسیاری از موقعیتها مفید است. این الگو را در کد Rust زیاد خواهید دید: match
روی یک enum، اتصال یک متغیر به داده داخل، و سپس اجرای کد بر اساس آن. ممکن است در ابتدا کمی سخت باشد، اما وقتی به آن عادت کنید، آرزو خواهید کرد که در همه زبانها وجود داشته باشد. این سازه همواره یکی از ویژگیهای مورد علاقه کاربران است.
تطابقها Exhaustive هستند
یکی دیگر از جنبههای عبارت match
این است که الگوهای بازوها باید تمام حالتهای ممکن را پوشش دهند. به این نسخه از تابع plus_one
که یک باگ دارد و کامپایل نمیشود توجه کنید:
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
--> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/core/src/option.rs:571:1
|
571 | pub enum Option<T> {
| ^^^^^^^^^^^^^^^^^^
...
575 | None,
| ---- 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 و Placeholder _
با استفاده از Enumها، میتوانیم اقدامات ویژهای برای چند مقدار خاص انجام دهیم، اما برای تمام مقادیر دیگر یک عمل پیشفرض داشته باشیم. تصور کنید که در حال پیادهسازی یک بازی هستید که اگر بازیکن عدد 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 نیاز تطابق exhaustive را برآورده میکند. توجه داشته باشید که باید بازوی catch-all را در آخر قرار دهیم زیرا الگوها به ترتیب ارزیابی میشوند. اگر بازوی catch-all را زودتر قرار دهیم، بازوهای دیگر هرگز اجرا نخواهند شد، بنابراین Rust به ما هشدار میدهد اگر بعد از یک بازوی catch-all بازوهای دیگری اضافه کنیم!
Rust همچنین یک الگو به نام _
دارد که میتوانیم از آن استفاده کنیم وقتی که میخواهیم یک catch-all داشته باشیم اما نمیخواهیم مقدار در الگوی catch-all را استفاده کنیم. این به Rust میگوید که ما قصد نداریم مقدار را استفاده کنیم، بنابراین Rust درباره یک متغیر استفاده نشده به ما هشدار نمیدهد.
بیایید قوانین بازی را تغییر دهیم: حالا اگر بازیکن هر چیزی به غیر از 3 یا 7 بیاورد، باید دوباره تاس بیندازد. دیگر نیازی به استفاده از مقدار catch-all نیست، بنابراین میتوانیم کد خود را بهجای متغیری به نام 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() {} }
این مثال نیز نیاز تطابق exhaustive را برآورده میکند زیرا ما صریحاً تمام مقادیر دیگر را در بازوی آخر نادیده گرفتهایم و چیزی را فراموش نکردهایم.
در نهایت، قوانین بازی را یک بار دیگر تغییر میدهیم، بنابراین اگر بازیکن هر چیزی غیر از 3 یا 7 بیاورد، هیچ کار دیگری در نوبت او انجام نمیشود. میتوانیم این موضوع را با استفاده از مقدار واحد (نوع tuple
خالی که قبلاً در بخش “نوع Tuple” ذکر شد) به عنوان کدی که با بازوی _
همراه است بیان کنیم:
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
کمی طولانی به نظر میرسد، مفید باشد.