Step 5 of 7
RAII guards and Drop
RAII ("resource acquisition is initialization", a name from C++) means: tie the cleanup of a resource to the lifetime of a value. When the value goes out of scope, its Drop code runs and the cleanup happens, no matter how the scope ends: normally, by return, by ?, by break, or by a panic unwinding the stack. You've relied on it all along: a File closes itself, a MutexGuard unlocks, a Vec frees its memory.
A guard is a small value you create just for its Drop. It's the Rust answer to try/finally and defer, and it can't be forgotten on an early return.
struct Account {
balance: i64,
log: Vec<String>,
}
struct Transaction<'a> {
account: &'a mut Account,
start_balance: i64,
committed: bool,
}
impl<'a> Transaction<'a> {
fn begin(account: &'a mut Account) -> Self {
let start_balance = account.balance;
Transaction { account, start_balance, committed: false }
}
fn withdraw(&mut self, amount: i64) -> Result<(), String> {
if amount > self.account.balance {
return Err(format!("cannot withdraw {amount}, balance is {}", self.account.balance));
}
self.account.balance -= amount;
Ok(())
}
fn commit(mut self) {
self.committed = true;
self.account.log.push(String::from("committed"));
}
}
impl Drop for Transaction<'_> {
fn drop(&mut self) {
if !self.committed {
self.account.balance = self.start_balance;
self.account.log.push(String::from("rolled back"));
}
}
}
fn pay_bills(account: &mut Account) -> Result<(), String> {
let mut tx = Transaction::begin(account);
tx.withdraw(700)?;
tx.withdraw(300)?;
tx.commit();
Ok(())
}
fn main() {
for start in [1500, 900] {
let mut acc = Account { balance: start, log: Vec::new() };
let result = pay_bills(&mut acc);
println!("{result:?} -> balance {} {:?}", acc.balance, acc.log);
}
}
Ok(()) -> balance 500 ["committed"]
Err("cannot withdraw 300, balance is 200") -> balance 900 ["rolled back"]
Piece by piece:
Transactionholds&mut Account: while it's alive, nobody else can touch the account. The borrow checker enforces the "transaction" for free.- In the second run, the second
withdrawfails and?returns early.txgoes out of scope,Dropruns, seescommitted == false, and restores the balance. There's no rollback code inpay_billsat all. commit(mut self)takes the guard by value: after committing you can't use the transaction again, and whencommitends,selfis dropped withcommitted == true, so nothing is undone.- Values are dropped in reverse order of declaration, so guards nest naturally.
let _guard vs let _
A subtle trap: let _ = guard(); drops the guard immediately, because _ doesn't bind anything. let _guard = guard(); keeps it alive until the end of the scope.
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
fn main() {
let _a = Noisy("a");
let _ = Noisy("b");
println!("body");
{
let _c = Noisy("c");
println!("inner");
}
drop(Noisy("d"));
println!("end of main");
}
drop b
body
inner
drop c
drop d
end of main
drop a
drop(x) (the function std::mem::drop) ends a value's life early, for example to release a lock before slow work.
Common mistakes
- Fallible cleanup in
Drop.dropcan't return an error, and panicking inside it is a bad idea. When cleanup can fail (flushing a file), offer an explicitfn close(self) -> Result<..>and keepDropas the fallback, likeBufWriterdoes. std::process::exitskipsDrop, as the CLI module said. So doesstd::mem::forget. Guards protect you from early returns and panics, not from killing the process.- Calling
x.drop()directly isn't allowed:error[E0040]: explicit use of destructor method. Usedrop(x).
Your task
Count active work with a guard. Tracker::enter(&self) increments active, updates peak (the highest active ever seen) and returns an ActiveGuard; dropping the guard decrements active. The fields are Cell<u32> so they can change through &self (interior mutability, from the smart pointers module).
Challenge 2
An undo guard for a list. Undo::begin(list) remembers the list's length; push adds items through the guard; keep(self) makes the changes permanent. If the guard is dropped without keep, the list is truncated back to its old length.
Challenge 3
A temporary file that cleans up after itself. TempFile::new(path, contents) writes the file and returns the guard (or the I/O error); path() returns the path; dropping the guard deletes the file (ignore errors while deleting: let _ = std::fs::remove_file(..)).
Previous: Conversions and flexible parameters: From, Into, AsRef Next: Returning iterators