120 companion flashcards · AI-assisted study content · Open the deck →
This deck walks through one of the most distinctive parts of learning Rust: the ownership and borrowing system. The cards start with the three core ownership rules and what happens to a value when its owner goes out of scope, then move into related ideas like moves, the Copy and Clone traits, and the drop function. Later cards cover references and borrowing rules, including how many mutable or immutable references can exist at once. Together they cover the mental model that replaces the garbage collector or manual memory management you'd find in other languages.
It's a good fit if you're fairly new to Rust and have run into compiler errors about "moved values," "borrowed value," or "cannot borrow as mutable" and want to build a clearer picture of what those messages actually mean. It's also useful if you have some Rust experience but want a quick way to check that your understanding of ownership, moves, and borrowing rules is solid before moving on to trickier topics like lifetimes or interior mutability.
The concepts build on each other, so studying the cards in order tends to work well rather than jumping around randomly. When you get a card wrong, it's worth re-reading the surrounding ones, since later questions often assume you remember earlier rules. Spacing your reviews over a few short sessions instead of one long block also helps, because ownership rules are easier to internalize when you meet them in slightly different phrasing across separate sittings rather than all at once.
Rust's ownership system rests on three simple rules that govern every value in the language: every value has exactly one owner, there can only be a single owner at any given moment, and the value is dropped when that owner goes out of scope. The "drop" here is not a casual phrase — it means the standard library's drop function from the Drop trait is called automatically at the closing brace of the variable's scope, freeing any resources the value owns. This pattern, sometimes called RAII (Resource Acquisition Is Initialization), ties resource lifetimes to object lifetimes so that cleanup is guaranteed by the structure of the program rather than by the discipline of the programmer.
The distinction between the stack and the heap is essential to understanding why these rules matter. The stack is a last-in, first-out region of memory for values whose size is known at compile time — integers, booleans, characters, and tuples of such primitives live there directly. The heap is used for values whose size or lifetime is dynamic, and accessing them is done through pointers stored on the stack. A String, for example, has a small struct (a pointer, length, and capacity) sitting on the stack, while the actual UTF-8 bytes it owns are stored on the heap. When the String goes out of scope, the heap buffer is freed at the same time as the stack-resident struct, preventing the leaks and double-frees that plague languages with manual memory management.
Scope in Rust is delimited by curly braces, and variables are dropped at the closing brace that introduced them. Even constructs like match arms create fresh scopes, so bindings introduced inside an arm are dropped when that arm completes. Shadowing, declared with a second let using the same name, does not move or drop the previous binding — it simply rebinds the name to a new value, and crucially, the new binding may have a different type. This is distinct from mutation with let mut, which keeps the same binding and type in place but changes the underlying value. Shadowing can therefore change a variable's type across declarations, while mutation cannot.
When you assign a value to another variable or pass it to a function, Rust moves the value rather than copying it by default. After a move, the source binding is invalidated, and any attempt to use it is rejected at compile time. This design avoids double-free bugs that would otherwise arise if two bindings each believed they owned the same heap allocation: copying the pointer of a String would produce two would-be droppers of the same buffer. Move semantics keep the type system simple by ensuring only one entity is ever responsible for cleanup.
Some types opt out of moving through the Copy trait, a marker trait that signifies bitwise duplication is safe and that no special cleanup is needed. Integers, bool, char, tuples whose elements are all Copy, and any other type without heap-allocated resources typically implement Copy. The compiler refuses to let a type implement both Copy and Drop, because Copy implies that nothing of importance happens at the end of the value's lifetime. Copy technically requires Clone, but the two traits have very different flavors: Copy is implicit and free, while Clone is an explicit method .clone() that may perform arbitrary and potentially expensive work to produce a deep, independent copy.
Calling clone() is the standard way to deliberately duplicate data so that two bindings each own their own copy. It should be used when an independent value is genuinely required — for example, when multiple threads each need their own data — rather than as a habitual escape hatch from the borrow checker. The Clone trait has a simple signature, fn clone(&self) -> Self, taking a shared reference and returning a freshly owned value. Arrays follow the same rules as their elements: an array of Copy integers is itself Copy and duplicates on assignment, while an array of String is not Copy and will move element-by-element when needed.
Borrowing is the act of letting another piece of code access your value without taking ownership, by handing out a reference. A shared reference &T permits read-only access, while a mutable reference &mut T permits exclusive write access. The cornerstone rule — sometimes called "aliasing XOR mutability" — is that at any program point, a value may be accessed either through any number of shared references or through exactly one mutable reference, but never both at once. This rule is fundamental to Rust's safety guarantees: by preventing both data races at compile time and the kind of unsynchronized aliasing that can break compiler optimizations, it underpins the correctness of transformations like LLVM's noalias attribute on &mut T.
The enforcement of these rules is the job of the borrow checker, which runs at compile time on every Rust program. It rejects code that would create a dangling reference — a reference pointing to memory that has been freed or to a value that has gone out of scope — long before the program runs, so safe Rust cannot produce use-after-frees or double-frees. The borrow checker's lifetime tracking has steadily improved: the NLL (Non-Lexical Lifetimes) refinement, stabilized in Rust 1.31 in December 2018, ends a reference's lifetime at its last use rather than at the closing brace of its enclosing block, allowing more natural code to compile. An even more precise analyzer called Polonius is under development, designed to compute loan lifetimes through dataflow analysis and accept programs that NLL cannot.
References must always point to valid data, and the compiler enforces this even in subtle cases. You cannot, for instance, return a reference to a local variable from a function, because the local is dropped at the closing brace and would leave the reference dangling — the compiler rejects such code at compile time. Inside a single function, you may create any number of immutable references to a value, but the moment a mutable reference appears, all immutable references must be gone, and only one mutable reference may exist. These rules apply to iteration as well: v.iter() yields &T by immutably borrowing the vector, v.iter_mut() yields &mut T by exclusively borrowing it, and v.into_iter() consumes the collection entirely, yielding owned T values.
Lifetime annotations are the mechanism by which the borrow checker understands how multiple references relate in time. Written as generic parameters like 'a, they appear in function signatures, struct definitions, and trait bounds. A reference such as &'a i32 is one that lives at least as long as the lifetime 'a. The compiler applies a set of elision rules so that explicit annotations are usually unnecessary: each input reference gets its own lifetime parameter, and if a function has exactly one input reference, that lifetime is automatically assigned to all output references. The special lifetime 'static describes values that live for the entire duration of the program, and string literals such as "hello" have type &'static str.
Lifetime parameters on structs, written as struct Ref<'a> { r: &'a i32 }, express that the struct cannot outlive the data it borrows. A lifetime bound like T: 'a on a generic says that every reference inside T must outlive 'a. When one lifetime is known to live longer than another, written 'a: 'b, references with 'a can be substituted wherever references with 'b are expected — this relationship is called lifetime subtyping. A higher-ranked trait bound, written with the for<'a> syntax, expresses that a condition must hold for every possible lifetime, and is especially useful when describing closures and function pointers.
Variance describes how substitutions in a type position propagate. &T is covariant in both T and its lifetime — substituting a longer-lived reference for a shorter-lived one is always safe. &mut T, however, is invariant in T because write access would otherwise allow type confusion; raw pointers split similarly, with *const T covariant and *mut T invariant. The zero-sized marker PhantomData<T> is a common tool for influencing variance, drop-check behavior, and auto-trait inference in smart-pointer-like types — it tells the compiler to treat a struct as if it owned or borrowed a T even when T is not stored directly. The drop check itself is the compile-time analysis that prevents generic types from outliving values they may need to drop during teardown; PhantomData participates in this analysis, and the unsafe attribute #[may_dangle] on a Drop impl can be used to declare that the drop implementation does not access data of the type's generic parameters, relaxing the check when implementing low-level structures like intrusive linked lists.
A slice is a reference to a contiguous sequence of elements, written &[T] for a shared slice or &mut [T] for a mutable slice. Slices always borrow data and consist of a pointer plus a length, without owning the underlying storage. They are constructed with range syntax: [..] takes the whole sequence, [a..] from index a through the end, [..b] from the start to b exclusive, and [a..b] the elements from a to b exclusive. Because of unsized coercion, a function accepting &[T] can be called with a &Vec<T>, an array reference, or any other borrowing form that points to a contiguous run of Ts.
The relationship between String and &str is one of the most common applications of these ideas. A String is an owned, growable, heap-allocated UTF-8 buffer, while a &str is an immutable borrowed view into UTF-8 data — there is no way to mutate through a &str, so growing or modifying the text requires a String (or a &mut str). Creating a String from a literal can be done with String::from("hello") or "hello".to_string(), and extracting a borrowed view is as simple as &s (via deref coercion), &s[..], or the explicit s.as_str().
The reason s.len() works whether s is a String or a &str lies in the interaction between Deref and method resolution. The Deref trait, with its Deref::deref method, allows the compiler to dereference through a value with * and to coerce references automatically — a &String coerces to a &str because String implements Deref<Target = str>. This is called deref coercion, and it is what makes function arguments flexible: passing a &Vec<T> where a &[T] is expected works seamlessly. Auto-deref takes this further during method lookup, repeatedly dereferencing the receiver until a method is found, so chained methods on smart pointers often Just Work. The same principle applies to Box<T>, a smart pointer that owns a heap-allocated T and is dropped (along with the inner value) when it goes out of scope.
Interior mutability is a family of patterns that allows mutation through a shared reference, with the borrowing rules enforced at runtime rather than at compile time. This is needed when the borrow checker cannot statically prove that a mutation is safe, but you can still guarantee it through other means such as single-threaded access or explicit locking. Two of the most common interior-mutability types are Cell<T> and RefCell<T>.
Cell<T> is restricted to Copy types and works by swapping the value in place: you call cell.set(x) to replace the contents, and cell.get() to copy them out. There is no runtime tracking of borrows, and so no possibility of a runtime panic from this style. RefCell<T>, by contrast, works for any T and enforces the aliasing-XOR-mutability rule dynamically. RefCell::borrow() returns a Ref guard that acts as a shared reference and releases the borrow when dropped, while RefCell::borrow_mut() returns a RefMut guard that acts as a mutable reference. Violating the rules — for example, calling borrow_mut() while a Ref is alive — triggers a panic! rather than a compile-time error, shifting the failure mode from build time to runtime.
For multi-threaded code, the interior-mutability story is anchored by Mutex<T> and RwLock<T>. A Mutex<T> provides exclusive access: locking returns a MutexGuard smart pointer that dereferences to T and releases the lock when dropped. An RwLock<T> is a reader-writer lock that allows either many concurrent shared readers or exactly one exclusive writer, with the choice enforced at runtime. These primitives are essential when the type system alone cannot prove the absence of data races, but the combination of Send/Sync bounds on what they guard keeps safe Rust data-race-free in practice.
When multiple parts of a program legitimately need to own the same data, Rust offers reference-counted smart pointers. Rc<T> is the single-threaded choice: cloning an Rc<T> increments a reference count, dropping one decrements it, and the inner value is freed when the count reaches zero. Because the count is updated without atomic operations, Rc<T> is not Send or Sync, but it is the fastest shared-ownership pointer available outside of multithreading.
When ownership must be shared between threads, Arc<T> takes its place. The API is identical to Rc, but the count is updated with atomic operations, making it slightly slower but safe to send across thread boundaries. Both Rc and Arc support non-owning companions through Weak<T>, a reference counted alongside its strong counterpart but one that does not keep the value alive. Weak references cannot directly access the inner value; instead, upgrade() returns an Option<Rc<T>> (or Arc<T>), where None indicates that the value has already been dropped. This pattern is essential for breaking cycles: if two Rcs pointed at each other, neither would ever reach a count of zero. By making one direction Weak, the cycle is broken and memory can be reclaimed.
The compiler's Send and Sync marker traits are how Rust propagates thread-safety information through the type system. A type that implements Send can have its ownership transferred to another thread, while a type that implements Sync can be shared between threads through an &T. Both are auto-implemented for types whose components are all Send or Sync, and either can be implemented manually with unsafe when the programmer takes responsibility for upholding the guarantees. These markers prevent data races in safe Rust: a &mut T can only be Send if T: Send, and shared access through &T requires T: Sync. The intentional escape hatch from compile-time enforcement — when, for example, you need a graph data structure that references its own nodes — is the Rc<RefCell<T>> pattern, which admittedly can produce reference cycles and the memory leaks that the borrow checker alone cannot prevent.
Pattern matching interacts with ownership in subtle but useful ways. By default, destructuring a struct or tuple in a let or match moves the fields out — prefix a binding with ref to take it by shared reference, or ref mut to take it by mutable reference. Rust 1.26 introduced match ergonomics, so when you match on a reference, the compiler automatically inserts the ref for you, sparing the boilerplate. Destructuring assignments like let (a, b) = tuple; move (or copy) the fields into individual bindings, and a partial move — pulling some fields out of a struct while leaving others — leaves the parent binding in a partially moved state from which the unmoved fields can still be borrowed, but through which the original whole cannot be used.
For loops and iteration also reflect ownership choices. A for x in collection expression expands to collection.into_iter() by default and consumes the collection; iterating by reference requires collection.iter() (yielding &T) and iterating mutably requires collection.iter_mut() (yielding &mut T). The iterator returned by vec.iter() borrows from the vector, so its lifetime is tied to that borrow and it cannot outlive the borrow. Enums follow the same rules: an enum owns exactly one variant's worth of data, and moving the enum moves that data. A match can move out of one variant by binding its fields directly in the arm, and the compiler tracks which bindings belong to which variant so that partial moves across variants remain safe.
Rust also features types whose sizes are not known at compile time — dynamically sized types, or DSTs, such as [T] and str. DSTs can only be used behind a pointer: &[T], Box<[T]>, and so on. The implicit bound T: Sized is added to every generic type parameter by default; opting out is done by writing T: ?Sized, which is necessary whenever you want a parameter to be a slice or a trait object. The compile-time analysis that checks types cannot outlive the references they contain is called the drop check, and it is what prevents generic containers from accidentally outliving the data they would need to drop. When that analysis is too conservative — as in some smart-pointer designs — the #[may_dangle] attribute on a Drop impl relaxes it. For values that must not be moved at all, such as the self-referential state machines used by async runtimes, Pin<P> provides a way to fix a value at a stable memory address. Finally, the orphan rule guards against conflicting trait implementations: a foreign trait may be implemented for a foreign type only when at least one of the trait or the type is local to the current crate — a constraint that, together with all the rest, lets the broader ecosystem of crates remain coherent.
a to end; from start to b (exclusive); elements a to b (exclusive).std::mem::drop(value). You cannot call value.drop() directly because Drop::drop takes &mut self and would still leave the binding valid.panic!.let; the new binding shadows the old until the new one goes out of scope or is shadowed again.Borrowed(&'a T) | Owned(<T as ToOwned>::Owned) that avoids cloning when a borrowed value is sufficient.T, for variance, drop check, and auto-trait purposes.Copy elements, so the array itself is Copy and is duplicated on assignment without moving.ref explicitly.Drill this topic
120 flashcards on Rust Ownership And Borrowing Explained — free, no signup needed to start.
Study Rust Ownership And Borrowing Explained flashcardsLearnWiki pages are generated with AI assistance from LearnCoachAssist's reviewed study catalog and may contain errors — verify anything critical against your course materials.