توابع پیشرفته و 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
فراخوانی میکند.
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}"); }
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(); }
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(); }
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(); }
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 } }
impl Trait
با این حال، همانطور که در بخش “استنباط نوع کلوزر و حاشیهنویسی” در فصل ۱۳ اشاره شد،
هر کلوزر همچنین نوع خاص خودش را دارد. اگر نیاز دارید با چندین تابع که امضای یکسانی دارند اما پیادهسازی متفاوتی دارند کار کنید،
باید از یک آبجکت trait
برای آنها استفاده کنید. ببینید چه اتفاقی میافتد اگر کدی مشابه لیست 20-33 بنویسید.
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
}
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) }
Vec<T>
از کلوزرهایی که توسط توابعی بازمیگردند که مقدار Box<dyn Fn>
را برمیگردانند تا نوع آنها یکسان باشداین کد بدون مشکل کامپایل خواهد شد. برای اطلاعات بیشتر در مورد trait objectها، به بخش “استفاده از trait objectهایی که اجازه استفاده از مقادیر با نوعهای متفاوت را میدهند” در فصل ۱۸ مراجعه کنید.
حال بیایید نگاهی به ماکروها بیندازیم!