Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

معالجة سلسلة من العناصر باستخدام المكررات (Processing a Series of Items with Iterators)

يسمح لك نمط المكرر (iterator pattern) بأداء بعض المهام على تسلسل من العناصر بالدور. المكرر (iterator) مسؤول عن منطق التكرار (iterating) عبر كل عنصر وتحديد متى ينتهي التسلسل. عندما تستخدم iterators، ليس عليك إعادة تنفيذ ذلك المنطق بنفسك.

في Rust، تكون الـ iterators كسولة (lazy)، مما يعني أنه ليس لها أي تأثير حتى تستدعي طرقاً (methods) تستهلك (consume) الـ iterator لاستخدامه بالكامل. على سبيل المثال، الكود في القائمة 13-10 ينشئ iterator عبر العناصر الموجودة في المتجه (vector) v1 عن طريق استدعاء طريقة iter المعرفة على Vec<T>. هذا الكود بحد ذاته لا يفعل أي شيء مفيد.

fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();
}

يتم تخزين الـ iterator في المتغير v1_iter. بمجرد إنشاء iterator، يمكننا استخدامه بطرق متنوعة. في القائمة 3-5، قمنا بالتكرار عبر مصفوفة (array) باستخدام حلقة 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}");
    }
}

في اللغات التي لا توفر iterators في مكتباتها القياسية، من المحتمل أن تكتب هذه الوظيفة نفسها عن طريق بدء متغير عند الفهرس (index) 0، واستخدام ذلك المتغير للفهرسة في الـ vector للحصول على قيمة، وزيادة قيمة المتغير في حلقة حتى يصل إلى العدد الإجمالي للعناصر في الـ vector.

تتعامل الـ iterators مع كل هذا المنطق نيابة عنك، مما يقلل من الكود المتكرر الذي قد تخطئ فيه. تمنحك الـ iterators مرونة أكبر لاستخدام نفس المنطق مع أنواع مختلفة من التسلسلات، وليس فقط هياكل البيانات التي يمكنك الفهرسة فيها، مثل vectors. دعونا نفحص كيف تفعل الـ iterators ذلك.

سمة المكرر وطريقة التالي (The Iterator Trait and the next Method)

تنفذ جميع الـ iterators سمة (trait) تسمى Iterator معرفة في المكتبة القياسية. يبدو تعريف الـ trait هكذا:

#![allow(unused)]
fn main() {
pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // methods with default implementations elided
}
}

لاحظ أن هذا التعريف يستخدم بعض الصيغ (syntax) الجديدة: type Item و Self::Item ، والتي تعرف نوعاً مرتبطاً (associated type) بهذا الـ trait. سنتحدث عن associated types بعمق في الفصل العشرين. في الوقت الحالي، كل ما تحتاج لمعرفته هو أن هذا الكود يقول إن تنفيذ Iterator trait يتطلب منك أيضاً تعريف نوع Item ، ويتم استخدام نوع Item هذا في نوع الإرجاع لطريقة next. بعبارة أخرى، سيكون نوع Item هو النوع الذي يتم إرجاعه من الـ iterator.

تتطلب Iterator trait من المنفذين تعريف طريقة واحدة فقط: طريقة next ، والتي تعيد عنصراً واحداً من الـ iterator في كل مرة، مغلفاً في Some ، وعندما ينتهي التكرار، تعيد None.

يمكننا استدعاء طريقة next على الـ iterators مباشرة؛ توضح القائمة 13-12 القيم التي يتم إرجاعها من الاستدعاءات المتكررة لـ next على الـ iterator المنشأ من الـ vector.

#[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);
    }
}

لاحظ أننا احتجنا إلى جعل v1_iter قابلاً للتغيير (mutable): استدعاء طريقة next على iterator يغير الحالة الداخلية التي يستخدمها الـ iterator لتتبع مكانه في التسلسل. بعبارة أخرى، هذا الكود يستهلك (consumes)، أو يستنفد، الـ iterator. كل استدعاء لـ next يأكل عنصراً من الـ iterator. لم نكن بحاجة إلى جعل v1_iter mutable عندما استخدمنا حلقة for ، لأن الحلقة أخذت ملكية (ownership) v1_iter وجعلته mutable خلف الكواليس.

لاحظ أيضاً أن القيم التي نحصل عليها من الاستدعاءات لـ next هي مراجع غير قابلة للتغيير (immutable references) للقيم الموجودة في الـ vector. تنتج طريقة iter مكرراً عبر immutable references. إذا أردنا إنشاء iterator يأخذ ownership لـ v1 ويعيد قيم مملوكة، يمكننا استدعاء into_iter بدلاً من iter. وبالمثل، إذا أردنا التكرار عبر مراجع قابلة للتغيير (mutable references)، يمكننا استدعاء iter_mut بدلاً من iter.

الطرق التي تستهلك المكرر (Methods That Consume the Iterator)

