You've already forked comprehensive-rust
mirror of
https://github.com/google/comprehensive-rust.git
synced 2025-12-17 05:19:22 +02:00
RAII chapter for idiomatic rust (#2820)
This PR adds the RAII chapter for the idiomatic Rust deep dive.
This commit is contained in:
committed by
GitHub
parent
e42c8b36d4
commit
05b67d12a5
@@ -441,6 +441,15 @@
|
||||
- [Semantic Confusion](idiomatic/leveraging-the-type-system/newtype-pattern/semantic-confusion.md)
|
||||
- [Parse, Don't Validate](idiomatic/leveraging-the-type-system/newtype-pattern/parse-don-t-validate.md)
|
||||
- [Is It Encapsulated?](idiomatic/leveraging-the-type-system/newtype-pattern/is-it-encapsulated.md)
|
||||
- [RAII](idiomatic/leveraging-the-type-system/raii.md)
|
||||
- [Drop Skipped](idiomatic/leveraging-the-type-system/raii/drop_skipped.md)
|
||||
- [Mutex](idiomatic/leveraging-the-type-system/raii/mutex.md)
|
||||
- [Drop Guards](idiomatic/leveraging-the-type-system/raii/drop_guards.md)
|
||||
- [Drop Bomb](idiomatic/leveraging-the-type-system/raii/drop_bomb.md)
|
||||
- [Drop Bomb Forget](idiomatic/leveraging-the-type-system/raii/drop_bomb_forget.md)
|
||||
- [forget and drop functions](idiomatic/leveraging-the-type-system/raii/forget_and_drop.md)
|
||||
- [Scope Guard](idiomatic/leveraging-the-type-system/raii/scope_guard.md)
|
||||
- [Drop Option](idiomatic/leveraging-the-type-system/raii/drop_option.md)
|
||||
- [Extension Traits](idiomatic/leveraging-the-type-system/extension-traits.md)
|
||||
- [Extending Foreign Types](idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-types.md)
|
||||
- [Method Resolution Conflicts](idiomatic/leveraging-the-type-system/extension-traits/method-resolution-conflicts.md)
|
||||
|
||||
94
src/idiomatic/leveraging-the-type-system/raii.md
Normal file
94
src/idiomatic/leveraging-the-type-system/raii.md
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
minutes: 15
|
||||
---
|
||||
|
||||
# RAII: `Drop` trait
|
||||
|
||||
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
|
||||
pub struct File(std::os::fd::RawFd);
|
||||
|
||||
impl File {
|
||||
pub fn open(path: &str) -> Result<Self, std::io::Error> {
|
||||
// [...]
|
||||
Ok(Self(0))
|
||||
}
|
||||
|
||||
pub fn read_to_end(&mut self) -> Result<Vec<u8>, std::io::Error> {
|
||||
// [...]
|
||||
Ok(b"example".to_vec())
|
||||
}
|
||||
|
||||
pub fn close(self) -> Result<(), std::io::Error> {
|
||||
// [...]
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), std::io::Error> {
|
||||
let mut file = File::open("example.txt")?;
|
||||
println!("content: {:?}", file.read_to_end()?);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- Easy to miss: `file.close()` is never called. Ask the class if they noticed.
|
||||
|
||||
- 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 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 File {
|
||||
fn drop(&mut self) {
|
||||
// libc::close(...);
|
||||
println!("file descriptor was closed");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Note that `Drop::drop()` cannot return a `Result`. Any failures must be
|
||||
handled internally or ignored. In the standard library, errors during FD
|
||||
closure inside `Drop` are silently discarded. See the implementation:
|
||||
<https://doc.rust-lang.org/src/std/os/fd/owned.rs.html#169-196>
|
||||
|
||||
- When is `Drop::drop` called?
|
||||
|
||||
Normally, when the `file` variable in `main()` goes out of scope (either on
|
||||
return or due to a panic), `drop()` is called automatically.
|
||||
|
||||
If the file is moved into another function (as is this case with
|
||||
`File::close()`), the value is dropped when that function returns — not in
|
||||
`main`.
|
||||
|
||||
In contrast, C++ runs destructors in the original scope even for moved-from
|
||||
values.
|
||||
|
||||
- Demo: insert `panic!("oops")` at the start of `read_to_end()` and run it.
|
||||
`drop()` still runs during unwinding.
|
||||
|
||||
### More to Explore
|
||||
|
||||
The `Drop` trait has another important limitation: it is not `async`.
|
||||
|
||||
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:
|
||||
<https://rust-lang.github.io/async-fundamentals-initiative/roadmap/async_drop.html>
|
||||
- There is an experimental `AsyncDrop` trait available on nightly:
|
||||
<https://doc.rust-lang.org/nightly/std/future/trait.AsyncDrop.html>
|
||||
|
||||
</details>
|
||||
92
src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md
Normal file
92
src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
minutes: 15
|
||||
---
|
||||
|
||||
# Drop Bombs: Enforcing API Correctness
|
||||
|
||||
Use `Drop` to enforce invariants and detect incorrect API usage. A "drop bomb"
|
||||
panics if a value is dropped without being explicitly finalized.
|
||||
|
||||
This pattern is often used when the finalizing operation (like `commit()` or
|
||||
`rollback()`) needs to return a `Result`, which cannot be done from `Drop`.
|
||||
|
||||
```rust,editable
|
||||
use std::io::{self, Write};
|
||||
|
||||
struct Transaction {
|
||||
active: bool,
|
||||
}
|
||||
|
||||
impl Transaction {
|
||||
fn start() -> Self {
|
||||
Self { active: true }
|
||||
}
|
||||
|
||||
fn commit(mut self) -> io::Result<()> {
|
||||
writeln!(io::stdout(), "COMMIT")?;
|
||||
self.active = false;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Transaction {
|
||||
fn drop(&mut self) {
|
||||
if self.active {
|
||||
panic!("Transaction dropped without commit!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let tx = Transaction::start();
|
||||
// Use `tx` to build the transaction, then commit it.
|
||||
// Comment out the call to `commit` to see the panic.
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- In some systems, a value must be finalized by a specific API before it is
|
||||
dropped.
|
||||
|
||||
For example, a `Transaction` might need to be committed or rolled back.
|
||||
|
||||
- A drop bomb ensures that a value like `Transaction` cannot be silently dropped
|
||||
in an unfinished state. The destructor panics if the transaction has not been
|
||||
explicitly finalized (for example, with `commit()`).
|
||||
|
||||
- The finalizing operation (such as `commit()`) usually takes `self` by value.
|
||||
This ensures that once the transaction is finalized, the original object can
|
||||
no longer be used.
|
||||
|
||||
- A common reason to use this pattern is when cleanup cannot be done in `Drop`,
|
||||
either because it is fallible or asynchronous.
|
||||
|
||||
- This pattern is appropriate even in public APIs. It can help users catch bugs
|
||||
early when they forget to explicitly finalize a transactional object.
|
||||
|
||||
- If cleanup can safely happen in `Drop`, some APIs choose to panic only in
|
||||
debug builds. Whether this is appropriate depends on the guarantees your API
|
||||
must enforce.
|
||||
|
||||
- Panicking in release builds is reasonable when silent misuse would cause major
|
||||
correctness or security problems.
|
||||
|
||||
- Question: Why do we need an `active` flag inside `Transaction`? Why can't
|
||||
`drop()` panic unconditionally?
|
||||
|
||||
Expected answer: `commit()` takes `self` by value and runs `drop()`, which
|
||||
would panic.
|
||||
|
||||
## More to explore
|
||||
|
||||
Several related patterns help enforce correct teardown or prevent accidental
|
||||
drops.
|
||||
|
||||
- The [`drop_bomb` crate](https://docs.rs/drop_bomb/latest/drop_bomb/): A small
|
||||
utility that panics if dropped unless explicitly defused with `.defuse()`.
|
||||
Comes with a `DebugDropBomb` variant that only activates in debug builds.
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
minutes: 10
|
||||
---
|
||||
|
||||
# Drop Bombs: using `std::mem::forget`
|
||||
|
||||
```rust,editable
|
||||
use std::io::{self, Write};
|
||||
|
||||
struct Transaction;
|
||||
|
||||
impl Transaction {
|
||||
fn start() -> Self {
|
||||
Transaction
|
||||
}
|
||||
|
||||
fn commit(self) -> io::Result<()> {
|
||||
writeln!(io::stdout(), "COMMIT")?;
|
||||
|
||||
// Defuse the drop bomb by preventing Drop from ever running.
|
||||
std::mem::forget(self);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Transaction {
|
||||
fn drop(&mut self) {
|
||||
// This is the "drop bomb"
|
||||
panic!("Transaction dropped without commit!");
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let tx = Transaction::start();
|
||||
// Use `tx` to build the transaction, then commit it.
|
||||
// Comment out the call to `commit` to see the panic.
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
This example removes the flag from the previous slide and makes the drop method
|
||||
panic unconditionally. To avoid that panic on a successful commit, the commit
|
||||
method now takes ownership of the transaction and calls
|
||||
[`std::mem::forget`](https://doc.rust-lang.org/std/mem/fn.forget.html), which
|
||||
prevents the `Drop::drop()` method from running.
|
||||
|
||||
If the forgotten value owned heap allocated memory that would normally be freed
|
||||
in its `drop()` implementation, one consequence is a memory leak. That is not
|
||||
the case for the `Transaction` in the example above, since it does not own any
|
||||
heap memory.
|
||||
|
||||
We can avoid needing a runtime flag by using `mem::forget()` in a tactical way.
|
||||
When the transaction commits successfully, we can defuse the drop bomb by
|
||||
calling `std::mem::forget` on the value, which prevents its `Drop`
|
||||
implementation from running.
|
||||
|
||||
</details>
|
||||
69
src/idiomatic/leveraging-the-type-system/raii/drop_guards.md
Normal file
69
src/idiomatic/leveraging-the-type-system/raii/drop_guards.md
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
minutes: 10
|
||||
---
|
||||
|
||||
# Drop Guards
|
||||
|
||||
A **drop guard** in Rust is a temporary object that performs some kind of
|
||||
cleanup when it goes out of scope. In the case of `Mutex`, the `lock` method
|
||||
returns a `MutexGuard` that automatically unlocks the mutex on `drop`:
|
||||
|
||||
```rust
|
||||
struct Mutex {
|
||||
is_locked: bool,
|
||||
}
|
||||
|
||||
struct MutexGuard<'a> {
|
||||
mutex: &'a mut Mutex,
|
||||
}
|
||||
|
||||
impl Mutex {
|
||||
fn new() -> Self {
|
||||
Self { is_locked: false }
|
||||
}
|
||||
|
||||
fn lock(&mut self) -> MutexGuard<'_> {
|
||||
self.is_locked = true;
|
||||
MutexGuard { mutex: self }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MutexGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.mutex.is_locked = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- The example above shows a simplified `Mutex` and its associated guard.
|
||||
|
||||
- Even though it is not a production-ready implementation, it illustrates the
|
||||
core idea:
|
||||
|
||||
- the guard represents exclusive access,
|
||||
- and its `Drop` implementation releases the lock when it goes out of scope.
|
||||
|
||||
## More to Explore
|
||||
|
||||
This example shows a C++ style mutex that does not contain the data it protects.
|
||||
While this is non-idiomatic in Rust, the goal here is only to illustrate the
|
||||
core idea of a drop guard, not to demonstrate a proper Rust mutex design.
|
||||
|
||||
For brevity, several features are omitted:
|
||||
|
||||
- A real `Mutex<T>` stores the protected value inside the mutex.\
|
||||
This toy example omits the value entirely to focus only on the drop guard
|
||||
mechanism.
|
||||
- Ergonomic access via `Deref` and `DerefMut` on `MutexGuard` (letting the guard
|
||||
behave like a `&T` or `&mut T`).
|
||||
- A fully blocking `.lock()` method and a non-blocking `try_lock` variant.
|
||||
|
||||
You can explore the
|
||||
[`Mutex` implementation in Rust’s std library](https://doc.rust-lang.org/std/sync/struct.Mutex.html)
|
||||
as an example of a production-ready mutex. The
|
||||
[`Mutex` from the `parking_lot` crate](https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html)
|
||||
is another worthwhile reference.
|
||||
|
||||
</details>
|
||||
88
src/idiomatic/leveraging-the-type-system/raii/drop_option.md
Normal file
88
src/idiomatic/leveraging-the-type-system/raii/drop_option.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
minutes: 10
|
||||
---
|
||||
|
||||
# Drop: Option
|
||||
|
||||
```rust,editable
|
||||
struct File(Option<Handle>);
|
||||
|
||||
impl File {
|
||||
fn open(path: &'static str) -> std::io::Result<Self> {
|
||||
Ok(Self(Some(Handle { path })))
|
||||
}
|
||||
|
||||
fn write(&mut self, data: &str) -> std::io::Result<()> {
|
||||
match &mut self.0 {
|
||||
Some(handle) => println!("write '{data}' to file '{}'", handle.path),
|
||||
None => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn close(mut self) -> std::io::Result<&'static str> {
|
||||
Ok(self.0.take().unwrap().path)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for File {
|
||||
fn drop(&mut self) {
|
||||
if let Some(handle) = self.0.take() {
|
||||
println!("automatically closing handle for file: {}", handle.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Handle {
|
||||
path: &'static str,
|
||||
}
|
||||
impl Drop for Handle {
|
||||
fn drop(&mut self) {
|
||||
println!("closed handle for file: {}", self.path)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let mut file = File::open("foo.txt")?;
|
||||
file.write("hello")?;
|
||||
println!("manually closed file: {}", file.close()?);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- In this example we want to let the user call `close()` manually so that errors
|
||||
from closing the file can be reported explicitly.
|
||||
|
||||
- At the same time we still want RAII semantics: if the user forgets to call
|
||||
`close()`, the handle must be cleaned up automatically in `Drop`.
|
||||
|
||||
- Wrapping the handle in an `Option` gives us both behaviors. `close()` extracts
|
||||
the handle with `take()`, and `Drop` only runs cleanup if a handle is still
|
||||
present.
|
||||
|
||||
Demo: remove the `.close()` call and run the code — `Drop` now prints the
|
||||
automatic cleanup.
|
||||
|
||||
- The main downside is ergonomics. `Option` forces us to handle both the `Some`
|
||||
and `None` case even in places where, logically, `None` cannot occur. Rust’s
|
||||
type system cannot express that relationship between `File` and its `Handle`,
|
||||
so we handle both cases manually.
|
||||
|
||||
## More to explore
|
||||
|
||||
Instead of `Option` we could use
|
||||
[`ManuallyDrop`](https://doc.rust-lang.org/std/mem/struct.ManuallyDrop.html),
|
||||
which suppresses automatic destruction by preventing Rust from calling `Drop`
|
||||
for the value; you must handle teardown yourself.
|
||||
|
||||
The [_scopeguard_ example](./scope_guard.md) on the previous slide shows how
|
||||
`ManuallyDrop` can replace `Option` to avoid handling `None` in places where the
|
||||
value should always exist.
|
||||
|
||||
In such designs we typically track the drop state with a separate flag next to
|
||||
the `ManuallyDrop<Handle>`, which lets us track whether the handle has already
|
||||
been manually consumed.
|
||||
|
||||
</details>
|
||||
122
src/idiomatic/leveraging-the-type-system/raii/drop_skipped.md
Normal file
122
src/idiomatic/leveraging-the-type-system/raii/drop_skipped.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
minutes: 15
|
||||
---
|
||||
|
||||
# Drop can be skipped
|
||||
|
||||
There are cases where destructors will not run.
|
||||
|
||||
```rust,editable
|
||||
#[derive(Debug)]
|
||||
struct OwnedFd(i32);
|
||||
|
||||
impl Drop for OwnedFd {
|
||||
fn drop(&mut self) {
|
||||
println!("OwnedFd::drop() called with raw fd: {:?}", self.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TmpFile {
|
||||
fn drop(&mut self) {
|
||||
println!("TmpFile::drop() called with owned fd: {:?}", self.0);
|
||||
// libc::unlink("/tmp/file")
|
||||
// panic!("TmpFile::drop() panics");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TmpFile(OwnedFd);
|
||||
|
||||
impl TmpFile {
|
||||
fn open() -> Self {
|
||||
Self(OwnedFd(2))
|
||||
}
|
||||
|
||||
fn close(&self) {
|
||||
panic!("TmpFile::close(): not implemented yet");
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let owned_fd = OwnedFd(1);
|
||||
|
||||
let file = TmpFile::open();
|
||||
|
||||
std::process::exit(0);
|
||||
|
||||
// std::mem::forget(file);
|
||||
|
||||
// file.close();
|
||||
|
||||
let _ = owned_fd;
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- Drop is not guaranteed to always run. There is a number of cases when drop is
|
||||
skipped: the program can crash or exit, the value with the drop implementation
|
||||
can be leaked etc.
|
||||
|
||||
- In the version that calls
|
||||
[`std::process::exit`](https://doc.rust-lang.org/std/process/fn.exit.html),
|
||||
`TmpFile::drop()` is never run because `exit()` terminates the process
|
||||
immediately without any opportunity for a `drop()` method to be called.
|
||||
|
||||
- You can prevent accidental use of `exit` by denying the
|
||||
[`clippy::exit`](https://rust-lang.github.io/rust-clippy/stable/index.html#exit)
|
||||
lint.
|
||||
|
||||
- If you remove the `std::process::exit(0)` line, each `drop()` method in this
|
||||
simple case will run in turn.
|
||||
|
||||
- Try uncommenting the
|
||||
[`std::mem::forget`](https://doc.rust-lang.org/std/mem/fn.forget.html) call.
|
||||
What do you think will happen?
|
||||
|
||||
`mem::forget()` takes ownership and "forgets" about the value `file` without
|
||||
running its **destructor** `Drop::drop()`. The destructor of `owned_fd` is
|
||||
still run.
|
||||
|
||||
- Remove the `mem::forget()` call, then uncomment the `file.close()` call below
|
||||
it. What do you expect now?
|
||||
|
||||
With the default `panic = "unwind"` setting, the stack still unwinds and
|
||||
destructors run, even when the panic starts in `main`.
|
||||
|
||||
- With
|
||||
[`panic = "abort"`](https://doc.rust-lang.org/cargo/reference/profiles.html#panic)
|
||||
no destructors are run.
|
||||
|
||||
- As a last step, uncomment the `panic!` inside `TmpFile::drop()` and run it.
|
||||
Ask the class: which destructors run before the abort?
|
||||
|
||||
After a double panic, Rust no longer guarantees that remaining destructors
|
||||
will run:
|
||||
|
||||
- Some cleanup that was already in progress may still complete (for example,
|
||||
field destructors of the value currently being dropped),
|
||||
- but anything scheduled later in the unwind path might be skipped entirely.
|
||||
- This is why we say you cannot rely solely on `drop()` for critical external
|
||||
cleanup, nor assume that a double panic aborts without running any further
|
||||
destructors.
|
||||
|
||||
- Some languages forbid or restrict exceptions in destructors. Rust allows
|
||||
panicking in `Drop::drop`, but it is almost never a good idea, since it can
|
||||
disrupt unwinding and lead to unpredictable cleanup. It is best avoided unless
|
||||
there is a very specific need, such as in the case of a **drop bomb**.
|
||||
|
||||
- Drop is suitable for cleaning up resources within the scope of a process, but
|
||||
it is not the right tool for providing hard guarantees that something happens
|
||||
outside of the process (e.g., on local disk, or in another service in a
|
||||
distributed system).
|
||||
|
||||
- For example, deleting a temporary file in `drop()` is fine in a toy example,
|
||||
but in a real program you would still need an external cleanup mechanism such
|
||||
as a temp file reaper.
|
||||
|
||||
- In contrast, we can rely on `drop()` to unlock a mutex, since it is a
|
||||
process-local resource. If `drop()` is skipped and the mutex is left locked,
|
||||
it has no lasting effects outside the process.
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
minutes: 10
|
||||
---
|
||||
|
||||
# forget and drop functions
|
||||
|
||||
Below are the signatures for the
|
||||
[`drop()`](https://doc.rust-lang.org/std/mem/fn.drop.html) and
|
||||
[`forget()`](https://doc.rust-lang.org/std/mem/fn.forget.html) functions:
|
||||
|
||||
```rust
|
||||
// std::mem::forget
|
||||
fn forget<T>(t: T) {
|
||||
let _ = std::mem::ManuallyDrop::new(t);
|
||||
}
|
||||
|
||||
// std::mem::drop
|
||||
fn drop<T>(_x: T) {}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- Both `mem::forget()` and `mem::drop()` take ownership of the value `t`.
|
||||
|
||||
- Despite having the same function signature, they have opposite effects:
|
||||
|
||||
- `forget()` uses
|
||||
[`ManuallyDrop`](https://doc.rust-lang.org/std/mem/struct.ManuallyDrop.html)
|
||||
to prevent the destructor `Drop::drop()` from being invoked.
|
||||
|
||||
This is useful for scenarios such as implementing a drop bomb or otherwise
|
||||
opting out of destructor behavior.
|
||||
|
||||
Be careful though, since any resources the value exclusively owns such as
|
||||
heap allocated memory or file handles will remain in an unreachable state.
|
||||
|
||||
- `drop()` is a convenience function for disposing of a value. Because `t` is
|
||||
moved into the function, it is automatically dropped which triggers its
|
||||
`Drop::drop()` implementation before the parent function returns.
|
||||
|
||||
</details>
|
||||
43
src/idiomatic/leveraging-the-type-system/raii/mutex.md
Normal file
43
src/idiomatic/leveraging-the-type-system/raii/mutex.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
minutes: 10
|
||||
---
|
||||
|
||||
# Mutex and MutexGuard
|
||||
|
||||
In earlier examples, RAII was used to manage concrete resources like file
|
||||
descriptors. With a `Mutex`, the "resource" is mutable access to a value. You
|
||||
access the value by calling `lock`, which then returns a `MutexGuard` which will
|
||||
unlock the `Mutex` automatically when dropped.
|
||||
|
||||
```rust
|
||||
use std::sync::Mutex;
|
||||
|
||||
fn main() {
|
||||
let m = Mutex::new(vec![1, 2, 3]);
|
||||
|
||||
let mut guard = m.lock().unwrap();
|
||||
guard.push(4);
|
||||
guard.push(5);
|
||||
println!("{guard:?}");
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- A `Mutex` controls exclusive access to a value. Unlike earlier RAII examples,
|
||||
the resource here is logical: temporary exclusive access to the data inside.
|
||||
|
||||
- This right is represented by a `MutexGuard`. Only one guard for this mutex can
|
||||
exist at a time. While it lives, it provides `&mut T` access.
|
||||
|
||||
- Although `lock()` takes `&self`, it returns a `MutexGuard` with mutable
|
||||
access. This works through _interior mutability_, where a type manages its own
|
||||
borrowing rules internally to allow mutation through `&self`.
|
||||
|
||||
- `MutexGuard` implements `Deref` and `DerefMut`, making access ergonomic. You
|
||||
lock the mutex and use the guard like a `&mut T`.
|
||||
|
||||
- The mutex is released by `MutexGuard::drop()`. You never call an explicit
|
||||
unlock function.
|
||||
|
||||
</details>
|
||||
78
src/idiomatic/leveraging-the-type-system/raii/scope_guard.md
Normal file
78
src/idiomatic/leveraging-the-type-system/raii/scope_guard.md
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
minutes: 15
|
||||
---
|
||||
|
||||
# Scope Guards
|
||||
|
||||
A scope guard uses the `Drop` trait to run cleanup code automatically when a
|
||||
scope exits, even during unwinding.
|
||||
|
||||
```rust,editable,compile_fail
|
||||
use scopeguard::{ScopeGuard, guard};
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
|
||||
fn download_successful() -> bool {
|
||||
// [...]
|
||||
true
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let path = "download.tmp";
|
||||
let mut file = File::create(path).expect("cannot create temporary file");
|
||||
|
||||
// Set up cleanup immediately after file creation
|
||||
let cleanup = guard(path, |path| {
|
||||
println!("download failed, deleting: {:?}", path);
|
||||
let _ = fs::remove_file(path);
|
||||
});
|
||||
|
||||
writeln!(file, "partial data...").unwrap();
|
||||
|
||||
if download_successful() {
|
||||
// Download succeeded, keep the file
|
||||
let path = ScopeGuard::into_inner(cleanup);
|
||||
println!("Download '{path}' complete!");
|
||||
}
|
||||
// Otherwise, the guard runs and deletes the file
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- This example models a download workflow. We create a temporary file first,
|
||||
then use a scope guard to ensure that the file is deleted if the download
|
||||
fails.
|
||||
|
||||
- The `scopeguard` crate allows you to conveniently define a single-use
|
||||
`Drop`-based cleanup without defining a custom type with a custom `Drop`
|
||||
implementation.
|
||||
|
||||
- The guard is created directly after creating the file, so even if `writeln!()`
|
||||
fails, the file will still be cleaned up. This ordering is essential for
|
||||
correctness.
|
||||
|
||||
- The `guard()` creates a `ScopeGuard` instance. It a user-defined value (in
|
||||
this case, `path`) and the cleanup closure that later receives this value.
|
||||
|
||||
- The guard's closure runs on scope exit unless it is _defused_ with
|
||||
`ScopeGuard::into_inner` (removing the value so the guard does nothing on
|
||||
drop). In the success path, we call `into_inner` so the guard will not delete
|
||||
the file.
|
||||
|
||||
- A scope guard is similar to the `defer` feature in Go.
|
||||
|
||||
- This pattern is ideal for "cleanup on failure" scenarios, where a cleanup
|
||||
should run by default unless a success path is explicitly taken.
|
||||
|
||||
- This pattern is also useful when you don't control the cleanup strategy of the
|
||||
resource object. In this example, `File::drop()` closes the file but does not
|
||||
delete it, and we can't change the standard library to delete the file instead
|
||||
(nor should we, it is not a good idea anyway).
|
||||
|
||||
- The `scopeguard` crate also supports cleanup strategies via the
|
||||
[`Strategy`](https://docs.rs/scopeguard/latest/scopeguard/trait.Strategy.html)
|
||||
trait. You can choose to run the guard on unwind only, or on success only, not
|
||||
just always.
|
||||
|
||||
</details>
|
||||
@@ -32,6 +32,13 @@ decisions within the context and constraints of your own projects.
|
||||
The course will cover the topics listed below. Each topic may be covered in one
|
||||
or more slides, depending on its complexity and relevance.
|
||||
|
||||
## Target Audience
|
||||
|
||||
Engineers with at least 2-3 years of coding experience in C, C++11 or newer,
|
||||
Java 7 or newer, Python 2 or 3, Go or any other similar imperative programming
|
||||
language. We have no expectation of experience with more modern or feature-rich
|
||||
languages like Swift, Kotlin, C#, or TypeScript.
|
||||
|
||||
### Foundations of API design
|
||||
|
||||
- Golden rule: prioritize clarity and readability at the callsite. People will
|
||||
|
||||
Reference in New Issue
Block a user