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

การรันโค้ดเมื่อทำการล้างทำความสะอาดด้วยเทรต Drop

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

เรากำลังแนะนำ Drop ในบริบทของพอยน์เตอร์อัจฉริยะเพราะคุณสมบัติการทำงานของเทรต Drop เกือบทั้งหมดจะถูกใช้เมื่อประยุกต์ใช้งานพอยน์เตอร์อัจฉริยะ ตัวอย่างเช่น เมื่อ Box<T> ถูกคืนหน่วยความจำ (dropped) มันจะคืนพื้นที่บนฮีปที่ box ชี้ไป

ในบางภาษา สำหรับบางชนิดข้อมูล โปรแกรมเมอร์ต้องเรียกใช้โค้ดเพื่อคืนหน่วยความจำหรือทรัพยากรทุกครั้งเมื่อพวกเขาสูญเสียหรือเลิกใช้งานอินสแตนซ์ของชนิดข้อมูลเหล่านั้น ตัวอย่างเช่น ตัวจัดการไฟล์ (file handles), ซ็อกเก็ต (sockets), และล็อก (locks) หากโปรแกรมเมอร์ลืม ระบบอาจทำงานหนักเกินไปและล่มได้ ใน Rust คุณสามารถระบุให้โค้ดบางส่วนถูกรันทุกครั้งที่ค่าหลุดออกจากขอบเขต และคอมไพเลอร์จะแทรกโค้ดนี้ให้อย่างอัตโนมัติ เป็นผลให้คุณไม่จำเป็นต้องระมัดระวังในการวางโค้ดล้างทำความสะอาดไว้ทุกที่ในโปรแกรมเมื่ออินสแตนซ์ของชนิดข้อมูลนั้นใช้งานเสร็จแล้ว—และคุณจะไม่ทำทรัพยากรรั่วไหล!

คุณระบุโค้ดที่จะให้รันเมื่อค่าหลุดออกจากขอบเขตโดยการประยุกต์ใช้เทรต Drop เทรต Drop กำหนดให้คุณต้องประยุกต์ใช้หนึ่งเมธอดชื่อ drop ซึ่งรับการอ้างอิงแบบแก้ไขได้ไปยัง self ในการดูว่า Rust เรียกใช้ drop เมื่อใด ให้เราลองประยุกต์ใช้ drop ร่วมกับคำสั่ง println! สำหรับตอนนี้

โค้ดตัวอย่างที่ 15-14 แสดง struct CustomSmartPointer ซึ่งฟังก์ชันการทำงานที่กำหนดเองเพียงอย่างเดียวของมันคือจะพิมพ์ Dropping CustomSmartPointer! ออกมาเมื่ออินสแตนซ์หลุดออกจากขอบเขต เพื่อแสดงให้เห็นว่าเมื่อใดที่ Rust จะรันเมธอด drop

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("my stuff"),
    };
    let d = CustomSmartPointer {
        data: String::from("other stuff"),
    };
    println!("CustomSmartPointers created");
}

เทรต Drop ถูกรวมไว้ในพรีลูด (prelude) แล้ว ดังนั้นเราจึงไม่จำเป็นต้องนำมันเข้ามาในขอบเขตอย่างชัดเจน เราประยุกต์ใช้เทรต Drop บน CustomSmartPointer และจัดเตรียมการประยุกต์ใช้งานสำหรับเมธอด drop ที่เรียกใช้ println! เนื้อหาของเมธอด drop คือตำแหน่งที่คุณจะวางตรรกะใด ๆ ที่คุณต้องการให้รันเมื่ออินสแตนซ์ของชนิดข้อมูลของคุณหลุดออกจากขอบเขต เรากำลังพิมพ์ข้อความบางอย่างตรงนี้เพื่อสาธิตให้เห็นทางสายตาว่า Rust จะเรียกใช้ drop เมื่อใด

ใน main เราสร้างสองอินสแตนซ์ของ CustomSmartPointer แล้วพิมพ์ CustomSmartPointers created ณ จุดสิ้นสุดของ main อินสแตนซ์ของ CustomSmartPointer ของเราจะหลุดออกจากขอบเขต และ Rust จะเรียกใช้โค้ดที่เราใส่ไว้ในเมธอด drop เพื่อพิมพ์ข้อความสุดท้ายของเรา สังเกตว่าเราไม่จำเป็นต้องเรียกใช้เมธอด drop อย่างชัดเจนเลย

เมื่อเรารันโปรแกรมนี้ เราจะเห็นผลลัพธ์ต่อไปนี้:

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s
     Running `target/debug/drop-example`
CustomSmartPointers created
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!

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

น่าเสียดายที่การปิดการทำงานของฟังก์ชัน drop อัตโนมัตินั้นทำได้ไม่ตรงไปตรงมา การปิดการทำงานของ drop มักไม่จำเป็น จุดประสงค์ทั้งหมดของเทรต Drop คือมันได้รับการดูแลโดยอัตโนมัติ อย่างไรก็ตาม ในบางครั้ง คุณอาจต้องการล้างทำความสะอาดค่าก่อนเวลา ตัวอย่างหนึ่งคือเมื่อใช้พอยน์เตอร์อัจฉริยะที่จัดการกับล็อก (locks): คุณอาจต้องการบังคับเมธอด drop เพื่อปลดล็อกเพื่อให้โค้ดอื่นในขอบเขตเดียวกันสามารถครอบครองล็อกได้ Rust ไม่ยอมให้คุณเรียกใช้เมธอด drop ของเทรต Drop ด้วยตนเอง แต่คุณต้องเรียกใช้ฟังก์ชัน std::mem::drop ที่จัดเตรียมโดยไลบรารีมาตรฐานหากคุณต้องการบังคับให้ค่านั้นถูกล้างออกก่อนที่จะถึงจุดสิ้นสุดของขอบเขตของมัน

