توابع پیشرفته و Closureها
این بخش به بررسی برخی از ویژگیهای پیشرفته مربوط به توابع و Closureها میپردازد، از جمله Pointerهای تابع و بازگرداندن Closureها.
Pointerهای تابع
قبلاً در مورد چگونگی ارسال Closureها به توابع صحبت کردیم؛ شما همچنین میتوانید توابع معمولی را به توابع دیگر ارسال کنید! این تکنیک زمانی مفید است که بخواهید تابعی که قبلاً تعریف کردهاید را ارسال کنید به جای اینکه یک Closureها جدید تعریف کنید. توابع به نوع fn
(با f کوچک) تبدیل میشوند، که نباید با ویژگی Closureها Fn
اشتباه گرفته شود. نوع fn
به عنوان یک اشارهگر (Pointer) تابع شناخته میشود. ارسال توابع با استفاده از Pointerهای تابع به شما این امکان را میدهد که از توابع به عنوان آرگومان برای توابع دیگر استفاده کنید.
سینتکس مشخص کردن اینکه یک پارامتر یک اشارهگر (Pointer) تابع است، مشابه Closureها است، همانطور که در لیست ۲۰-۲۸ نشان داده شده است. در این مثال، تابعی به نام add_one
تعریف کردهایم که یک واحد به پارامتر خود اضافه میکند. تابع do_twice
دو پارامتر میگیرد: یک اشارهگر (Pointer) تابع به هر تابعی که یک پارامتر 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ها ندارد.
به عنوان مثالی از جایی که میتوانید از یک Closureها تعریفشده درونخطی یا یک تابع نامگذاریشده استفاده کنید، بیایید به استفاده از متد map
که توسط ویژگی Iterator
در کتابخانه استاندارد ارائه شده است نگاهی بیندازیم. برای استفاده از تابع map
برای تبدیل یک بردار اعداد به یک بردار رشتهها، میتوانیم از یک Closureها به این صورت استفاده کنیم:
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
ارسال کنیم، به این صورت:
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(); }
توجه داشته باشید که باید از سینتکس کاملاً مشخصی که قبلاً در بخش “ویژگیهای پیشرفته” توضیح داده شد استفاده کنیم، زیرا چندین تابع با نام to_string
در دسترس هستند. در اینجا، ما از تابع to_string
که در ویژگی ToString
تعریف شده است استفاده میکنیم، که کتابخانه استاندارد برای هر نوعی که ویژگی Display
را پیادهسازی کند، آن را پیادهسازی کرده است.
به یاد بیاورید که در بخش “مقادیر Enum” از فصل ۶ گفته شد که نام هر واریانت enum که تعریف میکنیم، همچنین به یک تابع مقداردهی اولیه تبدیل میشود. میتوانیم از این توابع مقداردهی اولیه به عنوان اشارهگر (Pointer)های تابع که ویژگیهای کلوزر را پیادهسازی میکنند استفاده کنیم، به این معنی که میتوانیم توابع مقداردهی اولیه را به عنوان آرگومان برای متدهایی که کلوزرها (closures) را میپذیرند مشخص کنیم، به این صورت:
fn main() { enum Status { Value(u32), Stop, } let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect(); }
در اینجا با استفاده از تابع مقداردهی اولیه Status::Value
، نمونههایی از Status::Value
ایجاد میکنیم که از هر مقدار u32
در محدودهای که map
روی آن فراخوانی میشود استفاده میکند. برخی افراد این سبک را ترجیح میدهند و برخی دیگر ترجیح میدهند از کلوزرها (closures) استفاده کنند. اینها به کدی یکسان کامپایل میشوند، بنابراین هر سبکی که برای شما واضحتر است را انتخاب کنید.
بازگرداندن کلوزرها (closures) (Returning Closures)
کلوزرها (closures) با ویژگیها نمایش داده میشوند، به این معنی که نمیتوانید مستقیماً کلوزرها (closures) را بازگردانید. در بیشتر مواردی که ممکن است بخواهید یک ویژگی را بازگردانید، میتوانید به جای آن از نوع مشخصی که ویژگی را پیادهسازی میکند به عنوان مقدار بازگشتی تابع استفاده کنید. با این حال، نمیتوانید این کار را با کلوزرها (closures) انجام دهید زیرا آنها نوع مشخصی که قابل بازگشت باشد ندارند؛ به عنوان مثال، نمیتوانید از اشارهگر (Pointer) تابع fn
به عنوان نوع بازگشتی استفاده کنید.
در عوض، معمولاً از سینتکس impl Trait
که در فصل ۱۰ یاد گرفتیم استفاده میکنید. میتوانید هر نوع تابعی را با استفاده از Fn
، FnOnce
و FnMut
بازگردانید. برای مثال، این کد به خوبی کار میکند:
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
با این حال، همانطور که در بخش “استنتاج نوع کلوزر و حاشیهنویسی” از فصل ۱۳ اشاره کردیم، هر کلوزر نوع مشخص خود را دارد. اگر نیاز داشته باشید با چندین تابع که امضای یکسانی دارند اما پیادهسازیهای متفاوتی دارند کار کنید، باید از یک شیء ویژگی (trait object) برای آنها استفاده کنید:
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)
}
این کد به خوبی کامپایل میشود—اما اگر تلاش میکردیم از impl Fn(i32) -> i32
استفاده کنیم، کامپایل نمیشد. برای اطلاعات بیشتر در مورد اشیاء ویژگی، به بخش “استفاده از اشیاء ویژگی که امکان مقادیر با تایپهای مختلف را فراهم میکنند” در فصل ۱۸ مراجعه کنید.
در ادامه، بیایید نگاهی به ماکروها بیندازیم!