Overview
Master Rust's unique ownership system that provides memory safety without garbage collection through compile-time checks.
Ownership Rules
Three fundamental ownership rules:
- Each value in Rust has a variable that's its owner
- There can only be one owner at a time
- When the owner goes out of scope, the value is dropped
fn ownership_basics() {
// s owns the String
let s = String::from("hello");
// Ownership moved to s2
let s2 = s;
// Error: s no longer owns the value
// println!("{}", s);
println!("{}", s2); // OK
} // s2 dropped here, memory freed
Move Semantics
Ownership transfer (move):
fn move_semantics() {
let s1 = String::from("hello");
// Ownership moved to function
takes_ownership(s1);
// Error: s1 no longer valid
// println!("{}", s1);
}
fn takes_ownership(s: String) {
println!("{}", s);
} // s dropped here
// Return ownership from function
fn gives_ownership() -> String {
String::from("hello")
}
fn main() {
let s = gives_ownership();
println!("{}", s);
}
Copy trait for stack types:
fn copy_types() {
// Types implementing Copy are duplicated, not moved
let x = 5;
let y = x; // x copied to y
println!("x: {}, y: {}", x, y); // Both valid
// Copy types: integers, floats, bool, char, tuples of Copy types
let tuple = (1, 2.5, true);
let tuple2 = tuple;
println!("{:?} {:?}", tuple, tuple2); // Both valid
}
Borrowing
Immutable borrowing (references):
fn immutable_borrow() {
let s1 = String::from("hello");
// Borrow s1 (immutable reference)
let len = calculate_length(&s1);
println!("Length of '{}' is {}", s1, len); // s1 still valid
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but doesn't drop the value
// Multiple immutable borrows allowed
fn multiple_immutable_borrows() {
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{}, {}, {}", r1, r2, r3); // OK
}
Mutable borrowing:
fn mutable_borrow() {
let mut s = String::from("hello");
// Mutable borrow
change(&mut s);
println!("{}", s); // "hello, world"
}
fn change(s: &mut String) {
s.push_str(", world");
}
// Only ONE mutable borrow allowed at a time
fn mutable_borrow_rules() {
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // Error: cannot borrow mutably twice
println!("{}", r1);
}
// Cannot mix mutable and immutable borrows
fn no_mix_borrows() {
let mut s = String::from("hello");
let r1 = &s; // Immutable borrow
let r2 = &s; // Another immutable borrow
// let r3 = &mut s; // Error: cannot borrow mutably while immutably borrowed
println!("{} {}", r1, r2);
}
Non-lexical lifetimes (NLL):
fn non_lexical_lifetimes() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
// r1 and r2 no longer used after this point
// OK: immutable borrows ended
let r3 = &mut s;
println!("{}", r3);
}
Lifetimes
Lifetime annotations:
// Lifetime 'a ensures returned reference lives as long as both inputs
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string");
let string2 = String::from("short");
let result = longest(&string1, &string2);
println!("Longest: {}", result);
}
Lifetime in structs:
// Struct holds a reference, needs lifetime annotation
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention: {}", announcement);
self.part
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = ImportantExcerpt {
part: first_sentence,
};
println!("{}", excerpt.part);
}
Lifetime elision rules:
// Compiler infers lifetimes in these cases:
// Rule 1: Each reference parameter gets its own lifetime
fn first_word(s: &str) -> &str {
// Expanded: fn first_word<'a>(s: &'a str) -> &'a str
s.split_whitespace().next().unwrap_or("")
}
// Rule 2: If one input lifetime, assign to all outputs
fn foo(s: &str) -> &str {
s
}
// Rule 3: If &self or &mut self, its lifetime assigned to outputs
impl<'a> ImportantExcerpt<'a> {
fn get_part(&self) -> &str {
// Expanded: fn get_part<'a>(&'a self) -> &'a str
self.part
}
}
Static lifetime:
// 'static means reference lives for entire program duration
fn static_lifetime() -> &'static str {
"This string is stored in binary"
}
// String literals have 'static lifetime
let s: &'static str = "hello world";
Smart Pointers
Box for heap allocation:
fn box_pointer() {
// Allocate value on heap
let b = Box::new(5);
println!("b = {}", b);
} // b deallocated when out of scope
// Recursive types require Box
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn recursive_type() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
Rc for reference counting:
use std::rc::Rc;
fn rc_example() {
let a = Rc::new(5);
// Clone Rc pointer, increment count
let b = Rc::clone(&a);
let c = Rc::clone(&a);
println!("Reference count: {}", Rc::strong_count(&a)); // 3
// All owners must go out of scope before value is dropped
}
// Sharing data in graph structures
enum RcList {
Cons(i32, Rc<RcList>),
Nil,
}
use RcList::{Cons as RcCons, Nil as RcNil};
fn shared_ownership() {
let a = Rc::new(RcCons(5, Rc::new(RcCons(10, Rc::new(RcNil)))));
// b and c both reference a
let b = RcCons(3, Rc::clone(&a));
let c = RcCons(4, Rc::clone(&a));
}
RefCell for interior mutability:
use std::cell::RefCell;
fn refcell_example() {
let value = RefCell::new(5);
// Borrow mutably
*value.borrow_mut() += 1;
// Borrow immutably
println!("Value: {}", value.borrow());
}
// Combine Rc and RefCell for shared mutable data
use std::rc::Rc;
use std::cell::RefCell;
fn rc_refcell() {
let value = Rc::new(RefCell::new(5));
let a = Rc::clone(&value);
let b = Rc::clone(&value);
*a.borrow_mut() += 10;
*b.borrow_mut() += 20;
println!("Value: {}", value.borrow()); // 35
}
Ownership Patterns
Taking ownership vs borrowing:
// Take ownership when you need to consume the value
fn consume(s: String) {
println!("{}", s);
}
// Borrow when you only need to read
fn read(s: &String) {
println!("{}", s);
}
// Borrow mutably when you need to modify
fn modify(s: &mut String) {
s.push_str(" modified");
}
fn main() {
let mut s = String::from("hello");
read(&s); // Still own s
modify(&mut s); // Still own s
consume(s); // No longer own s
}
Builder pattern with ownership:
struct Config {
name: String,
value: i32,
}
struct ConfigBuilder {
name: Option<String>,
value: Option<i32>,
}
impl ConfigBuilder {
fn new() -> Self {
ConfigBuilder {
name: None,
value: None,
}
}
// Take ownership and return ownership
fn name(mut self, name: String) -> Self {
self.name = Some(name);
self
}
fn value(mut self, value: i32) -> Self {
self.value = Some(value);
self
}
fn build(self) -> Config {
Config {
name: self.name.unwrap_or_default(),
value: self.value.unwrap_or(0),
}
}
}
fn main() {
let config = ConfigBuilder::new()
.name(String::from("app"))
.value(42)
.build();
}
Slice Types
String slices:
fn string_slices() {
let s = String::from("hello world");
// Slice references part of string
let hello = &s[0..5];
let world = &s[6..11];
// Shorthand
let hello = &s[..5];
let world = &s[6..];
let whole = &s[..];
println!("{} {}", hello, world);
}
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}
Array slices:
fn array_slices() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // &[i32]
assert_eq!(slice, &[2, 3]);
}
Clone vs Copy
Understanding Clone trait:
#[derive(Clone)]
struct Point {
x: f64,
y: f64,
}
fn clone_example() {
let p1 = Point { x: 1.0, y: 2.0 };
// Explicit clone (deep copy)
let p2 = p1.clone();
// Both valid
println!("{} {}", p1.x, p2.x);
}
Copy trait limitations:
// Copy requires all fields to implement Copy
#[derive(Copy, Clone)]
struct Coord {
x: i32,
y: i32,
}
// Cannot derive Copy with String field
// #[derive(Copy, Clone)] // Error
struct Person {
name: String, // String doesn't implement Copy
}
Drop Trait
Custom cleanup with Drop:
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data: {}", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers created");
} // d dropped, then c dropped
Manual drop:
fn manual_drop() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("Before drop");
drop(c); // Manually drop early
println!("After drop");
}
When to Use This Skill
Use rust-ownership-system when you need to:
- Understand Rust's memory management model
- Write memory-safe code without garbage collection
- Handle ownership transfer between functions
- Work with references and borrowing
- Implement structs with lifetime parameters
- Use smart pointers (Box, Rc, RefCell)
- Debug borrow checker errors
- Choose between ownership, borrowing, and cloning
- Implement custom Drop behavior
- Work with slices and references safely
Best Practices
- Prefer borrowing over ownership transfer when possible
- Use immutable borrows by default, mutable only when needed
- Keep borrow scopes as small as possible
- Use lifetime elision when compiler can infer lifetimes
- Choose appropriate smart pointer for use case
- Avoid RefCell in performance-critical code
- Use slices instead of owned types in function signatures
- Clone only when necessary (it's explicit and visible)
- Implement Drop for custom cleanup logic
- Let compiler guide you with borrow checker errors
Common Pitfalls
- Moving value and trying to use it afterward
- Creating multiple mutable borrows simultaneously
- Mixing mutable and immutable borrows
- Returning references to local variables
- Fighting the borrow checker instead of understanding it
- Overusing clone() to avoid ownership issues
- Not understanding lifetime relationships
- Circular references with Rc (use Weak)
- Panicking with RefCell borrow violations at runtime
- Using 'static lifetime incorrectly