การพยายามเรียกใช้เมธอด drop ของเทรต Drop ด้วยตนเองโดยการแก้ไขฟังก์ชัน main จากโค้ดตัวอย่างที่ 15-14 จะไม่ทำงาน ดังแสดงในโค้ดตัวอย่างที่ 15-15

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created");
    c.drop();
    println!("CustomSmartPointer dropped before the end of main");
}

เมื่อเราลองคอมไพล์โค้ดนี้ เราจะได้ข้อผิดพลาดนี้:

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
error[E0040]: explicit use of destructor method
  --> src/main.rs:16:7
   |
16 |     c.drop();
   |       ^^^^ explicit destructor calls not allowed
   |
help: consider using `drop` function
   |
16 -     c.drop();
16 +     drop(c);
   |

For more information about this error, try `rustc --explain E0040`.
error: could not compile `drop-example` (bin "drop-example") due to 1 previous error

ข้อความผิดพลาดนี้ระบุว่าเราไม่ได้รับอนุญาตให้เรียกใช้ drop อย่างชัดเจน ข้อความผิดพลาดใช้คำว่า ตัวทำลายล้าง (destructor) ซึ่งเป็นคำทางโปรแกรมทั่วไปสำหรับฟังก์ชันที่ทำหน้าที่ล้างทำความสะอาดอินสแตนซ์ ตัวทำลายล้าง (destructor) มีลักษณะสอดคล้องกับ ตัวสร้าง (constructor) ซึ่งทำหน้าที่สร้างอินสแตนซ์ขึ้นมา ฟังก์ชัน drop ใน Rust ถือเป็นตัวทำลายล้างชนิดหนึ่ง

Rust ไม่ยอมให้เราเรียกใช้ drop อย่างชัดเจนเพราะ Rust จะยังคงเรียกใช้ drop บนค่านั้นโดยอัตโนมัติอีกครั้ง ณ จุดสิ้นสุดของ main สิ่งนี้จะทำให้เกิดข้อผิดพลาดการคืนหน่วยความจำซ้ำซ้อน (double free error) เนื่องจาก Rust จะพยายามล้างทำความสะอาดค่าเดิมสองครั้ง

เราไม่สามารถปิดการแทรก drop อัตโนมัติเมื่อค่าหลุดออกจากขอบเขตได้ และเราไม่สามารถเรียกใช้เมธอด drop อย่างชัดเจนได้ ดังนั้น หากเราจำเป็นต้องบังคับให้ล้างทำความสะอาดค่าก่อนเวลา เราจึงใช้ฟังก์ชัน std::mem::drop

ฟังก์ชัน std::mem::drop มีความแตกต่างจากเมธอด drop ในเทรต Drop เราเรียกใช้มันโดยส่งค่าที่เราต้องการบังคับล้างเป็นอาร์กิวเมนต์ ฟังก์ชันนี้อยู่ในพรีลูด ดังนั้นเราจึงสามารถแก้ไข main ในโค้ดตัวอย่างที่ 15-15 เพื่อเรียกใช้ฟังก์ชัน drop ได้ ดังแสดงในโค้ดตัวอย่างที่ 15-16

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created");
    drop(c);
    println!("CustomSmartPointer dropped before the end of main");
}

การรันโค้ดนี้จะพิมพ์ผลลัพธ์ดังนี้:

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/drop-example`
CustomSmartPointer created
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main

ข้อความ Dropping CustomSmartPointer with data `some data`! จะถูกพิมพ์อยู่ระหว่างข้อความ CustomSmartPointer created และ CustomSmartPointer dropped before the end of main ซึ่งแสดงให้เห็นว่าโค้ดเมธอด drop ถูกเรียกใช้เพื่อคืนหน่วยความจำ c ณ จุดนั้น

คุณสามารถใช้โค้ดที่ระบุในการประยุกต์ใช้เทรต Drop ได้หลายวิธีเพื่อทำให้การล้างทำความสะอาดสะดวกและปลอดภัย: ตัวอย่างเช่น คุณสามารถใช้มันเพื่อสร้างตัวจัดสรรหน่วยความจำ (memory allocator) ของคุณเองได้! ด้วยเทรต Drop และระบบความเป็นเจ้าของของ Rust คุณไม่ต้องคอยจำว่าต้องล้างทำความสะอาด เพราะ Rust จะทำให้โดยอัตโนมัติ

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

ตอนนี้เมื่อเราได้ตรวจสอบ Box<T> และคุณลักษณะบางประการของพอยน์เตอร์อัจฉริยะแล้ว มาดูพอยน์เตอร์อัจฉริยะอื่น ๆ ที่นิยามไว้ในไลบรารีมาตรฐานกันบ้าง