closureهای Rust توابع ناشناسی هستند که می‌توانید آن‌ها را در یک متغیر ذخیره کنید یا به عنوان آرگومان به توابع دیگر ارسال کنید. شما می‌توانید closure را در یک مکان ایجاد کنید و سپس آن را در جای دیگری فراخوانی کنید تا در یک زمینه متفاوت ارزیابی شود. برخلاف توابع، closureها می‌توانند مقادیر را از محیطی که در آن تعریف شده‌اند، بگیرند. ما نشان خواهیم داد که چگونه این ویژگی‌های closure امکان استفاده مجدد از کد و سفارشی‌سازی رفتار را فراهم می‌کند.

گرفتن محیط با closureها

ابتدا بررسی خواهیم کرد که چگونه می‌توان از closureها برای گرفتن مقادیر از محیطی که در آن تعریف شده‌اند، برای استفاده در آینده استفاده کرد. سناریوی زیر را در نظر بگیرید: هر چند وقت یک بار، شرکت تی‌شرت ما یک تی‌شرت انحصاری و نسخه محدود به شخصی از لیست پستی خود به عنوان تبلیغ هدیه می‌دهد. افرادی که در لیست پستی هستند می‌توانند به صورت اختیاری رنگ مورد علاقه خود را به پروفایل خود اضافه کنند. اگر شخصی که برای تی‌شرت رایگان انتخاب شده است رنگ مورد علاقه خود را تنظیم کرده باشد، آن رنگ را دریافت می‌کند. اگر شخص رنگ مورد علاقه‌ای مشخص نکرده باشد، رنگی که شرکت بیشترین تعداد آن را دارد، به او داده می‌شود.

راه‌های زیادی برای پیاده‌سازی این سناریو وجود دارد. در این مثال، ما از یک enum به نام ShirtColor استفاده می‌کنیم که شامل مقادیر Red و Blue است (برای سادگی تعداد رنگ‌های موجود را محدود کرده‌ایم). موجودی شرکت را با یک ساختار Inventory نشان می‌دهیم که یک فیلد به نام shirts دارد که یک Vec<ShirtColor> از رنگ‌های تی‌شرت موجود را نشان می‌دهد. متدی به نام giveaway که در Inventory تعریف شده است، اولویت رنگ تی‌شرت کاربر برنده را دریافت کرده و رنگ تی‌شرتی که به آن فرد داده می‌شود را برمی‌گرداند. این تنظیمات در لیستینگ 13-1 نشان داده شده است:

Filename: src/main.rs
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
}

struct Inventory {
    shirts: Vec<ShirtColor>,
}

impl Inventory {
    fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
        user_preference.unwrap_or_else(|| self.most_stocked())
    }

    fn most_stocked(&self) -> ShirtColor {
        let mut num_red = 0;
        let mut num_blue = 0;

        for color in &self.shirts {
            match color {
                ShirtColor::Red => num_red += 1,
                ShirtColor::Blue => num_blue += 1,
            }
        }
        if num_red > num_blue {
            ShirtColor::Red
        } else {
            ShirtColor::Blue
        }
    }
}

fn main() {
    let store = Inventory {
        shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
    };

    let user_pref1 = Some(ShirtColor::Red);
    let giveaway1 = store.giveaway(user_pref1);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref1, giveaway1
    );

    let user_pref2 = None;
    let giveaway2 = store.giveaway(user_pref2);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref2, giveaway2
    );
}
Listing 13-1: سناریوی هدیه شرکت تی‌شرت

در این کد، store تعریف‌شده در main دو تی‌شرت آبی و یک تی‌شرت قرمز باقی‌مانده برای توزیع در این تبلیغ نسخه محدود دارد. ما متد giveaway را برای یک کاربر با ترجیح یک تی‌شرت قرمز و یک کاربر بدون هیچ ترجیحی فراخوانی می‌کنیم.

دوباره تأکید می‌کنیم که این کد را می‌توان به روش‌های مختلفی پیاده‌سازی کرد. در اینجا، برای تمرکز بر closureها، به مفاهیمی که قبلاً آموخته‌اید پایبند مانده‌ایم، به جز بخش بدنه متد giveaway که از یک closure استفاده می‌کند. در متد giveaway، ما اولویت کاربر را به عنوان یک آرگومان از نوع Option<ShirtColor> دریافت می‌کنیم و متد unwrap_or_else را روی user_preference فراخوانی می‌کنیم.

