Chạy Mã khi Dọn dẹp với Trait Drop

Trait quan trọng thứ hai cho mẫu con trỏ thông minh là Drop, cho phép bạn tùy chỉnh những gì xảy ra khi một giá trị sắp ra khỏi phạm vi. Bạn có thể cung cấp một triển khai cho trait Drop trên bất kỳ kiểu nào, và mã đó có thể được sử dụng để giải phóng tài nguyên như tệp hoặc kết nối mạng.

Chúng ta giới thiệu Drop trong ngữ cảnh của con trỏ thông minh vì chức năng của trait Drop gần như luôn được sử dụng khi triển khai một con trỏ thông minh. Ví dụ, khi một Box<T> bị hủy, nó sẽ giải phóng không gian trên heap mà box trỏ tới.

Trong một số ngôn ngữ, đối với một số kiểu, lập trình viên phải gọi mã để giải phóng bộ nhớ hoặc tài nguyên mỗi khi họ sử dụng xong một thể hiện của những kiểu đó. Ví dụ bao gồm file handles, sockets và locks. Nếu họ quên, hệ thống có thể bị quá tải và gặp sự cố. Trong Rust, bạn có thể chỉ định một đoạn mã cụ thể được chạy bất cứ khi nào một giá trị ra khỏi phạm vi, và trình biên dịch sẽ tự động chèn mã này. Kết quả là, bạn không cần phải cẩn thận đặt mã dọn dẹp ở mọi nơi trong chương trình mà một thể hiện của một kiểu cụ thể đã hoàn thành—bạn vẫn sẽ không rò rỉ tài nguyên!

Bạn chỉ định mã để chạy khi một giá trị ra khỏi phạm vi bằng cách triển khai trait Drop. Trait Drop yêu cầu bạn triển khai một phương thức có tên drop nhận một tham chiếu có thể thay đổi tới self. Để xem khi nào Rust gọi drop, hãy triển khai drop với các câu lệnh println! tạm thời.

Listing 15-14 hiển thị một struct CustomSmartPointer mà chức năng tùy chỉnh duy nhất của nó là sẽ in Dropping CustomSmartPointer! khi thể hiện ra khỏi phạm vi, để hiển thị khi Rust chạy phương thức 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.");
}

Trait Drop được bao gồm trong prelude, vì vậy chúng ta không cần phải đưa nó vào phạm vi. Chúng ta triển khai trait Drop trên CustomSmartPointer và cung cấp một triển khai cho phương thức drop gọi println!. Phần thân của phương thức drop là nơi bạn sẽ đặt bất kỳ logic nào mà bạn muốn chạy khi một thể hiện của kiểu của bạn ra khỏi phạm vi. Chúng ta đang in một số văn bản ở đây để minh họa trực quan khi Rust sẽ gọi drop.

Trong main, chúng ta tạo hai thể hiện của CustomSmartPointer và sau đó in CustomSmartPointers created. Ở cuối main, các thể hiện của CustomSmartPointer của chúng ta sẽ ra khỏi phạm vi, và Rust sẽ gọi mã mà chúng ta đặt trong phương thức drop, in thông báo cuối cùng của chúng ta. Lưu ý rằng chúng ta không cần phải gọi phương thức drop một cách rõ ràng.

Khi chúng ta chạy chương trình này, chúng ta sẽ thấy đầu ra sau:

$ 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 tự động gọi drop cho chúng ta khi các thể hiện của chúng ta ra khỏi phạm vi, gọi mã mà chúng ta đã chỉ định. Các biến được hủy theo thứ tự ngược lại so với việc tạo, vì vậy d đã bị hủy trước c. Mục đích của ví dụ này là cung cấp cho bạn một hướng dẫn trực quan về cách phương thức drop hoạt động; thông thường, bạn sẽ chỉ định mã dọn dẹp mà kiểu của bạn cần chạy thay vì một thông báo in.

Không may, việc vô hiệu hóa chức năng drop tự động không đơn giản. Vô hiệu hóa drop thường không cần thiết; toàn bộ ý nghĩa của trait Drop là nó được xử lý tự động. Tuy nhiên, đôi khi bạn có thể muốn dọn dẹp một giá trị sớm. Một ví dụ là khi sử dụng con trỏ thông minh quản lý khóa: bạn có thể muốn buộc phương thức drop giải phóng khóa để mã khác trong cùng phạm vi có thể lấy khóa. Rust không cho phép bạn gọi phương thức drop của trait Drop theo cách thủ công; thay vào đó, bạn phải gọi hàm std::mem::drop được cung cấp bởi thư viện chuẩn nếu bạn muốn buộc một giá trị bị hủy trước khi kết thúc phạm vi của nó.

Nếu chúng ta cố gắng gọi phương thức drop của trait Drop theo cách thủ công bằng cách sửa đổi hàm main từ Listing 15-14, như trong Listing 15-15, chúng ta sẽ gặp lỗi trình biên dịch.

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.");
}

Khi chúng ta cố gắng biên dịch mã này, chúng ta sẽ gặp lỗi này:

$ 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 |     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

Thông báo lỗi này nêu rõ rằng chúng ta không được phép gọi drop một cách rõ ràng. Thông báo lỗi sử dụng thuật ngữ destructor, đó là thuật ngữ lập trình chung cho một hàm dọn dẹp một thể hiện. Một destructor tương tự như một constructor, tạo ra một thể hiện. Hàm drop trong Rust là một destructor cụ thể.

Rust không cho phép chúng ta gọi drop một cách rõ ràng vì Rust vẫn sẽ tự động gọi drop trên giá trị ở cuối main. Điều này sẽ gây ra lỗi double free vì Rust sẽ cố gắng dọn dẹp cùng một giá trị hai lần.

Chúng ta không thể vô hiệu hóa việc chèn drop tự động khi một giá trị ra khỏi phạm vi, và chúng ta không thể gọi phương thức drop một cách rõ ràng. Vì vậy, nếu chúng ta cần buộc một giá trị được dọn dẹp sớm, chúng ta sử dụng hàm std::mem::drop.

Hàm std::mem::drop khác với phương thức drop trong trait Drop. Chúng ta gọi nó bằng cách truyền một đối số là giá trị mà chúng ta muốn buộc hủy. Hàm này nằm trong prelude, vì vậy chúng ta có thể sửa đổi main trong Listing 15-15 để gọi hàm drop, như trong Listing 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.");
}

Chạy mã này sẽ in ra như sau:

$ 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.

Văn bản Dropping CustomSmartPointer with data `some data`! được in giữa văn bản CustomSmartPointer created.CustomSmartPointer dropped before the end of main., cho thấy rằng mã phương thức drop được gọi để hủy c tại thời điểm đó.

Bạn có thể sử dụng mã được chỉ định trong triển khai trait Drop theo nhiều cách để làm cho việc dọn dẹp trở nên thuận tiện và an toàn: ví dụ, bạn có thể sử dụng nó để tạo bộ cấp phát bộ nhớ của riêng bạn! Với trait Drop và hệ thống sở hữu của Rust, bạn không phải nhớ dọn dẹp vì Rust làm điều đó tự động.

Bạn cũng không phải lo lắng về các vấn đề do vô tình dọn dẹp các giá trị vẫn đang được sử dụng: hệ thống sở hữu đảm bảo các tham chiếu luôn hợp lệ cũng đảm bảo rằng drop chỉ được gọi một lần khi giá trị không còn được sử dụng.

Bây giờ chúng ta đã xem xét Box<T> và một số đặc điểm của con trỏ thông minh, hãy xem một vài con trỏ thông minh khác được định nghĩa trong thư viện chuẩn.