ไดนามิกดิสแพตช์บนสแตก
คำอธิบาย
เราสามารถทำ Dynamic Dispatch ข้ามชนิดข้อมูลที่แตกต่างกันได้โดยตรงบน Stack โดยไม่จำเป็นต้องใช้ Box เพื่อจัดสรรหน่วยความจำบน heap เสมอไป ดังตัวอย่างด้านล่างนี้:
ตัวอย่าง
use std::io;
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let arg = "-";
// We need to describe the type to get dynamic dispatch.
let readable: &mut dyn io::Read = if arg == "-" {
&mut io::stdin()
} else {
&mut fs::File::open(arg)?
};
// Read from `readable` here.
Ok(())
}
ที่มาและเหตุผล
โดยค่าเริ่มต้นแล้ว ภาษา Rust จะใช้กระบวนการ Monomorphization กับโค้ดที่มีการใช้ Generics ซึ่งหมายความว่าคอมไพเลอร์จะคัดลอกและสร้างฟังก์ชันชุดใหม่ขึ้นมาแยกกันสำหรับแต่ละชนิดข้อมูลที่ถูกส่งเข้ามาใช้งาน และทำการปรับแต่ง (Optimize) แต่ละชุดอย่างเป็นอิสระ แม้ว่าวิธีนี้จะช่วยให้โค้ดส่วนที่ประมวลผลบ่อย (Hot path) ทำงานได้รวดเร็วอย่างยิ่ง แต่ก็แลกมาด้วยการทำให้ขนาดไฟล์ไบนารีบวมขึ้นในจุดที่ประสิทธิภาพอาจไม่ใช่เรื่องสำคัญที่สุด และทำให้คอมไพเลอร์ต้องใช้เวลาคอมไพล์นานขึ้น ตลอดจนสิ้นเปลืองพื้นที่ CPU Cache
โชคดีที่ Rust ยังเปิดทางให้เราสามารถเลือกใช้ Dynamic Dispatch (ผ่าน Trait Object เช่น dyn Trait) ได้เมื่อต้องการ เพียงแต่เราต้องระบุชนิดข้อมูลกำกับไว้อย่างชัดเจน
ข้อดี
เราไม่จำเป็นต้องจัดสรรหน่วยความจำบน heap (ไม่จำเป็นต้องใช้ Box) ไม่จำเป็นต้องเตรียมสร้างออบเจกต์ตัวที่เราไม่ได้ใช้งานจริงในภายหลัง และไม่ต้องทำให้คอมไพเลอร์ต้องทำ Monomorphization โค้ดทั้งหมดที่ตามมาเพื่อให้รองรับทั้ง File หรือ Stdin
ข้อเสีย
ก่อนยุคของ Rust 1.79.0 โค้ดจำเป็นต้องประกาศตัวแปรด้วยคำสั่ง let แยกกันถึงสองตัว พร้อมกับใช้เทคนิคการกำหนดค่าเริ่มต้นแบบมีเงื่อนไข (Deferred Initialization) ซึ่งทำให้มีชิ้นส่วนโค้ดที่ต้องเขียนมากกว่าการใช้ Box:
// We still need to ascribe the type for dynamic dispatch.
let readable: Box<dyn io::Read> = if arg == "-" {
Box::new(io::stdin())
} else {
Box::new(fs::File::open(arg)?)
};
// Read from `readable` here.
แต่โชคดีที่ตั้งแต่ Rust 1.79.0 เป็นต้นมา ข้อจำกัดที่น่ารำคาญนี้ได้หมดไปแล้ว!
เจาะลึกรายละเอียด
ตั้งแต่ Rust 1.79.0 เป็นต้นมา คอมไพเลอร์จะช่วยขยายอายุขัย (Lifetime Extension) ของค่าชั่วคราวที่อยู่ภายใต้การอ้างอิง & หรือ &mut ให้ยืนยาวที่สุดเท่าที่จะเป็นไปได้ภายในขอบเขตของฟังก์ชันโดยอัตโนมัติ
นั่นหมายความว่าเราสามารถส่งค่า &mut ออกมาจากนิพจน์ if-else ได้โดยตรงอย่างสบายใจ โดยไม่ต้องคอยกังวลว่าจะต้องสร้างตัวแปร let พักข้อมูลไว้ก่อนเหมือนเมื่อก่อน
คอมไพเลอร์ยังคงจัดสรรพื้นที่ชั่วคราวบน Stack สำหรับแต่ละค่าได้อย่างแม่นยำ คอมไพเลอร์รู้ขนาดที่แน่นอนของข้อมูลแต่ละตัว และตัวแปรที่ถูกยืมจะมีอายุขัยยืนยาวกว่าการอ้างอิงทั้งหมดที่ยืมไปจากมันเสมอ
ดูเพิ่มเติม
- การทำขั้นสุดท้ายในเดสทรัคเตอร์ (Finalisation in Destructors) และ RAII guards ที่ได้ประโยชน์จากการควบคุม อายุขัย (Lifetime) ของตัวแปรอย่างรัดกุม
- สำหรับการสร้างเรฟเฟอเรนซ์แบบมีเงื่อนไขในรูป
Option<&T>หรือOption<&mut T>เราสามารถเก็บค่าลงในOption<T>โดยตรง แล้วใช้เมท็อด.as_ref()เพื่อดึงเรฟเฟอเรนซ์ออกมาได้เมื่อต้องการ