متد unwrap_or_else روی Option<T> توسط کتابخانه استاندارد تعریف شده است. این متد یک آرگومان می‌گیرد: یک closure بدون هیچ آرگومانی که یک مقدار T را بازمی‌گرداند (همان نوعی که در متغیر Some از Option<T> ذخیره شده است، در این مورد ShirtColor). اگر Option<T> مقدار Some داشته باشد، unwrap_or_else مقدار داخل Some را بازمی‌گرداند. اگر Option<T> مقدار None باشد، unwrap_or_else closure را فراخوانی کرده و مقداری که closure بازمی‌گرداند را بازمی‌گرداند.

ما عبارت closure || self.most_stocked() را به عنوان آرگومان به unwrap_or_else ارسال می‌کنیم. این یک closure است که خود هیچ آرگومانی نمی‌گیرد (اگر closure آرگومان‌هایی داشت، آن‌ها بین دو خط عمودی قرار می‌گرفتند). بدنه closure متد self.most_stocked() را فراخوانی می‌کند. ما closure را اینجا تعریف می‌کنیم و پیاده‌سازی unwrap_or_else در صورت نیاز، closure را ارزیابی می‌کند.

اجرای این کد موارد زیر را چاپ می‌کند:

$ cargo run
   Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue

یکی از جنبه‌های جالب در اینجا این است که ما یک closure ارسال کرده‌ایم که متد self.most_stocked() را روی نمونه فعلی Inventory فراخوانی می‌کند. کتابخانه استاندارد نیازی به دانستن چیزی درباره انواع Inventory یا ShirtColor که تعریف کرده‌ایم یا منطقی که می‌خواهیم در این سناریو استفاده کنیم، ندارد. closure یک ارجاع غیرقابل تغییر به نمونه self از Inventory را می‌گیرد و آن را همراه با کدی که مشخص کرده‌ایم به متد unwrap_or_else ارسال می‌کند. از طرف دیگر، توابع قادر به گرفتن محیط خود به این صورت نیستند.

استنباط نوع closure و حاشیه‌نویسی

تفاوت‌های بیشتری بین توابع و closureها وجود دارد. closureها معمولاً نیازی به حاشیه‌نویسی انواع آرگومان‌ها یا مقدار بازگشتی ندارند، برخلاف توابع fn که به این حاشیه‌نویسی نیاز دارند. حاشیه‌نویسی انواع در توابع ضروری است زیرا این انواع بخشی از رابط کاربری صریحی هستند که برای کاربران شما ارائه می‌شود. تعریف این رابط به صورت سختگیرانه برای اطمینان از توافق همه در مورد انواع مقادیر استفاده شده و بازگشتی یک تابع مهم است. از طرف دیگر، closureها به این صورت در یک رابط کاربری صریح استفاده نمی‌شوند: آن‌ها در متغیرها ذخیره می‌شوند و بدون نام‌گذاری و افشای آن‌ها به کاربران کتابخانه ما استفاده می‌شوند.

closureها معمولاً کوتاه هستند و فقط در یک زمینه محدود مرتبط هستند، نه در هر سناریوی دلخواه. در این زمینه‌های محدود، کامپایلر می‌تواند انواع پارامترها و مقدار بازگشتی را استنباط کند، مشابه آنچه که می‌تواند انواع اکثر متغیرها را استنباط کند (موارد نادری وجود دارند که کامپایلر به حاشیه‌نویسی نوع closure نیز نیاز دارد).

همانند متغیرها، ما می‌توانیم حاشیه‌نویسی نوع اضافه کنیم اگر بخواهیم وضوح و شفافیت را افزایش دهیم، به قیمت پرحرف‌تر شدن از آنچه که به طور دقیق ضروری است. افزودن حاشیه‌نویسی نوع برای یک closure به این صورت است که در لیستینگ 13-2 نشان داده شده است. در این مثال، ما یک closure تعریف کرده و آن را در یک متغیر ذخیره می‌کنیم، به جای اینکه closure را در مکانی که به عنوان آرگومان ارسال می‌کنیم تعریف کنیم، همانطور که در لیستینگ 13-1 انجام دادیم.
Filename: src/main.rs
use std::thread;
use std::time::Duration;

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_closure = |num: u32| -> u32 {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };

    if intensity < 25 {
        println!("Today, do {} pushups!", expensive_closure(intensity));
        println!("Next, do {} situps!", expensive_closure(intensity));
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_closure(intensity)
            );
        }
    }
}

