Iterators
Method สร้าง Iterator สามตัว
หัวข้อที่มีชื่อว่า “Method สร้าง Iterator สามตัว”ทุก collection เปิดวิธีสร้าง iterator สามแบบ:
| Method | ให้ค่า | Ownership |
|---|---|---|
.iter() | &T | Borrow collection |
.iter_mut() | &mut T | Borrow collection แบบ mutable |
.into_iter() | T | Consume (ย้าย) collection |
let v = vec![1, 2, 3];
for x in v.iter() { } // v still valid afterfor x in v.into_iter() { } // v moved — cannot use v afterคอมไพเลอร์พูดว่าอะไร: การแก้ไข Collection ระหว่างวนซ้ำมันอยู่
หัวข้อที่มีชื่อว่า “คอมไพเลอร์พูดว่าอะไร: การแก้ไข Collection ระหว่างวนซ้ำมันอยู่”.iter() ถือ shared borrow ของ collection ไว้ตลอดที่ loop ทำงาน การ push เข้า collection เดียวกันภายใน loop ต้องการ mutable borrow — และ borrow checker จะไม่ยอมให้ทั้งสองมีอยู่พร้อมกัน แม้ว่า “แค่ต่อท้ายเข้าไป” จะรู้สึกไม่มีอันตราย:
// @expect-error E0502fn main() { let mut v = vec![1, 2, 3]; for x in &v { v.push(*x); // ERROR: cannot mutate v while iterating over &v } println!("{:?}", v);}error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable --> src/main.rs:4:9 |3 | for x in &v { | -- | | | immutable borrow occurs here | immutable borrow later used here4 | v.push(*x); // ERROR: cannot mutate v while iterating over &v | ^^^^^^^^^^ mutable borrow occurs here
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0502`.นี่ไม่ใช่ borrow checker ที่ระมัดระวังเกินไป: iterator ของ Vec จริงๆ แล้วคือ raw pointer บวก length ที่เหลือเข้าไปใน buffer ของมัน ถ้า push trigger การ reallocation (ดูส่วน capacity ของบท vec-string) buffer จะย้ายที่ และ raw pointer นั้นจะชี้ไปยังหน่วยความจำที่ถูกปล่อยไปแล้ว — บั๊ก iterator-invalidation แบบคลาสสิกที่ crash แบบเงียบๆ ใน C++ แต่ถูกปฏิเสธตอน compile time ที่นี่ ถ้าคุณต้องสร้าง Vec ใหม่จาก Vec เก่า ให้ collect เข้า Vec แยกต่างหากแทนที่จะแก้ไขตัวที่คุณกำลังวนซ้ำอยู่
ความ Lazy ที่พิสูจน์แล้ว
หัวข้อที่มีชื่อว่า “ความ Lazy ที่พิสูจน์แล้ว”adaptor อย่าง map และ filter ไม่รัน closure ของมันตอนที่คุณเรียกมัน — มันจะรันก็ต่อเมื่อ consumer ดึงค่าผ่าน chain เท่านั้น นี่คือหลักฐาน ไม่ใช่แค่คำกล่าวอ้าง: closure ของ map ที่ print ทำงานสองแบบต่างกัน
fn main() { let v = vec![1, 2, 3]; let iter = v.iter().map(|x| { println!("mapping {}", x); x * 2 }); println!("iterator built, nothing printed yet"); let sum: i32 = iter.sum(); println!("sum = {}", sum);}iterator built, nothing printed yetmapping 1mapping 2mapping 3sum = 12"iterator built, nothing printed yet" พิมพ์ก่อนบรรทัด "mapping" ใดๆ เลย — การสร้าง chain ของ map ไม่ได้ทำงานอะไรเลย มีแค่ .sum() ซึ่งเป็น consumer เท่านั้นที่ดึง element ผ่านมาและรัน closure ทีนี้ลองเทียบกับสิ่งที่เกิดขึ้นถ้า iterator ไม่เคยถูก consume:
fn main() { let v = vec![1, 2, 3]; v.iter().map(|x| { println!("mapping {}", x); x * 2 }); println!("done");}การ compile โค้ดนี้ (โดยไม่ปิด warning) จะพิมพ์ warning ออกมา และการรันมันจะพิมพ์แค่ "done" — ไม่มีบรรทัด "mapping" ทำงานเลยแม้แต่บรรทัดเดียว:
$ cargo buildwarning: unused `Map` that must be used --> src/main.rs:3:5 |3 | / v.iter().map(|x| {4 | | println!("mapping {}", x);5 | | x * 26 | | }); | |______^ | = note: iterators are lazy and do nothing unless consumed = note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by defaultคอมไพเลอร์ไม่ได้แค่เตือนเรื่องค่าที่ถูกทิ้งไป — note ที่มันแนบมา “iterators are lazy and do nothing unless consumed” คือแบบจำลองความคิดเป๊ะๆ ที่ section นี้กำลังพิสูจน์อยู่ adaptor chain ที่ไม่ถูก consume ไม่ใช่แค่ถูกทิ้งไปเฉยๆ มันคือ no-op และไลบรารีมาตรฐานทำเครื่องหมาย method ที่คืน iterator ด้วย #[must_use] โดยเฉพาะเพื่อจับสิ่งนี้ทันทีที่คุณเขียนมัน
flowchart LR
S[Source: v.iter] --> F[filter: keep even] --> M[map: multiply by 10] --> C{Consumer?}
C -->|collect / sum / for loop| R[Values flow through, work happens now]
C -->|nothing — chain dropped| N[No work ever happens] collect() — Turbofish, การ Short-circuit ของ Result, และ String
หัวข้อที่มีชื่อว่า “collect() — Turbofish, การ Short-circuit ของ Result, และ String”.collect() เป็น generic เหนือ target type ของมัน ดังนั้นคอมไพเลอร์ต้องรู้ว่าคุณกำลัง collect เข้าอะไร — ไม่ว่าจะจาก context รอบๆ (let v: Vec<i32> = ...) หรือจาก turbofish ที่ชัดเจน (::<Type>):
struct Countdown(u32);
impl Iterator for Countdown { type Item = u32; fn next(&mut self) -> Option<u32> { if self.0 == 0 { None } else { self.0 -= 1; Some(self.0 + 1) } }}
fn evens_up_to(n: u32) -> impl Iterator<Item = u32> { (1..=n).filter(|x| x % 2 == 0)}
fn main() { let collected: Vec<u32> = Countdown(3).collect(); println!("custom Iterator: {:?}", collected);
let evens: Vec<u32> = evens_up_to(10).collect(); println!("impl Iterator return: {:?}", evens);
// collect::<T>() turbofish let doubled = (1..5).map(|x| x * 2).collect::<Vec<i32>>(); println!("turbofish collect: {:?}", doubled);
// collect::<Result<Vec<_>, _>>() short-circuits on the first Err let good = vec!["1", "2", "3"]; let bad = vec!["1", "oops", "3"]; let ok_result: Result<Vec<i32>, _> = good.iter().map(|s| s.parse::<i32>()).collect(); let err_result: Result<Vec<i32>, _> = bad.iter().map(|s| s.parse::<i32>()).collect(); println!("collect Result ok: {:?}", ok_result); println!("collect Result err: {:?}", err_result);
// collect::<String>() let word: String = vec!['R', 'u', 's', 't'].into_iter().collect(); println!("collect String: {}", word);}custom Iterator: [3, 2, 1]impl Iterator return: [2, 4, 6, 8, 10]turbofish collect: [2, 4, 6, 8]collect Result ok: Ok([1, 2, 3])collect Result err: Err(ParseIntError { kind: InvalidDigit })collect String: Rustมีสองอย่างที่ควรดึงออกมาจากการรันนั้น: Countdown คือ struct ที่เขียนเองพร้อม impl Iterator for Countdown — .next() ที่คืน Option<Self::Item> คือสัญญาทั้งหมด และ .collect(), .sum(), adaptor ทุกตัวในบทนี้ทำงานกับมันได้ฟรีทันทีที่ method ตัวนั้นมีอยู่ evens_up_to คืนค่า impl Iterator<Item = u32> แทนที่จะเอ่ยชื่อ adaptor type ที่เป็นรูปธรรม (Filter<RangeInclusive<u32>, ...> ที่คุณต้องสะกดออกมาถ้าไม่ใช้แบบนี้) — ผู้เรียกแค่ต้องรู้ว่า “สิ่งนี้ให้ค่า u32” ไม่ต้องรู้ว่า adaptor ตัวไหนสร้างมัน และ collect::<Result<Vec<i32>, _>>() บน bad หยุดที่ความล้มเหลวในการ parse ครั้งแรกและคืน Err ตัวนั้นตัวเดียว — มันไม่ collect ความสำเร็จก่อนหน้ามันและไม่เดินหน้าต่อเพื่อหา error เพิ่ม
sum() ต้องรู้ Type ของตัวเอง
หัวข้อที่มีชื่อว่า “sum() ต้องรู้ Type ของตัวเอง”.sum() เป็น generic เหนือ return type ของมันเหมือนกับ .collect() — และไม่เหมือน .collect() ตรงที่ไม่มี pattern แบบ let x: Vec<i32> = ... ให้คอมไพเลอร์พึ่งพา เว้นแต่คุณจะเขียน type จริงๆ:
// @expect-error E0283fn main() { let nums = vec![1, 2, 3, 4]; let total = nums.iter().sum(); println!("{}", total);}error[E0283]: type annotations needed --> src/main.rs:3:9 | 3 | let total = nums.iter().sum(); | ^^^^^ --- type must be known at this point | = note: the type must implement `Sum<&i32>` = help: the following types implement trait `Sum<A>`: `Duration` implements `Sum<&'a Duration>` `Duration` implements `Sum` `Option<T>` implements `Sum<Option<U>>` `Result<T, E>` implements `Sum<Result<U, E>>` `Saturating<u128>` implements `Sum<&'a Saturating<u128>>` `Saturating<u128>` implements `Sum` `Saturating<u16>` implements `Sum<&'a Saturating<u16>>` `Saturating<u16>` implements `Sum` and 88 othershelp: consider giving `total` an explicit type | 3 | let total: /* Type */ = nums.iter().sum(); | ++++++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0283`.หลาย type implement Sum (i32, f64, Duration, แม้แต่ Option<T> และ Result<T, E> สำหรับผลรวมแบบ short-circuit) ดังนั้นคอมไพเลอร์จึงเดาไม่ได้จริงๆ ว่าคุณหมายถึงตัวไหน — วิธีแก้คือให้ type ที่ let อย่างชัดเจน หรือใช้ turbofish แบบเดียวกับที่ collect() ใช้:
fn main() { let nums = vec![1, 2, 3, 4]; let total: i32 = nums.iter().sum(); println!("total = {}", total); let total2 = nums.iter().sum::<i32>(); println!("total2 = {}", total2);}total = 10total2 = 10ทัวร์ Adaptor แบบเต็ม
หัวข้อที่มีชื่อว่า “ทัวร์ Adaptor แบบเต็ม”ทุก adaptor ข้างล่างนี้ lazy ทั้งหมด — ไม่มีตัวไหนทำงานอะไรเลยจนกว่า consumer (ที่นี่คือ collect() หรือ loop for) จะขับเคลื่อนมัน:
fn main() { let names = vec!["ann", "bo", "carl", "dee", "eve"]; let scores = vec![90, 85, 70, 95];
// zip stops at the shorter iterator let zipped: Vec<(&&str, &i32)> = names.iter().zip(scores.iter()).collect(); println!("zip: {:?}", zipped);
// enumerate pairs index with element for (i, n) in names.iter().enumerate().take(2) { println!("enumerate[{}] = {}", i, n); }
// filter_map: parse and drop failures in one step let words = vec!["1", "two", "3", "four", "5"]; let parsed: Vec<i32> = words.iter().filter_map(|s| s.parse().ok()).collect(); println!("filter_map: {:?}", parsed);
// flat_map: flatten a transformation that yields iterators let nested = vec![vec![1, 2], vec![3, 4], vec![5]]; let flat: Vec<i32> = nested.iter().flat_map(|v| v.iter().copied()).collect(); println!("flat_map: {:?}", flat);
// take_while / skip_while let nums = vec![1, 2, 3, 10, 4, 5]; let taken: Vec<&i32> = nums.iter().take_while(|&&x| x < 5).collect(); let skipped: Vec<&i32> = nums.iter().skip_while(|&&x| x < 5).collect(); println!("take_while: {:?}", taken); println!("skip_while: {:?}", skipped);
// windows / chunks (slices only) let data = [1, 2, 3, 4, 5]; let windows: Vec<&[i32]> = data.windows(2).collect(); let chunks: Vec<&[i32]> = data.chunks(2).collect(); println!("windows(2): {:?}", windows); println!("chunks(2): {:?}", chunks);
// rev let reversed: Vec<&i32> = nums.iter().rev().collect(); println!("rev: {:?}", reversed);
// peekable let mut p = nums.iter().peekable(); while let Some(&x) = p.next() { if let Some(&&next) = p.peek() { print!("{}->{} ", x, next); } } println!();
// scan: running sum let running: Vec<i32> = nums.iter().scan(0, |acc, &x| { *acc += x; Some(*acc) }).collect(); println!("scan running sum: {:?}", running);
// fold let product = nums.iter().fold(1, |acc, &x| acc * x); println!("fold product: {}", product);
// any / all / position / find println!("any > 9: {}", nums.iter().any(|&x| x > 9)); println!("all > 0: {}", nums.iter().all(|&x| x > 0)); println!("position of 10: {:?}", nums.iter().position(|&x| x == 10)); println!("find > 3: {:?}", nums.iter().find(|&&x| x > 3));}zip: [("ann", 90), ("bo", 85), ("carl", 70), ("dee", 95)]enumerate[0] = annenumerate[1] = bofilter_map: [1, 3, 5]flat_map: [1, 2, 3, 4, 5]take_while: [1, 2, 3]skip_while: [10, 4, 5]windows(2): [[1, 2], [2, 3], [3, 4], [4, 5]]chunks(2): [[1, 2], [3, 4], [5]]rev: [5, 4, 10, 3, 2, 1]1->2 2->3 3->10 10->4 4->5scan running sum: [1, 3, 6, 16, 20, 25]fold product: 1200any > 9: trueall > 0: trueposition of 10: Some(3)find > 3: Some(10)zip ตัดให้เท่ากับ iterator ที่สั้นกว่าในสองตัว (5 ชื่อ แต่มีแค่ 4 คะแนน ดังนั้น zipped จึงมี 4 คู่) windows(2) ให้ทุกคู่ที่ overlap กัน; chunks(2) ให้ทุกคู่ที่ ไม่ overlap กัน โดยมี chunk สุดท้ายที่สั้นกว่าถ้าความยาวเป็นเลขคี่ any/position/find ล้วน short-circuit — มันหยุดที่การ match ครั้งแรกแทนที่จะสแกนไปจนจบ ซึ่งสำคัญเมื่อ iterator มีต้นทุนสูงหรือไม่มีที่สิ้นสุด
fn main() { let numbers = vec![1, 2, 3, 4, 5, 6];
// filter + map + collect let evens_doubled: Vec<i32> = numbers .iter() .filter(|&&x| x % 2 == 0) .map(|&x| x * 2) .collect(); println!("evens doubled: {:?}", evens_doubled);
// enumerate for (i, val) in numbers.iter().enumerate() { println!("[{}] = {}", i, val); }
// zip two iterators let letters = vec!["a", "b", "c"]; for (n, l) in numbers.iter().take(3).zip(letters.iter()) { println!("{} -> {}", n, l); }
// consumers: sum and fold let total: i32 = numbers.iter().sum(); let product: i32 = numbers.iter().fold(1, |acc, &x| acc * x); println!("sum={} product={}", total, product);}Compiling…
แบบจำลองความคิด
หัวข้อที่มีชื่อว่า “แบบจำลองความคิด”iterator คือ method เดียว — fn next(&mut self) -> Option<Self::Item> — และไม่มีอะไรมากกว่านั้น; adaptor ทุกตัว (map, filter, zip, …) เป็น wrapper struct ที่เก็บ iterator ข้างในบวกกับสิ่งที่มันต้องการ (closure, iterator ตัวที่สอง) และ implement next() โดยเรียก next() ของ iterator ข้างในแล้วแปลงผลลัพธ์ นั่นคือเหตุผลทั้งหมดที่ทำให้ความ lazy เป็นแบบอัตโนมัติแทนที่จะเป็นสิ่งที่คุณต้องเลือกใช้: ไม่มีงานเกิดขึ้นจนกว่า บางสิ่ง จะเรียก .next() และสิ่งเดียวที่เรียก .next() เลยคือ consumer
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”.iter()/.iter_mut()/.into_iter() แลกความยืดหยุ่นกับข้อจำกัดของ borrow checker: การ borrow ทำให้ source ใช้งานต่อได้แต่ห้ามการแก้ไขเชิงโครงสร้าง (รวมถึงการโต) ระหว่าง loop; การย้ายปลดล็อกคุณจากข้อจำกัดนั้นแลกกับการ consume source การใส่ type annotation ชัดเจนบน collect()/sum() เสียตัวอักษรเพิ่มไม่กี่ตัวแต่บันทึกเจตนาไว้ที่จุดเรียก; การพึ่ง inference ที่อื่นในฟังก์ชันกระชับกว่าแต่พังทันทีที่ context การ infer นั้นหายไป การ implement Iterator เองมีต้นทุนคือคุณต้องเขียน method next() หนึ่งตัว แต่แลกกับ adaptor และ consumer ทุกตัวในบทนี้ (และไลบรารีมาตรฐานทั้งหมด) ทำงานกับ type ของคุณได้ฟรี
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “การเรียก
.map()รัน closure บนทุก element ทันที เหมือนกับ loopfor” มันสร้าง wrapper แบบ lazy และไม่รันอะไรเลย — การพิสูจน์ความ lazy ข้างบนแสดงให้เห็นว่าprintln!ของ closure เองทำงานก็ต่อเมื่อ.sum()(consumer) ขับเคลื่อน chain เท่านั้น ไม่ใช่ตอนที่เรียก.map() - “
v.iter().map(...)โดยไม่มี.collect()ต่อท้าย ก็ยังทำการแปลงอยู่ แค่ไม่เก็บผลลัพธ์” มันไม่ทำอะไรเลย — ไม่ใช่ “งานเกิดขึ้นแต่ผลลัพธ์ถูกทิ้งไป” แต่คือ “งานไม่เคยเกิดขึ้นเลย” ซึ่งตรงกับที่ noteunused_must_useของคอมไพเลอร์ (“iterators are lazy and do nothing unless consumed”) กำลังบอกคุณพอดี - “
sum()ควร compile ได้เสมอโดยไม่ต้องมี annotation เพราะ element ถูกกำหนด type ไว้แล้ว (i32)”sum()เป็น generic เหนือ type ที่คืนกลับ และหลาย type implementSum<i32>(หรือSum<&i32>) — คอมไพเลอร์ต้องการให้คุณเลือกตัวหนึ่ง เหมือนกับที่มันต้องการให้คุณเลือก target ของcollect()
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”โค้ดที่ AI สร้างบางครั้ง collect iterator เข้า Vec แค่เพื่อจะวนซ้ำมันอีกครั้งทันทีหลังจากนั้น — ขั้นตอนที่เพิ่มการ allocate โดยไม่ได้อะไรเลย เพราะ iterator ต้นฉบับ (หรือตัวใหม่) วนซ้ำได้โดยตรงอยู่แล้ว
🐛 หา bug ในโค้ดนี้
fn print_evens(nums: &[i32]) { let evens: Vec<i32> = nums.iter().filter(|&&x| x % 2 == 0).copied().collect(); for e in evens { println!("{}", e); }}เฉลย
evens ถูกสร้างขึ้น แล้วถูก consume ทันทีด้วย loop for เดียว และไม่ถูกใช้อีกเลย — Vec ตรงกลางไม่ได้ให้อะไรนอกจากการ allocate และการ copy เลขคู่ทุกตัว วิธีแก้คือตัด collect() ทิ้งไปเลย: for e in nums.iter().filter(|&&x| x % 2 == 0) { println!("{}", e); } วนซ้ำ adaptor chain ที่ filter แล้วโดยตรง เพราะ for เองก็เป็น consumer ที่ดีอยู่แล้ว