closureهای Rust توابع ناشناسی هستند که میتوانید آنها را در یک متغیر ذخیره کنید یا به عنوان آرگومان به توابع دیگر ارسال کنید. شما میتوانید closure را در یک مکان ایجاد کنید و سپس آن را در جای دیگری فراخوانی کنید تا در یک زمینه متفاوت ارزیابی شود. برخلاف توابع، closureها میتوانند مقادیر را از محیطی که در آن تعریف شدهاند، بگیرند. ما نشان خواهیم داد که چگونه این ویژگیهای closure امکان استفاده مجدد از کد و سفارشیسازی رفتار را فراهم میکند.
ابتدا بررسی خواهیم کرد که چگونه میتوان از closureها برای گرفتن مقادیر از محیطی که در آن تعریف شدهاند، برای استفاده در آینده استفاده کرد. سناریوی زیر را در نظر بگیرید: هر چند وقت یک بار، شرکت تیشرت ما یک تیشرت انحصاری و نسخه محدود به شخصی از لیست پستی خود به عنوان تبلیغ هدیه میدهد. افرادی که در لیست پستی هستند میتوانند به صورت اختیاری رنگ مورد علاقه خود را به پروفایل خود اضافه کنند. اگر شخصی که برای تیشرت رایگان انتخاب شده است رنگ مورد علاقه خود را تنظیم کرده باشد، آن رنگ را دریافت میکند. اگر شخص رنگ مورد علاقهای مشخص نکرده باشد، رنگی که شرکت بیشترین تعداد آن را دارد، به او داده میشود.
راههای زیادی برای پیادهسازی این سناریو وجود دارد. در این مثال، ما از یک enum
به نام ShirtColor
استفاده میکنیم که شامل مقادیر Red
و Blue
است (برای سادگی تعداد رنگهای موجود را محدود کردهایم). موجودی شرکت را با یک ساختار Inventory
نشان میدهیم که یک فیلد به نام shirts
دارد که یک Vec<ShirtColor>
از رنگهای تیشرت موجود را نشان میدهد. متدی به نام giveaway
که در Inventory
تعریف شده است، اولویت رنگ تیشرت کاربر برنده را دریافت کرده و رنگ تیشرتی که به آن فرد داده میشود را برمیگرداند. این تنظیمات در لیستینگ 13-1 نشان داده شده است:
#[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
);
}
در این کد، 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ها معمولاً نیازی به حاشیهنویسی انواع آرگومانها یا مقدار بازگشتی ندارند، برخلاف توابع fn
که به این حاشیهنویسی نیاز دارند. حاشیهنویسی انواع در توابع ضروری است زیرا این انواع بخشی از رابط کاربری صریحی هستند که برای کاربران شما ارائه میشود. تعریف این رابط به صورت سختگیرانه برای اطمینان از توافق همه در مورد انواع مقادیر استفاده شده و بازگشتی یک تابع مهم است. از طرف دیگر، closureها به این صورت در یک رابط کاربری صریح استفاده نمیشوند: آنها در متغیرها ذخیره میشوند و بدون نامگذاری و افشای آنها به کاربران کتابخانه ما استفاده میشوند.
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); }
با اضافه کردن حاشیهنویسی نوع، نحوه نوشتن 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
را با یک عدد صحیح فراخوانی کنیم، خطایی دریافت خواهیم کرد.
fn main() {
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
}
کامپایلر این خطا را میدهد:
$ 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
را میگیرد زیرا فقط به یک ارجاع غیرقابل تغییر نیاز دارد تا مقدار را چاپ کند:
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:?}"); }
این مثال همچنین نشان میدهد که یک متغیر میتواند به تعریف یک 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 اکنون یک ارجاع قابل تغییر میگیرد:
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:?}"); }
این کد کامپایل شده، اجرا میشود و نتیجه زیر را چاپ میکند:
$ 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 را اصلاح میکند تا بردار را در یک نخ جدید چاپ کند به جای اینکه در نخ اصلی این کار را انجام دهد:
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(); }
move
برای مجبور کردن closure به گرفتن مالکیت list
برای نخما یک نخ جدید ایجاد میکنیم و به نخ یک closure میدهیم تا به عنوان آرگومان اجرا شود. بدنه closure لیست را چاپ میکند. در لیستینگ 13-4، closure فقط با استفاده از یک ارجاع غیرقابل تغییر list
را گرفت زیرا این کمترین دسترسی مورد نیاز برای چاپ list
بود. در این مثال، اگرچه بدنه closure هنوز فقط به یک ارجاع غیرقابل تغییر نیاز دارد، باید مشخص کنیم که list
باید به داخل closure منتقل شود. این کار را با قرار دادن کلمه کلیدی move
در ابتدای تعریف closure انجام میدهیم.
نخ جدید ممکن است قبل از تکمیل نخ اصلی تمام شود، یا نخ اصلی ممکن است زودتر تمام شود. اگر نخ اصلی مالکیت list
را حفظ میکرد اما قبل از نخ جدید به پایان میرسید و list
را حذف میکرد، ارجاع غیرقابل تغییر در نخ دیگر معتبر نبود. بنابراین، کامپایلر نیاز دارد که list
به داخل closure دادهشده به نخ جدید منتقل شود تا ارجاع معتبر باقی بماند. سعی کنید کلمه کلیدی move
را حذف کنید یا از list
در نخ اصلی پس از تعریف closure استفاده کنید تا ببینید چه خطاهای کامپایلری دریافت میکنید!
Fn
پس از اینکه یک closure ارجاعی را گرفت یا مالکیت مقداری را از محیطی که closure در آن تعریف شده است دریافت کرد (و به این ترتیب تعیین کرد چه چیزی، اگر وجود داشته باشد، به داخل closure منتقل شود)، کد در بدنه closure مشخص میکند که چه اتفاقی برای ارجاعات یا مقادیر هنگام ارزیابی closure در آینده میافتد (و به این ترتیب تعیین میکند چه چیزی، اگر وجود داشته باشد، به خارج از closure منتقل شود). بدنه یک closure میتواند هر یک از موارد زیر را انجام دهد:
نحوه گرفتن و مدیریت مقادیر توسط closure از محیط مشخص میکند که closure کدام صفات را پیادهسازی میکند. صفات روشی هستند که توابع و ساختارها میتوانند مشخص کنند از چه نوع closureهایی میتوانند استفاده کنند. closureها به صورت خودکار یکی، دو یا هر سه این صفات Fn
را پیادهسازی میکنند، به صورت افزایشی، بسته به نحوه مدیریت مقادیر توسط بدنه closure:
FnOnce
: برای closureهایی که میتوانند فقط یک بار فراخوانی شوند اعمال میشود. همه closureها حداقل این صفت را پیادهسازی میکنند، زیرا همه closureها قابل فراخوانی هستند. closureی که مقادیر گرفتهشده را از بدنه خود انتقال میدهد فقط صفت FnOnce
را پیادهسازی میکند و هیچیک از دیگر صفات Fn
را پیادهسازی نمیکند، زیرا فقط یک بار قابل فراخوانی است.
FnMut
: برای closureهایی که مقادیر گرفتهشده را از بدنه خود انتقال نمیدهند اما ممکن است مقادیر گرفتهشده را تغییر دهند اعمال میشود. این closureها میتوانند بیش از یک بار فراخوانی شوند.
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
از کم به زیاد استفاده میکنیم:
#[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:#?}"); }
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
استفاده کنیم:
#[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:#?}");
}
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
را میگیرد و بنابراین میتواند بیش از یک بار فراخوانی شود:
#[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"); }
FnMut
با sort_by_key
مجاز استصفات Fn
هنگام تعریف یا استفاده از توابع یا انواعی که از closureها استفاده میکنند، مهم هستند. در بخش بعدی، ما درباره iteratorها بحث خواهیم کرد. بسیاری از متدهای iterator آرگومانهای closure میگیرند، بنابراین این جزئیات closure را هنگام ادامه مطالعه در نظر داشته باشید!