پردازش یک سری از آیتم‌ها با استفاده از Iteratorها

الگوی iterator به شما اجازه می‌دهد تا به ترتیب روی یک دنباله از آیتم‌ها کاری انجام دهید. یک iterator مسئول منطق پیمایش هر آیتم و تعیین زمان پایان دنباله است. وقتی از iteratorها استفاده می‌کنید، نیازی به پیاده‌سازی مجدد آن منطق ندارید.

در Rust، iteratorها تنبل هستند، به این معنی که تا زمانی که متدهایی که iterator را مصرف می‌کنند فراخوانی نشوند، هیچ اثری ندارند. به عنوان مثال، کد در لیست 13-10 یک iterator را بر روی آیتم‌های وکتور v1 با فراخوانی متد iter که روی Vec<T> تعریف شده است، ایجاد می‌کند. این کد به تنهایی هیچ کار مفیدی انجام نمی‌دهد.

Filename: src/main.rs
fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();
}
Listing 13-10: ایجاد یک iterator

این iterator در متغیر v1_iter ذخیره شده است. پس از ایجاد یک iterator، می‌توانیم از آن به روش‌های مختلف استفاده کنیم. در لیست 3-5 از فصل 3، ما روی یک آرایه با استفاده از یک حلقه for تکرار کردیم تا کدی را روی هر یک از آیتم‌های آن اجرا کنیم. در پشت صحنه، این کار به طور ضمنی یک iterator ایجاد و سپس مصرف می‌کرد، اما تا کنون دقیقاً توضیح ندادیم که چگونه کار می‌کند.

در مثال لیست 13-11، ما ایجاد iterator را از استفاده از آن در حلقه for جدا می‌کنیم. وقتی حلقه for با استفاده از iterator در v1_iter فراخوانی می‌شود، هر عنصر در iterator در یک تکرار از حلقه استفاده می‌شود، که هر مقدار را چاپ می‌کند.

Filename: src/main.rs
fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    for val in v1_iter {
        println!("Got: {val}");
    }
}
Listing 13-11: استفاده از یک iterator در حلقه 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 ایجادشده از وکتور بازگردانده می‌شود.

Filename: src/lib.rs
#[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);
    }
}
Listing 13-12: فراخوانی متد 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 را نشان می‌دهد:

Filename: src/lib.rs
#[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);
    }
}
Listing 13-13: فراخوانی متد sum برای گرفتن مجموع همه آیتم‌ها در iterator

ما اجازه نداریم پس از فراخوانی متد sum از v1_iter استفاده کنیم، زیرا sum مالکیت iteratorی که روی آن فراخوانی می‌شود را به عهده می‌گیرد.

متدهایی که Iteratorهای دیگری تولید می‌کنند

تطبیق‌دهنده‌های Iterator متدهایی هستند که روی صفت Iterator تعریف شده‌اند و iterator را مصرف نمی‌کنند. در عوض، آن‌ها با تغییر برخی جنبه‌های iterator اصلی، iteratorهای متفاوتی تولید می‌کنند.

لیست 13-14 مثالی از فراخوانی متد تطبیق‌دهنده iterator به نام map را نشان می‌دهد که یک closure را برای فراخوانی روی هر آیتم هنگام پیمایش از میان آیتم‌ها می‌گیرد. متد map یک iterator جدید بازمی‌گرداند که آیتم‌های تغییر یافته را تولید می‌کند. closure در اینجا یک iterator جدید ایجاد می‌کند که در آن هر آیتم از وکتور ۱ واحد افزایش می‌یابد:

Filename: src/main.rs
fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];

    v1.iter().map(|x| x + 1);
}
Listing 13-14: فراخوانی تطبیق‌دهنده iterator 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 را در یک وکتور جمع‌آوری می‌کنیم. این وکتور در نهایت شامل هر آیتم از وکتور اصلی با افزایش ۱ واحد خواهد بود.

Filename: src/main.rs
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]);
}
Listing 13-15: فراخوانی متد 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 پیمایش کنیم. این متد فقط کفش‌هایی را که اندازه مشخص شده دارند بازمی‌گرداند.

Filename: src/lib.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()
}

#[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")
                },
            ]
        );
    }
}
Listing 13-16: استفاده از متد 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 را فراخوانی می‌کنیم، فقط کفش‌هایی را دریافت می‌کنیم که اندازه آن‌ها با مقداری که مشخص کرده‌ایم یکسان است.