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 ในกรณีเฉพาะทางนี้ได้อย่างหมดจด