Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

mem::{take(_), replace(_)} เพื่อรักษาค่าที่เป็นเจ้าของไว้ใน enum ที่เปลี่ยนแปลง

คำอธิบาย

สมมติว่าเรามีตัวแปร &mut MyEnum ซึ่งมีอย่างน้อย 2 variants คือ A { name: String, x: u8 } และ B { name: String } และเราต้องการเปลี่ยนค่าจาก MyEnum::A ให้กลายเป็น B หากค่า x มีค่าเท่ากับศูนย์ โดยยังคงรักษาข้อมูล MyEnum::B เดิมไว้

เราสามารถทำสิ่งนี้ได้โดยไม่จำเป็นต้องทำการ .clone() ตัวแปร name เลยแม้แต่น้อย

ตัวอย่าง

#![allow(unused)]
fn main() {
use std::mem;

enum MyEnum {
    A { name: String, x: u8 },
    B { name: String },
}

fn a_to_b(e: &mut MyEnum) {
    if let MyEnum::A { name, x: 0 } = e {
        // This takes out our `name` and puts in an empty String instead
        // (note that empty strings don't allocate).
        // Then, construct the new enum variant (which will
        // be assigned to `*e`).
        *e = MyEnum::B {
            name: mem::take(name),
        }
    }
}
}

วิธีนี้ยังใช้ได้กับกรณีที่มีหลาย variant:

#![allow(unused)]
fn main() {
use std::mem;

enum MultiVariateEnum {
    A { name: String },
    B { name: String },
    C,
    D,
}

fn swizzle(e: &mut MultiVariateEnum) {
    use MultiVariateEnum::*;
    *e = match e {
        // Ownership rules do not allow taking `name` by value, but we cannot
        // take the value out of a mutable reference, unless we replace it:
        A { name } => B {
            name: mem::take(name),
        },
        B { name } => A {
            name: mem::take(name),
        },
        C => D,
        D => C,
    }
}
}

ที่มาและเหตุผล

เมื่อทำงานกับ enum เราอาจต้องการเปลี่ยนค่าภายในของ enum ณ ตำแหน่งหน่วยความจำเดิม (In-place) ไปสู่ variant อื่น ซึ่งในทางปฏิบัติต้องแบ่งออกเป็น 2 จังหวะเพื่อให้ borrow checker ยอมผ่าน: ในจังหวะแรก เราจะอ่านตรวจสอบค่าที่มีอยู่เดิมและส่วนประกอบย่อยของมันเพื่อตัดสินใจว่าจะทำอะไรต่อ และในจังหวะที่สอง เราจึงค่อยเปลี่ยนค่าตามเงื่อนไขที่ต้องการ (ดังตัวอย่างข้างต้น)

กฎของ borrow checker จะไม่อนุญาตให้เราดึง name ออกมาจาก enum ดื้อๆ (เพราะข้อมูลใน enum จะเกิดภาวะว่างเปล่าไม่ได้ ต้องมีบางสิ่งอยู่ตรงนั้นเสมอ) แน่นอนว่าเราอาจแก้ปัญหาง่ายๆ ด้วยการเรียก .clone() ข้อมูล name แล้วนำตัวโคลนนั้นไปใส่ใน MyEnum::B แต่นั่นจะกลายเป็นการตกหลุมพรางของแอนติแพตเทิร์น โคลนเพื่อเอาใจ borrow checker ทันที แต่เราสามารถหลีกเลี่ยงการจัดสรรหน่วยความจำ (Allocation) ส่วนเกินได้ โดยการสลับค่าใน e ผ่าน mutable reference

ฟังก์ชัน mem::take ช่วยให้เราสลับดึงเอาค่าเดิมออกมา โดยแทนที่ตำแหน่งนั้นด้วยค่าเริ่มต้น (Default value) ของมัน และคืนค่าเดิมก่อนหน้ากลับมา สำหรับ String ค่า default คือสตริงว่าง ซึ่งไม่ต้องมีการจัดสรรหน่วยความจำบน heap เพิ่มเติม ผลลัพธ์คือเราจะได้ name ตัวเดิมในฐานะค่าที่เราเป็นเจ้าของ (Owned Value) และสามารถนำไปบรรจุลงใน enum ตัวใหม่ได้ทันที

ข้อสังเกต: mem::replace มีการทำงานที่คล้ายคลึงกันอย่างยิ่ง แต่เปิดให้เราระบุค่าที่จะนำมาสลับแทนที่ได้ตามใจชอบ โดยบรรทัดที่เทียบเท่ากับ mem::take ของเราคือ mem::replace(name, String::new())

นอกจากนี้ หากเรากำลังทำงานกับ Option และต้องการดึงค่าเดิมออกมาแล้วแทนที่ตำแหน่งนั้นด้วย None เมท็อด take() ของตัว Option เองถือเป็นทางเลือกที่สั้น กระชับ และเป็นสำนวนที่ถูกต้องตรงจุดยิ่งกว่า

ข้อดี

เห็นไหมล่ะ ไม่มีการจัดสรรหน่วยความจำเพิ่มเลยสักไบต์! แถมระหว่างที่เขียนโค้ด คุณอาจจะรู้สึกเท่เหมือนอินเดียนา โจนส์ ที่กำลังสลับของอีกด้วย

ข้อเสีย

โค้ดจะดูยาวขึ้นเล็กน้อย และหากเขียนผิดซ้ำๆ ก็อาจทำให้คุณหงุดหงิดกับ borrow checker ได้ นอกจากนี้ คอมไพเลอร์อาจไม่สามารถปรับลดทอนขั้นตอนการเขียนทับสองรอบ (Double store) ออกไปได้ ส่งผลให้ประสิทธิภาพลดลงเล็กน้อยเมื่อเทียบกับการเขียนโค้ดในภาษาตระกูล Unsafe

ยิ่งไปกว่านั้น ชนิดข้อมูลที่คุณต้องการดึงค่าออกมาจะต้องอิมพลีเมนต์ trait Default ไว้ด้วย แต่หากชนิดข้อมูลนั้นไม่ได้รองรับ Default คุณก็สามารถเปลี่ยนมาใช้ mem::replace เพื่อกำหนดค่าแทนที่ด้วยตัวเองได้

เจาะลึกรายละเอียด

แพตเทิร์นนี้เป็นเรื่องที่น่าสนใจและจำเป็นเฉพาะในภาษา Rust เท่านั้น เพราะในภาษาที่มี Garbage Collection (GC) คุณจะส่งผ่านการอ้างอิงถึงค่าเป็นปกติอยู่แล้ว (โดยมี GC คอยนับการอ้างอิงให้) ส่วนในภาษาระดับต่ำอย่าง C คุณก็แค่คัดลอกพอยน์เตอร์แล้วค่อยตามไปจัดการทีหลัง

แต่ใน Rust เราต้องออกแรงเพิ่มขึ้นอีกนิด เพราะค่าที่เป็นเจ้าของ (Owned Value) จะมีเจ้าของได้เพียงคนเดียวในเวลาเดียวกัน หากเราต้องการจะฉกมันออกมา เราก็จำเป็นต้องหาของชิ้นอื่นมาวางสลับแทนที่ไว้ทันที — ไม่ต่างอะไรกับอินเดียนา โจนส์ ที่ต้องรีบวางถุงทรายลงไปแทนที่รูปปั้นโบราณวัตถุ

ดูเพิ่มเติม

แพตเทิร์นนี้ช่วยกำจัดแอนติแพตเทิร์น โคลนเพื่อเอาใจ borrow checker ในกรณีเฉพาะทางนี้ได้อย่างหมดจด