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ها

این بخش به بررسی برخی از ویژگی‌های پیشرفته مربوط به توابع و Closureها می‌پردازد، از جمله Pointerهای تابع و بازگرداندن Closureها.

Pointerهای تابع

ما درباره نحوهٔ ارسال کلوزرها به توابع صحبت کردیم؛ همچنین می‌توانید توابع معمولی را نیز به توابع دیگر ارسال کنید! این تکنیک زمانی مفید است که بخواهید تابعی را که از قبل تعریف کرده‌اید ارسال کنید، به‌جای آن‌که یک کلوزر جدید تعریف کنید. توابع به نوع fn (با f کوچک) تبدیل می‌شوند، که نباید با Fn که یک trait برای کلوزرهاست، اشتباه گرفته شود. نوع fn یک پویتر به تابع نامیده می‌شود. ارسال توابع با استفاده از پویترهای تابع این امکان را می‌دهد که از توابع به‌عنوان آرگومان به توابع دیگر استفاده کنید.

نحو مشخص‌کردن اینکه یک پارامتر از نوع پویتر تابع است، مشابه نحوهٔ تعریف کلوزرهاست؛ همان‌طور که در لیست 20-28 نشان داده شده است. در آنجا تابعی به نام add_one تعریف کرده‌ایم که ۱ واحد به پارامتر خود اضافه می‌کند. تابع do_twice دو پارامتر می‌گیرد: یک پویتر به تابعی که یک پارامتر از نوع i32 می‌گیرد و مقدار i32 برمی‌گرداند، و یک مقدار i32. تابع do_twice تابع f را دو بار با مقدار arg فراخوانی می‌کند، سپس نتایج این دو فراخوانی را با هم جمع می‌زند. تابع main تابع do_twice را با آرگومان‌های add_one و 5 فراخوانی می‌کند.

Filename: src/main.rs
fn add_one(x: i32) -> i32 {
    x + 1
}

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(add_one, 5);

    println!("The answer is: {answer}");
}
Listing 20-28: استفاده از نوع fn برای پذیرش یک اشاره‌گر (Pointer) تابع به عنوان آرگومان

این کد مقدار The answer is: 12 را چاپ می‌کند. ما مشخص کرده‌ایم که پارامتر f در do_twice یک fn است که یک پارامتر از نوع i32 می‌گیرد و یک i32 باز می‌گرداند. سپس می‌توانیم f را در بدنه تابع do_twice فراخوانی کنیم. در main، می‌توانیم نام تابع add_one را به عنوان آرگومان اول به do_twice ارسال کنیم.

برخلاف Closureها fn یک نوع است و نه یک ویژگی، بنابراین ما fn را به طور مستقیم به عنوان نوع پارامتر مشخص می‌کنیم، به جای اعلام یک پارامتر جنریک با یکی از ویژگی‌های Fn به عنوان محدودیت ویژگی.

Pointerهای تابع تمام سه ویژگی Closureها (Fn، FnMut، و FnOnce) را پیاده‌سازی می‌کنند، به این معنی که شما همیشه می‌توانید یک اشاره‌گر (Pointer) تابع را به عنوان آرگومان برای یک تابع که انتظار یک Closureها را دارد ارسال کنید. بهتر است توابع را با استفاده از یک نوع جنریک و یکی از ویژگی‌های Closureها بنویسید تا توابع شما بتوانند هم توابع و هم Closureها را بپذیرند.

با این حال، یک مثال از جایی که ممکن است بخواهید فقط fn را بپذیرید و نه Closureها زمانی است که با کد خارجی که Closureها ندارد تعامل می‌کنید: توابع C می‌توانند توابع را به عنوان آرگومان بپذیرند، اما C Closureها ندارد.