fn main() {
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(simulated_user_specified_value, simulated_random_number);
}
Listing 13-2: افزودن حاشیه‌نویسی‌های اختیاری برای انواع آرگومان‌ها و مقدار بازگشتی در closure

با اضافه کردن حاشیه‌نویسی نوع، نحوه نوشتن closureها بیشتر شبیه به نوشتن توابع می‌شود. در اینجا، ما یک تابع تعریف کرده‌ایم که 1 به آرگومان خود اضافه می‌کند و یک closure که همان رفتار را دارد، برای مقایسه. ما فضاهایی اضافه کرده‌ایم تا بخش‌های مرتبط را هم‌ردیف کنیم. این نشان می‌دهد که نحو closure چقدر شبیه به نحو توابع است، به جز استفاده از خطوط عمودی و میزان نحوی که اختیاری است.

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

خط اول تعریف یک تابع را نشان می‌دهد، و خط دوم تعریف یک closure با حاشیه‌نویسی کامل را نمایش می‌دهد. در خط سوم، حاشیه‌نویسی انواع از تعریف closure حذف شده است. در خط چهارم، براکت‌ها را حذف می‌کنیم، که اختیاری هستند زیرا بدنه closure فقط یک عبارت دارد. همه این‌ها تعاریف معتبری هستند که هنگام فراخوانی رفتار یکسانی تولید می‌کنند. خطوط add_one_v3 و add_one_v4 نیاز دارند که closureها ارزیابی شوند تا کامپایل شوند زیرا انواع از نحوه استفاده آن‌ها استنباط خواهند شد. این مشابه با let v = Vec::new(); است که نیاز دارد یا حاشیه‌نویسی نوع داشته باشد یا مقادیر از نوعی در Vec وارد شوند تا Rust بتواند نوع را استنباط کند.

برای تعریف closureها، کامپایلر یک نوع مشخص برای هر یک از پارامترها و مقدار بازگشتی آن‌ها استنباط می‌کند. برای مثال، لیستینگ 13-3 تعریف یک closure کوتاه را نشان می‌دهد که فقط مقداری که به عنوان پارامتر دریافت می‌کند را بازمی‌گرداند. این closure برای اهداف این مثال استفاده چندانی ندارد. توجه کنید که هیچ حاشیه‌نویسی نوعی به تعریف اضافه نکرده‌ایم. چون هیچ حاشیه‌نویسی وجود ندارد، می‌توانیم closure را با هر نوعی فراخوانی کنیم، همان‌طور که اولین بار این کار را با String انجام دادیم. اگر سپس سعی کنیم example_closure را با یک عدد صحیح فراخوانی کنیم، خطایی دریافت خواهیم کرد.

Filename: src/main.rs
fn main() {
    let example_closure = |x| x;

    let s = example_closure(String::from("hello"));
    let n = example_closure(5);
}
Listing 13-3: تلاش برای فراخوانی یک closure که انواع آن با استفاده از دو نوع مختلف استنباط شده است

کامپایلر این خطا را می‌دهد:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
 --> src/main.rs:5:29
  |
5 |     let n = example_closure(5);
  |             --------------- ^- help: try using a conversion method: `.to_string()`
  |             |               |
  |             |               expected `String`, found integer
  |             arguments to this function are incorrect
  |
note: expected because the closure was earlier called with an argument of type `String`
 --> src/main.rs:4:29
  |
4 |     let s = example_closure(String::from("hello"));
  |             --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
  |             |
  |             in this closure call
note: closure parameter defined here
 --> src/main.rs:2:28
  |
2 |     let example_closure = |x| x;
  |                            ^

For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error

