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

Slices

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 คุณสร้าง 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
slice คือ view แบบ pointer-บวก-length เข้าไปใน buffer ของคนอื่น

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);
}

&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-panic
fn 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");
}
}

ถ้าไม่มี 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);
}

ฟังก์ชันที่ทำงานกับลำดับข้อมูลควรรับ slice แทน owned type การทำแบบนี้ทำให้มันทำงานกับ array, vector, และส่วนใด ๆ ของมันได้โดยไม่ต้องการ ownership

  • เลือก &[T] มากกว่า &Vec<T> สำหรับ parameter ที่เป็นลำดับข้อมูล
  • เลือก &str มากกว่า &String สำหรับ parameter ที่เป็น string

ทั้ง &Vec<T> และ &String จะ coerce เป็น slice ที่เทียบเท่ากันโดยอัตโนมัติที่จุดเรียก

.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 อยู่ตรงไหน

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 แทน

ทุกอย่างในโมดูลนี้จนถึงตอนนี้ — 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]);
}

Slice คือ fat pointer: pointer ไปยัง element แรกบวกกับความยาว ไม่มีอะไรมากกว่านั้น มันไม่เคยเป็นเจ้าของสิ่งที่มันชี้ไปและไม่เคย allocate เลย — &s[..5] มีต้นทุนเท่ากับการ copy usize สองตัวพอดี panic เรื่อง byte-boundary เกิดขึ้นเพราะคู่ pointer-บวก-length นั้นไม่รู้เลยว่า char boundary อยู่ตรงไหน มันรู้แค่ byte offset ดังนั้นทันทีที่คุณส่งค่าที่ตกกลางตัวอักษรให้มัน ก็ไม่มี &str ที่ valid ให้ผลิตออกมา

Slice มีต้นทุนถูกที่สุดเท่าที่ reference จะทำได้ — ไม่มี allocation ไม่มีการ copy ข้อมูลข้างใต้ — แต่ความถูกนั้นเองก็เป็นเหตุผลว่าทำไมมันถึง index ด้วย byte ไม่ใช่ character: การคำนวณ “byte offset ของตัวอักษรที่ N” สำหรับ String คือการสแกน O(n) (ไม่มี encoding ความกว้างคงที่ให้กระโดดผ่าน) ดังนั้น &str จึงตั้งใจไม่มี character indexing แบบ O(1) เลย ถ้าคุณต้อง index ตามตำแหน่ง character บ่อย ๆ ทางเลือกที่ตรงไปตรงมาคือจ่ายต้นทุนสร้าง Vec<char> ครั้งเดียวล่วงหน้า แทนที่จะสแกน &str ซ้ำทุกครั้งที่เข้าถึง

  • “&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

การ 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 ใด ๆ

Type ใดแทน view ที่ borrow มาของส่วนหนึ่งของ String?
range syntax [1..4] เลือกอะไรจาก slice?
Slice เป็นเจ้าของข้อมูลที่มันชี้ไปไหม?
ทำไม function parameter ควรใช้ &[T] แทน &Vec<T>?
โค้ดนี้ panic ไหม? ถ้าไม่ panic มัน print อะไร?
fn main() {
    let s = "สวัสดี";
    let oops = &s[1..];
    println!("{oops}");
}
โปรแกรมนี้ print อะไร?
fn main() {
    let numbers = [1, 2, 3, 4, 5];
    let (left, right) = numbers.split_at(2);
    println!("{:?} {:?}", left, right);
}