Ownership Borrowing
Rust ownership, borrowing, and move semantics for writing memory-safe code without a garbage collector
You are an expert in Rust's ownership and borrowing system for writing memory-safe, performant code without a garbage collector.
## Key Points
1. Each value in Rust has exactly one owner.
2. When the owner goes out of scope, the value is dropped.
3. Ownership can be transferred (moved), but once moved, the original binding is invalid.
- You can have either one `&mut T` or any number of `&T` at the same time — never both.
- References must always be valid (no dangling references).
- Non-lexical lifetimes (NLL) allow borrows to end before the scope ends, as soon as the reference is last used.
- Prefer borrowing (`&T` / `&mut T`) over taking ownership when the function does not need to store the value.
- Accept `&str` instead of `&String`, and `&[T]` instead of `&Vec<T>`, to be more flexible.
- Use `String` fields in structs for owned data; return `&str` from accessor methods.
- Avoid unnecessary `.clone()` calls — they indicate a design issue more often than a real need.
- Leverage destructuring and pattern matching to work with owned and borrowed data ergonomically.
- **Moving out of a collection**: You cannot move an element out of a `Vec` by index directly. Use `.remove()`, `.swap_remove()`, or iterate with `.into_iter()`.
## Quick Example
```rust
let s1 = String::from("hello");
let s2 = s1; // s1 is moved into s2
// println!("{s1}"); // ERROR: s1 is no longer valid
println!("{s2}"); // OK
```
```rust
let x = 42;
let y = x; // x is copied, both are valid
println!("{x} {y}"); // OK
```skilldb get rust-skills/Ownership BorrowingFull skill: 171 linesOwnership & Borrowing — Rust Programming
You are an expert in Rust's ownership and borrowing system for writing memory-safe, performant code without a garbage collector.
Core Philosophy
Ownership is not a limitation imposed by Rust -- it is an explicit model of how memory and resources actually work. Every value has one owner because every allocation needs exactly one point of responsibility for deallocation. The borrow checker enforces at compile time what C++ programmers enforce by convention and discipline, eliminating use-after-free, double-free, and data races as entire categories of bugs.
The ownership model trains you to think about data flow. When you pass a value to a function, you decide: does the function need to own it, or just look at it? This decision, made explicit through T vs &T vs &mut T, documents intent in the type signature. Code that compiles under Rust's ownership rules communicates its memory and aliasing story to every reader, not just the compiler.
When the borrow checker rejects your code, it is revealing a real tension in your design -- two parts of the program want incompatible access to the same data. The fix is almost never to reach for clone() or Rc as a silencer. Instead, restructure so that ownership flows naturally: pass data down, return results up, and keep mutable access narrow and short-lived.
Anti-Patterns
- Cloning to silence the borrow checker: Reaching for
.clone()every time the compiler complains trains you to ignore the design signal the error carries. Clone when you genuinely need independent copies; restructure ownership when you do not. - Accepting
&Stringor&Vec<T>in function signatures: These are strictly less flexible than&strand&[T], which accept both owned and borrowed forms. Using the concrete reference type forces callers into unnecessary allocation. - Moving values into functions that only need to read them: Taking
Stringwhen&strwould suffice forces the caller to give up ownership or clone. Accept borrows by default; take ownership only when the function needs to store or consume the value. - Fighting the borrow checker with interior mutability as a first resort: Wrapping everything in
RefCellorMutexto get around borrow errors defeats compile-time checking and pushes failures to runtime. Use interior mutability when shared mutation is genuinely required, not as an escape hatch. - Partial moves without understanding the consequences: Moving one field out of a struct invalidates the entire struct. If you need to take a field, use
std::mem::takeorOption::taketo leave a valid value behind.
Overview
Rust's ownership system is its most distinctive feature. Every value has exactly one owner, and the value is dropped when the owner goes out of scope. Borrowing allows references to data without taking ownership, enforced at compile time by the borrow checker.
The three rules of ownership:
- Each value in Rust has exactly one owner.
- When the owner goes out of scope, the value is dropped.
- Ownership can be transferred (moved), but once moved, the original binding is invalid.
Core Concepts
Move Semantics
Types that do not implement Copy are moved on assignment or when passed to functions:
let s1 = String::from("hello");
let s2 = s1; // s1 is moved into s2
// println!("{s1}"); // ERROR: s1 is no longer valid
println!("{s2}"); // OK
Copy Types
Primitive types (i32, f64, bool, char) and tuples of Copy types implement Copy and are duplicated on assignment rather than moved:
let x = 42;
let y = x; // x is copied, both are valid
println!("{x} {y}"); // OK
Immutable Borrowing (&T)
You can have any number of simultaneous immutable references:
fn calculate_length(s: &String) -> usize {
s.len()
}
let s = String::from("hello");
let len = calculate_length(&s);
println!("{s} has length {len}"); // s is still valid
Mutable Borrowing (&mut T)
Only one mutable reference is allowed at a time, and no immutable references can coexist:
fn push_world(s: &mut String) {
s.push_str(", world");
}
let mut s = String::from("hello");
push_world(&mut s);
println!("{s}"); // "hello, world"
Borrow Checker Rules
- You can have either one
&mut Tor any number of&Tat the same time — never both. - References must always be valid (no dangling references).
- Non-lexical lifetimes (NLL) allow borrows to end before the scope ends, as soon as the reference is last used.
let mut v = vec![1, 2, 3];
let first = &v[0]; // immutable borrow starts
println!("{first}"); // immutable borrow ends here (NLL)
v.push(4); // mutable borrow is now OK
Implementation Patterns
Returning Ownership
fn create_greeting(name: &str) -> String {
format!("Hello, {name}!")
}
let greeting = create_greeting("Rust");
Taking Ownership in Structs
struct Config {
name: String,
values: Vec<i32>,
}
impl Config {
fn new(name: String, values: Vec<i32>) -> Self {
Self { name, values }
}
fn name(&self) -> &str {
&self.name
}
}
Slice Borrowing
Slices borrow a contiguous section of a collection without owning it:
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[..i];
}
}
s
}
The Clone Escape Hatch
When you need a deep copy, use clone(). Use it intentionally, not as a way to silence the borrow checker:
let original = vec![1, 2, 3];
let cloned = original.clone();
// Both are now independent and valid
Best Practices
- Prefer borrowing (
&T/&mut T) over taking ownership when the function does not need to store the value. - Accept
&strinstead of&String, and&[T]instead of&Vec<T>, to be more flexible. - Use
Stringfields in structs for owned data; return&strfrom accessor methods. - Avoid unnecessary
.clone()calls — they indicate a design issue more often than a real need. - Leverage destructuring and pattern matching to work with owned and borrowed data ergonomically.
Common Pitfalls
- Moving out of a collection: You cannot move an element out of a
Vecby index directly. Use.remove(),.swap_remove(), or iterate with.into_iter(). - Borrowing across
.await: Holding a reference across an await point in async code requires the reference to beSend; this often forces owned data instead. - Partial moves in structs: Moving one field out of a struct makes the whole struct unusable unless remaining fields implement
Copy. - Confusing
&Stringand&str: Always prefer&strin function signatures —&Stringauto-derefs to&str, but accepting&stralso handles string literals and slices. - Mutable borrow in a loop: Borrowing mutably inside a loop that also reads the same data requires careful scoping or restructuring with indices.
Install this skill directly: skilldb add rust-skills
Related Skills
Async Rust
Async Rust programming with async/await, Tokio runtime, futures, and concurrent task patterns
Cargo Workspace
Cargo workspaces, project structure, dependency management, and multi-crate Rust project organization
Error Handling
Rust error handling with Result, Option, the ? operator, and ecosystem crates anyhow and thiserror
Lifetimes
Rust lifetime annotations for ensuring reference validity and understanding the borrow checker
Pattern Matching
Rust pattern matching with match, if let, while let, destructuring, and advanced match patterns
Smart Pointers
Rust smart pointers Box, Rc, Arc, RefCell, and their combinations for heap allocation and shared ownership