Skip to content

Commit

Permalink
Split interior mutability, mention OnceCell/OnceLock (#2573)
Browse files Browse the repository at this point in the history
These types are really only useful as a static or in a user-defined
type, neither of which are covered at this point.
  • Loading branch information
djmitche authored Jan 20, 2025
1 parent 5f7e0c3 commit 8121e7d
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 68 deletions.
2 changes: 2 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@
- [Borrow Checking](borrowing/borrowck.md)
- [Borrow Errors](borrowing/examples.md)
- [Interior Mutability](borrowing/interior-mutability.md)
- [`Cell`](borrowing/interior-mutability/cell.md)
- [`RefCell`](borrowing/interior-mutability/refcell.md)
- [Exercise: Health Statistics](borrowing/exercise.md)
- [Solution](borrowing/solution.md)
- [Lifetimes](lifetimes.md)
Expand Down
69 changes: 1 addition & 68 deletions src/borrowing/interior-mutability.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,77 +12,10 @@ The "interior mutability" pattern allows exclusive (mutable) access behind a
shared reference. The standard library provides several ways to do this, all
while still ensuring safety, typically by performing a runtime check.

## `Cell`

`Cell` wraps a value and allows getting or setting the value using only a shared
reference to the `Cell`. However, it does not allow any references to the inner
value. Since there are no references, borrowing rules cannot be broken.

```rust,editable
use std::cell::Cell;
fn main() {
// Note that `cell` is NOT declared as mutable.
let cell = Cell::new(5);
cell.set(123);
println!("{}", cell.get());
}
```

## `RefCell`

`RefCell` allows accessing and mutating a wrapped value by providing alternative
types `Ref` and `RefMut` that emulate `&T`/`&mut T` without actually being Rust
references.

These types perform dynamic checks using a counter in the `RefCell` to prevent
existence of a `RefMut` alongside another `Ref`/`RefMut`.

By implementing `Deref` (and `DerefMut` for `RefMut`), these types allow calling
methods on the inner value without allowing references to escape.

```rust,editable
use std::cell::RefCell;
fn main() {
// Note that `cell` is NOT declared as mutable.
let cell = RefCell::new(5);
{
let mut cell_ref = cell.borrow_mut();
*cell_ref = 123;
// This triggers an error at runtime.
// let other = cell.borrow();
// println!("{}", *other);
}
println!("{cell:?}");
}
```

<details>

The main thing to take away from this slide is that Rust provides _safe_ ways to
modify data behind a shared reference. There are a variety of ways to ensure
that safety, and `RefCell` and `Cell` are two of them.

- `RefCell` enforces Rust's usual borrowing rules (either multiple shared
references or a single exclusive reference) with a runtime check. In this
case, all borrows are very short and never overlap, so the checks always
succeed.

- The extra block in the `RefCell` example is to end the borrow created by the
call to `borrow_mut` before we print the cell. Trying to print a borrowed
`RefCell` just shows the message `"{borrowed}"`.

- `Cell` is a simpler means to ensure safety: it has a `set` method that takes
`&self`. This needs no runtime check, but requires moving values, which can
have its own cost.

- Both `RefCell` and `Cell` are `!Sync`, which means `&RefCell` and `&Cell`
can't be passed between threads. This prevents two threads trying to access
the cell at once.
that safety, and the next sub-slides present a few of them.

</details>
25 changes: 25 additions & 0 deletions src/borrowing/interior-mutability/cell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# `Cell`

`Cell` wraps a value and allows getting or setting the value using only a shared
reference to the `Cell`. However, it does not allow any references to the inner
value. Since there are no references, borrowing rules cannot be broken.

```rust,editable
use std::cell::Cell;
fn main() {
// Note that `cell` is NOT declared as mutable.
let cell = Cell::new(5);
cell.set(123);
println!("{}", cell.get());
}
```

<details>

- `Cell` is a simple means to ensure safety: it has a `set` method that takes
`&self`. This needs no runtime check, but requires moving values, which can
have its own cost.

</details>
50 changes: 50 additions & 0 deletions src/borrowing/interior-mutability/refcell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# `RefCell`

`RefCell` allows accessing and mutating a wrapped value by providing alternative
types `Ref` and `RefMut` that emulate `&T`/`&mut T` without actually being Rust
references.

These types perform dynamic checks using a counter in the `RefCell` to prevent
existence of a `RefMut` alongside another `Ref`/`RefMut`.

By implementing `Deref` (and `DerefMut` for `RefMut`), these types allow calling
methods on the inner value without allowing references to escape.

```rust,editable
use std::cell::RefCell;
fn main() {
// Note that `cell` is NOT declared as mutable.
let cell = RefCell::new(5);
{
let mut cell_ref = cell.borrow_mut();
*cell_ref = 123;
// This triggers an error at runtime.
// let other = cell.borrow();
// println!("{}", *other);
}
println!("{cell:?}");
}
```

<details>

- `RefCell` enforces Rust's usual borrowing rules (either multiple shared
references or a single exclusive reference) with a runtime check. In this
case, all borrows are very short and never overlap, so the checks always
succeed.

- The extra block in the example is to end the borrow created by the call to
`borrow_mut` before we print the cell. Trying to print a borrowed `RefCell`
just shows the message `"{borrowed}"`.

## More to Explore

There are also `OnceCell` and `OnceLock`, which allow initialization on first
use. Making these useful requires some more knowledge than students have at this
time.

</details>
3 changes: 3 additions & 0 deletions src/user-defined-types/static.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ Interior mutability is possible through a
[`Mutex`](https://doc.rust-lang.org/std/sync/struct.Mutex.html), atomic or
similar.

It is common to use `OnceLock` in a static as a way to support initialization on
first use. `OnceCell` is not `Sync` and thus cannot be used in this context.

Thread-local data can be created with the macro `std::thread_local`.

</details>
Expand Down

0 comments on commit 8121e7d

Please sign in to comment.