جریان کنترلی مختصر با 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}"),
        _ => (),
    }
}
Listing 6-6: یک 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 نشان داده شده است.

Filename: src/main.rs
#[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}");
    }
}
Listing 6-7: استفاده از if let برای تولید یک مقدار یا بازگشت زودهنگام.

این کار انجام می‌شود، اما کار را به داخل بدنه دستور if let منتقل می‌کند، و اگر کار انجام‌شده پیچیده‌تر باشد، ممکن است دشوار باشد که دقیقاً چگونه شاخه‌های سطح بالا به هم مربوط هستند. ما همچنین می‌توانیم از این واقعیت استفاده کنیم که عبارات یک مقدار تولید می‌کنند، یا برای تولید state از if let یا برای بازگشت زودهنگام، همان‌طور که در لیستینگ 6-8 نشان داده شده است. (شما می‌توانید مشابه آن را با یک match نیز انجام دهید!)

Filename: src/main.rs
#[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}");
    }
}
Listing 6-8: استفاده از if let برای تولید یک مقدار یا بازگشت زودهنگام.

این تا حدی آزاردهنده است! یک شاخه if let یک مقدار تولید می‌کند و دیگری کاملاً از تابع بازمی‌گردد.

برای زیباتر کردن این الگوی رایج، Rust از let-else استفاده می‌کند. سینتکس let-else یک الگو در سمت چپ و یک عبارت در سمت راست می‌گیرد، بسیار شبیه به if let، اما شاخه if ندارد و فقط یک شاخه else دارد. اگر الگو تطابق داشته باشد، مقدار الگو را در دامنه خارجی بایند می‌کند. اگر الگو تطابق نداشته باشد، برنامه به شاخه else منتقل می‌شود که باید از تابع بازگردد.

در لیستینگ 6-9، می‌توانید ببینید که لیستینگ 6-8 چگونه با استفاده از let else به جای if let به نظر می‌رسد. توجه کنید که این روش “در مسیر خوشحال” در بدنه اصلی تابع باقی می‌ماند، بدون اینکه کنترل جریان برای دو شاخه به طور قابل توجهی متفاوت باشد همان‌طور که if let انجام داد.

Filename: src/main.rs
#[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}");
    }
}
Listing 6-9: استفاده از let else برای واضح‌تر کردن جریان درون تابع.

اگر در موقعیتی هستید که منطق برنامه شما برای استفاده از یک match بسیار پرحجم است، به یاد داشته باشید که if let و let else نیز در ابزارهای Rust شما موجود هستند.

خلاصه

ما اکنون پوشش داده‌ایم که چگونه از enumها برای ایجاد انواع سفارشی که می‌توانند یکی از مجموعه مقادیر شمارش‌شده باشند استفاده کنید. ما نشان داده‌ایم که چگونه نوع Option<T> از کتابخانه استاندارد به شما کمک می‌کند از سیستم نوع برای جلوگیری از خطاها استفاده کنید. وقتی مقادیر enum داده‌هایی درون خود دارند، می‌توانید از match یا if let برای استخراج و استفاده از آن مقادیر استفاده کنید، بسته به تعداد مواردی که باید مدیریت کنید.

برنامه‌های Rust شما اکنون می‌توانند مفاهیمی را در حوزه خود بیان کنند و از ساختارها و enumها استفاده کنند. ایجاد انواع سفارشی برای استفاده در API شما ایمنی نوع را تضمین می‌کند: کامپایلر مطمئن می‌شود که توابع شما فقط مقادیری از نوعی که هر تابع انتظار دارد دریافت می‌کنند.

برای ارائه یک API سازمان‌یافته به کاربران خود که استفاده از آن ساده باشد و فقط دقیقاً آنچه کاربران شما نیاز دارند را آشکار کند، حالا به ماژول‌های Rust می‌پردازیم.