การประมวลผลลำดับข้อมูลด้วยตัวซ้ำ (Iterators)
รูปแบบการออกแบบตัวซ้ำ (Iterator pattern) ช่วยให้คุณสามารถทำงานบางอย่างกับลำดับของสมาชิกตามลำดับได้ ตัวซ้ำ (Iterator) มีหน้าที่รับผิดชอบตรรกะในการวนซ้ำไปตามแต่ละสมาชิกและตัดสินว่าลำดับข้อมูลนั้นสิ้นสุดลงเมื่อใด เมื่อคุณใช้ตัวซ้ำ คุณไม่จำเป็นต้องเขียนตรรกะนั้นขึ้นใหม่ด้วยตัวเอง
ในภาษา Rust ตัวซ้ำจะมีลักษณะเกียจคร้าน (lazy) ซึ่งหมายความว่าตัวซ้ำจะยังไม่มีผลใด ๆ จนกว่าคุณจะเรียกใช้เมธอดที่บริโภคตัวซ้ำนั้นเพื่อนำไปใช้งานจนหมด ตัวอย่างเช่น โค้ดในโค้ดตัวอย่างที่ 13-10 ได้สร้างตัวซ้ำเหนือสมาชิกในเวกเตอร์ v1 โดยการเรียกใช้เมธอด iter ที่นิยามไว้บน Vec<T> โค้ดนี้เพียงอย่างเดียวไม่ได้ทำสิ่งที่เป็นประโยชน์ใด ๆ
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
}
ตัวซ้ำนี้ถูกเก็บไว้ในตัวแปร v1_iter เมื่อเราสร้างตัวซ้ำขึ้นมาแล้ว เราสามารถนำมันไปใช้งานได้หลากหลายวิธี ในโค้ดตัวอย่างที่ 3-5 เราได้วนซ้ำเหนืออาเรย์โดยใช้ลูป for เพื่อประมวลผลโค้ดบางอย่างกับแต่ละสมาชิก ภายใต้ฉากหลัง ลูป for จะสร้างและบริโภคตัวซ้ำอย่างไม่เปิดเผย (implicitly) แต่เราได้ข้ามรายละเอียดการทำงานที่ชัดเจนไปจนถึงตอนนี้
ในตัวอย่างโค้ดตัวอย่างที่ 13-11 เราได้แยกการสร้างตัวซ้ำออกจากใช้งานตัวซ้ำในลูป for เมื่อลูป for ถูกเรียกใช้โดยใช้ตัวซ้ำใน v1_iter สมาชิกแต่ละตัวในตัวซ้ำจะถูกใช้ในการวนซ้ำรอบหนึ่งของลูป ซึ่งจะพิมพ์แต่ละค่าออกมา
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter {
println!("Got: {val}");
}
}
ในภาษาอื่น ๆ ที่ไม่มีตัวซ้ำจัดเตรียมไว้ในไลบรารีมาตรฐาน คุณอาจจะต้องเขียนฟังก์ชันการทำงานเดียวกันนี้โดยเริ่มตัวแปรดัชนีที่ 0 ใช้ตัวแปรนั้นเป็นดัชนีชี้เข้าไปในเวกเตอร์เพื่อดึงค่า และเพิ่มค่าตัวแปรนั้นขึ้นทีละหนึ่งในลูปจนกว่าจะถึงจำนวนสมาชิกทั้งหมดในเวกเตอร์
ตัวซ้ำจะจัดการตรรกะทั้งหมดนั้นแทนคุณ ช่วยลดโค้ดที่ซ้ำซ้อนซึ่งอาจเกิดข้อผิดพลาดได้ง่าย นอกจากนี้ตัวซ้ำยังมอบความยืดหยุ่นในการใช้ตรรกะเดียวกันกับลำดับข้อมูลหลายประเภท ไม่ใช่เฉพาะโครงสร้างข้อมูลที่ใช้ดัชนีชี้ได้เหมือนอย่างเวกเตอร์เท่านั้น เรามาพิจารณากันว่าตัวซ้ำทำเช่นนั้นได้อย่างไร
เทรต Iterator และเมธอด next
ตัวซ้ำทั้งหมดจะประยุกต์ใช้ (implement) เทรตที่ชื่อว่า Iterator ซึ่งถูกนิยามไว้ในไลบรารีมาตรฐาน นิยามของเทรตมีลักษณะดังนี้:
#![allow(unused)]
fn main() {
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// methods with default implementations elided
}
}
สังเกตว่านิยามนี้ใช้ไวยากรณ์ใหม่บางอย่าง ได้แก่ type Item และ Self::Item ซึ่งเป็นการกำหนดชนิดข้อมูลที่เชื่อมโยง (associated type) ให้กับเทรตนี้ เราจะพูดถึงชนิดข้อมูลที่เชื่อมโยงอย่างละเอียดในบทที่ 20 สำหรับตอนนี้ สิ่งที่คุณต้องรู้คือ โค้ดนี้บอกว่าการประยุกต์ใช้เทรต Iterator กำหนดให้คุณต้องนิยามชนิดข้อมูล Item ด้วย และชนิดข้อมูล Item นี้ถูกใช้ในชนิดข้อมูลค่าคืนกลับของเมธอด next หรือกล่าวอีกนัยหนึ่ง ชนิดข้อมูล Item จะเป็นชนิดข้อมูลที่ถูกคืนกลับมาจากตัวซ้ำ
เทรต Iterator กำหนดให้ผู้ประยุกต์ใช้นิยามเพียงเมธอดเดียวเท่านั้น นั่นคือเมธอด next ซึ่งจะคืนค่าทีละหนึ่งสมาชิกของตัวซ้ำ โดยห่อหุ้มใน Some และเมื่อการวนซ้ำสิ้นสุดลง จะคืนค่า None
เราสามารถเรียกใช้เมธอด next บนตัวซ้ำได้โดยตรง โค้ดตัวอย่างที่ 13-12 แสดงให้เห็นว่าค่าใดบ้างที่ถูกคืนกลับมาจากการเรียก next ซ้ำ ๆ บนตัวซ้ำที่สร้างขึ้นจากเวกเตอร์
#[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 บนตัวซ้ำจะเปลี่ยนแปลงสถานะภายในที่ตัวซ้ำใช้ติดตามตำแหน่งปัจจุบันในลำดับข้อมูล หรือกล่าวอีกนัยหนึ่ง โค้ดนี้ได้บริโภค (consumes) หรือใช้ตัวซ้ำไปจนหมด การเรียก next แต่ละครั้งจะกินสมาชิกไปหนึ่งตัวจากตัวซ้ำ เราไม่จำเป็นต้องทำให้ v1_iter เป็นชนิดที่แก้ไขได้เมื่อเราใช้ลูป for เพราะลูปจะรับความเป็นเจ้าของ v1_iter และทำให้มันสามารถแก้ไขได้อยู่เบื้องหลัง
นอกจากนี้ สังเกตว่าค่าที่เราได้จากการเรียก next คือการอ้างอิงแบบแก้ไขไม่ได้ (immutable reference) ไปยังค่าในเวกเตอร์ เมธอด iter จะสร้างตัวซ้ำเหนือการอ้างอิงแบบแก้ไขไม่ได้ หากเราต้องการสร้างตัวซ้ำที่รับความเป็นเจ้าของ v1 และคืนค่าที่เป็นเจ้าของเอง เราสามารถเรียก into_iter แทน iter ได้ ในทำนองเดียวกัน หากเราต้องการวนซ้ำเหนือการอ้างอิงแบบแก้ไขได้ เราสามารถเรียก iter_mut แทน iter ได้
เมธอดที่บริโภคตัวซ้ำ (Methods That Consume the Iterator)
เทรต Iterator มีเมธอดต่าง ๆ มากมายพร้อมการประยุกต์ใช้งานเริ่มต้น (default implementations) ที่จัดเตรียมโดยไลบรารีมาตรฐาน คุณสามารถค้นหาเมธอดเหล่านี้ได้โดยดูจากเอกสาร API ไลบรารีมาตรฐานสำหรับเทรต Iterator เมธอดเหล่านี้บางตัวเรียกใช้เมธอด next ในการนิยามของพวกมัน ซึ่งเป็นเหตุผลว่าทำไมคุณจึงจำเป็นต้องประยุกต์ใช้เมธอด next เมื่อประยุกต์ใช้เทรต Iterator
เมธอดที่เรียกใช้ next ถูกเรียกว่า ตัวปรับแต่งที่บริโภค (consuming adapters) เนื่องจากตัวมันเรียกใช้งานจนตัวซ้ำถูกใช้หมดไป ตัวอย่างหนึ่งคือเมธอด sum ซึ่งจะรับความเป็นเจ้าของของตัวซ้ำและวนซ้ำตามสมาชิกโดยการเรียก next ซ้ำ ๆ จึงเป็นการบริโภคตัวซ้ำ ในขณะที่มันวนซ้ำไป มันจะบวกแต่ละสมาชิกเข้ากับผลรวมสะสม และคืนค่าผลรวมเมื่อการวนซ้ำเสร็จสมบูรณ์ โค้ดตัวอย่างที่ 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 ได้รับความเป็นเจ้าของของตัวซ้ำที่เราเรียกใช้ไปแล้ว
เมธอดที่สร้างตัวซ้ำอื่นขึ้นมา (Methods That Produce Other Iterators)
ตัวปรับแต่งตัวซ้ำ (Iterator adapters) คือเมธอดที่นิยามไว้บนเทรต Iterator ที่ไม่บริโภคตัวซ้ำ แต่พวกมันจะสร้างตัวซ้ำอื่นที่แตกต่างออกไปขึ้นมาโดยการเปลี่ยนแง่มุมบางอย่างของตัวซ้ำดั้งเดิม
โค้ดตัวอย่างที่ 13-14 แสดงตัวอย่างการเรียกใช้เมธอดตัวปรับแต่งตัวซ้ำ map ซึ่งรับโคลเชอร์เพื่อเรียกใช้กับแต่ละสมาชิกขณะวนซ้ำ เมธอด map จะคืนค่าตัวซ้ำใหม่ที่ให้สมาชิกที่ถูกปรับเปลี่ยนแล้ว โคลเชอร์ในที่นี้จะสร้างตัวซ้ำใหม่ที่สมาชิกแต่ละตัวจากเวกเตอร์จะถูกเพิ่มค่าขึ้น 1
fn main() {
let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1);
}
อย่างไรก็ตาม โค้ดนี้จะสร้างคำเตือน (warning):
$ 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)]` (part of `#[warn(unused)]`) 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 ไม่ได้ทำสิ่งใดเลย โคลเชอร์ที่เรากำหนดไว้ไม่เคยถูกเรียกใช้ คำเตือนเตือนเราถึงเหตุผลว่า: ตัวปรับแต่งตัวซ้ำมีลักษณะเกียจคร้าน (lazy) และเราจำเป็นต้องบริโภคตัวซ้ำตรงนี้
ในการแก้ไขคำเตือนนี้และบริโภคตัวซ้ำ เราจะใช้เมธอด collect ซึ่งเราเคยใช้กับ env::args ในโค้ดตัวอย่างที่ 12-1 เมธอดนี้จะบริโภคตัวซ้ำและรวบรวมค่าที่ได้ลงในชนิดข้อมูลประเภทคอลเลกชัน (collection)
ในโค้ดตัวอย่างที่ 13-15 เรา รวบรวมผลลัพธ์ของการวนซ้ำเหนือตัวซ้ำที่ถูกคืนกลับมาจากการเรียก map ลงในเวกเตอร์ เวกเตอร์นี้จะจบลงด้วยการบรรจุสมาชิกแต่ละตัวจากเวกเตอร์เดิมที่ถูกบวกเพิ่มขึ้น 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 รับโคลเชอร์ เราจึงสามารถระบุการทำงานใด ๆ ที่เราต้องการทำกับแต่ละสมาชิกได้ นี่เป็นตัวอย่างที่ดีของโคลเชอร์ที่ช่วยให้คุณปรับแต่งพฤติกรรมบางอย่างได้ในขณะที่ใช้พฤติกรรมการวนซ้ำซ้ำ ซึ่งเทรต Iterator จัดเตรียมไว้ให้
คุณสามารถเชื่อมต่อ (chain) การเรียกตัวปรับแต่งตัวซ้ำหลาย ๆ ตัวเพื่อทำแอ็กชันที่ซับซ้อนในรูปแบบที่อ่านง่ายได้ แต่เนื่องจากตัวซ้ำทั้งหมดมีลักษณะเกียจคร้าน คุณจึงต้องเรียกใช้เมธอดตัวปรับแต่งที่บริโภคตัวใดตัวหนึ่งเพื่อให้ได้ผลลัพธ์จากการเรียกตัวปรับแต่งตัวซ้ำ
โคลเชอร์ที่ดึงค่าจากสภาพแวดล้อมของพวกมัน (Closures That Capture Their Environment)
ตัวปรับแต่งตัวซ้ำหลายตัวรับโคลเชอร์เป็นอาร์กิวเมนต์ และโดยทั่วไปโคลเชอร์ที่เราจะระบุเป็นอาร์กิวเมนต์ของตัวปรับแต่งตัวซ้ำก็มักจะเป็นโคลเชอร์ที่ดึงค่าจากสภาพแวดล้อมของมัน
สำหรับตัวอย่างนี้ เราจะใช้เมธอด filter ที่รับโคลเชอร์ โคลเชอร์จะได้สมาชิกจากตัวซ้ำและคืนค่าเป็น bool หากโคลเชอร์คืนค่า true ค่านั้นจะถูกรวมไว้ในตัวซ้ำที่สร้างขึ้นโดย filter แต่หากโคลเชอร์คืนค่า false ค่านั้นจะไม่ถูกรวมอยู่ด้วย
ในโค้ดตัวอย่างที่ 13-16 เราใช้ filter ร่วมกับโคลเชอร์ที่ดึงค่าจากตัวแปร shoe_size ในสภาพแวดล้อมของมัน เพื่อวนซ้ำเหนือคอลเลกชันของอินสแตนซ์ struct 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 รับความเป็นเจ้าของของเวกเตอร์ของรองเท้าและขนาดรองเท้าเป็นพารามิเตอร์ มันคืนค่าเป็นเวกเตอร์ที่บรรจุเฉพาะรองเท้าตามขนาดที่ระบุไว้เท่านั้น
ในเนื้อหาของ shoes_in_size เราเรียก into_iter เพื่อสร้างตัวซ้ำที่รับความเป็นเจ้าของของเวกเตอร์ จากนั้นเราเรียก filter เพื่อปรับแต่งตัวซ้ำนั้นเป็นตัวซ้ำใหม่ที่มีเฉพาะสมาชิกที่โคลเชอร์คืนค่าเป็น true
โคลเชอร์จะดึงพารามิเตอร์ shoe_size จากสภาพแวดล้อมและเปรียบเทียบค่านั้นกับขนาดของรองเท้าแต่ละคู่ โดยเก็บเฉพาะรองเท้าที่มีขนาดตรงตามที่ระบุไว้ สุดท้าย การเรียกใช้ collect จะรวบรวมค่าที่คืนมาจากตัวซ้ำที่ปรับแต่งแล้วลงในเวกเตอร์เพื่อคืนค่าจากฟังก์ชัน
การทดสอบแสดงให้เห็นว่าเมื่อเราเรียกใช้ shoes_in_size เราจะได้กลับมาเฉพาะรองเท้าที่มีขนาดเดียวกับค่าที่เราระบุไว้เท่านั้น