ปูสีส้มต้นฉบับยกตะกร้าข้อมูลเต็มออกจากช่องยืม แล้ววางตะกร้าเปล่าแทนบนโต๊ะสีมิ้นต์

Rust std::mem::take: ย้ายค่าออกจาก Borrow โดยไม่ Clone

พี่โชว์Engineering

ปัญหาคลาสสิกของ Rust คือเราถือ mutable borrow ของ struct อยู่ แต่อยากเอา field ที่ไม่ใช่ Copy ออกไปเป็นเจ้าของครับ การเขียน self.items มอบออกตรง ๆ จะทิ้งช่องว่างที่ไม่ใช่ค่าถูกต้อง compiler จึงไม่ยอม เหมือนยกถังน้ำออกจากชั้นโดยไม่วางอะไรรับน้ำหนักแทน

std::mem::take แก้โจทย์นี้ด้วยการวาง T::default() ลงจุดเดิม แล้วคืนค่าเก่าให้ผู้เรียก เราได้ ownership โดยไม่ clone และ object ที่ถูกยืมยังอยู่ในสถานะถูกต้อง

WHY: ลด Copy โดยรักษา Invariant

วิธีแก้แบบรีบ ๆ คือ clone field แล้ว clear ของเดิม แต่ต้องจัดสรรและคัดลอกข้อมูลโดยไม่จำเป็น อีกวิธีใช้ Option แล้วเรียก take ซึ่งดีเมื่อ “ไม่มีค่า” เป็น state ที่มีความหมาย แต่ถ้า field เป็น Vec, String หรือ map ที่ค่าว่างคือสภาพพร้อมรับงานตามปกติ mem::take อ่านเจตนาตรงกว่า

ตัวอย่าง batcher ต่อไปนี้ทดสอบทั้งการคืนของเดิม การรีเซ็ต buffer และการใช้ object ต่อหลัง take

#[derive(Default)]
struct Batcher {
    pending: Vec<String>,
}

impl Batcher {
    fn push(&mut self, value: impl Into<String>) {
        self.pending.push(value.into());
    }

    fn drain_batch(&mut self) -> Vec<String> {
        std::mem::take(&mut self.pending)
    }
}

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

    #[test]
    fn moves_batch_and_leaves_reusable_empty_buffer() {
        let mut batcher = Batcher::default();
        batcher.push("first");
        batcher.push("second");

        let batch = batcher.drain_batch();
        assert_eq!(batch, vec!["first", "second"]);
        assert!(batcher.pending.is_empty());

        batcher.push("third");
        assert_eq!(batcher.drain_batch(), vec!["third"]);
    }
}

HOW: เลือก take หรือ replace ให้ตรงความหมาย

mem::take ต้องการ T: Default ถ้าค่าทดแทนไม่ใช่ Default ให้ใช้ mem::replace และส่งค่าใหม่ชัด ๆ เช่นเปลี่ยน state จาก Running ไป Stopped พร้อมรับ Running เดิมออกมา ส่วน Option::take เหมาะเมื่อ field อาจว่างจริง

ต้นทุนของ take ไม่ใช่ศูนย์เสมอ Default ของชนิดที่ซับซ้อนอาจทำงานมาก จึงอ่าน implementation ก่อนใช้ใน hot path สำหรับ Vec และ String ค่า Default เป็น container ว่างโดยไม่คัดลอกสมาชิก แต่ capacity เดิมเดินไปกับค่าที่คืนออกมา ถ้าต้องการเก็บ allocation ไว้ใช้ซ้ำ การ clear อาจเหมาะกว่า

ระวัง invariant ข้ามหลาย field หากเอา field หนึ่งออกแล้วอีก field ยังอ้างความหมายเดิม object อาจถูกต้องตาม type แต่ผิดทางธุรกิจ ให้รวม state เป็น enum หรือเขียน method ที่ปรับทุก fieldในจุดเดียว

Recap

  • ใช้ mem::take เมื่อต้องการ ownership และค่าว่างแบบ Default ถูกต้อง
  • ใช้ mem::replace เมื่อค่าทดแทนต้องระบุเอง
  • ใช้ Option::take เมื่อ absence เป็น state จริง
  • ตรวจ allocation/capacity ก่อนใช้ใน hot path
  • รักษา invariant ของทุก field ไม่ใช่แค่ให้ compiler ผ่าน

ลองค้น clone แล้ว clear ที่อยู่ติดกันใน state machine สักจุด อาจลดเหลือ take บรรทัดเดียวได้ครับ