به‌عنوان مثالی از جایی که می‌توانید از یک کلوزر تعریف‌شده به‌صورت درجا یا از یک تابع نام‌گذاری‌شده استفاده کنید، بیایید نگاهی بیندازیم به یک استفاده از متد map که توسط Iterator در کتابخانه استاندارد فراهم شده است. برای استفاده از متد map به‌منظور تبدیل یک vector از اعداد به یک vector از رشته‌ها، می‌توانیم از یک کلوزر استفاده کنیم، همان‌طور که در لیست 20-29 نشان داده شده است.

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(|i| i.to_string()).collect();
}
Listing 20-29: استفاده از یک کلوزر با متد map برای تبدیل اعداد به رشته‌ها

یا می‌توانیم به‌جای کلوزر، یک تابع را به‌عنوان آرگومان به map بدهیم. لیست 20-30 نشان می‌دهد که این کار چگونه انجام می‌شود.

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(ToString::to_string).collect();
}
Listing 20-30: استفاده از تابع String::to_string با متد map برای تبدیل اعداد به رشته‌ها

توجه داشته باشید که باید از سینتکس کاملاً مشخصی که در بخش «ویژگی‌های پیشرفته» درباره آن صحبت کردیم استفاده کنیم، زیرا توابع متعددی با نام to_string وجود دارند.

در این‌جا، از تابع to_string استفاده می‌کنیم که در trait به نام ToString تعریف شده و کتابخانه استاندارد آن را برای هر نوعی که Display را پیاده‌سازی کرده باشد، پیاده‌سازی کرده است.

به یاد بیاورید که در بخش «مقادیر enum» از فصل ۶ اشاره کردیم که نام هر واریانت enum که تعریف می‌کنیم، همچنین تبدیل به یک تابع سازنده (initializer function) می‌شود. ما می‌توانیم این توابع سازنده را به‌عنوان فانکشن‌پوینترهایی که closure trait‌ها را پیاده‌سازی می‌کنند استفاده کنیم؛ این یعنی می‌توانیم این توابع سازنده را به‌عنوان آرگومان برای متدهایی که کلوزر دریافت می‌کنند مشخص کنیم، همان‌طور که در لیست 31-20 مشاهده می‌کنید.

fn main() {
    enum Status {
        Value(u32),
        Stop,
    }

    let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}
Listing 20-31: استفاده از سازنده enum با متد map برای ایجاد نمونه‌ای از Status از روی اعداد

در این‌جا، با استفاده از تابع سازنده Status::Value، برای هر مقدار u32 در بازه‌ای که map روی آن فراخوانی شده، نمونه‌هایی از Status::Value ایجاد می‌کنیم. برخی افراد این سبک را ترجیح می‌دهند و برخی دیگر ترجیح می‌دهند از کلوزرها استفاده کنند. هر دو روش به یک کد کامپایل می‌شوند، پس از هر سبکی که برای شما واضح‌تر است استفاده کنید.

بازگرداندن کلوزرها (closures) (Returning Closures)

کلوزرها توسط traitها نمایش داده می‌شوند، به این معنا که نمی‌توان آن‌ها را مستقیماً به عنوان مقدار بازگشتی برگرداند. در بیشتر مواردی که ممکن است بخواهید یک trait را برگردانید، می‌توانید به جای آن از نوع مشخصی که آن trait را پیاده‌سازی می‌کند به‌عنوان مقدار بازگشتی تابع استفاده کنید. اما معمولاً نمی‌توانید این کار را با کلوزرها انجام دهید، چون آن‌ها نوع مشخصی ندارند که قابل بازگشت باشد؛ برای مثال، اگر کلوزری مقداری از اسکوپ خود را کپچر کند، مجاز به استفاده از نوع اشاره‌گر تابع fn به‌عنوان نوع بازگشتی نیستید.

در عوض، معمولاً از سینتکس impl Trait که در فصل ۱۰ یاد گرفتیم استفاده می‌شود. می‌توانید هر نوع تابعی را با استفاده از Fn، FnOnce و FnMut بازگردانید. برای مثال، کدی که در لیست 20-32 آمده است، بدون مشکل کامپایل می‌شود.

#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}
}
Listing 20-32: بازگرداندن یک کلوزر از تابع با استفاده از سینتکس impl Trait