تمتلك Iterator trait عدداً من الطرق المختلفة مع تنفيذات افتراضية توفرها المكتبة القياسية؛ يمكنك التعرف على هذه الطرق من خلال النظر في توثيق API للمكتبة القياسية لـ Iterator trait. تستدعي بعض هذه الطرق طريقة next في تعريفها، وهذا هو السبب في أنك مطالب بتنفيذ طريقة next عند تنفيذ Iterator trait.

تسمى الطرق التي تستدعي next بـ محولات الاستهلاك (consuming adapters) لأن استدعاءها يستنفد الـ iterator. أحد الأمثلة هو طريقة sum ، التي تأخذ ownership للـ 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);
    }
}

لا يُسمح لنا باستخدام v1_iter بعد استدعاء sum ، لأن sum تأخذ ownership للـ iterator الذي نستدعيها عليه.

الطرق التي تنتج مكررات أخرى (Methods That Produce Other Iterators)

محولات المكرر (Iterator adapters) هي طرق معرفة على Iterator trait لا تستهلك الـ iterator. بدلاً من ذلك، فإنها تنتج iterators مختلفة عن طريق تغيير بعض جوانب الـ iterator الأصلي.

توضح القائمة 13-14 مثالاً على استدعاء طريقة محول المكرر map ، والتي تأخذ إغلاقاً (closure) لاستدعائه على كل عنصر أثناء التكرار عبر العناصر. تعيد طريقة map مكرراً جديداً ينتج العناصر المعدلة. الـ closure هنا ينشئ iterator جديداً يتم فيه زيادة كل عنصر من الـ vector بمقدار 1.

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

    v1.iter().map(|x| x + 1);
}

ومع ذلك، ينتج هذا الكود تحذيراً:

$ 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 adapters كسولة، ونحن بحاجة إلى استهلاك الـ iterator هنا.

لإصلاح هذا التحذير واستهلاك الـ iterator، سنستخدم طريقة collect ، التي استخدمناها مع env::args في القائمة 12-1. تستهلك هذه الطريقة الـ iterator وتجمع القيم الناتجة في نوع بيانات تجميعي (collection).

في القائمة 13-15، نجمع نتائج التكرار عبر الـ iterator الذي يتم إرجاعه من الاستدعاء لـ map في vector. سينتهي هذا الـ vector باحتواء كل عنصر من الـ vector الأصلي، مزيداً بمقدار 1.

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 تأخذ closure، يمكننا تحديد أي عملية نريد القيام بها على كل عنصر. هذا مثال رائع على كيفية سماح closures لك بتخصيص بعض السلوك مع إعادة استخدام سلوك التكرار الذي توفره Iterator trait.

يمكنك ربط استدعاءات متعددة لـ iterator adapters لأداء إجراءات معقدة بطريقة مقروءة. ولكن لأن جميع الـ iterators كسولة، يجب عليك استدعاء إحدى طرق consuming adapter للحصول على نتائج من الاستدعاءات لـ iterator adapters.

الإغلاقات التي تلتقط بيئتها (Closures That Capture Their Environment)

تأخذ العديد من iterator adapters إغلاقات كـ arguments، وعادة ما تكون الـ closures التي سنحددها كـ arguments لـ iterator adapters هي closures تلتقط بيئتها.

لهذا المثال، سنستخدم طريقة filter التي تأخذ closure. يحصل الـ closure على عنصر من الـ iterator ويعيد bool. إذا أعاد الـ closure القيمة true ، فسيتم تضمين القيمة في التكرار الناتج عن filter. إذا أعاد الـ closure القيمة false ، فلن يتم تضمين القيمة.

في القائمة 13-16، نستخدم filter مع closure يلتقط المتغير shoe_size من بيئته للتكرار عبر مجموعة من مثيلات (instances) هيكل 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")
                },
            ]
        );
    }
}

تأخذ دالة shoes_in_size ملكية vector من الأحذية وحجم حذاء كـ parameters. وتعيد vector يحتوي فقط على الأحذية بالحجم المحدد.

في جسم shoes_in_size ، نستدعي into_iter لإنشاء iterator يأخذ ownership للـ vector. ثم، نستدعي filter لتكييف ذلك الـ iterator إلى iterator جديد يحتوي فقط على العناصر التي يعيد الـ closure لها القيمة true.

يلتقط الـ closure الـ parameter shoe_size من البيئة ويقارن القيمة بحجم كل حذاء، محتفظاً فقط بالأحذية بالحجم المحدد. أخيراً، يؤدي استدعاء collect إلى جمع القيم التي أرجعها الـ iterator المكيف في vector يتم إرجاعه بواسطة الدالة.

يظهر الاختبار أنه عندما نستدعي shoes_in_size ، نحصل فقط على الأحذية التي لها نفس الحجم مثل القيمة التي حددناها.