Ownership Rules
Ownership in Rust
Ownership is Rust's most unique feature, enabling memory safety without a garbage collector. Understanding ownership is crucial for writing effective Rust code.
The Three Rules of Ownership
- Each value in Rust has a single owner
- There can only be one owner at a time
- When the owner goes out of scope, the value is dropped
Stack vs Heap
Understanding where data lives helps understand ownership:
fn main() {
// Stack data - fixed size, fast
let x = 5; // i32 on stack
let y = true; // bool on stack
// Heap data - dynamic size, slower
let s = String::from("hello"); // String on heap
let v = vec![1, 2, 3]; // Vec on heap
}
Move Semantics
When assigning heap data, ownership is moved:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2
// println!("{}", s1); // Error! s1 is no longer valid
println!("{}", s2); // This works
}
Copy Types
Some types implement Copy
and are duplicated instead of moved:
fn main() {
let x = 5;
let y = x; // x is copied to y
println!("x: {}, y: {}", x, y); // Both are valid
}
Types that implement Copy
:
- All integer types
- Boolean type
- Floating point types
- Character type
- Tuples containing only Copy types
Ownership and Functions
Passing values to functions transfers ownership:
fn main() {
let s = String::from("hello");
takes_ownership(s); // s is moved into the function
// println!("{}", s); // Error! s is no longer valid
let x = 5;
makes_copy(x); // x is copied
println!("x is still {}", x); // This works
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string goes out of scope and is dropped
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
}
Returning Values
Functions can transfer ownership back:
fn main() {
let s1 = gives_ownership(); // Function transfers ownership to s1
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); // s2 is moved in, s3 gets ownership
println!("s1: {}", s1);
println!("s3: {}", s3);
// println!("s2: {}", s2); // Error! s2 was moved
}
fn gives_ownership() -> String {
let some_string = String::from("yours");
some_string // Ownership is moved out
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // Return ownership
}
Clone
To deeply copy heap data, use clone()
:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // Deep copy
println!("s1: {}, s2: {}", s1, s2); // Both are valid
}
Drop Trait
When a value goes out of scope, Rust calls drop
:
struct CustomData {
data: String,
}
impl Drop for CustomData {
fn drop(&mut self) {
println!("Dropping CustomData with data: {}", self.data);
}
}
fn main() {
{
let c = CustomData {
data: String::from("my data"),
};
println!("CustomData created");
} // c goes out of scope, drop is called
println!("After scope");
}
Ownership in Collections
Collections own their elements:
fn main() {
let v = vec![String::from("hello"), String::from("world")];
// Moving out of vector
let first = v[0].clone(); // Must clone to avoid moving
println!("First: {}", first);
// Iterating transfers ownership
for s in v {
println!("{}", s);
}
// v is no longer accessible
}
Common Ownership Patterns
Builder Pattern
struct Config {
name: String,
value: i32,
}
impl Config {
fn new() -> Self {
Config {
name: String::new(),
value: 0,
}
}
fn name(mut self, name: String) -> Self {
self.name = name;
self // Return ownership
}
fn value(mut self, value: i32) -> Self {
self.value = value;
self // Return ownership
}
}
fn main() {
let config = Config::new()
.name(String::from("test"))
.value(42);
println!("Config: {} = {}", config.name, config.value);
}
Option for Nullable Ownership
fn main() {
let mut optional_string: Option<String> = Some(String::from("hello"));
// Take ownership out of Option
if let Some(s) = optional_string.take() {
println!("Took: {}", s);
}
println!("Option is now: {:?}", optional_string); // None
}
Memory Safety Examples
Preventing Use After Free
fn main() {
let reference;
{
let data = String::from("hello");
// reference = &data; // Error! data doesn't live long enough
}
// println!("{}", reference); // Would be use-after-free
}
Preventing Double Free
fn main() {
let s1 = String::from("hello");
let s2 = s1; // Ownership moved, not copied
// Only s2 will be dropped, preventing double free
}
Best Practices
- Prefer moving over cloning when possible for performance
- Use references (borrowing) when you don't need ownership
- Return owned values from functions when creating new data
- Use
String
for owned strings,&str
for borrowed strings - Consider
Rc
orArc
for shared ownership when needed
Common Pitfalls
Trying to Use Moved Values
fn consume(s: String) {
println!("{}", s);
}
fn main() {
let s = String::from("hello");
consume(s);
// consume(s); // Error! s was moved
}
Moving in Loops
fn main() {
let data = vec![String::from("a"), String::from("b")];
// This moves data
for s in data {
println!("{}", s);
}
// println!("{:?}", data); // Error! data was moved
// Use reference to avoid moving
let data2 = vec![String::from("a"), String::from("b")];
for s in &data2 {
println!("{}", s);
}
println!("{:?}", data2); // This works
}
Debugging Ownership
Use these techniques to understand ownership:
fn print_type_of<T>(_: &T) {
println!("{}", std::any::type_name::<T>());
}
fn main() {
let s = String::from("hello");
print_type_of(&s); // Borrow to check type without moving
// Check if Copy
let x = 5;
let y = x;
println!("x: {}, y: {}", x, y); // Both valid means Copy
}