Vec และ String
Vec<T> — Array บน Heap ที่ขยายได้
หัวข้อที่มีชื่อว่า “Vec<T> — Array บน Heap ที่ขยายได้”Vec<T> คือ collection แบบ sequential หลักของ Rust มันเก็บ element ชนิด T แบบต่อเนื่องกันบน heap และขยายขนาดเองอัตโนมัติเมื่อคุณ push เกิน capacity ปัจจุบันของมัน
let mut v: Vec<i32> = Vec::new(); // empty Veclet w = vec![1, 2, 3]; // vec! macro shorthandPush, Pop และ Index
หัวข้อที่มีชื่อว่า “Push, Pop และ Index”let mut v = vec![10, 20, 30];
v.push(40); // append an elementlet last = v.pop(); // remove and return the last element → Some(40)let first = v[0]; // index — panics if out of boundslet safe = v.get(100); // returns Option<&T> — None if out of boundsให้ใช้ .get(i) เมื่อ index อาจอยู่นอกช่วง เพื่อจัดการกรณี None แทนที่จะปล่อยให้ panic
Capacity และการขยายขนาด วัดจริง
หัวข้อที่มีชื่อว่า “Capacity และการขยายขนาด วัดจริง”Vec<T> ติดตามตัวเลขสองตัวแยกกัน: len() (จำนวน element ที่มีอยู่ตอนนี้) และ capacity() (จำนวน element ที่มันเก็บได้ก่อนต้อง allocate buffer ที่ใหญ่ขึ้น) Vec::with_capacity(n) จอง buffer สำหรับ n element ไว้ล่วงหน้า — มีประโยชน์เมื่อคุณรู้คร่าวๆ ว่า Vec จะโตขนาดไหนและต้องการหลีกเลี่ยงการ reallocate ซ้ำๆ
flowchart LR A[push exceeds capacity] --> B[allocate a new, larger buffer] B --> C[copy every existing element into it] C --> D[free the old buffer] D --> E[push the new element]
fn main() { let mut v: Vec<i32> = Vec::with_capacity(2); println!("start: len={} cap={}", v.len(), v.capacity()); for i in 0..6 { v.push(i); println!("after push {}: len={} cap={}", i, v.len(), v.capacity()); } v.shrink_to_fit(); println!("after shrink_to_fit: len={} cap={}", v.len(), v.capacity());}นี่คือการรันจริงที่ verify แล้ว — ไม่ใช่สัญญาของ API ที่เอกสารระบุไว้ (ไลบรารีมาตรฐานไม่ได้สัญญา growth factor ที่แน่นอน) แต่เป็นพฤติกรรมจริงของ toolchain นี้:
start: len=0 cap=2after push 0: len=1 cap=2after push 1: len=2 cap=2after push 2: len=3 cap=4after push 3: len=4 cap=4after push 4: len=5 cap=8after push 5: len=6 cap=8after shrink_to_fit: len=6 cap=6capacity จะเพิ่มเป็นสองเท่าทุกครั้งที่ len() กำลังจะเกินมัน — 2 → 4 → 8 — ดังนั้นการ push element หนึ่งตัวที่เกิน capacity จึงเป็น O(1) แบบ amortized: การ push ส่วนใหญ่ไม่มีต้นทุนเพิ่ม และการ reallocate-แล้ว-copy ที่เกิดเป็นครั้งคราวก็ถูกกระจายออกไปตลอดการ push ทั้งหมดที่นำไปสู่มัน shrink_to_fit() ปล่อยพื้นที่ที่เหลือไม่ได้ใช้ (cap 8 → 6) เมื่อคุณรู้ว่า Vec จะไม่โตอีก
วนซ้ำ Vec: แบบ Value, แบบ Reference, แบบ Mutable Reference
หัวข้อที่มีชื่อว่า “วนซ้ำ Vec: แบบ Value, แบบ Reference, แบบ Mutable Reference”loop body เดียวกันอ่านได้ต่างกันมากขึ้นอยู่กับว่าคุณวนซ้ำอะไร:
fn main() { let v = vec![String::from("a"), String::from("b"), String::from("c")];
// by shared reference (&v): borrows, v still valid after for s in &v { print!("{} ", s); } println!("| still valid, len={}", v.len());
// by mutable reference (&mut v): borrows mutably, mutate elements in place let mut nums = vec![1, 2, 3]; for n in &mut nums { *n *= 10; } println!("{:?}", nums);
// by value (v): moves the Vec, v cannot be used after this loop for s in v { print!("{} ", s); } println!("(v moved)");}a b c | still valid, len=3[10, 20, 30]a b c (v moved)for s in &v ยืม (borrow) แต่ละ element เป็น &T; Vec ไม่ถูกแตะต้องและใช้งานต่อได้ for n in &mut v ยืมแต่ละ element เป็น &mut T ดังนั้น *n *= 10 จึงแก้ไข Vec ต้นฉบับได้ในที่ for s in v เรียก v.into_iter() โดยปริยาย ซึ่งเอา ownership ของ Vec ไปและคืนค่า T ที่เป็นเจ้าของ — หลัง loop นี้ v ไม่มีอยู่แล้ว ให้ใช้ &v เป็นค่าเริ่มต้น ใช้ &mut v เฉพาะเมื่อต้องแก้ไข element ในที่ และใช้ v เฉพาะเมื่อคุณใช้ Vec เสร็จแล้วและต้องการย้าย element ออกมา
String — ข้อความที่เป็นเจ้าของและขยายได้
หัวข้อที่มีชื่อว่า “String — ข้อความที่เป็นเจ้าของและขยายได้”String คือ text type ที่ mutable และ allocate บน heap ของ Rust มันเป็นเจ้าของ byte ของตัวเองและจัดการ deallocation
let mut s = String::from("hello");s.push(' '); // push a single chars.push_str("world"); // append a string slicelet combined = s + " again"; // + moves s and appendsString กับ &str
หัวข้อที่มีชื่อว่า “String กับ &str”String | &str | |
|---|---|---|
| Ownership | เป็นเจ้าของ | Slice ที่ยืมมา |
| ตำแหน่ง | Heap | Stack / static / อยู่ใน String |
| Mutable | ใช่ (ด้วย mut) | ไม่ |
| ใช้ทั่วไปสำหรับ | สร้าง/แก้ไขข้อความ | อ่านข้อความ |
&String coerce เป็น &str โดยอัตโนมัติ ดังนั้นให้ใช้ &str เป็น function parameter เพื่อความยืดหยุ่นสูงสุด
fn shout(s: &str) -> String { s.to_uppercase()}
let owned = String::from("hello");shout(&owned); // &String coerces to &strshout("world"); // string literal is already &strกลิ่นโค้ดเดียวกัน ต่างแค่ type เดียว: Vec<String> กับ &[String] / &[&str]
หัวข้อที่มีชื่อว่า “กลิ่นโค้ดเดียวกัน ต่างแค่ type เดียว: Vec<String> กับ &[String] / &[&str]”การตัดสินใจ String เทียบ &str ข้างต้นมี “ญาติ” ที่ตรงกันอยู่หนึ่งชั้นเหนือ type: function parameter ที่เป็น Vec<String> บังคับให้ผู้เรียกทุกคนต้องยก ownership ให้ (หรือ .clone() ทั้ง Vec) แม้ว่าฟังก์ชันจะแค่อ่าน element เท่านั้น &[String] (หรือ &[&str] ถ้าผู้เรียกมี string slice อยู่แล้ว) borrow แทน และยังรับ &some_vec ที่จุดเรียกได้เพราะ Vec<String> deref เป็น &[String]:
fn print_lines_bad(lines: Vec<String>) { for line in &lines { println!("{}", line); }}
fn print_lines_good(lines: &[String]) { for line in lines { println!("{}", line); }}
fn main() { let owned = vec![String::from("a"), String::from("b")]; print_lines_good(&owned); println!("owned still usable: {:?}", owned); print_lines_bad(owned); // must move — cannot reuse afterward}len() คือจำนวน Byte ไม่ใช่ตัวอักษร
หัวข้อที่มีชื่อว่า “len() คือจำนวน Byte ไม่ใช่ตัวอักษร”String เป็น UTF-8 ดังนั้น .len() จึงคืนจำนวน byte ไม่ใช่จำนวนตัวอักษร สำหรับข้อความ ASCII ทั้งสองค่าบังเอิญตรงกัน ซึ่งซ่อนความแตกต่างไว้จนกว่าคุณจะเจอข้อความหลาย byte — ตัวอักษรไทยแต่ละตัวข้างล่างนี้มี 3 byte:
fn main() { let s = "สวัสดี"; println!("len() = {}", s.len()); // bytes println!("chars().count() = {}", s.chars().count()); // characters}โค้ดนี้พิมพ์ len() = 18 และ chars().count() = 6 — 6 ตัวอักษร 18 byte
การ slice ตามช่วง byte (&s[start..end]) เข้าถึง byte buffer นั้นโดยตรง ถ้า start หรือ end ไม่ตรงกับขอบเขตตัวอักษร Rust จะ panic แทนที่จะยื่นตัวอักษรครึ่งเดียวให้คุณ:
// @expect-panicfn main() { let s = "สวัสดี"; let bad = &s[..1]; // byte index 1 lands inside the first character println!("{}", bad);}thread 'main' (<id>) panicked at src/main.rs:4:17:end byte index 1 is not a char boundary; it is inside 'ส' (bytes 0..3 of string)note: run with `RUST_BACKTRACE=1` environment variable to display a backtraceให้ใช้ตัวใดตัวหนึ่งข้างล่างนี้แทนการเดา byte offset:
fn main() { let s = "สวัสดี";
// char_indices() gives you (byte_offset, char) pairs — always on a boundary if let Some((offset, ch)) = s.char_indices().next() { println!("first char '{}' starts at byte {}", ch, offset); }
// chars().nth(n) gets the nth character without slicing bytes println!("2nd char = {:?}", s.chars().nth(1));
// is_char_boundary(i) lets you check before you slice println!("is_char_boundary(1) = {}", s.is_char_boundary(1)); println!("is_char_boundary(3) = {}", s.is_char_boundary(3));}Index Loop กับ Iterator
หัวข้อที่มีชื่อว่า “Index Loop กับ Iterator”index loop (for i in 0..v.len() { ... v[i] ... }) คือสิ่งที่ภาษาส่วนใหญ่ที่ไม่มี iterator protocol ระดับหนึ่งสอนให้คุณใช้ ใน Rust มันแทบจะเป็นค่าเริ่มต้นที่ผิดเสมอ: มันตรวจ bound ซ้ำทุกครั้งที่ v[i], บดบังเจตนา (“เดินผ่านทุก element” กลายเป็น “นับจาก 0 ถึง len แล้วค่อย index”) และ clippy ก็เรียกชื่อมันตรงๆ:
fn main() { let v = vec![10, 20, 30]; let mut total = 0; for i in 0..v.len() { total += v[i]; } println!("{}", total);}$ cargo clippywarning: the loop variable `i` is only used to index `v` --> src/main.rs:4:14 |4 | for i in 0..v.len() { | ^^^^^^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.96.0/index.html#needless_range_loop = note: `#[warn(clippy::needless_range_loop)]` on by defaulthelp: consider using an iterator |4 - for i in 0..v.len() {4 + for <item> in &v { |clippy::needless_range_loop จะแจ้งเตือนทุกครั้งที่ loop variable ถูกใช้เพื่อ index collection เดียวกันเท่านั้น — วิธีแก้เสมอคือ for item in &v (หรือ .iter().sum() ในกรณีนี้โดยเฉพาะ) ซึ่งคอมไพเลอร์ตรวจ bound ได้ครั้งเดียวแทนที่จะตรวจทุกครั้งที่เข้าถึง และอ่านได้ว่า “สำหรับทุก item” แทนที่จะเป็น “สำหรับทุก index เข้าไปใน Vec ตัวนี้”
fn main() { // Vec<i32> basics let mut nums: Vec<i32> = Vec::new(); nums.push(10); nums.push(20); nums.push(30); println!("len={}, first={}", nums.len(), nums[0]);
// pop returns Option<T> let last = nums.pop(); println!("popped: {:?}, remaining: {:?}", last, nums);
// safe get returns Option<&T> println!("get(5) = {:?}", nums.get(5));
// iterate by reference — nums still valid after for n in &nums { println!("item: {}", n); } println!("nums still valid: {:?}", nums);
// String vs &str let literal: &str = "hello"; let mut owned: String = String::from(literal); owned.push_str(" world"); println!("{}", owned);
// slice into a String let slice: &str = &owned[0..5]; println!("slice: {}", slice);}Compiling…
แบบจำลองความคิด
หัวข้อที่มีชื่อว่า “แบบจำลองความคิด”Vec<T> คือสามคำ (word) บน stack — pointer, length, และ capacity — ที่ชี้ไปยัง buffer ต่อเนื่องเดียวบน heap len คือส่วนของ buffer นั้นที่ “มีชีวิต”; capacity คือส่วนที่ allocate ไว้จริง การ push แต่ละครั้งจะเขียนลงในพื้นที่ที่ allocate ไว้แล้ว (ถูก) หรือไม่ก็ trigger “allocate ใหญ่ขึ้น, copy ทุกอย่าง, ปล่อย buffer เก่า” (แพง แต่เกิดไม่บ่อยเพราะ capacity เพิ่มเป็นสองเท่า) String มีรูปทรงสามคำแบบเดียวกันเป๊ะ เฉพาะทางสำหรับเก็บ byte UTF-8 แทนที่จะเป็น T แบบทั่วไป — นี่คือเหตุผลที่ .len() นับ byte: มันอ่าน field เดียวกับที่ Vec<u8> จะรายงาน
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”Vec::with_capacity(n) แลกการ allocate ล่วงหน้าครั้งหนึ่งกับการหลีกเลี่ยง reallocation ประมาณ N ครั้งในภายหลัง — คุ้มค่าเมื่อคุณรู้ (หรือประมาณได้) ขนาดสุดท้ายก่อน loop ที่จะเติมมัน shrink_to_fit() แลกการ reallocate ที่อาจเกิดขึ้นตอนนี้กับ memory footprint ที่เล็กลงในระยะยาว — คุ้มค่าสำหรับ Vec ที่คุณกำลังจะเก็บไว้นาน ไม่ใช่ตัวที่กำลังจะโตอีก การ index เข้า String ด้วย byte (&s[a..b]) เป็นการ slice ที่เร็วที่สุดเท่าที่จะทำได้ (ไม่ต้องสแกน) แต่มันผลักภาระ “ตรงกับขอบเขตตัวอักษรหรือเปล่า” มาให้คุณทั้งหมด; chars()/char_indices() สแกนให้คุณแลกกับการเดินผ่านสตริงทั้งหมด
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
Vec::new()กับVec::with_capacity(0)ทำงานต่างกัน” ไม่ต่างกันเลย — ทั้งคู่เริ่มด้วย capacity ศูนย์และ allocate ตอน push ครั้งแรกwith_capacityมีผลก็ต่อเมื่อคุณส่งตัวเลขที่มากกว่าศูนย์เท่านั้น - “
.len()บนStringนับตัวอักษร เพราะสตริงของ Rust รองรับ Unicode” มันนับ byte สตริงของ Rust รองรับ UTF-8 ซึ่งเป็นเหตุผลที่.len()ต้องรายงาน byte — ตำแหน่ง “ตัวอักษร” หนึ่งตำแหน่งไม่ได้สอดคล้องกับความกว้าง byte ที่คงที่ - “Index loop (
for i in 0..v.len()) ชัดเจนกว่าและปลอดภัยกว่าfor x in &v” จริงๆ แล้วปลอดภัยน้อยกว่าในทางปฏิบัติ: มันเป็นสไตล์เดียวที่ทำให้เกิด bounds panic จากการพิมพ์ผิดได้ (v[i + 1]) และ clippy แจ้งว่ามันไม่จำเป็นก็เพราะfor x in &vไม่ต้องตรวจ bound เลยเนื่องจากมันไม่เคยคำนวณ index ที่อยู่นอกช่วงตั้งแต่แรก
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”โค้ดที่ AI สร้างมักจะเลือก index loop อยู่เสมอ อาจเพราะมันเป็น pattern ที่พบบ่อยที่สุดในโค้ดที่ไม่ใช่ Rust ทั้งหมดในข้อมูลฝึกของมัน — แต่ใน Rust มันทั้งช้ากว่า (ตรวจ bound ซ้ำโดยไม่จำเป็น) และเป็น clippy warning ที่รอเวลาจะถูกจับได้
🐛 หา bug ในโค้ดนี้
fn sum_all(values: &[i32]) -> i32 { let mut total = 0; for i in 0..values.len() { total += values[i]; } total}เฉลย
i มีอยู่แค่เพื่อคำนวณ values[i] — loop ไม่เคยใช้ index เพื่ออย่างอื่นเลย นี่คือรูปแบบที่ clippy::needless_range_loop เตือนพอดี: total += values[i] คำนวณการเข้าถึงที่ตรวจ bound ใหม่ทุกรอบโดยไม่มีประโยชน์เพิ่ม วิธีแก้คือ for v in values { total += v; } หรือง่ายกว่านั้นคือ values.iter().sum() — ทั้งคู่ปล่อยให้ iterator จัดการการเดินผ่านและให้คอมไพเลอร์สร้างโค้ดที่แน่นกว่า