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

Vec และ String

Vec<T> คือ collection แบบ sequential หลักของ Rust มันเก็บ element ชนิด T แบบต่อเนื่องกันบน heap และขยายขนาดเองอัตโนมัติเมื่อคุณ push เกิน capacity ปัจจุบันของมัน

let mut v: Vec<i32> = Vec::new(); // empty Vec
let w = vec![1, 2, 3]; // vec! macro shorthand
let mut v = vec![10, 20, 30];
v.push(40); // append an element
let last = v.pop(); // remove and return the last element → Some(40)
let first = v[0]; // index — panics if out of bounds
let safe = v.get(100); // returns Option<&T> — None if out of bounds

ให้ใช้ .get(i) เมื่อ index อาจอยู่นอกช่วง เพื่อจัดการกรณี None แทนที่จะปล่อยให้ panic

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]
สิ่งที่เกิดขึ้นเมื่อ Vec โตเกิน capacity ของมัน
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=2
after push 0: len=1 cap=2
after push 1: len=2 cap=2
after push 2: len=3 cap=4
after push 3: len=4 cap=4
after push 4: len=5 cap=8
after push 5: len=6 cap=8
after shrink_to_fit: len=6 cap=6

capacity จะเพิ่มเป็นสองเท่าทุกครั้งที่ len() กำลังจะเกินมัน — 2 → 4 → 8 — ดังนั้นการ push element หนึ่งตัวที่เกิน capacity จึงเป็น O(1) แบบ amortized: การ push ส่วนใหญ่ไม่มีต้นทุนเพิ่ม และการ reallocate-แล้ว-copy ที่เกิดเป็นครั้งคราวก็ถูกกระจายออกไปตลอดการ push ทั้งหมดที่นำไปสู่มัน shrink_to_fit() ปล่อยพื้นที่ที่เหลือไม่ได้ใช้ (cap 8 → 6) เมื่อคุณรู้ว่า Vec จะไม่โตอีก

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 คือ text type ที่ mutable และ allocate บน heap ของ Rust มันเป็นเจ้าของ byte ของตัวเองและจัดการ deallocation

let mut s = String::from("hello");
s.push(' '); // push a single char
s.push_str("world"); // append a string slice
let combined = s + " again"; // + moves s and appends
String&str
Ownershipเป็นเจ้าของSlice ที่ยืมมา
ตำแหน่งHeapStack / 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 &str
shout("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
}

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-panic
fn 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 (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 clippy
warning: 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 default
help: 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);
}

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 สร้างมักจะเลือก 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 จัดการการเดินผ่านและให้คอมไพเลอร์สร้างโค้ดที่แน่นกว่า

Vec::pop() คืนอะไรเมื่อ Vec ว่างเปล่า?
ความแตกต่างระหว่างการวนซ้ำด้วย &nums กับ nums คืออะไร?
ทำไม function parameter ควรใช้ &str แทน &String?
ควรใช้ method ไหนแทน v[i] เมื่อ index อาจอยู่นอกช่วง?
การรันสองครั้งนี้พิมพ์อะไรตามลำดับ?
fn main() {
    let mut v: Vec<i32> = Vec::with_capacity(2);
    for i in 0..3 {
        v.push(i);
        println!("len={} cap={}", v.len(), v.capacity());
    }
}
ทำไม clippy::needless_range_loop ถึงแจ้งเตือน for i in 0..v.len() { total += v[i]; }?