جریان کنترلی مختصر با 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 میپردازیم.