Ownership and Borrowing in Rust
/ 5 min read
Table of Contents
Rust’s ownership system is probably most unique thing about the language. Coming from C++, it felt weird at first, but now I can’t imagine going back to manual memory management.
What Ownership Actually Is
Every value in Rust has exactly one owner at any time. When the owner goes out of scope, the value is automatically dropped:
fn main() { let s = String::from("hello"); // s owns the string println!("{}", s);} // s goes out of scope, string is automatically freedNo delete calls, no memory leaks, no double-free bugs. The compiler tracks everything.
The Move Semantics
Unlike C++ where you copy by default, Rust moves by default for non-Copy types:
let s1 = String::from("hello");let s2 = s1; // s1 is moved to s2
// println!("{}", s1); // Error! s1 no longer owns the dataprintln!("{}", s2); // OK, s2 owns it nowThis prevents the classic double-free bug where two variables try to free the same memory.
When I Actually Use Ownership
Most of the time, I work with ownership when:
- Building data structures: Deciding who owns what:
struct User { name: String, // User owns the name email: String, // User owns the email}
struct UserManager { users: Vec<User>, // UserManager owns all users}
impl UserManager { fn add_user(&mut self, user: User) { self.users.push(user); // Ownership transfers to the vector }
fn get_user(&self, index: usize) -> Option<&User> { self.users.get(index) // Return a borrow, not ownership }}- Function parameters: Choosing between taking ownership or borrowing:
// Takes ownership - function consumes the stringfn process_and_consume(data: String) { println!("Processing: {}", data); // data is dropped when function ends}
// Borrows immutably - can read but not modifyfn process_readonly(data: &String) { println!("Reading: {}", data); // Original owner still has the data}
// Borrows mutably - can modifyfn process_and_modify(data: &mut String) { data.push_str(" - processed"); println!("Modified: {}", data);}- Resource management: RAII patterns work beautifully:
use std::fs::File;use std::io::prelude::*;
fn write_log(message: &str) -> std::io::Result<()> { let mut file = File::create("app.log")?; // File owns the handle file.write_all(message.as_bytes())?; Ok(()) // File is automatically closed when it goes out of scope}- Avoiding unnecessary clones: Understanding when to move vs borrow:
struct Config { database_url: String, port: u16,}
impl Config { // Takes ownership of the string - no unnecessary clone fn new(database_url: String, port: u16) -> Self { Self { database_url, port } }
// Returns a borrow for reading fn get_database_url(&self) -> &str { &self.database_url }}
// Usagelet config = Config::new("postgres://localhost".to_string(), 8080);println!("Connecting to: {}", config.get_database_url());Borrowing Rules
Rust has strict borrowing rules that prevent data races:
- You can have either one mutable reference OR multiple immutable references
- References must always be valid
let mut data = vec![1, 2, 3];
let r1 = &data; // Immutable borrowlet r2 = &data; // Another immutable borrow - OKprintln!("{:?} {:?}", r1, r2);
let r3 = &mut data; // Mutable borrow// println!("{:?}", r1); // Error! Can't use immutable borrow while mutable existsr3.push(4); // OK, using the mutable borrowprintln!("{:?}", r3);Real World Example
Here’s a simple cache implementation showing ownership patterns:
use std::collections::HashMap;
struct Cache<K, V> { data: HashMap<K, V>, max_size: usize,}
impl<K, V> Cache<K, V>where K: std::hash::Hash + Eq + Clone,{ fn new(max_size: usize) -> Self { Self { data: HashMap::new(), max_size, } }
// Takes ownership of key and value fn insert(&mut self, key: K, value: V) { if self.data.len() >= self.max_size { // Simple eviction: clear half the cache let keys_to_remove: Vec<K> = self.data .keys() .take(self.max_size / 2) .cloned() .collect();
for key in keys_to_remove { self.data.remove(&key); } }
self.data.insert(key, value); }
// Borrows key to look up, returns optional borrow of value fn get(&self, key: &K) -> Option<&V> { self.data.get(key) }
// Takes ownership of key, returns ownership of value if found fn remove(&mut self, key: &K) -> Option<V> { self.data.remove(key) }}
// Usagelet mut cache = Cache::new(100);cache.insert("user:123".to_string(), "John Doe".to_string());
if let Some(name) = cache.get(&"user:123".to_string()) { println!("Found user: {}", name); // name is borrowed}Working with References
References are non-owning pointers. They’re like C++ references but with compile-time safety:
fn find_longest<'a>(s1: &'a str, s2: &'a str) -> &'a str { if s1.len() > s2.len() { s1 } else { s2 }}
let string1 = String::from("hello");let string2 = "world";
let result = find_longest(&string1, string2);println!("Longest: {}", result);The 'a lifetime annotation tells the compiler that the returned reference lives as long as the shorter of the two input references.
Common Patterns I Use
- Borrowing for function parameters when you don’t need ownership:
// Good: borrow when you only need to readfn print_user_info(user: &User) { println!("{}: {}", user.name, user.email);}
// Less efficient: taking ownership unnecessarilyfn print_user_info_bad(user: User) { println!("{}: {}", user.name, user.email); // user is dropped here, can't be used by caller anymore}- Using
&mutfor in-place modifications:
fn normalize_email(email: &mut String) { *email = email.to_lowercase(); email.retain(|c| !c.is_whitespace());}
let mut email = String::from(" John@Example.Com ");normalize_email(&mut email);println!("{}", email); // "john@example.com"- Clone when you need independent copies:
#[derive(Clone)]struct Config { settings: HashMap<String, String>,}
fn create_dev_config(base: &Config) -> Config { let mut dev_config = base.clone(); // Independent copy dev_config.settings.insert("debug".to_string(), "true".to_string()); dev_config}The Mental Model
Think of ownership like real-world ownership:
- Only one person can own a car at a time
- You can lend your car (borrow) but you still own it
- You can’t lend your car to two people for racing at the same time (no multiple mutable borrows)
- If you sell your car (move), you don’t own it anymore
Once this clicks, Rust’s error messages start making sense. The compiler is just enforcing these logical rules at compile time, preventing the runtime crashes you’d get in other languages.
The learning curve is steep initially, but the payoff is huge: memory safety without garbage collection, and fearless concurrency because data races are impossible.