پردازش یک سری از آیتمها با استفاده از Iteratorها
الگوی iterator به شما اجازه میدهد تا به ترتیب روی یک دنباله از آیتمها کاری انجام دهید. یک iterator مسئول منطق پیمایش هر آیتم و تعیین زمان پایان دنباله است. وقتی از iteratorها استفاده میکنید، نیازی به پیادهسازی مجدد آن منطق ندارید.
در Rust، iteratorها تنبل هستند، به این معنی که تا زمانی که متدهایی که iterator را مصرف میکنند فراخوانی نشوند، هیچ اثری ندارند. به عنوان مثال، کد در لیست 13-10 یک iterator را بر روی آیتمهای وکتور v1
با فراخوانی متد iter
که روی Vec<T>
تعریف شده است، ایجاد میکند. این کد به تنهایی هیچ کار مفیدی انجام نمیدهد.
fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); }
این iterator در متغیر v1_iter
ذخیره شده است. پس از ایجاد یک iterator، میتوانیم از آن به روشهای مختلف استفاده کنیم. در لیست 3-5 از فصل 3، ما روی یک آرایه با استفاده از یک حلقه for
تکرار کردیم تا کدی را روی هر یک از آیتمهای آن اجرا کنیم. در پشت صحنه، این کار به طور ضمنی یک iterator ایجاد و سپس مصرف میکرد، اما تا کنون دقیقاً توضیح ندادیم که چگونه کار میکند.
در مثال لیست 13-11، ما ایجاد iterator را از استفاده از آن در حلقه for
جدا میکنیم. وقتی حلقه for
با استفاده از iterator در v1_iter
فراخوانی میشود، هر عنصر در iterator در یک تکرار از حلقه استفاده میشود، که هر مقدار را چاپ میکند.
fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!("Got: {val}"); } }
for
در زبانهایی که iteratorها توسط کتابخانه استاندارد آنها ارائه نمیشوند، احتمالاً همین عملکرد را با شروع یک متغیر در شاخص 0، استفاده از آن متغیر برای شاخصگذاری در وکتور برای دریافت یک مقدار و افزایش مقدار متغیر در یک حلقه تا زمانی که به تعداد کل آیتمها در وکتور برسد، مینوشتید.
iteratorها تمام این منطق را برای شما مدیریت میکنند و کدهای تکراری را که ممکن است اشتباه کنید کاهش میدهند. iteratorها به شما انعطاف بیشتری میدهند تا از همان منطق با انواع مختلف دنبالهها استفاده کنید، نه فقط ساختارهای دادهای که میتوان به آنها شاخص زد، مانند وکتورها. بیایید بررسی کنیم که iteratorها چگونه این کار را انجام میدهند.
صفت Iterator
و متد next
همه iteratorها یک صفت به نام Iterator
را پیادهسازی میکنند که در کتابخانه استاندارد تعریف شده است. تعریف این صفت به صورت زیر است:
#![allow(unused)] fn main() { pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; // متدهایی با پیادهسازی پیشفرض حذف شدهاند } }
توجه کنید که این تعریف از یک نحو جدید استفاده میکند: type Item
و Self::Item
، که یک نوع مرتبط را با این صفت تعریف میکنند. ما در فصل 20 به طور مفصل درباره انواع مرتبط صحبت خواهیم کرد. فعلاً فقط باید بدانید که این کد میگوید پیادهسازی صفت Iterator
نیاز دارد که شما یک نوع Item
نیز تعریف کنید، و این نوع Item
در نوع بازگشتی متد next
استفاده میشود. به عبارت دیگر، نوع Item
همان نوعی خواهد بود که از iterator بازگردانده میشود.
صفت Iterator
فقط از پیادهکنندگان میخواهد یک متد را تعریف کنند: متد next
، که یک آیتم از iterator را در هر زمان بازمیگرداند، که در Some
بستهبندی شده است، و وقتی پیمایش تمام شد، None
بازمیگرداند.
ما میتوانیم مستقیماً متد next
را روی iteratorها فراخوانی کنیم؛ لیست 13-12 نشان میدهد چه مقادیری از فراخوانیهای مکرر next
روی iterator ایجادشده از وکتور بازگردانده میشود.
#[cfg(test)]
mod tests {
#[test]
fn iterator_demonstration() {
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
روی یک iteratorتوجه کنید که ما نیاز داشتیم v1_iter
را قابل تغییر (mutable) کنیم: فراخوانی متد next
روی یک iterator، وضعیت داخلی را تغییر میدهد که iterator از آن برای ردیابی موقعیت خود در دنباله استفاده میکند. به عبارت دیگر، این کد iterator را مصرف میکند یا از بین میبرد. هر فراخوانی به next
یک آیتم از iterator را مصرف میکند. نیازی نبود v1_iter
را هنگام استفاده از یک حلقه for
قابل تغییر کنیم، زیرا حلقه مالکیت v1_iter
را به عهده گرفت و به طور پنهانی آن را قابل تغییر کرد.
همچنین توجه کنید که مقادیری که از فراخوانیهای next
دریافت میکنیم، ارجاعات غیرقابل تغییر به مقادیر موجود در وکتور هستند. متد iter
یک iterator روی ارجاعات غیرقابل تغییر تولید میکند. اگر بخواهیم یک iterator ایجاد کنیم که مالکیت v1
را بگیرد و مقادیر مالکشده را بازگرداند، میتوانیم به جای iter
، into_iter
را فراخوانی کنیم. به همین ترتیب، اگر بخواهیم روی ارجاعات قابل تغییر پیمایش کنیم، میتوانیم به جای iter
، iter_mut
را فراخوانی کنیم.
متدهایی که Iterator را مصرف میکنند
صفت Iterator
تعداد زیادی متد مختلف با پیادهسازیهای پیشفرض ارائهشده توسط کتابخانه استاندارد دارد؛ میتوانید درباره این متدها با نگاه کردن به مستندات API کتابخانه استاندارد برای صفت Iterator
اطلاعات بیشتری کسب کنید. برخی از این متدها در تعریف خود متد next
را فراخوانی میکنند، به همین دلیل است که شما باید متد next
را هنگام پیادهسازی صفت Iterator
تعریف کنید.
متدهایی که next
را فراخوانی میکنند، تطبیقدهندههای مصرفی نامیده میشوند، زیرا فراخوانی آنها iterator را مصرف میکند. یک مثال، متد sum
است که مالکیت iterator را به عهده میگیرد و با فراخوانی مکرر next
، از میان آیتمها عبور میکند، بنابراین iterator را مصرف میکند. هنگام عبور، هر آیتم را به یک مجموع در حال اجرا اضافه میکند و وقتی پیمایش کامل شد، مجموع را بازمیگرداند. لیست 13-13 یک تست را نشان میدهد که استفاده از متد sum
را نشان میدهد:
#[cfg(test)]
mod tests {
#[test]
fn iterator_sum() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
}
}
sum
برای گرفتن مجموع همه آیتمها در iteratorما اجازه نداریم پس از فراخوانی متد sum
از v1_iter
استفاده کنیم، زیرا sum
مالکیت iteratorی که روی آن فراخوانی میشود را به عهده میگیرد.
متدهایی که Iteratorهای دیگری تولید میکنند
تطبیقدهندههای Iterator متدهایی هستند که روی صفت Iterator
تعریف شدهاند و iterator را مصرف نمیکنند. در عوض، آنها با تغییر برخی جنبههای iterator اصلی، iteratorهای متفاوتی تولید میکنند.
لیست 13-14 مثالی از فراخوانی متد تطبیقدهنده iterator به نام map
را نشان میدهد که یک closure را برای فراخوانی روی هر آیتم هنگام پیمایش از میان آیتمها میگیرد. متد map
یک iterator جدید بازمیگرداند که آیتمهای تغییر یافته را تولید میکند. closure در اینجا یک iterator جدید ایجاد میکند که در آن هر آیتم از وکتور ۱ واحد افزایش مییابد:
fn main() { let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1); }
map
برای ایجاد یک iterator جدیدبا این حال، این کد یک هشدار تولید میکند:
$ cargo run
Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
--> src/main.rs:4:5
|
4 | v1.iter().map(|x| x + 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: iterators are lazy and do nothing unless consumed
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
4 | let _ = v1.iter().map(|x| x + 1);
| +++++++
warning: `iterators` (bin "iterators") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/iterators`
کد در لیست 13-14 هیچ کاری انجام نمیدهد؛ closureی که مشخص کردهایم هرگز فراخوانی نمیشود. این هشدار به ما یادآوری میکند چرا: تطبیقدهندههای iterator تنبل هستند و ما باید iterator را در اینجا مصرف کنیم.
برای رفع این هشدار و مصرف iterator، از متد collect
استفاده میکنیم، که در فصل 12 با env::args
در لیست 12-1 استفاده کردیم. این متد iterator را مصرف کرده و مقادیر حاصل را در یک نوع داده مجموعه جمعآوری میکند.
در لیست 13-15، نتایج پیمایش بر روی iterator بازگرداندهشده از فراخوانی map
را در یک وکتور جمعآوری میکنیم. این وکتور در نهایت شامل هر آیتم از وکتور اصلی با افزایش ۱ واحد خواهد بود.
fn main() { let v1: Vec<i32> = vec![1, 2, 3]; let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); assert_eq!(v2, vec![2, 3, 4]); }
map
برای ایجاد یک iterator جدید و سپس فراخوانی متد collect
برای مصرف iterator جدید و ایجاد یک وکتوراز آنجا که map
یک closure میگیرد، میتوانیم هر عملیاتی را که میخواهیم روی هر آیتم انجام دهیم مشخص کنیم. این مثال بسیار خوبی است از اینکه چگونه closureها به شما اجازه میدهند تا برخی رفتارها را سفارشی کنید در حالی که از رفتار پیمایشی که صفت Iterator
فراهم میکند استفاده مجدد میکنید.
میتوانید چندین فراخوانی به تطبیقدهندههای iterator را زنجیره کنید تا اقدامات پیچیدهای را به شکلی خوانا انجام دهید. اما از آنجا که همه iteratorها تنبل هستند، باید یکی از متدهای تطبیقدهنده مصرفی را برای گرفتن نتایج از فراخوانی تطبیقدهندههای iterator فراخوانی کنید.
استفاده از closureهایی که محیط خود را میگیرند
بسیاری از تطبیقدهندههای iterator closureها را به عنوان آرگومان میپذیرند، و معمولاً closureهایی که به عنوان آرگومان به تطبیقدهندههای iterator مشخص میکنیم closureهایی هستند که محیط خود را میگیرند.
برای این مثال، از متد filter
استفاده خواهیم کرد که یک closure میگیرد. closure یک آیتم از iterator دریافت کرده و یک مقدار bool
بازمیگرداند. اگر closure مقدار true
بازگرداند، مقدار در پیمایش تولید شده توسط filter
گنجانده میشود. اگر closure مقدار false
بازگرداند، مقدار گنجانده نخواهد شد.
در لیست 13-16، از filter
با یک closure که متغیر shoe_size
را از محیط خود میگیرد استفاده میکنیم تا روی مجموعهای از نمونههای ساختار Shoe
پیمایش کنیم. این متد فقط کفشهایی را که اندازه مشخص شده دارند بازمیگرداند.
#[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()
}
#[cfg(test)]
mod tests {
use super::*;
#[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")
},
]
);
}
}
filter
با یک closure که shoe_size
را میگیردتابع shoes_in_size
مالکیت یک وکتور از کفشها و یک اندازه کفش را به عنوان پارامتر میگیرد. این تابع یک وکتور بازمیگرداند که فقط شامل کفشهایی با اندازه مشخص شده است.
در بدنه shoes_in_size
، ما into_iter
را فراخوانی میکنیم تا یک iterator ایجاد کنیم که مالکیت وکتور را میگیرد. سپس filter
را فراخوانی میکنیم تا آن iterator را به یک iterator جدید تبدیل کنیم که فقط شامل عناصری است که closure برای آنها مقدار true
بازمیگرداند.
closure پارامتر shoe_size
را از محیط میگیرد و مقدار آن را با اندازه هر کفش مقایسه میکند و فقط کفشهایی با اندازه مشخص شده را نگه میدارد. در نهایت، با فراخوانی collect
مقادیر بازگرداندهشده توسط iterator تطبیقیافته در یک وکتور جمعآوری میشوند که توسط تابع بازگردانده میشود.
تست نشان میدهد که وقتی shoes_in_size
را فراخوانی میکنیم، فقط کفشهایی را دریافت میکنیم که اندازه آنها با مقداری که مشخص کردهایم یکسان است.