Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

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

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

راه‌های زیادی برای پیاده‌سازی این سناریو وجود دارد. در این مثال، از یک enum به نام ShirtColor استفاده می‌کنیم که شامل دو حالت Red و Blue است (برای سادگی، تعداد رنگ‌ها را محدود کرده‌ایم). موجودی شرکت با یک struct به نام 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
   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
   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 برای نخ

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

اگر نخ اصلی (main thread) قبل از فراخوانی join عملیات بیشتری انجام دهد، ممکن است نخ جدید زودتر از نخ اصلی تمام شود، یا بالعکس، نخ اصلی زودتر خاتمه یابد. اگر نخ اصلی مالکیت list را حفظ کرده باشد ولی قبل از پایان نخ جدید خاتمه یابد و list را آزاد کند، رفرنسی که نخ جدید استفاده می‌کند نامعتبر خواهد شد.

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

سعی کنید کلمه‌ی کلیدی move را حذف کنید یا از list در نخ اصلی پس از تعریف closure استفاده کنید تا ببینید چه خطاهایی از سوی کامپایلر دریافت می‌کنید!

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

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

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

بدنه‌ی یک closure می‌تواند هر یک از موارد زیر را انجام دهد:

  • یک مقدار گرفته‌شده را به بیرون از closure منتقل کند (move)
  • مقدار گرفته‌شده را تغییر دهد (mutate)
  • نه مقداری را منتقل کند و نه تغییری ایجاد کند
  • هیچ چیزی از محیط را در ابتدا نگرفته باشد

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

  • FnOnce applies to closures that can be called once. All closures implement at least this trait because all closures can be called. A closure that moves captured values out of its body will only implement FnOnce and none of the other Fn traits because it can only be called once.
  • FnMut applies to closures that don’t move captured values out of their body, but that might mutate the captured values. These closures can be called more than once.
  • Fn applies to closures that don’t move captured values out of their body and that don’t mutate captured values, as well as closures that capture nothing from their environment. These closures can be called more than once without mutating their environment, which is important in cases such as calling a closure multiple times concurrently.

بیایید تعریف متد 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، از نام یک تابع استفاده کنیم؛ در جایی که نیاز به چیزی داریم که یکی از traitهای Fn را پیاده‌سازی کند. برای مثال، روی یک مقدار از نوع Option<Vec<T>> می‌توانیم unwrap_or_else(Vec::new) را فراخوانی کنیم تا در صورتی که مقدار None بود، یک vector جدید و خالی دریافت کنیم. کامپایلر به‌طور خودکار هرکدام از traitهای Fn که برای تعریف یک تابع مناسب باشند را پیاده‌سازی می‌کند.

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

اکنون بیایید به متد استاندارد کتابخانه 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 را هنگام ادامه مطالعه در نظر داشته باشید!