I've recently started learning the Rust programming language, I'm still very much a novice but thought it would be beneficial to attempt to explain some of its features here.
Ownership is one of the key features of Rust. Unlike other languages, Rust does not free unused memory with the use of a garbage collector or require the programmer to manage it manually. Instead, Rust uses a system of ownership to manage its values with rules which the compiler checks at compile time, meaning that you get the best of both worlds as your program is not slowed down at runtime by a garbage collector and you are not required to manage the memory usage yourself.
The basic idea of ownership is a simple one; every value is owned by a variable, it can only be owned by one variable at a time and when the variable goes out of scope, the value is dropped from memory.
Stack or Heap?
First, it's probably a good idea to talk about the two types of values that Rust deals with. Some values have a fixed size which is known at compile time and which will not change throughout the life of the program, for example u32. These values can be be stored on the stack and can be passed around freely without needing to worry about ownership. The other type of value is dynamic, it has a size which is not known at compile time and which could change whilst the program is running, such as a String. This second type is stored on the heap and a pointer to its location in memory is stored on the stack instead. The data on the heap can change size but its pointer will always remain the same size.
Ownership exists because of these dynamic values. Other languages encounter problems when pointers are left in the stack which reference memory that has already been reallocated; Rust does not have this problem. When a variable owns a value on the heap, no other variable can own it. It is possible to create pointers which reference the variable that owns the value and ownership can be moved to a different variable. When the variable that owns the value goes out of scope, the value is removed from the heap so that the memory can be reallocated.
To illustrate this, please take a look at the two examples below, the first uses &str (size known at compile time) and the second String (size not known at compile time).
With &str
fn main() {
let hello = "hello";
println!("{} from the main function", hello);
other(&hello);
println!("{} again from the main function", hello);
}
fn other(greeting: &str) {
println!("{} from the other function", greeting);
}
hello from the main function hello from the other function hello again from the main function
With String
fn main() {
let hello = String::from("hello");
println!("{} from the main function", hello);
other(hello);
println!("{} again from the main function", hello);
}
fn other(greeting: String) {
println!("{} from the other function", greeting);
}
error[E0382]: borrow of moved value:
hello
--> src/main.rs:5:49 | 2 | let hello = String::from("hello"); | ----- move occurs becausehello
has typestd::string::String
, which does not implement theCopy
trait 3 | println!("{} from the main function", hello); 4 | other(hello); | ----- value moved here 5 | println!("{} again from the main function", hello); | ^^^^^ value borrowed here after move
The compiler has told us that we've moved ownership of the value from the hello
variable in the main
function to the greeting
variable in the other
function and therefore it cannot be used again by the main
function at line 5. The message also says "move occurs because hello
has type std::string::String
, which does not implement the Copy
trait". I'll probably do a post about traits at some point so I won't go into them here but what the compiler is saying is that this is not a type that can be copied, hence why it has been moved. All types that can be stored on the stack can be copied, this is why the ownership rules do not apply to them; because their size is known, the performance cost of copying the values is not worth worrying about so that is what happens.
Cloning
This doesn't mean that we can't copy values on the heap though, cloning the value is one way we can make our code compile:
fn main() {
let hello = String::from("hello");
println!("{} from the main function", hello);
other(hello.clone());
println!("{} again from the main function", hello);
}
fn other(greeting: String) {
println!("{} from the other function", greeting);
}
hello from the main function hello from the other function hello again from the main function
Can you spot the difference? Now, on line 4, we call the other
function with a clone of the value held by the hello
variable. Obviously this is not the most efficient way to write your program, and it can be risky if you don't know the size of the values which are going to be getting cloned. Also, if your functions mutate the value in some way then this is not a good solution:
fn main() {
let mut hello_from = Vec::new();
hello_from.push("main");
one(hello_from.clone());
two(hello_from.clone());
println!("hello from {:?}", hello_from)
}
fn one(mut hello_from: Vec<&str>) {
hello_from.push("one");
}
fn two(mut hello_from: Vec<&str>) {
hello_from.push("two");
}
hello from ["main"]
Here, we're creating a Vector in which we want to collect the names of the functions which it has been passed to. Because we are using clone, each function is getting its own copy of the value which it is mutating, meaning that when we print the vector it only contains "main".
Moving
The first solution you might arrive at is to return the variable each time:
fn main() {
let mut hello_from = Vec::new();
hello_from.push("main");
hello_from = one(hello_from);
hello_from = two(hello_from);
println!("hello from {:?}", hello_from)
}
fn one(mut hello_from: Vec<&str>) -> Vec<&str> {
hello_from.push("one");
hello_from
}
fn two(mut hello_from: Vec<&str>) -> Vec<&str> {
hello_from.push("two");
hello_from
}
hello from ["main", "one", "two"]
In this example we are moving the ownership of the value to a new variable in each function scope. This is perfectly valid but in my opinion it has its downsides. First, it requires more code to be written, the one
and two
functions both now need to declare a return type and we need to overload the hello_from
variable in the main
function each time the value is returned (lines 4 & 5). Secondly, I think it's easier to understand code when the owner of values stays in one scope; this is made possible with borrowing.
Borrowing
fn main() {
let mut hello_from = Vec::new();
hello_from.push("main");
let hello_ref = &mut hello_from;
one(hello_ref);
two(hello_ref);
println!("hello from {:?}", hello_from)
}
fn one(hello_from: &mut Vec<&str>) {
hello_from.push("one");
}
fn two(hello_from: &mut Vec<&str>) {
hello_from.push("two");
}
hello from ["main", "one", "two"]
Here we create a mutable reference to our variable and pass that to the functions instead. Because it is a mutable reference we can push the values and they will be added to vector as if we had passed the vector itself, however the ownership of the value never leaves the hello_from
variable in the main
function.
Some Caveats
As well as ensuring memory safety, Rust also protects us from race conditions with some rules around references. You can create as many immutable references as you wish, but you are not allowed to create more than one mutable reference at a time:
fn main() {
let mut hello_from = Vec::new();
hello_from.push("main");
let hello_ref1 = &mut hello_from;
let hello_ref2 = &mut hello_from;
one(hello_ref1);
two(hello_ref2);
println!("hello from {:?}", hello_from)
}
fn one(hello_from: &mut Vec<&str>) {
hello_from.push("one");
}
fn two(hello_from: &mut Vec<&str>) {
hello_from.push("two");
}
error[E0499]: cannot borrow
hello_from
as mutable more than once at a time --> src/main.rs:6:26 | 5 | let hello_ref1 = &mut hello_from; | --------------- first mutable borrow occurs here 6 | let hello_ref2 = &mut hello_from; | ^^^^^^^^^^^^^^^ second mutable borrow occurs here 7 | one(hello_ref1); | -------------- first borrow later used here
You are also not allowed to create a mutable reference whilst there are already one or more immutable references:
fn main() {
let mut hello_from = Vec::new();
hello_from.push("main");
let display_ref = &hello_from;
let hello_ref = &mut hello_from;
one(hello_ref);
two(hello_ref);
display(display_ref)
}
fn one(hello_from: &mut Vec<&str>) {
hello_from.push("one");
}
fn two(hello_from: &mut Vec<&str>) {
hello_from.push("two");
}
fn display(hello_from: &Vec<&str>) {
println!("hello from {:?}", hello_from)
}
error[E0502]: cannot borrow
hello_from
as mutable because it is also borrowed as immutable --> src/main.rs:6:25 | 5 | let display_ref = &hello_from; | ----------- immutable borrow occurs here 6 | let hello_ref = &mut hello_from; | ^^^^^^^^^^^^^^^ mutable borrow occurs here ... 10 | display(display_ref) | --------------- immutable borrow later used here
But the compiler can tell when no further changes can be made via a mutable reference and allows you to start creating immutable references again!
fn main() {
let mut hello_from = Vec::new();
hello_from.push("main");
let hello_ref = &mut hello_from;
one(hello_ref);
two(hello_ref);
let display_ref = &hello_from;
display(display_ref)
}
fn one(hello_from: &mut Vec<&str>) {
hello_from.push("one");
}
fn two(hello_from: &mut Vec<&str>) {
hello_from.push("two");
}
fn display(hello_from: &Vec<&str>) {
println!("hello from {:?}", hello_from)
}
hello from ["main", "one", "two"]
So that's my first post on Rust, I hope it was useful (and correct!), all being well I intend to write a couple of these as I find that in doing so really helps to increase my own understanding and if they can help somebody else at the same time then that's even better. If you've got any comments then let me know on Twitter. 😃
Top comments (0)