Collections และ Iterators
โมดูลนี้ครอบคลุมอะไรบ้าง
หัวข้อที่มีชื่อว่า “โมดูลนี้ครอบคลุมอะไรบ้าง”Rust มาพร้อมชุด collection ของไลบรารีมาตรฐานที่ทรงพลัง และระบบ iterator ที่ lazy และไม่มีต้นทุนเพิ่ม (zero-cost) เมื่อรวมกันแล้วมันให้คุณเก็บ แปลง และบริโภคข้อมูลได้โดยไม่ต้องเขียน loop มือหรือ allocate โดยไม่จำเป็น
โมดูลนี้พาไปทีละบทใน 5 บท:
| บท | หัวข้อ |
|---|---|
Vec<T> & String | ลำดับที่ขยายได้และข้อความที่เป็นเจ้าของ |
HashMap<K, V> | การค้นหาแบบ key-value ด้วย entry API |
| Iterators | chain แบบ lazy: map, filter, collect, fold |
| Closures | การ capture environment; Fn / FnMut / FnOnce |
Collection หลักทั้งสี่
หัวข้อที่มีชื่อว่า “Collection หลักทั้งสี่”collection ที่ใช้บ่อยที่สุดของ Rust และเวลาที่ควรเลือกใช้แต่ละตัว:
Vec<T>— array บน heap ที่เรียงต่อกันและมีลำดับ ใช้เมื่อต้องการเข้าถึงด้วย index หรือ list ที่ขยายได้String— ข้อความ UTF-8 ที่เป็นเจ้าของและขยายได้ ใช้เมื่อต้องสร้างหรือแก้ไขข้อความตอน runtimeHashMap<K, V>— hash table ที่แม็ป key ไปยัง value ใช้สำหรับการค้นหาด้วย key ที่รวดเร็วHashSet<T>— เซตของค่าที่ไม่ซ้ำกัน ใช้เมื่อต้องการแค่ตรวจสอบสมาชิกภาพ
ระบบ Iterator
หัวข้อที่มีชื่อว่า “ระบบ Iterator”ทุก collection ของ Rust เปิดให้ใช้ iterator ได้ Iterator นั้น lazy — ไม่มีงานใดเกิดขึ้นจนกว่า consumer (เช่น collect, sum, หรือ for) จะดึงค่าผ่าน chain นั้น
let v = vec![1, 2, 3, 4, 5];
// Nothing executes yet — adaptor chain is just a descriptionlet chain = v.iter().filter(|&&x| x % 2 == 0).map(|&x| x * 10);
// Consumer drives the chainlet result: Vec<i32> = chain.collect();use std::collections::HashMap;use std::collections::HashSet;
fn main() { // Vec<i32> let mut v: Vec<i32> = vec![1, 2, 3]; v.push(4); println!("Vec: {:?}", v);
// String let mut s = String::from("Hello"); s.push_str(", Rust!"); println!("String: {}", s);
// HashMap<&str, i32> — sort keys for deterministic output let mut map = HashMap::new(); map.insert("one", 1); map.insert("two", 2); map.insert("three", 3); let mut keys: Vec<&str> = map.keys().cloned().collect(); keys.sort(); for k in &keys { println!("{}: {}", k, map[k]); }
// HashSet<i32> let set: HashSet<i32> = vec![1, 2, 3, 2, 1].into_iter().collect(); let mut items: Vec<i32> = set.into_iter().collect(); items.sort(); println!("Set: {:?}", items);}Compiling…
คอมไพเลอร์พูดว่าอะไร: Collection ไม่แปลงชนิดตัวเลขให้อัตโนมัติ
หัวข้อที่มีชื่อว่า “คอมไพเลอร์พูดว่าอะไร: Collection ไม่แปลงชนิดตัวเลขให้อัตโนมัติ”collection เป็น generic เหนือ element type เพียงชนิดเดียวเท่านั้น — Vec<i32> เก็บได้แค่ i32 เท่านั้น แม้แต่ชนิดที่ “ควร” แปลงได้ก็ไม่มีข้อยกเว้น Rust ไม่เคยแทรกการแปลงตัวเลขให้อัตโนมัติ ไม่ว่าจะใน collection หรือที่ไหนก็ตาม:
// @expect-error E0308fn main() { let mut v: Vec<i32> = vec![1, 2, 3]; v.push(3.0); println!("{:?}", v);}error[E0308]: mismatched types --> src/main.rs:3:12 | 3 | v.push(3.0); | ---- ^^^ expected `i32`, found floating-point number | | | arguments to this method are incorrect
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.วิธีแก้เป็นแบบชัดเจนเสมอ: เลือกชนิดที่คุณต้องการ (v.push(3) สำหรับ i32 หรือประกาศ Vec<f64> ถ้า collection ทั้งหมดควรเก็บ float) แล้วแปลงตรงจุดที่ต่อกันด้วย as หรือ From/Into ไม่ใช่หวังว่าคอมไพเลอร์จะเดาให้ อีกสี่บทที่ตามมาจะแสดงรูปแบบที่เฉพาะเจาะจงกว่านี้มาก — key ของ HashMap ที่เป็น float ไม่ได้, การเรียก sum() ที่คอมไพเลอร์เดา type เองไม่ได้ — นี่คือรูปแบบทั่วไปที่ทุกกรณีมีร่วมกัน: Rust ขอให้ คุณ ระบุ type ให้ชัดเจนแทนที่จะเลือกให้อย่างเงียบๆ
เลือก Collection ให้เหมาะ
หัวข้อที่มีชื่อว่า “เลือก Collection ให้เหมาะ”flowchart TD
A[Need to store values?] --> B{Look up by key?}
B -->|Yes| C{Need sorted iteration order?}
C -->|Yes| D[BTreeMap / BTreeSet]
C -->|No, speed matters more| E[HashMap / HashSet]
B -->|No, just a sequence| F{Growable, indexed list?}
F -->|Yes| G[Vec]
F -->|Owned, mutable text| H[String] แผนภาพนี้เป็นจุดเริ่มต้น ไม่ใช่กฎตายตัว: BTreeMap ก็เป็นตัวเลือกที่ถูกต้องเช่นกันเมื่อคุณต้องการ query แบบ “ขอ key ในช่วงหนึ่ง” ที่คล้าย Vec ซึ่ง HashMap ทำไม่ได้เลย ไม่ว่าความเร็วจะเป็นอย่างไร
แบบจำลองความคิด
หัวข้อที่มีชื่อว่า “แบบจำลองความคิด”ให้มองทุก collection เป็น ownership บวกกับรูปทรง: Vec<T> เป็นเจ้าของช่วงต่อเนื่องของ T, String เป็นเจ้าของช่วงต่อเนื่องของ byte แบบ UTF-8, HashMap<K, V> เป็นเจ้าของ hash table ของคู่ K ไปยัง V ระบบ iterator เป็นแนวคิดที่สอง แยกออกจากกันและวางซ้อนอยู่ด้านบน: มันคือวิธีเดินผ่านรูปทรงเหล่านั้นแบบสม่ำเสมอ ทีละสมาชิก แบบ lazy โดยที่โค้ดที่เดินไม่จำเป็นต้องรู้ว่ากำลังเดินผ่านรูปทรงไหน เมื่อคุณเห็น collection และ iterator เป็นสองเรื่องที่แยกกัน — “ข้อมูลถูกเก็บอย่างไร” กับ “ฉันประมวลผลมันอย่างไร” — ที่เหลือของโมดูลนี้ก็แค่รายละเอียดปลีกย่อยของสองคำถามนี้
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”Vec<T> ให้พื้นที่เก็บข้อมูลต่อเนื่องที่เป็นมิตรกับ cache และเข้าถึงด้วย index แบบ O(1) แลกกับการแทรก/ลบตรงกลางที่เป็น O(n) HashMap<K, V> ให้การค้นหาด้วย key แบบ O(1) โดยเฉลี่ย แลกกับการไม่มีการรับประกันลำดับและ overhead ของ seed สุ่มต่อ map หนึ่งตัว BTreeMap<K, V> ให้การวนซ้ำแบบเรียงลำดับและ query ตามช่วง แลกกับการดำเนินการ O(log n) แทนที่จะเป็น O(1) โดยเฉลี่ยแบบ HashMap ไม่มีตัวไหนดีกว่าเสมอไป — ตัวเลือกที่ถูกต้องขึ้นอยู่กับว่าโค้ดของคุณทำการดำเนินการไหน (“วนซ้ำตามลำดับ”, “ค้นหาด้วย key”, “เข้าถึงด้วยตำแหน่ง”) บ่อยที่สุด
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “Collection ของ Rust แปลง element type ให้อัตโนมัติเพื่อความสะดวก เหมือนภาษาที่เป็น dynamically-typed บางภาษา” ไม่เคยเป็นแบบนั้นเลย —
Vec<i32>รับได้แค่i32เท่านั้น จบ ทุกการแปลงคือ call ที่คุณต้องเขียนเอง - “Iterator เป็นแค่ syntax ที่สวยกว่าของ loop
forไม่มีความแตกต่างเชิงฟังก์ชัน” มันคอมไพล์ออกมาเป็นโค้ดเดียวกับ loop ที่เขียนมือ แต่มันก็เป็น ค่า (value) ด้วย — คุณสร้าง chain ส่งต่อไปได้ แล้วค่อยตัดสินใจทีหลัง (ด้วยcollect,sum, หรือ loopfor) ว่าจะบริโภคมันยังไง loopforเป็นแค่ consumer หนึ่งตัวในหลายๆ ตัว - “
HashSet<T>ก็แค่HashMap<T, ()>ที่ไม่ต้องคิดอะไรเพิ่ม” จริงในแง่รายละเอียดการ implement แต่มันก็ยังต้องการT: Eq + Hashซึ่งเป็นข้อกำหนดเดียวกันเป๊ะๆ กับที่ทำให้คนสับสนกับ key ของHashMap— บทhashmapครอบคลุมเรื่องนี้อย่างลึกซึ้ง
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”AI มักจะเลือก ownership ที่ “กว้างที่สุด” ที่ฟังก์ชันจะรับได้ — Vec<String> — แม้ว่าฟังก์ชันจะแค่อ่านข้อมูล ไม่เคยเพิ่มหรือแก้ไขมันเลย นั่นบังคับให้ผู้เรียกทุกคนต้องย้าย Vec<String> ที่เป็นเจ้าของเข้ามา หรือไม่ก็ต้องเสียค่า clone ทั้งก้อนแค่เพื่อให้ตรงกับ signature
🐛 หา bug ในโค้ดนี้
fn print_report(lines: Vec<String>) { for line in &lines { println!("{}", line); }}เฉลย
print_report ไม่เคย push เข้า lines ไม่เคยลบออกจากมัน ไม่เคยต้องเป็นเจ้าของมันเลย — มันแค่วนซ้ำผ่าน reference เท่านั้น การรับ Vec<String> แบบ by value บังคับให้ผู้เรียกทุกคนต้องสละ ownership (หรือ .clone() ทั้ง Vec<String> แค่เพื่อเก็บสำเนาของตัวเองไว้) วิธีแก้คือ borrow แทน: fn print_report(lines: &[String]) (หรือ &[&str] ถ้าผู้เรียกมี string slice อยู่แล้ว) รับมุมมองแบบ borrow โดยไม่ต้องเรียกร้อง ownership เลย และยังใช้กับ &some_vec ได้ในทุกจุดที่เรียก