جریان کنترلی مختصر با 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
به معنای تایپ کمتر، تورفتگی کمتر، و کدنویسی قالبی (boilerplate) کمتر است.
با این حال، بررسی جامعای که 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
،
اگر بخواهیم بر اساس قدمت ایالتی که روی سکهی ۲۵ سنتی (quarter) است،
چیزی خندهدار بگوییم، میتوانیم یک متد روی 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
دارد.
اگر الگو با مقدار مطابقت داشته باشد، مقدار از درون الگو در حوزهی بیرونی (outer scope) بایند خواهد شد.
اگر الگو مطابقت نداشته باشد، برنامه وارد شاخهی else
خواهد شد،
که باید از تابع بازگردد.
در لیستینگ 6-9 میتوانید ببینید که چگونه لیستینگ 6-8 با استفاده از let...else
به جای 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
برای شفافسازی جریان اجرای تابع.توجه داشته باشید که با این روش، بدنهی اصلی تابع در «مسیر خوشحال» باقی میماند،
بدون آنکه مانند if let
جریان کنترل متفاوت و قابلتوجهی بین دو شاخه ایجاد کند.
اگر در موقعیتی هستید که منطق برنامهتان برای بیان با استفاده از match
بیش از حد مفصل است،
به یاد داشته باشید که if let
و let...else
نیز در جعبهابزار Rust شما قرار دارند.
خلاصه
ما اکنون پوشش دادهایم که چگونه از enumها برای ایجاد انواع سفارشی که میتوانند یکی از مجموعه مقادیر شمارششده باشند استفاده کنید. ما نشان دادهایم که چگونه نوع Option<T>
از کتابخانه استاندارد به شما کمک میکند از سیستم نوع برای جلوگیری از خطاها استفاده کنید. وقتی مقادیر enum دادههایی درون خود دارند، میتوانید از match
یا if let
برای استخراج و استفاده از آن مقادیر استفاده کنید، بسته به تعداد مواردی که باید مدیریت کنید.
برنامههای Rust شما اکنون میتوانند مفاهیمی را در حوزه خود بیان کنند و از ساختارها و enumها استفاده کنند. ایجاد انواع سفارشی برای استفاده در API شما ایمنی نوع را تضمین میکند: کامپایلر مطمئن میشود که توابع شما فقط مقادیری از نوعی که هر تابع انتظار دارد دریافت میکنند.
برای ارائه یک API سازمانیافته به کاربران خود که استفاده از آن ساده باشد و فقط دقیقاً آنچه کاربران شما نیاز دارند را آشکار کند، حالا به ماژولهای Rust میپردازیم.