1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-08-08 08:22:52 +02:00

improve RAII intro segment further based on Luca's feedbacl

This commit is contained in:
Glen De Cauwsemaecker
2025-08-01 21:08:01 +02:00
parent 0baa990306
commit 9fa819a86c

View File

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