diff --git a/src/idiomatic/leveraging-the-type-system/raii.md b/src/idiomatic/leveraging-the-type-system/raii.md index 60a8455d..00698815 100644 --- a/src/idiomatic/leveraging-the-type-system/raii.md +++ b/src/idiomatic/leveraging-the-type-system/raii.md @@ -4,120 +4,110 @@ minutes: 30 # RAII: `Drop` trait -RAII (Resource Acquisition Is Initialization) means tying the lifetime of a -resource to the lifetime of a value. +RAII (**R**esource **A**cquisition **I**s **I**nitialization) ties the lifetime +of a resource to the lifetime of a value. [Rust uses RAII to manage memory](https://doc.rust-lang.org/rust-by-example/scope/raii.html), and the `Drop` trait allows you to extend this to other resources, such as file descriptors or locks. ```rust,editable -struct FileLock; -pub struct File { - stub: Option, - lock: FileLock, -} -#[derive(Debug)] -pub struct Error; +pub struct File(std::os::fd::RawFd); impl File { - pub fn open(path: &str) -> Result { - println!("acquire file descriptor: {path}"); - Ok(Self { stub: Some(1), lock: FileLock }) + pub fn open(path: &str) -> Result { + // [...] + Ok(Self(0)) } - pub fn read(&mut self) -> Result { - self.stub.take().ok_or(Error) + pub fn read_to_end(&mut self) -> Result, std::io::Error> { + // [...] + Ok(b"example".to_vec()) } - pub fn close(self) -> Result<(), Error> { - self.lock.release() - } -} - -impl FileLock { - fn release(self) -> Result<(), Error> { - println!("release file descriptor"); + pub fn close(self) -> Result<(), std::io::Error> { + // [...] Ok(()) } } -fn main() { - let mut file = File::open("example.txt").unwrap(); - - let mut content = Vec::new(); - while let Ok(byte) = file.read() { - content.push(byte); - } - - println!("content: {content:?}"); +fn main() -> Result<(), std::io::Error> { + let mut file = File::open("example.txt")?; + println!("content: {:?}", file.read_to_end()?); + Ok(()) } ```
- This example shows how easy it is to forget releasing a file descriptor when - managing it manually. In fact, the current code does not release it at all. - Did anyone notice that `file.close()` is missing? + managing it manually. The code as written does not call `file.close()`. Did + anyone in the class notice? -- Try inserting `file.close().unwrap();` at the end of `main`. Then try moving - it before the loop. Rust will reject this: once `file` is moved, it can no - longer be accessed. The borrow checker enforces this statically. +- To release the file descriptor correctly, `file.close()` must be called after + the last use — and also in early-return paths in case of errors. -- Instead of relying on the user to remember to call `close()`, we can implement - the `Drop` trait to release the resource automatically. This ties cleanup to - the lifetime of the `File` value. Note that `Drop` cannot return errors, so - any fallible logic must be handled internally or avoided. +- Instead of relying on the user to call `close()`, we can implement the `Drop` + trait to release the resource automatically. This ties cleanup to the lifetime + of the `File` value. ```rust,compile_fail - impl Drop for FileLock { + impl Drop for File { fn drop(&mut self) { println!("release file descriptor automatically"); } } ``` -- If both `drop()` and `close()` are present, the file descriptor is released +- Note that `Drop::drop` cannot return errors. Any fallible logic must be + handled internally or ignored. In the standard library, errors returned while + closing an owned file descriptor during `Drop` are silently discarded: + + +- If both `drop()` and `close()` exist, the file descriptor may be released twice. To avoid this, remove `close()` and rely solely on `Drop`. - This also illustrates that when a parent type is dropped, the `drop()` method - of its fields (such as `FileLock`) is automatically called — no extra code is - needed. +- When is `Drop::drop` called? -- Demonstrate ownership transfer by moving the file into a `read_all()` - function. The file is dropped when the local variable inside that function - goes out of scope, not in `main`. + Normally, when the `file` variable in `main` goes out of scope (either on + return or due to a panic), `drop()` is called automatically. - This differs from C++, where destructors are tied to the original scope, even - for moved-from values. + If the file is moved into another function, for example `read_all()`, the + value is dropped when that function returns — not in `main`. - The same mechanism underlies `std::mem::drop`, which lets you drop a value - early: + In contrast, C++ runs destructors in the original scope even for moved-from + values. + +- The same mechanism powers `std::mem::drop`: ```rust pub fn drop(_x: T) {} ``` -- Insert `panic!("oops")` at the start of `read_all()` to show that `drop()` is - still called during unwinding. Rust ensures this unless the panic strategy is + You can use it to force early destruction of a value before its natural end of + scope. + +- Insert `panic!("oops")` at the start of `read_to_end()` to show that `drop()` + still runs during unwinding. Rust guarantees this unless the panic strategy is set to `abort`. -- There are exceptions where destructors will not run: - - If a destructor panics during unwinding, the program aborts immediately. - - The program also aborts when using `std::process::exit()` or when compiled - with the `abort` panic strategy. +- There are cases where destructors will not run: + - If a destructor itself panics during unwinding, the program aborts + immediately. + - If the program exits with `std::process::exit()` or is compiled with the + `abort` panic strategy, destructors are skipped. ### More to Explore The `Drop` trait has another important limitation: it is not `async`. -You cannot `await` inside a destructor, which is often needed when cleaning up -asynchronous resources like sockets, database connections, or tasks that must -signal completion to another system. +This means you cannot `await` inside a destructor, which is often needed when +cleaning up asynchronous resources like sockets, database connections, or tasks +that must signal completion to another system. - Learn more: -- Available on nightly: +- There is an experimental `AsyncDrop` trait available on nightly: