Rust Ownership
The Three Rules
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED to s2
// println!("{}", s1); // ERROR: s1 is no longer valid
println!("{}", s2); // "hello"
Assignment of heap data moves ownership. The original variable is invalidated. This prevents double-free at compile time.
Move vs Copy
// Copy types: i32, f64, bool, char, tuples of Copy types
let x = 42;
let y = x; // x is COPIED (bits duplicated)
println!("{x}"); // works — x is still valid
// Heap types: String, Vec<T>, Box<T>
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED
// s1 is gone
// Explicit deep copy
let s3 = s2.clone();
println!("{s2}"); // "hello" — s2 still valid
println!("{s3}"); // "hello" — independent copy
Copy types live entirely on the stack. The compiler copies them bitwise on assignment. Heap types transfer ownership on assignment — clone() makes an independent deep copy.
Ownership Transfer in Functions
fn take_ownership(s: String) {
println!("took: {s}");
} // s is dropped here
fn give_ownership() -> String {
String::from("hello") // ownership moves to caller
}
fn main() {
let s = String::from("hello");
take_ownership(s);
// s is no longer valid here
let s2 = give_ownership();
println!("{s2}"); // "hello"
}
Passing a heap value to a function moves it. The caller loses access. Return values transfer ownership back. To use a value after passing it, either clone it or use references (borrowing).
Scope and Drop
{
let s = String::from("hello");
// s is valid here
} // s goes out of scope — drop is called, memory freed
// Custom drop
struct Connection {
host: String,
}
impl Drop for Connection {
fn drop(&mut self) {
println!("closing connection to {}", self.host);
}
}
Drop is Rust’s destructor. It runs automatically when a value goes out of scope. You cannot call drop() directly on a value — use std::mem::drop(value) to drop early.
Smart Pointers
// Box<T> — heap allocation, single owner
let b = Box::new(42);
println!("{}", *b); // dereference
// Rc<T> — reference counting, single thread
use std::rc::Rc;
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // increment count, not deep copy
println!("count: {}", Rc::strong_count(&a)); // 2
// Arc<T> — atomic reference counting, thread-safe
use std::sync::Arc;
let data = Arc::new(vec![1, 2, 3]);
let data_clone = Arc::clone(&data);
std::thread::spawn(move || {
println!("{:?}", data_clone);
});
Box is single ownership on the heap. Rc shares ownership within a single thread. Arc shares across threads. Rc::clone is cheap — it increments a counter, not a deep copy.