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

RAII พร้อมการ์ด (RAII with guards)

คำอธิบาย

RAII ย่อมาจาก “Resource Acquisition Is Initialization” ซึ่งถือเป็นชื่อที่ชวนสับสนไม่น้อย แต่สาระสำคัญที่แท้จริงของแพตเทิร์นนี้คือ “การจัดสรรและเตรียมทรัพยากรให้พร้อมในขั้นตอนการสร้างออบเจกต์ (constructor) และทำการคืนหรือปิดทรัพยากร (finalisation / cleanup) ในขั้นตอนการทำลายออบเจกต์ (destructor)” สำหรับในภาษา Rust แพตเทิร์นนี้ได้รับการต่อยอดโดยนำออบเจกต์ RAII มาทำหน้าที่เป็น “การ์ด” (guard) คอยคุ้มครองทรัพยากร และอาศัยระบบชนิดข้อมูล (type system) เพื่อรับประกันว่าการเข้าถึงทรัพยากรจะต้องกระทำผ่านออบเจกต์การ์ดนี้เสมอ

ตัวอย่าง

Mutex Guard ในไลบรารีมาตรฐานถือเป็นตัวอย่างคลาสสิกของแพตเทิร์นนี้ (โค้ดด้านล่างนี้เป็นเวอร์ชันอย่างย่อของการอิมพลีเมนต์จริง):

use std::ops::Deref;

struct Foo {}

struct Mutex<T> {
    // We keep a reference to our data: T here.
    //..
}

struct MutexGuard<'a, T: 'a> {
    data: &'a T,
    //..
}

// Locking the mutex is explicit.
impl<T> Mutex<T> {
    fn lock(&self) -> MutexGuard<T> {
        // Lock the underlying OS mutex.
        //..

        // MutexGuard keeps a reference to self
        MutexGuard {
            data: self,
            //..
        }
    }
}

// Destructor for unlocking the mutex.
impl<'a, T> Drop for MutexGuard<'a, T> {
    fn drop(&mut self) {
        // Unlock the underlying OS mutex.
        //..
    }
}

// Implementing Deref means we can treat MutexGuard like a pointer to T.
impl<'a, T> Deref for MutexGuard<'a, T> {
    type Target = T;

    fn deref(&self) -> &T {
        self.data
    }
}

fn baz(x: Mutex<Foo>) {
    let xx = x.lock();
    xx.foo(); // foo is a method on Foo.
              // The borrow checker ensures we can't store a reference to the underlying
              // Foo which will outlive the guard xx.

    // x is unlocked when we exit this function and xx's destructor is executed.
}

แรงจูงใจ

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

ข้อดี

ช่วยป้องกันทั้งข้อผิดพลาดจากการลืมคืนทรัพยากร และข้อผิดพลาดจากการเผลอนำทรัพยากรที่ถูกคืนไปแล้วกลับมาใช้งานซ้ำ (use-after-free)

การอภิปราย

RAII เป็นแพตเทิร์นที่มีประโยชน์อย่างยิ่งในการรับประกันว่าทรัพยากรจะถูกคืนหน่วยความจำหรือปิดการทำงานอย่างถูกต้องปลอดภัยเสมอ ในภาษา Rust เราสามารถใช้ประโยชน์จาก borrow checker เพื่อป้องกันข้อผิดพลาดจากการเข้าถึงทรัพยากรหลังถูกปิดได้ตั้งแต่ขั้นตอนการคอมไพล์

เป้าหมายหลักของ borrow checker คือการรับประกันว่าการอ้างอิง (reference) ไปยังข้อมูลจะต้องไม่คงอยู่ยาวนานกว่าตัวข้อมูลนั้น (do not outlive) แพตเทิร์น RAII Guard ทำงานได้อย่างมีประสิทธิภาพเนื่องจากตัวออบเจกต์การ์ดจะเก็บการอ้างอิงไปยังทรัพยากรเบื้องหลัง และเปิดให้เข้าถึงข้อมูลได้ผ่านการอ้างอิงนี้เท่านั้น โดย Rust จะรับประกันว่าตัวการ์ดจะไม่สามารถคงอยู่ยาวนานกว่าทรัพยากรเบื้องหลังได้ และการอ้างอิงไปยังทรัพยากรที่ส่งผ่านการ์ดก็จะไม่สามารถคงอยู่ยาวนานกว่าตัวการ์ดได้เช่นกัน เพื่อให้เห็นภาพการทำงานชัดเจนขึ้น ลองพิจารณาลายเซ็นของเมท็อด deref แบบระบุค่า lifetime ชัดเจน (โดยไม่ละเว้น lifetime elision):

fn deref<'a>(&'a self) -> &'a T {
    //..
}

การอ้างอิงไปยังทรัพยากรที่ส่งคืนกลับมาจะมี lifetime เดียวกันกับ self ('a) ดังนั้น borrow checker จึงช่วยรับประกันว่าอายุของการอ้างอิงไปยัง T จะต้องสั้นกว่าอายุของ self เสมอ

ข้อควรสังเกตคือ การนำ Deref trait มาใช้งานไม่ใช่หัวใจสำคัญที่ขาดไม่ได้ของแพตเทิร์นนี้ แต่มีไว้เพื่อเพิ่มความสะดวกสบายในการใช้งานออบเจกต์การ์ดเท่านั้น การเขียนเมท็อด get ธรรมดาบนตัวการ์ดก็ให้ผลลัพธ์ที่สมบูรณ์แบบไม่แพ้กัน

ดูเพิ่มเติม

สำนวนการคืนทรัพยากรใน destructor

RAII เป็นแพตเทิร์นที่พบเห็นได้ทั่วไปในภาษา C++: cppreference.com, wikipedia

Style guide entry (ปัจจุบันเป็นเพียงหน้าว่าง)