جریان کنترلی مختصر با if let
و let else
دستور if let
به شما اجازه میدهد که if
و let
را ترکیب کنید و به شکلی کمتر پرحجم، مقادیر مطابق با یک الگو را مدیریت کنید و سایر مقادیر را نادیده بگیرید. برنامهای که در لیستینگ 6-6 نشان داده شده است، بر روی یک مقدار Option<u8>
در متغیر config_max
مطابقت دارد، اما تنها زمانی که مقدار Some
باشد کد را اجرا میکند.
fn main() { let config_max = Some(3u8); match config_max { Some(max) => println!("The maximum is configured to be {max}"), _ => (), } }
match
که تنها به اجرای کد زمانی که مقدار Some
است اهمیت میدهداگر مقدار Some
باشد، مقدار موجود در متغیر Some
را با اتصال به متغیر max
در الگو چاپ میکنیم. ما نمیخواهیم با مقدار None
کاری انجام دهیم. برای برآورده کردن دستور match
، باید _ => ()
را بعد از پردازش تنها یک مورد اضافه کنیم، که کد اضافی آزاردهندهای است.
در عوض، میتوانیم این کد را به شکلی کوتاهتر با استفاده از if let
بنویسیم. کد زیر به همان شکل match
در لیستینگ 6-6 عمل میکند:
fn main() { let config_max = Some(3u8); if let Some(max) = config_max { println!("The maximum is configured to be {max}"); } }
دستور if let
یک الگو و یک عبارت را میگیرد که با یک علامت مساوی جدا شدهاند. این دستور همانند match
عمل میکند، جایی که عبارت به match
داده میشود و الگو بازوی اول آن است. در این مورد، الگو Some(max)
است و متغیر max
مقدار داخل Some
را میگیرد. سپس میتوانیم از max
در بدنه بلوک if let
همانطور که در بازوی متناظر match
استفاده کردیم، استفاده کنیم. کد در بلوک if let
تنها در صورتی اجرا میشود که مقدار با الگو مطابقت داشته باشد.
استفاده از if let
به معنای تایپ کمتر، تورفتگی کمتر و کد اضافی کمتر است. با این حال، شما بررسی کامل که match
اعمال میکند را از دست میدهید. انتخاب بین match
و if let
به این بستگی دارد که در وضعیت خاص شما چه کاری انجام میدهید و آیا بهدست آوردن اختصار به بهای از دست دادن بررسی کامل، معاملهی مناسبی است یا خیر.
به عبارت دیگر، میتوانید if let
را به عنوان یک قند سینتکس برای match
تصور کنید که کد را زمانی که مقدار با یک الگو مطابقت دارد اجرا میکند و سپس تمام مقادیر دیگر را نادیده میگیرد.
ما میتوانیم یک else
با یک if let
اضافه کنیم. بلوک کدی که با else
همراه میشود همان بلوک کدی است که با مورد _
در دستور match
که معادل if let
و else
است همراه میشود. دستور Coin
را در لیستینگ 6-4 به یاد بیاورید، جایی که نوع Quarter
یک مقدار UsState
را نیز در خود جای داده بود. اگر میخواستیم تمام سکههای غیر Quarter
را که میبینیم بشماریم، همزمان ایالتهای سکههای Quarter
را اعلام کنیم، میتوانستیم این کار را با یک دستور match
انجام دهیم، مانند این:
#[derive(Debug)] enum UsState { Alabama, Alaska, // --snip-- } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn main() { let coin = Coin::Penny; let mut count = 0; match coin { Coin::Quarter(state) => println!("State quarter from {state:?}!"), _ => count += 1, } }
یا میتوانستیم از یک عبارت if let
و else
استفاده کنیم، مانند این:
#[derive(Debug)] enum UsState { Alabama, Alaska, // --snip-- } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn main() { let coin = Coin::Penny; let mut count = 0; if let Coin::Quarter(state) = coin { println!("State quarter from {state:?}!"); } else { count += 1; } }
ماندن در “مسیر خوشحال” با let else
یک الگوی معمول این است که زمانی که مقداری وجود دارد محاسباتی را انجام دهید و در غیر این صورت یک مقدار پیشفرض را بازگردانید. با ادامه مثال ما از سکهها با مقدار UsState
، اگر میخواستیم چیزی خندهدار بگوییم که بسته به سن ایالت بر روی سکه بود، ممکن است متدی برای بررسی سن ایالت ایجاد کنیم، مانند این:
#[derive(Debug)] // so we can inspect the state in a minute enum UsState { Alabama, Alaska, // --snip-- } impl UsState { fn existed_in(&self, year: u16) -> bool { match self { UsState::Alabama => year >= 1819, UsState::Alaska => year >= 1959, // -- snip -- } } } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn describe_state_quarter(coin: Coin) -> Option<String> { if let Coin::Quarter(state) = coin { if state.existed_in(1900) { Some(format!("{state:?} is pretty old, for America!")) } else { Some(format!("{state:?} is relatively new.")) } } else { None } } fn main() { if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) { println!("{desc}"); } }
سپس ممکن است از if let
برای مطابقت با نوع سکه استفاده کنیم، متغیری به نام state
را در بدنه شرط معرفی کنیم، همانطور که در لیستینگ 6-7 نشان داده شده است.
#[derive(Debug)] // so we can inspect the state in a minute enum UsState { Alabama, Alaska, // --snip-- } impl UsState { fn existed_in(&self, year: u16) -> bool { match self { UsState::Alabama => year >= 1819, UsState::Alaska => year >= 1959, // -- snip -- } } } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn describe_state_quarter(coin: Coin) -> Option<String> { if let Coin::Quarter(state) = coin { if state.existed_in(1900) { Some(format!("{state:?} is pretty old, for America!")) } else { Some(format!("{state:?} is relatively new.")) } } else { None } } fn main() { if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) { println!("{desc}"); } }
if let
برای تولید یک مقدار یا بازگشت زودهنگام.این کار انجام میشود، اما کار را به داخل بدنه دستور if let
منتقل میکند، و اگر کار انجامشده پیچیدهتر باشد، ممکن است دشوار باشد که دقیقاً چگونه شاخههای سطح بالا به هم مربوط هستند. ما همچنین میتوانیم از این واقعیت استفاده کنیم که عبارات یک مقدار تولید میکنند، یا برای تولید state
از if let
یا برای بازگشت زودهنگام، همانطور که در لیستینگ 6-8 نشان داده شده است. (شما میتوانید مشابه آن را با یک match
نیز انجام دهید!)
#[derive(Debug)] // so we can inspect the state in a minute enum UsState { Alabama, Alaska, // --snip-- } impl UsState { fn existed_in(&self, year: u16) -> bool { match self { UsState::Alabama => year >= 1819, UsState::Alaska => year >= 1959, // -- snip -- } } } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn describe_state_quarter(coin: Coin) -> Option<String> { let state = if let Coin::Quarter(state) = coin { state } else { return None; }; if state.existed_in(1900) { Some(format!("{state:?} is pretty old, for America!")) } else { Some(format!("{state:?} is relatively new.")) } } fn main() { if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) { println!("{desc}"); } }
if let
برای تولید یک مقدار یا بازگشت زودهنگام.این تا حدی آزاردهنده است! یک شاخه if let
یک مقدار تولید میکند و دیگری کاملاً از تابع بازمیگردد.
برای زیباتر کردن این الگوی رایج، Rust از let
-else
استفاده میکند. سینتکس let
-else
یک الگو در سمت چپ و یک عبارت در سمت راست میگیرد، بسیار شبیه به if let
، اما شاخه if
ندارد و فقط یک شاخه else
دارد. اگر الگو تطابق داشته باشد، مقدار الگو را در دامنه خارجی بایند میکند. اگر الگو تطابق نداشته باشد، برنامه به شاخه else
منتقل میشود که باید از تابع بازگردد.
در لیستینگ 6-9، میتوانید ببینید که لیستینگ 6-8 چگونه با استفاده از let else
به جای if let
به نظر میرسد. توجه کنید که این روش “در مسیر خوشحال” در بدنه اصلی تابع باقی میماند، بدون اینکه کنترل جریان برای دو شاخه به طور قابل توجهی متفاوت باشد همانطور که if let
انجام داد.
#[derive(Debug)] // so we can inspect the state in a minute enum UsState { Alabama, Alaska, // --snip-- } impl UsState { fn existed_in(&self, year: u16) -> bool { match self { UsState::Alabama => year >= 1819, UsState::Alaska => year >= 1959, // -- snip -- } } } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn describe_state_quarter(coin: Coin) -> Option<String> { let Coin::Quarter(state) = coin else { return None; }; if state.existed_in(1900) { Some(format!("{state:?} is pretty old, for America!")) } else { Some(format!("{state:?} is relatively new.")) } } fn main() { if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) { println!("{desc}"); } }
let else
برای واضحتر کردن جریان درون تابع.اگر در موقعیتی هستید که منطق برنامه شما برای استفاده از یک match
بسیار پرحجم است، به یاد داشته باشید که if let
و let else
نیز در ابزارهای Rust شما موجود هستند.
خلاصه
ما اکنون پوشش دادهایم که چگونه از enumها برای ایجاد انواع سفارشی که میتوانند یکی از مجموعه مقادیر شمارششده باشند استفاده کنید. ما نشان دادهایم که چگونه نوع Option<T>
از کتابخانه استاندارد به شما کمک میکند از سیستم نوع برای جلوگیری از خطاها استفاده کنید. وقتی مقادیر enum دادههایی درون خود دارند، میتوانید از match
یا if let
برای استخراج و استفاده از آن مقادیر استفاده کنید، بسته به تعداد مواردی که باید مدیریت کنید.
برنامههای Rust شما اکنون میتوانند مفاهیمی را در حوزه خود بیان کنند و از ساختارها و enumها استفاده کنند. ایجاد انواع سفارشی برای استفاده در API شما ایمنی نوع را تضمین میکند: کامپایلر مطمئن میشود که توابع شما فقط مقادیری از نوعی که هر تابع انتظار دارد دریافت میکنند.
برای ارائه یک API سازمانیافته به کاربران خود که استفاده از آن ساده باشد و فقط دقیقاً آنچه کاربران شما نیاز دارند را آشکار کند، حالا به ماژولهای Rust میپردازیم.