ข้ามไปยังเนื้อหา

Collections และ Iterators

Rust มาพร้อมชุด collection ของไลบรารีมาตรฐานที่ทรงพลัง และระบบ iterator ที่ lazy และไม่มีต้นทุนเพิ่ม (zero-cost) เมื่อรวมกันแล้วมันให้คุณเก็บ แปลง และบริโภคข้อมูลได้โดยไม่ต้องเขียน loop มือหรือ allocate โดยไม่จำเป็น

โมดูลนี้พาไปทีละบทใน 5 บท:

บทหัวข้อ
Vec<T> & Stringลำดับที่ขยายได้และข้อความที่เป็นเจ้าของ
HashMap<K, V>การค้นหาแบบ key-value ด้วย entry API
Iteratorschain แบบ lazy: map, filter, collect, fold
Closuresการ capture environment; Fn / FnMut / FnOnce

collection ที่ใช้บ่อยที่สุดของ Rust และเวลาที่ควรเลือกใช้แต่ละตัว:

  • Vec<T> — array บน heap ที่เรียงต่อกันและมีลำดับ ใช้เมื่อต้องการเข้าถึงด้วย index หรือ list ที่ขยายได้
  • String — ข้อความ UTF-8 ที่เป็นเจ้าของและขยายได้ ใช้เมื่อต้องสร้างหรือแก้ไขข้อความตอน runtime
  • HashMap<K, V> — hash table ที่แม็ป key ไปยัง value ใช้สำหรับการค้นหาด้วย key ที่รวดเร็ว
  • HashSet<T> — เซตของค่าที่ไม่ซ้ำกัน ใช้เมื่อต้องการแค่ตรวจสอบสมาชิกภาพ

ทุก 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 description
let chain = v.iter().filter(|&&x| x % 2 == 0).map(|&x| x * 10);
// Consumer drives the chain
let 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);
}

คอมไพเลอร์พูดว่าอะไร: Collection ไม่แปลงชนิดตัวเลขให้อัตโนมัติ

หัวข้อที่มีชื่อว่า “คอมไพเลอร์พูดว่าอะไร: Collection ไม่แปลงชนิดตัวเลขให้อัตโนมัติ”

collection เป็น generic เหนือ element type เพียงชนิดเดียวเท่านั้น — Vec<i32> เก็บได้แค่ i32 เท่านั้น แม้แต่ชนิดที่ “ควร” แปลงได้ก็ไม่มีข้อยกเว้น Rust ไม่เคยแทรกการแปลงตัวเลขให้อัตโนมัติ ไม่ว่าจะใน collection หรือที่ไหนก็ตาม:

// @expect-error E0308
fn 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 ให้ชัดเจนแทนที่จะเลือกให้อย่างเงียบๆ

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]
การเลือก collection มาตรฐาน

แผนภาพนี้เป็นจุดเริ่มต้น ไม่ใช่กฎตายตัว: 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, หรือ loop for) ว่าจะบริโภคมันยังไง loop for เป็นแค่ consumer หนึ่งตัวในหลายๆ ตัว
  • “HashSet<T> ก็แค่ HashMap<T, ()> ที่ไม่ต้องคิดอะไรเพิ่ม” จริงในแง่รายละเอียดการ implement แต่มันก็ยังต้องการ T: Eq + Hash ซึ่งเป็นข้อกำหนดเดียวกันเป๊ะๆ กับที่ทำให้คนสับสนกับ key ของ HashMap — บท hashmap ครอบคลุมเรื่องนี้อย่างลึกซึ้ง

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 ได้ในทุกจุดที่เรียก

คุณจะเลือก collection ไหนสำหรับการค้นหาที่รวดเร็วด้วย key แบบ String?
iterator adaptor (เช่น map หรือ filter) จะรันงานจริงเมื่อไร?
วิธีที่ idiomatic ในการวนซ้ำ HashMap เพื่อให้ได้ลำดับ output ที่แน่นอนคืออะไร?
โปรแกรมนี้ทำอะไร?
fn main() {
    let mut v: Vec<i32> = vec![1, 2, 3];
    v.push(3.0);
}
BTreeMap เพิ่มข้อกำหนดอะไรเหนือ HashMap เพื่อแลกกับการวนซ้ำแบบเรียงลำดับ?
ทำไม fn print_report(lines: Vec<String>) มักจะเป็นกลิ่นโค้ดที่ไม่ดีเมื่อฟังก์ชันแค่อ่าน lines?