يسمح لك نمط المكرّر iterator pattern بإنجاز مهمة ما على سلسلة من العناصر بصورةٍ متتالية، والمكرّر مسؤول عن المنطق الخاص بالمرور على كل عنصر وتحديد مكان انتهاء السلسلة، إذ ليس من الواجب عليك إعادة تطبيق هذا المنطق بنفسك عند استخدام المكررات.
المكررات في رست كسولة، بمعنى أنها لا تمتلك أي تأثير حتى تستدعي أنت التابع الذي يستخدم المكرر، فعلى سبيل المثال نُنشئ الشيفرة 10 مكرّرًا على العناصر الموجودة في الشعاع v1
باستدعاء التابع iter
المعرّف على Vec<T>
، ولا تفعل تلك الشيفرة البرمجية أي شيء مفيد.
let v1 = vec![1, 2, 3]; let v1_iter = v1.iter();
[الشيفرة 10: إنشاء مكرر]
نخزّن المكرر في المتغير v1_iter
، ويمكننا استخدامه بطرق عدّة بعد إنشائه. على سبيل المثال، استخدمنا حلقة for
في الشيفرة 5 من فصل سابق للمرور على مصفوفة وذلك لتنفيذ شيفرة برمجية على كل من عناصرها، وفي الحقيقة كان المكرّر موجودًا ضمنيًا في تلك الشيفرة لتحقيق ذلك إلا أننا لم نتكلم عن ذلك سابقًا.
نفصل في الشيفرة 11 إنشاء المكرر من استخدامه في الحلقة for
، فعندما تُستدعى الحلقة for
باستخدام المكرر v1_iter
، يُستخدم كل عنصر في المكرر مرةً تلو الأخرى في كل دورة للحلقة، وهذا يطبع كل قيمة من قيم العناصر.
let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!("Got: {}", val); }
[الشيفرة 11: استخدام مكرر في حلقة for
]
يمكنك كتابة شيفرة برمجية تفعل الأمر نفسه في لغات البرمجة التي لا تستخدم المكررات في مكتبتها القياسية وذلك عن طريق البدء بالمتغير من الدليل 0 واستخدام قيمة المتغير يمثابة دليلٍ للشعاع للحصول على قيمة ومن ثم زيادة قيمة المتغير في الحلقة حتى تصل قيمته لعدد العناصر الكلية في الشعاع.
تتعامل المكررات مع المنطق البرمجي نيابةً عنك مما يقلل من الشيفرات البرمجية المتكررة التي من الممكن أن تتسبب بأخطاء، وتعطيك المكررات مرونةً أكبر في التعامل مع المنطق ذاته في الكثير من أنواع السلاسل وليس فقط هياكل البيانات data structures التي يمكنك الوصول إلى قيمها باستخدام دليل مثل الشعاع. دعنا نلقي نظرةً إلى كيفية تحقيق المكررات لكل هذا.
سمة Iterator وتابع next
تطبّق جميع المكررات السمة Iterator
المُعرّفة في المكتبة القياسية، ويبدو تعريف السمة شيء مماثل لهذا:
pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; // methods with default implementations elided }
لاحظ أن هذا التعريف يستخدم صيغتان جديدتان، هما: type Item
و Self::Item
اللتان تُعرِّفان نوعًا مترابطًا associated type مع هذه السمة. سنتحدث عن الأنواع المترابطة بالتفصيل لاحقًا، ويكفي للآن معرفتك أن هذه الشيفرة البرمجية تقول أن تطبيق السمة Iterator
يتطلب منك تعريف نوع Item
أيضًا وهذا النوع مُستخدم مثل نوع مُعاد من التابع next
، وبكلمات أخرى، سيكون النوع Item
هو النوع المُعاد من المكرّر.
تتطلب السمة Iterator
ممن يطبّقها فقط أن يعرّف تابعًا واحدًا هو next
، الذي يُعيد عنصرًا واحدًا من المكرر كل مرة ضمن Some
وعندما تنتهي العناصر (ينتهي التكرار)، يُعيد None
.
يمكننا استدعاء التابع next
على المكررات مباشرةً، وتوضح الشيفرة 12 القيم المُعادة من الاستدعاءات المتعاقبة على المكرّر باستخدام next
وهو المكرر الموجود في الشعاع.
اسم الملف: src/lib.rs
#[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); }
[الشيفرة 12: استدعاء التابع next
على المكرر]
لاحظ أننا احتجنا لإنشاء v1_iter
متغيّر mutable، إذ يغيّر استدعاء التابع next
على مكرر الحالة الداخلية التي يستخدمها المكرر لتتبع مكانه ضمن السلسلة، وبكلمات أخرى، تستهلك consumes الشيفرة البرمجية المكرّر، إذ يستهلك كل استدعاء للتابع next
عنصرًا واحدًا من المكرر. لم يكن هناك أي حاجة لإنشاء v1_iter
متغيّر عندما استخدمنا حلقة for
لأن الحلقة أخذت ملكية ownership المكرر v1_iter
وجعلته متغيّرًا ضمنيًا.
لاحظ أيضًا أن القيم التي نحصل عليها من استدعاءات التابع next
هي مراجع ثابتة immutable references للقيم الموجودة في الشعاع، ويعطينا التابع iter
مكرًرًا على المراجع الثابتة. إذا أردنا إنشاء مكرر يأخذ ملكية v1
ويُعيد القيم المملوكة فيمكننا استدعاء into_iter
بدلًا من iter
، ويمكننا بصورةٍ مماثلة استدعاء iter_mut
بدلًا من iter
إذا أردنا المرور على مراجع متغيّرة.
توابع تستهلك المكرر
للسمة Iterator
العديد من التوابع في تطبيقها الافتراضي والموجودة في المكتبة القياسية، ويمكنك الاطّلاع على هذه التوابع عن طريق النظر إلى توثيق واجهة المكتبة القياسية البرمجية للسمة Iterator
. تستدعي بعض هذه التوابع التابع next
في تعريفها وهو السبب في ضرورة تطبيق التابع next
عند تطبيق السمة Iterator
.
تُسمّى التوابع التي تستدعي التابع next
بالمحوّلات المُستهلكة consuming adaptors لأن استدعائها يستهلك المكرر. يُعد تابع sum
مثال على هذه التوابع، فهو يأخذ ملكية المكرر ويمرّ على عناصره بصورةٍ متتالية مع استدعاء next
مما يتسبب باستهلاك المكرر، وبينما يمرّ على العناصر فهو يجمع كل عنصر إلى قيمة كلية، ثم يعيد القيمة الكلية في النهاية. تحتوي الشيفرة 13 على اختبار يوضح استخدام التابع sum
:
اسم الملف: src/lib.rs
#[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); }
[الشيفرة 13: استدعاء التابع sum
للحصول على القيمة الكلية لجميع العناصر في المكرر]
لا يُسمح لنا باستخدام v1_iter
بعد استدعاء sum
لأن sum
يأخذ ملكية المكرّر الذي نستدعي sum
عليه.
التوابع التي تنشئ مكررات أخرى
محولات المكرر iterator adaptors هي توابع مُعرّفة على السمة Iterator
ولا تستهلك المكرر، بل تُنشئ مكررات مختلفة بدلًا من ذلك عن طريق تغيير بعض خصائص المكرر الأصل.
توضح الشيفرة 14 مثالًا عن استدعاء تابع محول مكرر map
وهو تابع يأخذ مغلّفًا closure ويستدعيه على كل من العناصر بصورةٍ متتالية. يُعيد التابع map
مكررًا جديدًا يُنشئ عناصرًا مُعدّلٌ عليها، ويُنشئ المغلف في هذه الحالة مكررًا جديدًا يزيد قيمة عناصره بمقدار 1 لكل عنصر في الشعاع:
اسم الملف: src/main.rs
let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1);
[الشيفرة 14: استدعاء محول المكرر map
لإنشاء مكرر جديد]
إلا أن الشيفرة البرمجية السابقة تعطينا إنذارًا:
$ 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: `#[warn(unused_must_use)]` on by default = note: iterators are lazy and do nothing unless consumed warning: `iterators` (bin "iterators") generated 1 warning Finished dev [unoptimized + debuginfo] target(s) in 0.47s Running `target/debug/iterators`
لا تفعل الشيفرة 14 أي شيء، إذ أن المغلف الذي حددناه لا يُستدعى أبدًا، ويذكرنا الإنذار بسبب ذلك: إذ أن محولات المكرر كسولة ونحتاج لاستهلاك المكرر هنا.
نستخدم التابع collect
لتصحيح هذا الإنذار واستهلاك المكرر وهو ما استخدمناه في (الشيفرة 1 من فصل سابق) باستخدام env::args
، إذ يستهلك هذا التابع المكرر ويجمّع القيم الناتجة إلى نوع بيانات تجميعة collection data type.
نجمع في الشيفرة 15 النتائج من عملية المرور على المكرر والمُعادة من استدعاء التابع map
على الشعاع، وسيحتوي هذا الشعاع في نهاية المطاف على جميع عناصره الموجودة مع زيادة على قيمة كل منها بمقدار 1.
اسم الملف: src/main.rs
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]);
[الشيفرة 15: استدعاء التابع map
لإنشاء مكرر جديد ومن ثم استدعاء التابع collect
لاستهلاك المكرر الجديد وإنشاء شعاع]
يمكننا تحديد أي عملية نريد إنجازها على كل عنصر بالنظر إلى أن map
تتلقى مغلفًا بمثابة وسيط لها. هذا مثال عظيم عن كيفية إنجاز مهام معقدة بطريقة مقروءة، ولأن جميع المكررات كسولة، فهذا يعني أنك بحاجة استدعاء واحد من توابع المحولات المستهلكة للحصول على نتائج استدعاءات محولات المكرر.
استخدام المغلفات التي تحصل على القيم من بيئتها
تأخذ الكثير من محولات المكرر المغلفات مثل وسطاء لها، وستحصل هذه المغلفات التي تُحدد مثل وسطاء لمحولات المكرر على قيم من بيئتها غالبًا.
نستخدم في هذا المثال التابع filter
الذي يأخذ مغلّفًا، ويحصل المغلف على عنصر من المكرر ويُعيد bool
، وتُضمّن هذه القيمة في التكرار المُنشئ بواسطة filter
إذا أعاد المغلف القيمة true
، وإذا أعاد المكرر القيمة false
فلن تُضمَّن القيمة.
نستخدم في الشيفرة 16 التابع filter
بمغلّف يحصل على المتغير shoe_size
من بيئته للمرور على تجميعة من نسخ instances الهيكل Shoe
، وسيُعيد فقط الأحذية ذات مقاس محدد.
اسم الملف: 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") }, ] ); } }
[الشيفرة 16: استخدام التابع filter
مع مغلف يحصل على القيمة shoe_size
]
تأخذ الدالة shoes_in_size
ملكية شعاع الأحذية وقياس الحذاء مثل معاملات، وتُعيد شعاعًا يحتوي على الأحذية بالمقاس المحدد.
نستدعي into_iter
في متن الدالة shoes_in_size
لإنشاء مكرر يأخذ ملكية الشعاع، ثم نستدعي filter
لتحويل المكرّر إلى مكرر جديد يحتوي فقط على عناصر يُعيد منها المغلّف القيمة true
.
يحصل المغلف على المعامل shoe_size
من البيئة ويقارنه مع قيمة كل مقاس حذاء مع إبقاء الأحذية التي يتطابق مقاسها، وأخيرًا يجمع استدعاء collect
القيم المُعادة بواسطة محول المكرر إلى شعاع يُعاد من الدالة.
يوضح هذا الاختبار أنه عندما نستدعي shoes_in_size
، فنحن نحصل فقط على الأحذية التي تتطابق مقاساتها مع القيمة التي حددناها.
ترجمة -وبتصرف- لقسم من الفصل Functional Language Features: Iterators and Closures من كتاب The Rust Programming Language.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.