Programming languages like C/C++ give developers a lot of control over memory management. But this comes with a big risk - it's easy to make mistakes that lead to crashes, security holes, or bugs!
Some common issues that C/C++ programmers face include:
To avoid these problems, Rust uses an ownership system. This adds some rules that the compiler checks to ensure memory safety.
The key idea is that every value in Rust has an owner. The owner is in charge of that value - managing its lifecycle, freeing it, allowing access to it, etc.
By tracking ownership, Rust's compiler can ensure values are valid when you use them, prevent data races, and free memory when needed. All without requiring a garbage collector!
This ownership model powers Rust's safety and speed. By following a few ownership rules, your Rust programs will be protected from entire classes of memory-related problems.
Let's walk through the 10 ownership superpowers that Rust provides:
In Rust, every value, like a string or integer, has an owner. The owner is the variable that is bound to that value. For example:
let x = 5;
Here, x
is the owner of the integer value 5
. The variable x
keeps track of and manages that 5
value.
Think of it like x
taking ownership and responsibility over that 5
value. x
is now the boss of that value!
This ownership system avoids confusing situations with multiple variables pointing to the same value. With single ownership, it's clear that x
is the unique owner of the data 5
.
When the owner variable goes out of scope, Rust will call the drop
function on the value and clean it up:
{
let y = 5; // y is the owner of 5
} // y goes out of scope and 5 is dropped
Scope refers to the block that a variable is valid for. In the above example, y
only exists within the {}
curly braces. Once execution leaves that block, y
disappears, and the value 5
is dropped.
This automatic freeing of data avoids memory leaks. As soon as the owner y
goes away, Rust cleans up the value. No more worrying about dangling pointers or memory bloat!
Rust enforces single ownership for each value. This avoids expensive reference counting schemes:
let z = 5; // z owns 5
let x = z; // z's ownership moved to x
// z no longer owns 5!
In this example, transferring ownership from z
to x
is cheap. Some languages use reference counting where multiple variables can point to a value, but that has overhead.
With single ownership, Rust just updates an internal owner variable to move ownership from z
to x
. No costly counter updates.
Assigning an owner variable to a new variable moves the data:
let s1 = "hello".to_string(); // s1 owns "hello"
let s2 = s1; // s1's ownership moved to s2
// s1 can no longer use "hello"
Here, we create the string "hello" and bind it to s1
. Then, we assign s1
to a new variable s2
.
This transfers ownership from s1
to s2
. s1
no longer owns the string! The data itself was not copied, just the ownership moved.
This prevents accidentally making expensive copies. To really copy the data, you must use Rust's clone()
method to make the intent clear.
We can create reference variables that borrow ownership:
let s = "hello".to_string(); // s owns "hello"
let r = &s; // r immutably borrows s
// s still owns "hello"
println!("{}", r); // prints "hello"
The &
operator creates a reference r
that borrows ownership from s
for this scope.
Think of r
as temporarily borrowing the data that s
owns. s
still retains full ownership over the data. r
is just allowed to read the "hello" string.
There can only be one mutable reference to data at a time:
let mut s = "hello".to_string();
let r1 = &mut s; // r1 mutably borrows s
let r2 = &mut s; // error!
This prevents data races at compile time. The mutable reference r1
has exclusive write access to s
, so no other references are allowed until r1
is done.
This saves you from subtle concurrency bugs by making simultaneous data access impossible.
References must have shorter lifetimes than what they are borrowing:
{
let r;
let s = "hello".to_string();
r = &s; // error! r does not live long enough
} // s is dropped here
Here r
goes out of scope before s
. So r
would be referencing data of s
after s
is dropped!
Rust prevents use after free bugs by enforcing this rule that references cannot outlive their owners.
We can transfer or borrow ownership of struct data:
struct User {
name: String,
age: u32
}
let user1 = User {
name: "John".to_string(),
age: 27
}; // user1 owns struct
let user2 = user1; // ownership moved to user2
// user1 can no longer use this
let borrow = &user1; // borrow the struct via reference
// user1 still owns data
Structs group related data together, but the ownership rules still apply to their fields.
We can pass struct ownership to functions and threads or immutably borrow them. The same single owner/borrowing rules make struct usage safe.
Ownership applies to heap-allocated data:
let s1 = String::from("hello"); // s1 on stack owns heap data
let s2 = s1.clone(); // heap data copied to new location
// s1 and s2 own separate data
let r = &s1; // r immutably borrows s1's heap data
// s1 still owns heap data
Here s1
is allocated on the stack but contains a String
that points to heap-allocated text. The same ownership rules apply even though it's on the heap.
Rust prevents duplicate frees or use after free bugs, even when working with pointers. The ownership system keeps heap allocations safe.
Ownership powers Rust's fearless concurrency:
use std::thread;
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
We move ownership of v
into the spawned thread by using a move
closure. This prevents concurrent access to v
from multiple threads.
The ownership system makes concurrency safe and easy in Rust. There's no need for locking because the compiler enforces single ownership.
Rust's ownership system is designed to keep your code safe and fast. By enforcing a few key rules, entire classes of memory bugs are eliminated!
Some key lessons around ownership:
So, while ownership forces you to think about memory management, it's for a good reason - it squashes tons of potential bugs!
The ownership system is a big part of what makes Rust so reliable and fast. Following Rust's ownership rules will keep your code safe even as your programs grow large.
So embrace Rust's compile-time checks as helpful guidance rather than restrictions. Your future self will thank you when your Rust program runs smoothly without crashes or security holes!
If you like my article, feel free to follow me on HackerNoon.
Also published here.