The first two posts mapped Rust onto territory where Swift already has good answers: the type system, then error handling. This one is the pivot. Memory and ownership is where Rust's design diverges most clearly from Swift's, because the underlying problem (memory safety without a garbage collector) admits two genuinely different solutions, and the two languages took opposite routes.
Swift's answer is Automatic Reference Counting (ARC). Every reference to a class instance feeds a retain count, and the runtime frees the object when that count hits zero. Aliasing is free as long as the count is honest; cycles need explicit help through weak and unowned. Access conflicts are caught by the Law of Exclusivity, the rule introduced in Swift 4 (2017) and tightened to runtime enforcement in Swift 5 (2019) that forbids modifying a variable while it is simultaneously being read or written through any other path. The runtime does the work; the programmer writes against a familiar model where references share and structs copy.
Rust's answer is the borrow checker. Memory safety becomes a property the compiler proves before the program runs: each value has exactly one owner, references are non-owning loans with strict aliasing rules, and lifetimes (the topic of the next post) keep references from outliving their referents. Zero runtime overhead. The cost is paid in the type system, which sometimes rejects programs that would have run correctly under ARC.
Most of the Swift mechanisms have direct Rust analogues, just reorganised: class and ARC map onto Rc<T> and Arc<T>; weak and unowned map onto Weak<T> and plain references; the Law of Exclusivity generalises into Rust's shared-or-exclusive borrow rule, applied to every reference rather than only to inout calls and mutating methods; copy-on-write is implicit in Swift's collection types and explicit through Cow in Rust. The genuinely new piece is the static enforcement: the borrow checker rejects programs ARC would have run, in exchange for compile-time guarantees ARC cannot make.
The single-owner rule and moves
In Swift, every reference to a class instance is co-equal. Each contributes to the retain count, and the runtime frees the object when the last reference goes out of scope. Two variables holding the same Person see the same object, and mutating through one is visible through the other. The reference graph is many-to-one: many owners, one object, with the runtime tracking who's still around.
Rust's model is one-to-one. Every value has exactly one owner at any given time. Assigning the value to another variable doesn't duplicate it (with the narrow exception of the next section's Copy types). Instead, ownership moves from one variable to the other, and the original variable becomes unusable.
struct Person {
name: String,
}
let alice = Person { name: String::from("Alice") };
let bob = alice; // ownership moves into `bob`
println!("{}", alice.name); // compile error: value used after move
The compiler's diagnostic spells it out: value used here after move. The binding alice is no longer a valid name for the value, because that value now lives in bob. There's no aliasing to track, no retain count to maintain, no second owner to reconcile. When bob goes out of scope, the value is dropped, which deallocates the inner String's heap buffer and runs any Drop impl on Person.
Function calls move arguments by default:
fn print_name(p: Person) {
println!("{}", p.name);
}
let alice = Person { name: String::from("Alice") };
print_name(alice);
println!("{}", alice.name); // compile error: value used after move
The parameter p inside print_name becomes the new owner. When the function returns, p goes out of scope and the value is dropped. The caller can't use alice afterwards, because the value it named no longer exists. This is the trade-off Rust makes for not having ARC: ownership is statically tracked, so passing a value around has a compile-time consequence the language enforces.
Swift's calling convention for a class is the opposite. Passing a Person instance to a function bumps the retain count, the function holds a reference for its duration, and the count drops back when the function returns. The caller's variable stays valid because the runtime maintained the count. Rust avoids both the bookkeeping and the runtime cost by drawing a hard line at the type system.
Returning a value transfers ownership outward:
fn make_person() -> Person {
Person { name: String::from("Carol") }
}
let carol = make_person(); // ownership moves out into `carol`
When carol itself goes out of scope, the value is finally dropped. There's exactly one owner at every point along this chain.
The single-owner rule is what removes ARC from the picture. Each value is created, moved possibly many times, and dropped exactly once at the end of the owner's scope. The compiler tracks the moves and inserts the drop. There's no count to maintain because there's no second co-owner. When code does need shared ownership, the answer is the explicit reference-counting types in section 5; the language reserves the unqualified type for the strict single-owner case.
The single-owner rule also gives Rust one of its quieter wins. Because the compiler knows precisely when a value is dropped, it inserts the deallocation directly. There's no garbage collector to schedule, no retain/release sequence to emit, no atomic counter to update. Memory management is just code generation, and the generated code looks like the code a careful C programmer would write by hand.
Copy types
The previous section said that assignment moves a value and the original binding becomes unusable. That's true for String, Vec<T>, the Person from above, and most types you'll define. It is not true for primitives:
let a: i32 = 7;
let b = a;
println!("{} {}", a, b); // 7 7 - both still valid
Both bindings are usable. The difference is that i32 implements the Copy trait, an opt-in marker for "duplicate on assignment instead of moving". Copy is short:
pub trait Copy: Clone {}
There are no methods. Implementing Copy is a promise to the compiler that the type can safely be duplicated by a bit-for-bit memory copy. When the compiler sees let b = a and a's type is Copy, it emits a memcpy and leaves a valid; when the type is not Copy, it emits a move and invalidates a.
The standard Copy types are the trivially-bit-copyable ones: every integer width, the float types, bool, char, shared references like &T, function pointers, and the unit type (). Arrays and tuples are Copy if their elements are. Notably, &mut T is not Copy, because duplicating an exclusive reference would create two exclusive references to the same value and break the rule from the next section. To make a struct Copy, derive both Copy and Clone:
#[derive(Copy, Clone)]
struct Point {
x: f64,
y: f64,
}
let p = Point { x: 1.0, y: 2.0 };
let q = p; // bit-copied; p is still valid
println!("{} {}", p.x, q.x);
Three rules govern eligibility. First, every field must itself be Copy. A struct that contains a String cannot be Copy, because String owns a heap buffer and bit-copying would alias it. Two structs pointing at the same buffer would both run cleanup at scope exit, freeing the same allocation twice. Second, the type must not implement Drop. A Drop impl is the type's signal that it owns a resource needing cleanup; bit-copying would duplicate the ownership and the cleanup would run twice. Third, Copy requires Clone as a supertrait, so deriving both together is the standard form.
The first rule is what disqualifies Person: its name: String field is not Copy, so neither is Person. That's why let bob = alice moved the value rather than duplicating it. The rule extends transitively: any struct that contains a String, Vec, Box, File, Mutex, or any other heap-or-resource-owning type cannot be Copy.
When duplication of a non-Copy type is needed, the explicit method is .clone():
let s = String::from("hello");
let t = s.clone(); // allocates a new buffer
println!("{} {}", s, t);
Clone is the trait that defines what duplication actually means for a type. Unlike Copy, it has a method (fn clone(&self) -> Self), so each implementation can do whatever the type needs: String::clone allocates a new buffer and copies the bytes, while a complex object's clone might walk a graph and rebuild it. The two traits split duplication along two axes, cost and explicitness. Copy types are duplicated implicitly at every assignment, function call, and pattern match: a fixed-cost memcpy of the bytes, no method call, no allocation. Clone types are duplicated only at an explicit .clone() call, and the method body can be arbitrarily expensive.
| Aspect | Copy | Clone |
|---|---|---|
| Trigger | Implicit at every assignment | Explicit .clone() call |
| Operation | Bit-for-bit memcpy | Whatever the type's clone impl does |
| Method | None (marker trait) | fn clone(&self) -> Self |
| Cost | Fixed, cheap | Type-defined, often heap allocation |
| Eligibility | All fields Copy, no Drop | Any type can opt in |
Every Copy type also implements Clone because of the supertrait bound, but most Clone types are not Copy. Heap-owning types like String, Vec<T>, and Box<T> implement only Clone, by design: a heap allocation should be a deliberate cost visible at every call site, not implicit.
Swift has no equivalent split. Every struct is value-copied on assignment, and the language gives the programmer no way to opt out:
struct Point {
var x: Double
var y: Double
}
var a = Point(x: 1, y: 2)
var b = a // automatic copy
b.x = 5
print(a.x, b.x) // 1.0 5.0
For small structs of primitive fields, Swift's behaviour matches Rust's Copy. The interesting case is the heap-backed types in the standard library. Array, String, Dictionary, and Set are all structs, so by the value-type rule each assignment copies them. Naively that would be ruinous: assigning a million-element array would copy a million elements. The standard library implements copy-on-write internally: each of these types wraps a class-backed buffer that tracks references, and the buffer is only actually copied when a mutation makes one alias differ from another. Section 8 covers Rust's explicit equivalent, Cow<'a, T>.
The two languages converge on the same engineering goal (no expensive implicit allocations) through opposite presentations. Swift's surface rule is uniform: structs always copy, and the runtime hides the cases where the copy would be expensive by deferring it through copy-on-write. Rust's surface rule is split: types that are cheap to bit-copy opt into Copy and behave like Swift values, while heap-owning types use move semantics by default and require .clone() for a deep copy.
Borrowing: shared and exclusive references
Passing a value to a function moves it, which works when you want to give the value away. Most function calls aren't really about ownership transfer though; they're about temporary access. Reading a Person's name doesn't require taking ownership of the Person.
Rust's mechanism for temporary access is the reference: a non-owning pointer that borrows a value for a bounded duration. References come in two flavours, and the rules around them are where the language meets the Law of Exclusivity from the intro.
A shared reference, written &T, gives read-only access to a value without taking ownership. The caller keeps the value, and the reference becomes invalid when its scope ends.
fn print_name(p: &Person) {
println!("{}", p.name);
}
let alice = Person { name: String::from("Alice") };
print_name(&alice); // borrow, do not move
println!("{}", alice.name); // alice still valid
The function takes &Person instead of Person, and the call site writes &alice. Ownership stays with the caller; the function gets a temporary view of the value, valid for the duration of the call. Multiple shared references to the same value can coexist freely:
let alice = Person { name: String::from("Alice") };
let r1 = &alice;
let r2 = &alice;
println!("{} {}", r1.name, r2.name); // both fine
A mutable reference, written &mut T, gives both read and write access. Creating one requires that the original binding is itself mut:
fn rename(p: &mut Person, new_name: String) {
p.name = new_name;
}
let mut alice = Person { name: String::from("Alice") };
rename(&mut alice, String::from("Alice II"));
println!("{}", alice.name); // "Alice II"
The function mutates the value through the reference, the reference goes out of scope when the function returns, and the caller continues to own the (now-mutated) value. The mut in &mut T is not just a permission to write; it also marks the reference as exclusive.
The borrow checker enforces a single rule on references:
At any point in the program, a value can have either any number of
&Treferences, or exactly one&mut Treference, but never both.
The classic diagnostic appears when the rule is violated:
let mut nums = vec![1, 2, 3];
let r = &nums; // shared borrow starts
nums.push(4); // ERROR: nums is borrowed
println!("{:?}", r); // shared borrow still live
error[E0502]: cannot borrow `nums` as mutable because it is also borrowed as immutable
--> src/main.rs:3:1
|
2 | let r = &nums;
| ----- immutable borrow occurs here
3 | nums.push(4);
| ^^^^^^^^^^^^ mutable borrow occurs here
4 | println!("{:?}", r);
| - immutable borrow later used here
Vec::push takes &mut self, so calling it requires an exclusive borrow of nums. But r already holds a shared borrow of the same value, and the rule forbids the two borrows from overlapping. The next section covers what happens when the trailing println! is removed: non-lexical lifetimes (NLL, stable since Rust 1.31, December 2018) shorten the shared borrow to nothing, so the conflict only matters when the shared borrow is actually used after the mutation point.
The rule prevents two concrete failure modes. A write racing with a read on the same memory across threads is a data race. A collection's buffer reallocating while iteration holds a pointer into it is iterator invalidation. Rust forbids both by construction, not by runtime check.
The Law of Exclusivity says two accesses to the same variable cannot overlap unless both are reads. That's the same shape as the borrow rule. The difference is the scale of enforcement. Swift opens an exclusivity-checked window only during specific operations: a function call with an inout parameter (which holds a write access for the call duration) and a mutating method call (which holds a write access on self). Outside those windows, multiple class references to the same instance can coexist freely, and so can multiple struct values that internally point at the same heap buffer through Swift's copy-on-write machinery. The runtime catches violations only when the windows overlap.
Rust applies the same shape to every reference, for the entire lifetime of the reference:
| Operation | Swift access window | Rust borrow |
|---|---|---|
| Read a value | (no window opened for plain reads) | &T borrow, lasts as long as it is used |
Write a value (inout) | Write access for the call duration | &mut T borrow |
mutating method on self | Write access on self for the call | &mut self method receiver |
Aliased class refs | (no window opened by aliasing alone) | Two &mut T to one value forbidden |
The first three rows have direct equivalents. The last is where the languages diverge: Swift permits multiple class references to coexist because ARC handles cleanup and exclusivity is checked only at access points; Rust does not, because the check happens uniformly on every reference. This is the structural reason the borrow checker rejects programs that would have compiled in Swift. Both languages forbid the same underlying conflict (a write overlapping any other access to the same memory). Swift catches it at the boundaries Swift considers significant; Rust catches it at every reference.
The practical effect of borrowing is that function signatures express intent precisely:
| Signature | Caller after the call | Function can |
|---|---|---|
fn f(p: Person) | No longer owns p | Read and mutate |
fn f(p: &Person) | Still owns p | Read only |
fn f(p: &mut Person) | Still owns p | Read and mutate |
Most idiomatic Rust signatures use &T or &mut T. The bare T form is reserved for cases where the function genuinely needs to consume the value: storing it in a struct, dropping it, returning a transformed version. Compared to Swift, where reference parameters are the default for class types and value parameters copy structs, Rust requires the choice at every signature: take ownership, borrow read-only, or borrow exclusively.
The borrow checker in practice
The shared-or-exclusive rule sounds simple. The challenge is that the compiler has to verify it across every code shape you write: loops, function calls, struct accesses, closures, control flow. The patterns where verification gets non-trivial are worth knowing in advance, because they trip up most newcomers and the way to think about a fix isn't always obvious from the diagnostic alone.
Non-lexical lifetimes
Before Rust 1.31 (December 2018), every borrow lived from its creation until the end of the enclosing lexical scope (the closing brace). This made the borrow rule's static check more conservative than it had to be. A reference that was created, used once, and then discarded would still hold its borrow open until the end of the function.
let mut v = vec![1, 2, 3];
let r = &v[0];
println!("{}", r);
v.push(4); // pre-NLL: ERROR
Pre-NLL, the shared borrow held by r was alive until the end of the block, so the push (which needs an exclusive borrow) was rejected even though r was never used again. The reasoning was sound (the rule says borrows cannot overlap), but the conclusion was unhelpful (the borrow was harmless past the println!).
Non-lexical lifetimes (NLL) changed the model: a borrow is alive only from its creation until its last use, not until its lexical scope ends. The example now compiles, because by the time v.push(4) runs, r's last use is in the past and its borrow has ended. NLL is also why the example with the trailing println! from the previous section failed but would have succeeded without that read; removing the trailing use shortens the borrow to nothing, which makes the subsequent mutable use legal.
The diagnostic points at the last use of the conflicting borrow, not just at its declaration:
error[E0502]: cannot borrow `nums` as mutable because it is also borrowed as immutable
|
2 | let r = &nums;
| ----- immutable borrow occurs here
3 | nums.push(4);
| ^^^^^^^^^^^^ mutable borrow occurs here
4 | println!("{:?}", r);
| - immutable borrow later used here
The "later used here" line is the one that matters: it tells you which read is keeping the borrow alive. Removing it, or moving it before the conflicting mutation, often resolves the diagnostic without restructuring the code.
Iterating while mutating
The canonical pattern that the borrow rule rejects, and the one most newcomers run into first, is iterating over a collection while mutating it:
let mut nums = vec![1, 2, 3];
for n in &nums {
if *n == 2 {
nums.push(*n * 10); // ERROR: nums is borrowed
}
}
The for n in &nums loop holds a shared borrow on nums for the duration of the loop. Calling nums.push(...) inside the body needs an exclusive borrow, which the rule forbids while the shared borrow is alive. The diagnostic points at both the borrow (the loop's &nums) and the conflicting use (nums.push).
This is iterator invalidation in concrete form. If push reallocated the vector's buffer, the iterator's pointer into the old buffer would become a dangling reference. The borrow checker rejects the pattern at compile time.
The standard ways out, in increasing order of generality:
// 1. Collect the work into a separate buffer, then apply it after.
let mut to_add = vec![];
for n in &nums {
if *n == 2 { to_add.push(*n * 10); }
}
nums.extend(to_add);
// 2. Iterate by index when the bound is known up-front.
for i in 0..nums.len() {
if nums[i] == 2 {
let value = nums[i] * 10;
nums.push(value);
}
}
The second form works because indexing returns a value (nums[i] for an i32 is a Copy of the element), not a reference, so no shared borrow is held across the body. The first form works because the inner loop only reads nums, and the mutation happens after the iteration ends.
Borrowing disjoint struct fields
The borrow rule applies to values, and the compiler is precise enough to recognise that two different fields of a struct are different values. Two &mut borrows to different fields can coexist:
struct Pair {
left: String,
right: String,
}
fn rename_both(p: &mut Pair) {
let l: &mut String = &mut p.left;
let r: &mut String = &mut p.right;
l.push_str(" (left)");
r.push_str(" (right)");
}
Both &mut references point at disjoint memory inside p, so the rule isn't violated. This is a static analysis: the compiler sees the field projections p.left and p.right, knows they cannot overlap, and accepts both borrows.
What the compiler doesn't see through is function calls. If a method on Pair returns a borrow into one field, the borrow is described as a borrow of the whole struct from the type system's perspective, and the second field is no longer accessible:
impl Pair {
fn left_mut(&mut self) -> &mut String { &mut self.left }
}
fn rename_both(p: &mut Pair) {
let l = p.left_mut();
let r = &mut p.right; // ERROR: p is already borrowed
l.push_str(" (left)");
r.push_str(" (right)");
}
The diagnostic says cannot borrow p as mutable more than once at a time. From the caller's view, p.left_mut() returned a &mut String whose lifetime is tied to &mut p, so p itself is considered fully borrowed for the duration. The fix is to write the field projection inline (which the compiler can analyse) rather than going through a method call (which it cannot peer inside). This is one of the more counter-intuitive parts of the borrow checker for newcomers: semantically equivalent code compiles or fails depending on whether the field access is direct or method-mediated.
split_at_mut for slices
Disjointness analysis works for struct fields because the compiler can see them by name. For slices, where the disjoint pieces are determined at runtime (by an index), the standard library provides explicit splitters that return two non-overlapping &mut borrows:
let mut v = vec![1, 2, 3, 4, 5];
let (left, right) = v.split_at_mut(2);
left[0] = 10;
right[0] = 30;
println!("{:?}", v); // [10, 2, 30, 4, 5]
split_at_mut takes &mut [T] and an index, and returns two &mut [T] slices that together cover the original. The compiler accepts both borrows because the function's signature declares that the two output slices borrow disjoint regions of the input. The implementation uses unsafe code internally, with the soundness argument that the index split guarantees the two halves cannot overlap. When code genuinely needs two &mut borrows into the same value, looking for a splitter or similar helper in the standard library is the first move; if one exists, the soundness argument is already worked out and the calling code stays in safe Rust.
Reborrows
A &mut T is exclusive, so passing it to a function and then using it again afterwards seems like it should violate the rule. The compiler accepts it because of an implicit reborrow:
fn touch(s: &mut String) { s.push_str("!") }
let mut name = String::from("hi");
let r = &mut name;
touch(r); // r is reborrowed for the call
r.push_str("?"); // r is still usable here
When r is passed to touch, the compiler implicitly creates a new &mut String that borrows from r for the duration of the call. The original r is "frozen" (cannot be used) for the duration of the inner reborrow, but becomes usable again after the call returns. The mechanism is essentially the same as if you had written touch(&mut *r) explicitly. Reborrowing is what makes &mut references practical to pass through layers of functions; without it, an exclusive borrow could be passed exactly once and then the original binding would be permanently unusable.
Together these patterns cover most of what a newcomer sees in their first few weeks. The harder cases, where the static analysis cannot prove soundness on its own and the language relies on lifetime annotations to bridge the gap, are the territory of the next post.
Reference counting: Rc<T> and Arc<T>
The single-owner rule handles most cases that need owned values, and borrowing covers most cases that need temporary access. The pattern that neither handles cleanly is genuine shared ownership: a value used from several places where the lifetime relationship between the users is not statically obvious.
A node referenced from two parents in a tree-like structure. An immutable configuration object that several subsystems hold onto for their lifetime. A cache shared across worker tasks. In Swift, the answer to all of these is the same: a class instance, with ARC managing the refcount. Rust offers the same machinery, made explicit through two standard library types.
Rc<T> for single-threaded shared ownership
Rc<T> is a smart pointer that holds a heap-allocated value alongside a non-atomic reference count. Each Rc<T> handle to the same allocation contributes one to the count. Cloning a handle is cheap: it bumps the count and returns another handle pointing at the same value. Dropping a handle decrements the count, and the inner value is freed when the last handle is dropped.
use std::rc::Rc;
let shared = Rc::new(String::from("hello"));
let copy_a = Rc::clone(&shared); // refcount = 2
let copy_b = Rc::clone(&shared); // refcount = 3
println!("{} {} {}", shared, copy_a, copy_b);
// all three handles can be used; they all refer to the same `String`
Rc::clone(&shared) is the idiomatic spelling. shared.clone() works too (because Rc<T> implements Clone), but the explicit Rc::clone is preferred to make the refcount bump visible at the call site rather than blending in with deep-copy .clone() calls on other types. The implementation is identical; only the spelling differs.
The trade-off compared to a single owner is the runtime cost (one increment on clone, one decrement on drop) and the constraint that the inner value is reachable only through &T, not &mut T. Multiple aliases to a mutable value would violate the borrow rule, so Rc<T> only hands out shared references. Section 7 covers the wrapper type that adds runtime-checked mutability when it's needed.
Arc<T> for thread-safe shared ownership
Arc<T> (atomically reference counted) is the same shape as Rc<T> with one change: the reference count is updated using atomic operations, so it's safe to clone, drop, and share Arc handles across threads. The API is identical; the thread-safety lives entirely in how the count is updated.
use std::sync::Arc;
use std::thread;
let shared = Arc::new(String::from("hello"));
let mut handles = vec![];
for _ in 0..4 {
let h = Arc::clone(&shared);
handles.push(thread::spawn(move || println!("{}", h)));
}
for h in handles { h.join().unwrap(); }
Each spawned thread receives its own Arc handle, which the compiler accepts because Arc<T> is safe to move and share across threads when T is. The marker traits expressing this property, Send and Sync, are covered properly in the concurrency post; the relevant point here is that the atomic count makes cloning and dropping safe across threads, while the inner value remains accessible only through &T, so cross-thread mutation still requires the wrapper from section 7.
The reason the standard library offers two types instead of one is performance. Atomic operations are noticeably slower than ordinary increments on most hardware (a memory barrier or a locked instruction, depending on the architecture). Code that doesn't need cross-thread sharing pays nothing for the guarantee by reaching for Rc<T>; code that does, pays only on clone and drop, not on every read.
Mapping to ARC
Swift's class instance with ARC is the design point that Arc<T> matches. The retain count is atomic, the runtime decrements it when a reference goes out of scope, and the instance is freed when the count reaches zero. The translation is direct:
| Pattern | Swift | Rust |
|---|---|---|
| Heap-shared instance | class instance | Arc<T> (or Rc<T> for single-threaded) |
| New owner | Implicit retain on assignment | Arc::clone(&handle) (explicit) |
| Owner goes out of scope | Implicit release | Drop decrements the count |
| Non-owning reference | weak / unowned | Weak<T> (next section) |
The difference is visibility. Swift inserts retain and release calls behind every reference assignment, parameter pass, and scope exit. Rust requires Arc::clone(&handle) at every site where a new owner is created, which makes the bumps explicit in the source. The runtime cost is the same; the choice the language makes is whether the cost shows up in the code or only in the runtime trace.
A common reflex coming from Swift is to assume that, since Arc<T> is the equivalent of a class reference, mutating the inner value should work the same way. It doesn't, because the borrow rule still applies. Multiple Arc<T> handles to one value is exactly the "many shared owners" pattern, which the rule allows only if every owner has read-only access. Obtaining &mut T from an Arc<T> would let one owner mutate while other handles still exist, which is the case the rule rules out. The standard pattern for shared mutable state is Arc<Mutex<T>> (or Rc<RefCell<T>> on a single thread), covered in section 7.
A typical Swift codebase reaches for class regularly: model objects, view controllers, services, coordinators, anything that has identity or reaches for inheritance. A typical Rust codebase reaches for Rc<T> and Arc<T> rarely. Two reasons. First, much of what Swift expresses through class identity, Rust expresses through ownership and borrowing. A model object created in one place and rendered in several is owned by the place that created it; the renderers borrow it. A service struct constructed at startup is owned by the startup code; the request handlers borrow it. The pattern works because the lifetime relationship is statically clear, and reference counting only earns its cost when the relationship is genuinely indeterminate. Second, the cost shows up in the source. Writing Arc::clone(&handle) at each site is a small but visible nudge to ask whether the new owner really needs to own the value, or whether it could borrow from the existing owner. Swift's automatic retain/release is invisible, so the choice is rarely revisited.
None of this makes Arc<T> exotic. It's in the standard library, well-tuned, and the right answer for genuine shared-ownership patterns: graph-shaped data structures, shared caches, anywhere the ownership graph is not a tree. The shift in proportion compared to Swift is the main thing to expect, not a discouragement from using the type.
Weak references and reference cycles
Reference counting has one well-known failure mode in both languages: when references form a cycle, the count never drops to zero, and the values stay alive past their useful scope. ARC suffers this by default; Rc<T> and Arc<T> suffer it equally. Both languages solve the problem the same way: a non-owning handle that doesn't contribute to the count. Swift calls it weak. Rust calls it Weak<T>.
The cycle problem
Two values that hold strong references to each other keep each other alive. When all external bindings go out of scope, each value's refcount drops by one but never to zero, because the cycle accounts for the remaining ref each way. Neither value is freed, and the memory leaks for the rest of the program's run.
In Swift, the canonical example is a parent holding a child while the child holds a strong back-reference to the parent:
class Parent {
var child: Child?
deinit { print("Parent deinit") }
}
class Child {
var parent: Parent? // strong by default, this is the bug
deinit { print("Child deinit") }
}
var p: Parent? = Parent()
var c: Child? = Child()
p?.child = c
c?.parent = p
p = nil
c = nil
// Neither "Parent deinit" nor "Child deinit" is ever printed.
// Both instances stay in memory; the cycle keeps each refcount at 1.
In Rust the same situation arises with two Rc<T> values pointing at each other through fields. The cycle can't be assembled at construction time, so the back-pointer has to be patched in after both values exist. That patching requires interior mutability (the ability to mutate a value through a shared reference), which section 7 covers in detail. For now, the relevant tool is RefCell<T>, a wrapper that stores a T and hands out runtime-checked mutable borrows through .borrow_mut(). Wrapping the back-pointer field in RefCell<Option<Rc<...>>> is what lets the assignment below run after both Parent and Child are already alive:
use std::cell::RefCell;
use std::rc::Rc;
struct Parent {
child: RefCell<Option<Rc<Child>>>,
}
struct Child {
parent: RefCell<Option<Rc<Parent>>>,
}
impl Drop for Parent {
fn drop(&mut self) { println!("Parent drop"); }
}
impl Drop for Child {
fn drop(&mut self) { println!("Child drop"); }
}
let p = Rc::new(Parent { child: RefCell::new(None) });
let c = Rc::new(Child { parent: RefCell::new(None) });
*p.child.borrow_mut() = Some(Rc::clone(&c));
*c.parent.borrow_mut() = Some(Rc::clone(&p));
drop(p);
drop(c);
// Neither drop impl runs.
// After drop(p), Parent's strong count is still 1 (held by c.parent).
// After drop(c), Child's strong count is still 1 (held by p.child).
// Both values leak for the rest of the program's lifetime.
The two examples have the same shape and the same bug. The runtime in either language can't tell the cycle apart from a legitimate sharing pattern, so without external help the values stay in memory until the program exits.
Weak<T> and the upgrade dance
Weak<T> is a handle to the same allocation that doesn't contribute to the strong refcount. It carries its own (separate) weak count, which the allocation uses to track when the inner value can be freed even though some weak handles still exist. To create one, call Rc::downgrade(&strong_handle). To access the underlying value, call .upgrade(), which returns Option<Rc<T>>: Some(rc) if the value is still alive, or None if all strong handles have been dropped.
use std::rc::{Rc, Weak};
let strong = Rc::new(String::from("hello"));
let weak: Weak<String> = Rc::downgrade(&strong);
if let Some(s) = weak.upgrade() {
println!("{}", s); // value still alive, prints "hello"
}
drop(strong); // last strong handle gone
assert!(weak.upgrade().is_none()); // weak now returns None
The "upgrade dance" is the one piece of API without an exact Swift parallel in syntax, though the semantics line up. To use a Swift weak self you check it for nil (or use optional chaining), which is exactly the pattern weak.upgrade() produces. The Rust spelling makes the temporary strong reference explicit: upgrade returns an Rc<T> that holds the value alive for as long as you hold the result, so the value cannot be freed mid-method by another handle.
Both languages avoid cycles the same way: make the strong references follow the structural ownership direction (the tree of "who owns whom") and use weak references for back-pointers, observers, and other non-owning relationships. Applied to a tree node:
use std::rc::{Rc, Weak};
struct Node {
name: String,
parent: Option<Weak<Node>>, // non-owning back-reference
children: Vec<Rc<Node>>, // strong, top-down ownership
}
The parent owns the children through Rc<Node>; each child references its parent through Weak<Node>. The strong refs form a tree (no cycle), the weak refs don't contribute to the count, and the whole structure is freed cleanly when the root goes out of scope. Same convention applies in Swift.
A practical use of Weak outside the cycle-breaking case is the observer pattern. An observer holds a Weak<Subject>. The subject is owned by something else (a controller, a service). When the subject is dropped, observers' .upgrade() calls return None, and they can clean up without preventing the subject from being freed in the first place.
Swift's weak and unowned, mapped
Swift offers two non-owning reference forms with subtly different semantics:
| Swift | Rust | Behaviour when the referent is gone |
|---|---|---|
weak var p: T? | Weak<T> + .upgrade() → Option<Rc<T>> | Reads as nil / None |
unowned var p: T | &T (when the borrow is statically valid) | Crashes (Swift) / compile error (Rust) |
[weak self] | Capture Weak, upgrade in closure | Closure body checks for None |
[unowned self] | Capture &self (next subsection) | Crashes (Swift) / compile error (Rust) |
weak and Weak<T> line up exactly: both yield an optional value, both require the caller to handle the None case. The translation is one-to-one.
unowned doesn't have a direct Rust counterpart, because the closest equivalent in spirit (a non-optional non-owning reference) is just &T, which the borrow checker requires to be statically valid. Swift's unowned says "trust me, the referent will outlive me, and crash if it doesn't"; Rust's &T says "the compiler will check that the referent outlives me, and refuse to compile if it can't be proved". The Swift form trades runtime crashes for cases the Rust form rules out at compile time.
Closures and [weak self]
The [weak self] capture is the most common Rust translation question, because the pattern crops up constantly: callbacks, completion handlers, async tasks, observers. The Rust translation depends on the closure's lifetime relative to the surrounding scope.
If the closure doesn't escape its enclosing scope (a for_each, a map over an iterator, a synchronous handler), the closure can borrow self directly:
items.iter().for_each(|item| self.process(item));
self is borrowed for the duration of for_each, which the borrow checker proves outlives the closure's invocations. No Rc, no Weak, no clone. This is the case Swift would handle with [unowned self], and Rust handles it with the same machinery as any other borrow.
If the closure escapes (stored in a struct, passed to thread::spawn, registered as a callback), self cannot be borrowed because the borrow would have to outlive the function returning the closure. The two patterns are:
// Strong capture: the closure keeps self alive.
let self_rc = Rc::clone(&self_rc);
let cb = move || self_rc.process();
// Weak capture: the closure does not keep self alive.
let self_weak = Rc::downgrade(&self_rc);
let cb = move || {
if let Some(s) = self_weak.upgrade() {
s.process();
}
};
The first form is a strong reference; the closure holds an Rc and the value cannot be freed while the closure exists. The second is the [weak self] translation: the closure holds a Weak, calls .upgrade() at invocation time, and returns early if the value has been dropped. Both forms are explicit at the capture site, the same kind of nudge as Arc::clone(&handle) from the previous section: the cost (or the safety contract) shows up in the code.
The pattern works the same way for Arc<T> and async tasks: let self_weak = Arc::downgrade(&self_arc); tokio::spawn(async move { ... }). The lifetime story is identical; only the underlying smart-pointer type changes.
Interior mutability: Cell, RefCell, Mutex
The borrow rule says a value can have any number of &T references or one &mut T, never both. That works for the cases the rule was designed for, but a few patterns sit outside it: a counter incremented from several call sites, a cache populated on demand, a configuration value updated by one subsystem and read by many others. Each of these wants shared access and mutability, which the static rule rules out directly.
The standard library's answer is interior mutability: a small set of wrapper types that move the borrow check from compile time to runtime, or substitute it with a different discipline (a lock, a value-replacement primitive). The wrappers are the only safe way to mutate through a shared reference, and they cover three distinct cases.
Cell<T> for Copy payloads
Cell<T> is the simplest of the three. It stores a T and exposes two methods: set(value) to overwrite the stored value, and get() to retrieve a copy. No references are handed out at all, which is what makes the type safe. The borrow rule cannot be violated by code that never receives a &T or &mut T to the inner value.
use std::cell::Cell;
let counter = Cell::new(0u32);
fn bump(c: &Cell<u32>) {
c.set(c.get() + 1);
}
bump(&counter);
bump(&counter);
println!("{}", counter.get()); // 2
The function takes &Cell<u32> (a shared reference) and still mutates the stored value. The borrow rule isn't violated because the mutation goes through Cell::set, which writes the new value into the storage without exposing a &mut u32. Nothing else holds a reference to the inner value, so nothing can be invalidated.
The constraint is that T must be Copy (with a few extensions like Cell::take for Default types). Without Copy, get() would have to either move the value out (leaving the cell in an invalid state) or hand out a reference (re-introducing the borrow problem). Cell<T> is the right tool when the inner value is a small primitive: a counter, a flag, a refcount, an enum tag.
RefCell<T> for runtime-checked borrows
RefCell<T> is the general-purpose wrapper for types that aren't Copy. Instead of get/set, it hands out borrows: .borrow() returns a Ref<T> (a smart pointer that derefs to &T) and .borrow_mut() returns a RefMut<T> (which derefs to &mut T). Both track an internal counter of active borrows, and the borrow rule is enforced at runtime by panicking when a violation occurs:
use std::cell::RefCell;
let cache = RefCell::new(Vec::<String>::new());
cache.borrow_mut().push(String::from("first"));
cache.borrow_mut().push(String::from("second"));
let view = cache.borrow();
println!("{:?}", view); // ["first", "second"]
// A borrow_mut while a Ref is alive would panic at runtime:
// let _ = cache.borrow_mut(); // PANIC: already borrowed
The Ref<T> and RefMut<T> smart pointers release their borrow when they go out of scope, so structuring code around short borrow scopes keeps the runtime check from tripping. The compiler doesn't know which borrows are alive (that's exactly what the static rule could not prove); the runtime counter does, at the cost of a small bookkeeping overhead and the possibility of a panic when the discipline is wrong.
RefCell<T> is single-threaded only. The internal counter isn't atomic, so sending or sharing one across threads would race on the bookkeeping itself. The compiler enforces this by making RefCell<T> !Sync, which the type system uses to reject any attempt to share it through Arc<T> across threads.
Mutex<T> for cross-thread mutation
Mutex<T> is the cross-thread version of the same idea. The wrapper holds a T and a lock. Calling .lock() blocks the current thread until the lock is free, then returns a MutexGuard<T> that derefs to &mut T. When the guard goes out of scope, the lock is released. Only one thread holds the guard at a time, so the borrow rule's "one &mut T" constraint is preserved across threads at runtime by the lock.
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0u32));
let mut handles = vec![];
for _ in 0..4 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut n = counter.lock().unwrap();
*n += 1;
}));
}
for h in handles { h.join().unwrap(); }
println!("{}", *counter.lock().unwrap()); // 4
The .lock() call returns a Result because the lock can become "poisoned" if a thread panics while holding it; .unwrap() propagates that failure. The dereferenced *n += 1 mutates the inner value through the guard. The surrounding Arc<T> is the shared-ownership wrapper from the previous section.
Mutex<T> is Sync when T is Send, which is what makes the Arc<Mutex<T>> pattern compose cleanly. The cost compared to RefCell<T> is the lock acquisition (an atomic operation or a system call, depending on contention); the gain is correctness across threads, which RefCell<T> cannot provide.
Composed patterns: Rc<RefCell<T>> and Arc<Mutex<T>>
The composition rule is straightforward: outer wrapper for ownership, inner wrapper for mutation. Rc<RefCell<T>> is the single-threaded shared-mutable type, Arc<Mutex<T>> is the cross-thread version, and the choice between them is the Rc/Arc choice multiplied by the RefCell/Mutex choice.
use std::rc::Rc;
use std::cell::RefCell;
struct State { count: u32 }
let shared = Rc::new(RefCell::new(State { count: 0 }));
let copy_a = Rc::clone(&shared);
let copy_b = Rc::clone(&shared);
copy_a.borrow_mut().count += 1;
copy_b.borrow_mut().count += 10;
println!("{}", shared.borrow().count); // 11
Both layers are needed. Rc<T> alone gives shared ownership but read-only access. RefCell<T> alone gives runtime-checked mutation but no way to share ownership across multiple call sites that each need their own handle. Composed, the outer gives the handles and the inner gives the mutation. The cycle example earlier uses exactly this composition.
Why Swift has no direct analogue
Swift has nothing equivalent to RefCell or Cell, and the absence is a consequence of how it checks exclusivity. The Law of Exclusivity is already a runtime check; the compiler tries to catch violations statically, but the underlying enforcement model is "trap at runtime when accesses overlap". A class property mutated through any reference that has access is permitted, and the runtime catches the overlap if one happens.
Rust's borrow rule is a compile-time check on every reference. Interior mutability types exist precisely because some patterns can't be proved sound statically; the wrappers move the check to runtime so the pattern can compile, paying the cost only at sites that opt into it. Swift has nothing to opt out of, because the language never opted in to the static check in the first place.
The closest Swift parallels exist for the cross-thread case: DispatchQueue.sync(execute:) for serial-queue-based mutual exclusion, or os_unfair_lock and NSLock for direct lock-based critical sections. These match Mutex<T> in role, though they're bare locks rather than typed wrappers around the data they protect. Idiomatic Swift commonly wraps the lock and the data together: a class with private storage and accessors that take the lock before reading or writing, recovering by convention the property Mutex<T> enforces by construction in Rust.
Copy-on-write: Swift's implicit, Rust's explicit
Swift's heap-backed value types (Array, String, Dictionary, Set) are structs and therefore copied on assignment, but the standard library implements copy-on-write internally so the copy is deferred until a mutation makes one alias differ from another. The optimisation is invisible: the user writes let b = a for a million-element array, the assignment runs in constant time, and the buffer is only actually duplicated if either alias is mutated.
var a = "hello"
var b = a // refcount bumped, no copy
b.append(" world") // mutation: now b copies the buffer
print(a, b) // "hello", "hello world"
Rust spells the same idea out as a type. Cow<'a, T> (short for "clone-on-write") is an enum from std::borrow with two variants: Cow::Borrowed, holding a &'a T reference, and Cow::Owned, holding a freshly allocated value. For string handling, the typical instantiation is Cow<'a, str>, where the Owned variant holds a String. The variant chosen depends on what the producing code knows: if no transformation is needed, return Cow::Borrowed(&input) and skip the allocation; if a transformation is needed, return Cow::Owned(transformed) carrying a fresh value. Either way, the consumer treats the result as a single type, and reading from it gives &T regardless of which variant was produced.
The classic use site is a function that conditionally transforms its input:
use std::borrow::Cow;
fn escape_html(input: &str) -> Cow<'_, str> {
let needs_escape = input.contains('<') || input.contains('>') || input.contains('&');
if !needs_escape {
return Cow::Borrowed(input);
}
let mut out = String::with_capacity(input.len());
for c in input.chars() {
match c {
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'&' => out.push_str("&"),
_ => out.push(c),
}
}
Cow::Owned(out)
}
let safe = escape_html("hello world"); // Cow::Borrowed, no allocation
let escaped = escape_html("a < b & c"); // Cow::Owned, with allocation
When the input has no special characters, the function returns the original slice with no allocation at all. When it does, it builds a new String and returns it as the owned variant. The caller works with the result without having to know which case it got.
Two situations call for Cow. The first is a function that may or may not need to allocate, like the escape example above. The second is a struct that holds a string-or-slice depending on construction context: a config value parsed from a static file is Cow::Borrowed(&'static str), while the same field assigned at runtime is Cow::Owned(String). Most string-handling code doesn't need Cow. The simpler choice is &str when the function only reads, or String when it owns. Cow earns its complexity when the producing code legitimately doesn't know whether it will need to allocate, and the consumer shouldn't have to care.
Same optimisation, expressed differently. Swift hides the variant in the standard library's implementation; Rust elevates it into a named type the function signature carries. The 'a lifetime parameter is where Rust's design diverges further from Swift's: Swift's COW types are class-backed and refcounted, so the buffer stays alive as long as any handle points at it, while Rust's Cow doesn't refcount and instead ties the borrow's lifetime statically to the input it came from. Either approach prevents use-after-free; the mechanism (runtime refcounting versus compile-time lifetime checking) is the same divergence the rest of this post has been tracking.
The next post is lifetimes: the syntax that makes the borrow checker work across function boundaries and struct definitions, which is everything Cow<'a, T> already gestured at with that 'a. After that, concurrency reuses the borrow rules unchanged (Send and Sync are the cross-thread analogues), and tooling closes the series.