Slices
Slice คืออะไร?
หัวข้อที่มีชื่อว่า “Slice คืออะไร?”Slice คือ reference ไปยังลำดับของ element ที่ต่อเนื่องกันใน collection มันไม่ได้เป็นเจ้าของข้อมูล — มัน borrow view เข้าไปในหน่วยความจำที่มีอยู่แล้ว
มี slice type ที่พบบ่อยสองแบบ:
&[T]— slice ของ array หรือ vector ประเภทTใด ๆ&str— string slice; view เข้าไปในข้อมูล string แบบ UTF-8
เพราะ slice เป็น reference เสมอ lifetime ของมันจึงผูกกับ collection ที่มัน borrow มา
Array และ Vector Slices
หัวข้อที่มีชื่อว่า “Array และ Vector Slices”เมื่อมี array หรือ vector คุณสร้าง slice ได้โดยใช้ range syntax ในวงเล็บเหลี่ยม:
fn main() { let numbers = [10, 20, 30, 40, 50];
let all: &[i32] = &numbers[..]; // entire array let first_three: &[i32] = &numbers[..3]; // indices 0, 1, 2 let middle: &[i32] = &numbers[1..4]; // indices 1, 2, 3 let last_two: &[i32] = &numbers[3..];// indices 3, 4
println!("all: {:?}", all); println!("first three: {:?}", first_three); println!("middle: {:?}", middle); println!("last two: {:?}", last_two);}สรุป range syntax:
| Syntax | ความหมาย |
|---|---|
[..] | ทั้ง collection |
[..n] | n element แรก (index 0 ถึง n-1) |
[m..n] | index m ถึง n-1 |
[m..] | จาก index m ถึงจุดจบ |
flowchart LR
subgraph Owner
ARR["numbers: 10 20 30 40 50 - owns this buffer"]
end
subgraph Slice
FP["middle: pointer plus length 3 - a fat pointer, owns nothing"]
end
FP -->|points into index 1| ARR String Slices (&str)
หัวข้อที่มีชื่อว่า “String Slices (&str)”String slice (&str) คือ reference ไปยังส่วนหนึ่งของ String หรือ string literal ตัว string literal เองก็เป็น &str — มันคือ slice ของข้อมูล read-only ที่ฝังอยู่ใน binary
fn main() { let s = String::from("hello world");
let hello: &str = &s[..5]; // "hello" let world: &str = &s[6..]; // "world"
println!("{} {}", hello, world);
// A string literal is already &str let literal: &str = "Rust is fast"; println!("{}", literal);}Byte Index ไม่ใช่ Character Index
หัวข้อที่มีชื่อว่า “Byte Index ไม่ใช่ Character Index”&s[..n] slice ตาม offset ของ byte ไม่ใช่ตาม character ซึ่งจะมองไม่เห็นความต่างเลยกับ ASCII ที่แต่ละตัวอักษรมี 1 byte — แต่ string ของ Rust เป็น UTF-8 และตัวอักษรที่ไม่ใช่ ASCII อย่างสระไทยหรือ emoji อาจใช้ 2, 3, หรือ 4 byte การ slice ที่ byte offset ที่ตกอยู่ กลาง ตัวอักษรหลาย byte เหล่านั้นจะไม่ตัดหรือประมาณค่าอะไรเลย — มันจะ panic
// @expect-panicfn main() { let s = "สวัสดี"; let oops = &s[1..]; // byte 1 is inside the first character, not a boundary println!("{oops}");}thread 'main' (<id>) panicked at src/main.rs:4:18:start 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ส (ตัวอักษรแรกของ “สวัสดี”) กว้าง 3 byte ดังนั้น byte index 1 จึงตกอยู่ข้างในมัน ไม่มี char boundary ที่ valid ตรงนั้นเลย ทางแก้คือเลิกคิดเป็น byte offset แล้วใช้ API ที่รู้จัก character แทน:
fn main() { let s = "สวัสดี";
// chars() iterates by Unicode scalar value, never by byte let first_char: Option<char> = s.chars().next(); println!("first char: {:?}", first_char);
// char_indices() pairs each char with its byte offset — always a valid boundary if let Some((idx, ch)) = s.char_indices().nth(1) { println!("second char {:?} starts at byte {}", ch, idx); }
// is_char_boundary() lets you check before slicing by byte offset let idx = 1; if s.is_char_boundary(idx) { println!("{}", &s[idx..]); } else { println!("byte {idx} is not a char boundary — cannot slice there"); }}Slices ป้องกัน Dangling Index
หัวข้อที่มีชื่อว่า “Slices ป้องกัน Dangling Index”ถ้าไม่มี slice คุณอาจเก็บ index เข้าไปใน string ไว้ แล้วแก้ไข string นั้น — ทำให้ index เหลือแค่ชี้ไปยังข้อมูลที่ไม่มีความหมาย ด้วย string slice borrow checker รับประกันว่า slice จะไม่มีชีวิตอยู่นานกว่าข้อมูลที่มันอ้างถึง
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &byte) in bytes.iter().enumerate() { if byte == b' ' { return &s[..i]; // slice up to the space } } s // whole string is one word}
fn main() { let sentence = String::from("hello world"); let word = first_word(&sentence); // sentence cannot be mutated while word (a slice of it) is alive println!("first word: {}", word);}Slices เป็น Function Parameter
หัวข้อที่มีชื่อว่า “Slices เป็น Function Parameter”ฟังก์ชันที่ทำงานกับลำดับข้อมูลควรรับ slice แทน owned type การทำแบบนี้ทำให้มันทำงานกับ array, vector, และส่วนใด ๆ ของมันได้โดยไม่ต้องการ ownership
- เลือก
&[T]มากกว่า&Vec<T>สำหรับ parameter ที่เป็นลำดับข้อมูล - เลือก
&strมากกว่า&Stringสำหรับ parameter ที่เป็น string
ทั้ง &Vec<T> และ &String จะ coerce เป็น slice ที่เทียบเท่ากันโดยอัตโนมัติที่จุดเรียก
แบ่ง Slice เป็นสองส่วนด้วย split_at
หัวข้อที่มีชื่อว่า “แบ่ง Slice เป็นสองส่วนด้วย split_at”.split_at(mid) แบ่ง slice ออกเป็นสองส่วนที่ boundary ของ byte/index แล้ว return ทั้งสองครึ่งเป็น tuple — building block ที่พบบ่อยเวลา algorithm ต้องการ “ทุกอย่างก่อนหน้า” กับ “ทุกอย่างตั้งแต่” จุดหนึ่ง โดยไม่ต้องเขียน range expression สองอัน:
fn main() { let numbers = [1, 2, 3, 4, 5]; let (left, right) = numbers.split_at(2); println!("left={:?} right={:?}", left, right);}left=[1, 2] right=[3, 4, 5]split_at ทำงานได้ทั้งบน &[T] และ &str (ซึ่งมันจะ split ที่ byte offset และเหมือนกับการ index มันจะ panic ถ้า offset นั้นไม่ใช่ char boundary) มันเทียบเท่ากับการเขียน (&numbers[..2], &numbers[2..]) เองทุกประการ เพียงแต่เป็นการเรียกครั้งเดียวที่ไม่มีทางขัดแย้งกันเองว่าจุด split อยู่ตรงไหน
หา Byte Index อย่างปลอดภัยจาก find
หัวข้อที่มีชื่อว่า “หา Byte Index อย่างปลอดภัยจาก find”panic เรื่อง byte-boundary ก่อนหน้านี้เกิดจากการ เดา offset method ที่ค้นหาใน string อย่าง .find() จะคืน byte offset ที่รับประกันว่าตกบน character boundary เสมอ — เพราะมันเจอตัวอักษรจริง (หรือตัวเว้นวรรค ด้านล่าง) ที่ตำแหน่งนั้น:
fn main() { let s = "hello world"; if let Some(space_at) = s.find(' ') { let word = &s[..space_at]; // find() returns a byte index, always a valid boundary here println!("first word: {}", word); }}first word: helloนี่คือ pattern ทั่วไปสำหรับการ slice text อย่างปลอดภัย: อย่าเขียน byte offset เป็นตัวเลข literal เลย เว้นแต่คุณจะรู้อยู่แล้วว่าข้อมูลเป็น ASCII ล้วน ให้เอา offset มาจาก find, char_indices, หรือ is_char_boundary แทน
แอบดูล่วงหน้า: Box<T>
หัวข้อที่มีชื่อว่า “แอบดูล่วงหน้า: Box<T>”ทุกอย่างในโมดูลนี้จนถึงตอนนี้ — move, reference, slice — เกี่ยวข้องกับค่าที่อยู่บน stack หรือ borrow มา Box<T> คือวิธีที่ง่ายที่สุดของ Rust ในการเอาค่าไปไว้บน heap ในขณะที่ยังมีเจ้าของเดียวที่เช็กแบบ static ได้อยู่: Box::new(42) จะ allocate i32 บน heap แล้วคืน pointer ขนาด 8 byte ให้ ไม่ว่า type ที่ box ไว้จะใหญ่แค่ไหนก็ตาม มันจะมามีบทบาทจริงจังตอนที่คุณต้องการ recursive type หรือ trait object — เอ่ยถึงตรงนี้แค่เพื่อไม่ให้ชื่อนี้เป็นเรื่องแปลกใจทีหลังในคอร์ส
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &byte) in bytes.iter().enumerate() { if byte == b' ' { return &s[..i]; } } s}
fn sum_slice(nums: &[i32]) -> i32 { let mut total = 0; for &n in nums { total += n; } total}
fn main() { // String slice let sentence = String::from("hello world"); let word = first_word(&sentence); println!("first word: {}", word);
// Integer slice let numbers = [10, 20, 30, 40, 50]; let middle: &[i32] = &numbers[1..4]; println!("middle slice: {:?}", middle); println!("sum of middle: {}", sum_slice(middle));
// String slice literal let greeting: &str = "Rust is safe"; println!("slice: {}", &greeting[..4]);}Compiling…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”Slice คือ fat pointer: pointer ไปยัง element แรกบวกกับความยาว ไม่มีอะไรมากกว่านั้น มันไม่เคยเป็นเจ้าของสิ่งที่มันชี้ไปและไม่เคย allocate เลย — &s[..5] มีต้นทุนเท่ากับการ copy usize สองตัวพอดี panic เรื่อง byte-boundary เกิดขึ้นเพราะคู่ pointer-บวก-length นั้นไม่รู้เลยว่า char boundary อยู่ตรงไหน มันรู้แค่ byte offset ดังนั้นทันทีที่คุณส่งค่าที่ตกกลางตัวอักษรให้มัน ก็ไม่มี &str ที่ valid ให้ผลิตออกมา
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”Slice มีต้นทุนถูกที่สุดเท่าที่ reference จะทำได้ — ไม่มี allocation ไม่มีการ copy ข้อมูลข้างใต้ — แต่ความถูกนั้นเองก็เป็นเหตุผลว่าทำไมมันถึง index ด้วย byte ไม่ใช่ character: การคำนวณ “byte offset ของตัวอักษรที่ N” สำหรับ String คือการสแกน O(n) (ไม่มี encoding ความกว้างคงที่ให้กระโดดผ่าน) ดังนั้น &str จึงตั้งใจไม่มี character indexing แบบ O(1) เลย ถ้าคุณต้อง index ตามตำแหน่ง character บ่อย ๆ ทางเลือกที่ตรงไปตรงมาคือจ่ายต้นทุนสร้าง Vec<char> ครั้งเดียวล่วงหน้า แทนที่จะสแกน &str ซ้ำทุกครั้งที่เข้าถึง
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
&s[..n]ตัดอย่างปลอดภัยให้เหลือ n byte เหมือนฟังก์ชัน substring ในภาษาอื่นที่ตัดให้เหลือ n character” มัน slice ตาม byte และถ้า byte ที่ n ไม่ใช่ character boundary มันจะ panic แทนที่จะผลิตผลลัพธ์ที่ผิดนิดหน่อยแบบเงียบ ๆ — ตัวอย่าง"สวัสดี"ก่อนหน้านี้คือตัวอย่างนี้เป๊ะ - “เพราะ slice ถูกมาก การ clone มันทุกครั้งที่ borrow checker บ่นก็แทบไม่มีต้นทุนเหมือนกัน” การ clone
&[T]/&str(จริง ๆ คือCopyเพราะ slice เป็นCopy) copy แค่ pointer กับความยาว — แต่ข้อผิดพลาดที่พบบ่อยกว่ามากคือการหยิบ.to_vec()/.to_string()บนเป้าหมายของ slice มาใช้ ซึ่ง allocate และ copy ข้อมูลจริง หยิบมันมาใช้เฉพาะตอนที่คุณต้องการ copy ที่เป็นเจ้าของและเป็นอิสระจริง ๆ เท่านั้น - “
&Vec<T>กับ&[T]ใช้แทนกันได้พื้นฐาน ดังนั้น function signature จะใช้ตัวไหนก็ไม่ต่างกัน”&[T]รับได้ทุกอย่างที่&Vec<T>รับ (ผ่าน coercion) บวก array และ sub-slice ใด ๆ — ไม่มีสถานการณ์ไหนที่&Vec<T>รับได้แต่&[T]ปฏิเสธ ดังนั้น&[T]จึงดีกว่าอย่างชัดเจนในฐานะ parameter type
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”การ slice text ที่ user ส่งเข้ามาที่จำนวน byte ตายตัวคือบั๊กที่ไม่ชัดเจนที่พบบ่อยที่สุดในโค้ด Rust ที่ AI เขียน — มันทำงานสมบูรณ์แบบในทุก test ที่ assistant ลอง (text ภาษาอังกฤษ) แล้ว panic ทันทีที่ user จริงวาง input ที่ไม่ใช่ ASCII เข้ามา
🐛 หา bug ในโค้ดนี้
fn preview(input: &str) -> &str { &input[..10] // assumes at least 10 ASCII bytes}
fn main() { let greeting = "Hello, world!"; println!("{}", preview(greeting));}เฉลย
โค้ดนี้รันได้ปกติสำหรับ "Hello, world!" — 10 byte ของ ASCII คือ 10 ตัวอักษรพอดี และ string ก็ยาวพอ มันจะ panic ทันทีที่ caller ส่ง text ที่สั้นกว่า 10 byte ("Hi") หรือ text ที่ byte ที่ 10 ตกอยู่ข้างในตัวอักษรหลาย byte (input ที่ไม่ใช่ ASCII ใกล้ความยาวนั้น) ทางแก้คือคำนวณ boundary ที่ปลอดภัยแทนการเดา: input.char_indices().nth(10).map(|(i, _)| &input[..i]).unwrap_or(input) จะเอา 10 ตัวอักษร แรก (หรือทั้ง string ถ้าสั้นกว่านั้น) และไม่ panic เลยกับ &str ที่ valid ใด ๆ