اولین باری که example_closure را با مقدار String فراخوانی می‌کنیم، کامپایلر نوع x و مقدار بازگشتی closure را به عنوان String استنباط می‌کند. سپس این انواع در closure example_closure قفل می‌شوند و هنگام تلاش برای استفاده از یک نوع دیگر با همان closure، یک خطای نوع دریافت می‌کنیم.

گرفتن ارجاعات یا انتقال مالکیت

closureها می‌توانند مقادیر را از محیط خود به سه روش بگیرند که مستقیماً به سه روشی که یک تابع می‌تواند یک پارامتر بگیرد، نگاشت می‌شوند: قرض‌گیری غیرقابل تغییر، قرض‌گیری قابل تغییر، و گرفتن مالکیت. closure تصمیم می‌گیرد که کدام یک از این‌ها را بر اساس کاری که بدنه تابع با مقادیر گرفته شده انجام می‌دهد، استفاده کند.

در لیستینگ 13-4، یک closure تعریف می‌کنیم که یک ارجاع غیرقابل تغییر به بردار با نام list را می‌گیرد زیرا فقط به یک ارجاع غیرقابل تغییر نیاز دارد تا مقدار را چاپ کند:

Filename: src/main.rs
fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    let only_borrows = || println!("From closure: {list:?}");

    println!("Before calling closure: {list:?}");
    only_borrows();
    println!("After calling closure: {list:?}");
}
Listing 13-4: تعریف و فراخوانی یک closure که یک ارجاع غیرقابل تغییر می‌گیرد

این مثال همچنین نشان می‌دهد که یک متغیر می‌تواند به تعریف یک closure متصل شود و بعداً می‌توان closure را با استفاده از نام متغیر و پرانتزها فراخوانی کرد، گویی که نام متغیر یک نام تابع است.

از آنجا که می‌توانیم چندین ارجاع غیرقابل تغییر به list به طور همزمان داشته باشیم، list همچنان از کدی که قبل از تعریف closure، بعد از تعریف closure اما قبل از فراخوانی closure و بعد از فراخوانی closure وجود دارد، قابل دسترسی است. این کد کامپایل شده، اجرا می‌شود و نتیجه زیر را چاپ می‌کند:

$ cargo run
     Locking 1 package to latest compatible version
      Adding closure-example v0.1.0 (/Users/chris/dev/rust-lang/book/tmp/listings/ch13-functional-features/listing-13-04)
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]

در ادامه، در لیستینگ 13-5، بدنه closure را تغییر می‌دهیم تا یک عنصر به بردار list اضافه کند. closure اکنون یک ارجاع قابل تغییر می‌گیرد:

Filename: src/main.rs
fn main() {
    let mut list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    let mut borrows_mutably = || list.push(7);

    borrows_mutably();
    println!("After calling closure: {list:?}");
}
Listing 13-5: تعریف و فراخوانی یک closure که یک ارجاع قابل تغییر می‌گیرد

این کد کامپایل شده، اجرا می‌شود و نتیجه زیر را چاپ می‌کند:

$ cargo run
     Locking 1 package to latest compatible version
      Adding closure-example v0.1.0 (/Users/chris/dev/rust-lang/book/tmp/listings/ch13-functional-features/listing-13-05)
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]

توجه داشته باشید که دیگر println! بین تعریف و فراخوانی closure borrows_mutably وجود ندارد: زمانی که borrows_mutably تعریف می‌شود، یک ارجاع قابل تغییر به list می‌گیرد. ما بعد از فراخوانی closure دوباره از آن استفاده نمی‌کنیم، بنابراین قرض‌گیری قابل تغییر پایان می‌یابد. بین تعریف closure و فراخوانی آن، قرض‌گیری غیرقابل تغییر برای چاپ مجاز نیست، زیرا هیچ قرض دیگری هنگام وجود یک قرض قابل تغییر مجاز نیست. سعی کنید یک println! در آنجا اضافه کنید تا ببینید چه پیام خطایی دریافت می‌کنید!

اگر بخواهید closure را مجبور کنید که مالکیت مقادیر استفاده‌شده در محیط را بگیرد، حتی اگر بدنه closure به طور دقیق به مالکیت نیاز نداشته باشد، می‌توانید از کلیدواژه move قبل از لیست پارامترها استفاده کنید.

