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

ไดนามิกดิสแพตช์บนสแตก

คำอธิบาย

เราสามารถทำ 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() เพื่อดึงเรฟเฟอเรนซ์ออกมาได้เมื่อต้องการ