با این حال، همان‌طور که در بخش “استنباط نوع کلوزر و حاشیه‌نویسی” در فصل ۱۳ اشاره شد، هر کلوزر همچنین نوع خاص خودش را دارد. اگر نیاز دارید با چندین تابع که امضای یکسانی دارند اما پیاده‌سازی متفاوتی دارند کار کنید، باید از یک آبجکت trait برای آن‌ها استفاده کنید. ببینید چه اتفاقی می‌افتد اگر کدی مشابه لیست 20-33 بنویسید.

Filename: src/main.rs
fn main() {
    let handlers = vec![returns_closure(), returns_initialized_closure(123)];
    for handler in handlers {
        let output = handler(5);
        println!("{output}");
    }
}

fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}

fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
    move |x| x + init
}
Listing 20-33: ایجاد یک Vec<T> از کلوزرهایی که توسط توابعی بازگردانده می‌شوند که نوع impl Fn دارند

در این‌جا دو تابع داریم به نام‌های returns_closure و returns_initialized_closure، که هر دو مقدار impl Fn(i32) -> i32 را بازمی‌گردانند. توجه داشته باشید که کلوزرهایی که این توابع بازمی‌گردانند با یکدیگر متفاوت‌اند، حتی اگر هر دو trait یکسانی را پیاده‌سازی کنند. اگر سعی کنیم این کد را کامپایل کنیم، Rust به ما اطلاع می‌دهد که این کار امکان‌پذیر نیست:

$ cargo build
   Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0308]: mismatched types
  --> src/main.rs:2:44
   |
2  |     let handlers = vec![returns_closure(), returns_initialized_closure(123)];
   |                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
9  | fn returns_closure() -> impl Fn(i32) -> i32 {
   |                         ------------------- the expected opaque type
...
13 | fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
   |                                              ------------------- the found opaque type
   |
   = note: expected opaque type `impl Fn(i32) -> i32` (opaque type at <src/main.rs:9:25>)
              found opaque type `impl Fn(i32) -> i32` (opaque type at <src/main.rs:13:46>)
   = note: distinct uses of `impl Trait` result in different opaque types

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

پیام خطا به ما می‌گوید که زمانی که ما یک impl Trait را بازمی‌گردانیم، Rust یک نوع مبهم (opaque type) منحصربه‌فرد ایجاد می‌کند؛ نوعی که نمی‌توانیم جزئیات آن را ببینیم و همچنین نمی‌توانیم نوع تولیدشده توسط Rust را حدس بزنیم یا خودمان بنویسیم. بنابراین، حتی اگر این توابع کلوزرهایی را بازگردانند که trait یکسانی مانند Fn(i32) -> i32 را پیاده‌سازی می‌کنند، نوع‌های مبهم تولیدشده توسط Rust برای هر کدام متفاوت‌اند. (این مشابه نحوه‌ای است که Rust نوع‌های مشخص مختلفی برای بلوک‌های async متمایز تولید می‌کند، حتی اگر خروجی آن‌ها یکسان باشد، همان‌طور که در بخش “کار با هر تعداد future” در فصل ۱۷ دیدیم.) ما پیش از این نیز چندین بار راه‌حل این مشکل را دیده‌ایم: می‌توانیم از یک trait object استفاده کنیم، مانند نمونه‌ای که در Listing 20-34 نشان داده شده است.

fn main() {
    let handlers = vec![returns_closure(), returns_initialized_closure(123)];
    for handler in handlers {
        let output = handler(5);
        println!("{output}");
    }
}

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> {
    Box::new(move |x| x + init)
}
Listing 20-34: ساخت یک Vec<T> از کلوزرهایی که توسط توابعی بازمی‌گردند که مقدار Box<dyn Fn> را برمی‌گردانند تا نوع آن‌ها یکسان باشد

این کد بدون مشکل کامپایل خواهد شد. برای اطلاعات بیشتر در مورد trait object‌ها، به بخش “استفاده از trait objectهایی که اجازه استفاده از مقادیر با نوع‌های متفاوت را می‌دهند” در فصل ۱۸ مراجعه کنید.

حال بیایید نگاهی به ماکروها بیندازیم!