این تکنیک بیشتر زمانی مفید است که یک closure را به یک نخ جدید ارسال می‌کنید تا داده‌ها به گونه‌ای انتقال داده شوند که توسط نخ جدید مالکیت پیدا کنند. ما موضوع نخ‌ها و دلایلی که ممکن است بخواهید از آن‌ها استفاده کنید را به تفصیل در فصل 16 زمانی که در مورد هم‌زمانی صحبت می‌کنیم، بررسی خواهیم کرد. اما برای حالا، بیایید به صورت مختصر ایجاد یک نخ جدید با استفاده از یک closure که به کلیدواژه move نیاز دارد را بررسی کنیم. لیستینگ 13-6 لیستینگ 13-4 را اصلاح می‌کند تا بردار را در یک نخ جدید چاپ کند به جای اینکه در نخ اصلی این کار را انجام دهد:

Filename: src/main.rs
use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    thread::spawn(move || println!("From thread: {list:?}"))
        .join()
        .unwrap();
}
Listing 13-6: استفاده از move برای مجبور کردن closure به گرفتن مالکیت list برای نخ

ما یک نخ جدید ایجاد می‌کنیم و به نخ یک closure می‌دهیم تا به عنوان آرگومان اجرا شود. بدنه closure لیست را چاپ می‌کند. در لیستینگ 13-4، closure فقط با استفاده از یک ارجاع غیرقابل تغییر list را گرفت زیرا این کمترین دسترسی مورد نیاز برای چاپ list بود. در این مثال، اگرچه بدنه closure هنوز فقط به یک ارجاع غیرقابل تغییر نیاز دارد، باید مشخص کنیم که list باید به داخل closure منتقل شود. این کار را با قرار دادن کلمه کلیدی move در ابتدای تعریف closure انجام می‌دهیم.

نخ جدید ممکن است قبل از تکمیل نخ اصلی تمام شود، یا نخ اصلی ممکن است زودتر تمام شود. اگر نخ اصلی مالکیت list را حفظ می‌کرد اما قبل از نخ جدید به پایان می‌رسید و list را حذف می‌کرد، ارجاع غیرقابل تغییر در نخ دیگر معتبر نبود. بنابراین، کامپایلر نیاز دارد که list به داخل closure داده‌شده به نخ جدید منتقل شود تا ارجاع معتبر باقی بماند. سعی کنید کلمه کلیدی move را حذف کنید یا از list در نخ اصلی پس از تعریف closure استفاده کنید تا ببینید چه خطاهای کامپایلری دریافت می‌کنید!

انتقال مقادیر گرفته‌شده به خارج از closureها و صفات Fn

پس از اینکه یک closure ارجاعی را گرفت یا مالکیت مقداری را از محیطی که closure در آن تعریف شده است دریافت کرد (و به این ترتیب تعیین کرد چه چیزی، اگر وجود داشته باشد، به داخل closure منتقل شود)، کد در بدنه closure مشخص می‌کند که چه اتفاقی برای ارجاعات یا مقادیر هنگام ارزیابی closure در آینده می‌افتد (و به این ترتیب تعیین می‌کند چه چیزی، اگر وجود داشته باشد، به خارج از closure منتقل شود). بدنه یک closure می‌تواند هر یک از موارد زیر را انجام دهد:

  • انتقال یک مقدار گرفته‌شده به خارج از closure،
  • تغییر مقدار گرفته‌شده،
  • نه انتقال و نه تغییر مقدار،
  • یا از ابتدا هیچ چیزی از محیط نگرفتن.

