مقدمه
در درس قبل با کلوژرها آشنا شدیم. «تکرارگر» یا Iterator یکی دیگر از ویژگیهای برنامهنویسی
تابعی در Rust است که به ما اجازه میدهد تا روی یک دنباله از آیتمها، مانند عناصر یک وکتور،
پیمایش کنیم. تکرارگرها به صورت «تنبل» (lazy) عمل میکنند؛ این یعنی تا زمانی که شما به صورت صریح
از آنها نخواهید، هیچ کاری انجام نمیدهند و هیچ مقداری را مصرف نمیکنند. این ویژگی باعث میشود که
آنها بسیار کارآمد باشند.
تمام تکرارگرها، Trait مربوط به Iterator را پیادهسازی میکنند که در کتابخانه استاندارد تعریف
شده و تنها یک متد اصلی به نام next() دارد. این متد هر بار که
فراخوانی میشود، یک آیتم از دنباله را در داخل یک Some برمیگرداند و وقتی دنباله به پایان برسد،
None را برمیگرداند.
ایجاد و مصرف یک تکرارگر
کالکشنهای کتابخانه استاندارد Rust، مانند Vec<T>، متدهایی را برای ایجاد تکرارگر فراهم میکنند:
- iter(): یک تکرارگر بر روی رفرنسهای تغییرناپذیر (&T) به عناصر
کالکشن ایجاد میکند.
- iter_mut(): یک تکرارگر بر روی رفرنسهای تغییرپذیر (&mut T)
ایجاد میکند.
- into_iter(): مالکیت کالکشن را به خود منتقل کرده و یک تکرارگر بر
روی مقادیر مالک (T) ایجاد میکند.
src/main.rs
fn main() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}
در این مثال، ما به صورت دستی متد next را فراخوانی میکنیم. اما قدرت واقعی تکرارگرها در
ترکیب آنها با متدهای دیگری است که خودشان next را مصرف میکنند.
متدهای مصرفکننده و آداپتورهای تکرارگر
Trait مربوط به Iterator دارای متدهای پیشفرض فراوانی است که میتوان آنها را به دو دسته تقسیم
کرد:
متدهای مصرفکننده (Consuming Adaptors)
این متدها تکرارگر را مصرف کرده و یک مقدار نهایی تولید میکنند. پس از فراخوانی یک متد مصرفکننده،
دیگر نمیتوان از آن تکرارگر استفاده کرد. متد sum() یک مثال خوب است:
src/main.rs
fn main() {
let v1 = vec![1, 2, 3];
let total: i32 = v1.iter().sum();
assert_eq!(total, 6);
}
آداپتورهای تکرارگر (Iterator Adaptors)
این متدها به ما اجازه میدهند تا یک تکرارگر را به یک تکرارگر دیگر با رفتار متفاوت تبدیل کنیم.
این متدها «تنبل» هستند و تا زمانی که یک متد مصرفکننده فراخوانی نشود، هیچ کاری انجام نمیدهند.
متد map یک مثال عالی است. این متد یک کلوژر دریافت کرده و با اعمال آن روی هر عنصر، یک
تکرارگر جدید میسازد.
src/main.rs
fn main() {
let v1: Vec<i32> = vec![1, 2, 3];
let map_iter = v1.iter().map(|x| x + 1);
let v2: Vec<_> = map_iter.collect();
assert_eq!(v2, vec![2, 3, 4]);
}
این قابلیت زنجیرهسازی (chaining) متدها، یکی از ویژگیهای اصلی برنامهنویسی تابعی است و به ما
اجازه میدهد تا منطقهای پیچیده پردازش داده را به صورت بسیار خوانا و مختصر بیان کنیم.
یک مثال کاربردی: فیلتر کردن با کلوژرها
بیایید یک مثال دیگر را ببینیم که در آن از آداپتور filter برای انتخاب آیتمهایی که یک شرط
خاص را برآورده میکنند، استفاده میکنیم.
src/main.rs
#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}
#[test]
fn filters_by_size() {
let shoes = vec![
Shoe { size: 10, style: String::from("sneaker") },
Shoe { size: 13, style: String::from("sandal") },
Shoe { size: 10, style: String::from("boot") },
];
let in_my_size = shoes_in_size(shoes, 10);
assert_eq!(
in_my_size,
vec![
Shoe { size: 10, style: String::from("sneaker") },
Shoe { size: 10, style: String::from("boot") },
]
);
}
در تابع shoes_in_size، ما از into_iter() استفاده میکنیم تا
مالکیت وکتور را به دست آوریم. سپس با filter و یک کلوژر، تنها کفشهایی را که سایز مورد نظر
را دارند، نگه میداریم. در نهایت، با collect نتایج را در یک وکتور جدید جمعآوری میکنیم.
این کد بسیار گویاتر از نوشتن یک حلقه for و یک if به صورت دستی است.
در این درس با الگوی تکرارگر و نحوه استفاده از آن برای پردازش کارآمد و بیانی کالکشنها آشنا شدیم.
دیدیم که چگونه ترکیب تکرارگرها، آداپتورها و کلوژرها، به ما اجازه میدهد تا منطقهای پیچیده را به
صورت زنجیرهای و خوانا بنویسیم. در درس بعدی، این مفاهیم را در پروژه minigrep خود به کار
خواهیم بست تا کد آن را بهبود دهیم.