ปูสีส้มแดงต้นฉบับใช้ก้ามแบ่งริบบิ้นสีเขียวที่จุดบาก ทำให้เกิดมุมมองสองส่วนบนกระดานเดียวกัน

Rust slice::split_off: หยิบช่วงออกจากมุมมองโดยไม่ allocate

พี่โชว์Engineering

parser ที่อ่านข้อมูลทีละช่วงมักเขียน let head = &input[..n]; input = &input[n..]; ซ้ำไปเรื่อย ๆ โค้ดนี้เร็วและไม่ allocate แต่ถ้า index เกินขอบจะ panic และการอัปเดตตัวแปรสองบรรทัดเปิดช่องให้หยิบจาก slice หนึ่งแล้วเลื่อนอีก slice ผิดตัว slice::split_off รวมการ “หยิบช่วง” กับ “เลื่อนมุมมองที่เหลือ” ไว้ใน operation เดียว และคืน Option เมื่อช่วงเกินขอบครับ

WHY: slice คือหน้าต่าง ไม่ใช่กล่องข้อมูลใหม่

เมธอดนี้ทำงานบน &mut &[T] หรือ mutable reference ไปยัง มุมมอง ของ slice ไม่ได้ย้ายสมาชิก ไม่ได้ copy และไม่จอง heap เพิ่ม ให้นึกถึงริบบิ้นเส้นเดิมที่เราวางกรอบดูไว้ ก้ามปูเลื่อนกรอบไปด้านที่เหลือ แล้วส่งกรอบของส่วนที่หยิบออกมาให้เรา ทั้งสองกรอบยังชี้ผืนเดียวกัน

นี่ต่างจาก Vec::split_off ซึ่งแยกสมาชิกท้ายไปเป็น Vec ใหม่และอาจ allocate ชื่อเหมือนกันแต่ต้นทุนและชนิดผลลัพธ์ต่างกันมาก

HOW: ทำ cursor ที่ล้มเหลวแบบไม่ panic

fn take_front<'a, T>(input: &mut &'a [T], count: usize) -> Option<&'a [T]> {
    input.split_off(..count)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn consumes_prefix_without_copying() {
        let bytes = [10_u8, 20, 30, 40, 50];
        let mut cursor: &[u8] = &bytes;

        assert_eq!(take_front(&mut cursor, 2), Some(&bytes[..2]));
        assert_eq!(cursor, &bytes[2..]);
        assert_eq!(take_front(&mut cursor, 0), Some(&bytes[2..2]));
    }

    #[test]
    fn out_of_bounds_keeps_the_cursor() {
        let bytes = [1_u8, 2, 3];
        let mut cursor: &[u8] = &bytes;

        assert_eq!(take_front(&mut cursor, 4), None);
        assert_eq!(cursor, &bytes);
    }
}

ช่วง ..count หมายถึงหยิบ prefix ออกและปล่อยให้ cursor ชี้ส่วนท้าย ถ้าใช้ count.. จะหยิบ suffix แล้ว cursor เหลือ prefix เมธอดรับเฉพาะ one-sided range จึงไม่รับ a..b; ถ้าต้องแบ่งกลางแบบตรง ๆ ใช้ split_at หรือ split_at_checked จะสื่อความหมายกว่า

Trade-off และ lifetime

ต้นทุนเวลาเป็น O(1) และความจำเพิ่ม O(1) เพราะเปลี่ยน pointer กับ length เท่านั้น ผลลัพธ์ยืมข้อมูลจาก slice ต้นทาง จึงอยู่ไม่ได้นานกว่าข้อมูลนั้น ข้อดีคือ compiler กัน use-after-free ให้ แต่ก็หมายความว่าเราแก้หรือย้าย owner บางแบบไม่ได้ตราบใดที่ reference ยังถูกใช้

สำหรับ parser ให้ห่อ None เป็น error ที่มีบริบท เช่นจำนวน byte ที่ต้องการและจำนวนที่เหลือ แทนการ unwrap() แล้วเปลี่ยน malformed input เป็น panic ถ้าข้อมูลมาจาก network ควรกำหนด maximum frame size ก่อนอ่าน ไม่ใช่เชื่อ length field แล้วค่อยขอ slice ขนาดมหาศาล

กรณีต้องแก้ข้อมูลใช้ split_off_mut บน &mut &mut [T] แต่ borrow จะเข้มขึ้นเพื่อกัน mutable aliasing อย่าใช้ unsafe pointer เพื่อหลบเพียงเพราะ signature ดูยาว เพราะ invariant ว่าสองช่วงไม่ซ้อนกันคือสิ่งที่เมธอดมาตรฐานช่วยพิสูจน์ให้

เลือกเครื่องมือให้ตรงงาน

  • ต้องการสองช่วงพร้อมกันและ index เชื่อถือได้: split_at
  • ต้องการสองช่วงโดยไม่ panic: split_at_checked
  • ต้องการ cursor ที่ถูกเลื่อนไปเรื่อย ๆ: split_off
  • ต้องการ ownership และ buffer แยก: Vec::split_off
  • ต้องการอ่านโครงสร้างซ้ำ: iterator/chunks อาจอ่านง่ายกว่า cursor manual

Recap

  • จำว่าเมธอดอยู่บน &mut &[T] และแก้เฉพาะมุมมอง
  • ..n หยิบหัว ส่วน n.. หยิบท้าย
  • None ไม่แก้ cursor เมื่อเกินขอบ
  • แปลง malformed input เป็น error ไม่ใช่ panic
  • อย่าสับสนกับ Vec::split_off ที่มี ownership และ allocation คนละแบบ

ถ้า parser มีคู่บรรทัด “slice ก่อน แล้วเลื่อน cursor” หลายจุด ลองแทนจุดหนึ่งด้วย helper ที่คืน Option แล้วทดสอบทั้งศูนย์ พอดี และเกินขอบครับ