نحوه گرفتن و مدیریت مقادیر توسط closure از محیط مشخص می‌کند که closure کدام صفات را پیاده‌سازی می‌کند. صفات روشی هستند که توابع و ساختارها می‌توانند مشخص کنند از چه نوع closureهایی می‌توانند استفاده کنند. closureها به صورت خودکار یکی، دو یا هر سه این صفات Fn را پیاده‌سازی می‌کنند، به صورت افزایشی، بسته به نحوه مدیریت مقادیر توسط بدنه closure:

  1. FnOnce: برای closureهایی که می‌توانند فقط یک بار فراخوانی شوند اعمال می‌شود. همه closureها حداقل این صفت را پیاده‌سازی می‌کنند، زیرا همه closureها قابل فراخوانی هستند. closureی که مقادیر گرفته‌شده را از بدنه خود انتقال می‌دهد فقط صفت FnOnce را پیاده‌سازی می‌کند و هیچ‌یک از دیگر صفات Fn را پیاده‌سازی نمی‌کند، زیرا فقط یک بار قابل فراخوانی است.
  2. FnMut: برای closureهایی که مقادیر گرفته‌شده را از بدنه خود انتقال نمی‌دهند اما ممکن است مقادیر گرفته‌شده را تغییر دهند اعمال می‌شود. این closureها می‌توانند بیش از یک بار فراخوانی شوند.
  3. Fn: برای closureهایی که مقادیر گرفته‌شده را از بدنه خود انتقال نمی‌دهند و مقادیر گرفته‌شده را تغییر نمی‌دهند، همچنین closureهایی که هیچ چیزی از محیط نمی‌گیرند اعمال می‌شود. این closureها می‌توانند بیش از یک بار بدون تغییر محیط خود فراخوانی شوند، که در مواردی مانند فراخوانی یک closure به طور همزمان چندین بار مهم است.

بیایید تعریف متد unwrap_or_else در Option<T> را که در لیستینگ 13-1 استفاده کردیم بررسی کنیم:

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
}

به یاد داشته باشید که T نوع جنریک است که نوع مقدار موجود در واریانت Some از Option را نشان می‌دهد. این نوع T همچنین نوع بازگشتی تابع unwrap_or_else است: به عنوان مثال، کدی که unwrap_or_else را روی یک Option<String> فراخوانی می‌کند، یک String دریافت خواهد کرد.

بعدی، توجه داشته باشید که تابع unwrap_or_else پارامتر نوع جنریک اضافی F را دارد. نوع F نوع پارامتر نام‌گذاری‌شده f است، که closureی است که هنگام فراخوانی unwrap_or_else ارائه می‌دهیم.

محدودیت صفت مشخص‌شده روی نوع جنریک F، FnOnce() -> T است، که به این معناست که F باید بتواند یک بار فراخوانی شود، هیچ آرگومانی نگیرد و یک T بازگرداند. استفاده از FnOnce در محدودیت صفت، محدودیت این موضوع را بیان می‌کند که unwrap_or_else حداکثر یک بار f را فراخوانی خواهد کرد. در بدنه unwrap_or_else، می‌بینیم که اگر Option برابر با Some باشد، f فراخوانی نمی‌شود. اگر Option برابر با None باشد، f یک بار فراخوانی خواهد شد. از آنجایی که تمام closureها FnOnce را پیاده‌سازی می‌کنند، unwrap_or_else همه انواع سه‌گانه closureها را می‌پذیرد و به اندازه کافی انعطاف‌پذیر است.

نکته: اگر کاری که می‌خواهیم انجام دهیم نیاز به گرفتن مقداری از محیط نداشته باشد، می‌توانیم به جای closure از نام یک تابع استفاده کنیم. به عنوان مثال، می‌توانیم unwrap_or_else(Vec::new) را روی یک مقدار Option<Vec<T>> فراخوانی کنیم تا اگر مقدار None بود، یک وکتور جدید و خالی دریافت کنیم. کامپایلر به طور خودکار هر کدام از صفات Fn که برای تعریف تابع کاربرد دارد را پیاده‌سازی می‌کند.

اکنون بیایید به متد استاندارد کتابخانه sort_by_key که روی برش‌ها (slices) تعریف شده است نگاهی بیندازیم تا ببینیم چگونه با unwrap_or_else متفاوت است و چرا sort_by_key به جای FnOnce از FnMut برای محدودیت صفت استفاده می‌کند. closure یک آرگومان به شکل یک ارجاع به آیتم جاری در برشی که در نظر گرفته می‌شود می‌گیرد و یک مقدار از نوع K را که قابل مرتب‌سازی است بازمی‌گرداند. این تابع زمانی مفید است که بخواهید یک برش را بر اساس ویژگی خاصی از هر آیتم مرتب کنید. در لیست 13-7، ما لیستی از نمونه‌های Rectangle داریم و از sort_by_key برای مرتب کردن آن‌ها بر اساس ویژگی width از کم به زیاد استفاده می‌کنیم:

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    list.sort_by_key(|r| r.width);
    println!("{list:#?}");
}
Listing 13-7: استفاده از sort_by_key برای مرتب‌سازی مستطیل‌ها بر اساس عرض

این کد خروجی زیر را چاپ می‌کند:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/rectangles`
[
    Rectangle {
        width: 3,
        height: 5,
    },
    Rectangle {
        width: 7,
        height: 12,
    },
    Rectangle {
        width: 10,
        height: 1,
    },
]

دلیل اینکه sort_by_key به گونه‌ای تعریف شده که یک closure FnMut بگیرد این است که closure را چندین بار فراخوانی می‌کند: یک بار برای هر آیتم در برش. closure |r| r.width چیزی را از محیط خود نمی‌گیرد، تغییر نمی‌دهد یا منتقل نمی‌کند، بنابراین با الزامات محدودیت صفت مطابقت دارد.

در مقابل، لیست 13-8 مثالی از closureی را نشان می‌دهد که فقط صفت FnOnce را پیاده‌سازی می‌کند، زیرا مقداری را از محیط منتقل می‌کند. کامپایلر اجازه نمی‌دهد از این closure با sort_by_key استفاده کنیم:

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut sort_operations = vec![];
    let value = String::from("closure called");

    list.sort_by_key(|r| {
        sort_operations.push(value);
        r.width
    });
    println!("{list:#?}");
}
Listing 13-8: تلاش برای استفاده از closure FnOnce با sort_by_key

این یک روش مصنوعی و پیچیده (که کار نمی‌کند) برای تلاش در شمارش تعداد دفعاتی است که sort_by_key closure را هنگام مرتب کردن list فراخوانی می‌کند. این کد سعی می‌کند این شمارش را با افزودن value—یک String از محیط closure—به وکتور sort_operations انجام دهد. closure، value را می‌گیرد و سپس با انتقال مالکیت value به وکتور sort_operations، value را از closure منتقل می‌کند. این closure فقط یک بار می‌تواند فراخوانی شود؛ تلاش برای فراخوانی آن برای بار دوم کار نمی‌کند زیرا value دیگر در محیط وجود ندارد که دوباره به sort_operations اضافه شود! بنابراین، این closure فقط صفت FnOnce را پیاده‌سازی می‌کند. وقتی سعی می‌کنیم این کد را کامپایل کنیم، این خطا دریافت می‌شود که value نمی‌تواند از closure منتقل شود، زیرا closure باید FnMut را پیاده‌سازی کند:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
  --> src/main.rs:18:30
   |
15 |     let value = String::from("closure called");
   |         ----- captured outer variable
16 |
17 |     list.sort_by_key(|r| {
   |                      --- captured by this `FnMut` closure
18 |         sort_operations.push(value);
   |                              ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait
   |
help: consider cloning the value if the performance cost is acceptable
   |
18 |         sort_operations.push(value.clone());
   |                                   ++++++++

For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error

این خطا به خطی در بدنه closure اشاره می‌کند که value را از محیط منتقل می‌کند. برای رفع این مشکل، باید بدنه closure را تغییر دهیم تا مقادیر را از محیط منتقل نکند. برای شمارش تعداد دفعاتی که closure فراخوانی می‌شود، نگه داشتن یک شمارنده در محیط و افزایش مقدار آن در بدنه closure روشی ساده‌تر برای محاسبه آن است. closure در لیست 13-9 با sort_by_key کار می‌کند زیرا فقط یک ارجاع قابل تغییر به شمارنده num_sort_operations را می‌گیرد و بنابراین می‌تواند بیش از یک بار فراخوانی شود:

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut num_sort_operations = 0;
    list.sort_by_key(|r| {
        num_sort_operations += 1;
        r.width
    });
    println!("{list:#?}, sorted in {num_sort_operations} operations");
}
Listing 13-9: استفاده از یک closure FnMut با sort_by_key مجاز است

صفات Fn هنگام تعریف یا استفاده از توابع یا انواعی که از closureها استفاده می‌کنند، مهم هستند. در بخش بعدی، ما درباره iteratorها بحث خواهیم کرد. بسیاری از متدهای iterator آرگومان‌های closure می‌گیرند، بنابراین این جزئیات closure را هنگام ادامه مطالعه در نظر داشته باشید!