Ownership rules
The rules of ownership in Rust are simple but strict:
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
Variable scope
We've already seen that variables have a scope in Rust and once the scope ends, the value is dropped and they no longer take up memory. This is a key part of Rust's memory safety guarantees.
Let's look at an example to refresh our memory:
The value of y
is dropped sooner than x
because it has a different scope, after the block of code where y
is declared ends, y
is no longer valid and it's dropped.
Transferring ownership (Moving values)
In most programming languages, when you assign a value to a variable, you are copying the value to the variable if it's a primitive type like an integer or a boolean and if it's a object or a reference type, you are copying the reference to the object.
But in Rust, it's a bit different, when you assign a value to a variable, you are moving the value to the variable and if you re-assign that variable to another one, the value is moved to the new variable and the old variable is no longer valid.
Here's an example:
In the code above, x
is moved to y
and x
is no longer valid after that. This is because String
is a complex type and it's stored on the heap, and moving it to another variable is more efficient than copying it.
Running the code:
Moving ownership
When we try to compile the code, we'll get a message from the compiler value borrowed here after move. This is because x
is no longer valid and now the variable y
is the owner of the value Hello
, therefore, x
is no longer valid and we can't use it.
Copy types
This behavior of moving variables from one place to another is only true for complex types like String
, Vec
, etc. Types that implement the Copy
trait are copied instead of moved.
We already touched upon the different types of variables in Rust and how they are stored in memory, it'd be good if you can go back and read that chapter to understand the different types of variables in Rust.
Some types implement the Copy
trait (we'll discuss traits in more details later), and these types can be efficiently copied by simply assigning them to another variable. This is because they are simple types and stored on the stack, taking a copy of them is extremely cheap, since their size is fixed and known at compile time.
The primitive types that were discussed in the data types chapter are the types in Rust that implement the Copy
trait and can be efficiently copied. These types are:
- All the integer types like
i32
,u32
,i64
,u64
, etc. - The boolean type
bool
. - The character type
char
. - The floating-point types like
f32
andf64
. - Tuples that contain only types that implement the
Copy
trait, for example,(i32, i32)
.
If we did the same thing as we did with the String
above but with a simple type like an integer, it would work:
The value of x
is copied to y
and it is not moved, therefore making both x
and y
valid after the assignment.
Copy
types are types that are not being moved whenever assigned to a new variable or passed to a function, instead, they are copied. This is because they are simple types, stored on the stack, and their size is fixed and known at compile time.
Complex types
Types like String
and Vec
do not implement the Copy
trait and can not be copied. This is because they are more complex, stored on the heap, their size is not fixed and is not known at compile time and they can be changed at runtime, and making a copy of them every time they are assigned to a new variable will be extremely expensive and eat up a lot of memory, and the Ownership does not automatically create a new reference to the value, instead, it moves the value to the new variable.
Let's do the same thing we did above but with a String
instead:
Let's explain the code above, step by step:
- First,
x
is the owner of the valueHello
.
Moving values step 1
- Then,
x
is moved toy
and nowy
is the owner of the valueHello
.
Moving values step 2
- When running
println!("x is {}", x);
, it will give a compile error becausex
is no longer the owner of the valueHello
, it has been moved toy
.x
is no longer a valid variable.
How to work with ownership?
What if you don't want to move the value to another variable, but you want to keep the original value and use it in another place?
Rust gives you a few options to handle such situations, you can either make a clone of the value or you can use the reference to the original value and read from that.
These two approaches have their own advantages and disadvantages, and their own use cases, let's discuss them in more detail.
Cloning complex types
Any other type that is not so simple to just copy like an i32
or bool
can be cloned if they implement the Clone
trait. This is entirely different from the copying and understanding the difference and how they work is crucial to understanding Rust's ownership system.
You already have read about the stack and the heap and you know how data is stored in memory, the difference between the Copy
and Clone
traits can be understood in the same way.
When you assign a variable to another variable which holds a type that implements Copy
like i32
, you are making a shallow copy of the value, and each value will be stored separately and independently on the stack.
But cloning is different, cloning is for those data types that their size is not fixed and unknown at compile time, cloning will create a deep copy of the value along with it's nested values and store it in a new location in memory which usually requires memory allocation on the heap.
Let's visualize this concept with an example:
Cloned values
In this example, both x
and y
are valid, because x
is cloned and a new value is created on the heap and assigned to y
. This way, you can keep the original value and use it in another place.
Using clone
can sometimes be expensive, especially when you are working with large chunks of data, because you are copying the entire value to a new location in memory, so you should be as conservative as possible when using clone
.
Functions and ownership
Ownership has a huge influence on the way you work with functions in Rust. You might find it a bit confusing at first, but once you get the hang of it, you'll see how powerful it is.
Let's look at an example:
This is a common mistake that beginners make, when you pass a value to a function, the value is moved to the function and the function becomes the owner of that value therefore the value is no longer available in the function which it was declared at and you can not use it in that function anymore.
This is not a good approach to give up every ownership of values to functions, sometimes you'd want to re-use the variable in the same function and share it between different functions.
Functions can also give up ownership to their return values, this will let you re-use the variable in the parent function, look closely at the return type of this function:
The return type of the function is -> String
, which means it gives back the ownership of the value, or we can say it returns an owned String
. This way, you can re-use the value in the parent function.
While this approach works, it's not desirable to move values back and forth between functions, the code becomes less readable and harder to maintain, and it's not the best practice to do so. It's also a bad approach because only one function can use the value at a time, makes concurrency harder to implement.
There's a few ways you can do that, just like we discussed before, you can make a clone of the value and pass that to the function and re-use the original in the same function, that way, you have 2 different values stored separately on the heap.
But this is not quite efficient, as we mentioned before that making a clone of a value could be quite expensive, in this case it's just a simple String
, but in real-world scenarios, it could be huge chunks of data.
The most efficient way to pass values for other functions to read is by using references, instead of giving up ownership, we'll send the function the address of the value stored in memory (the reference) and the function can read from that address without taking ownership of the value.
In the next lesson we're going to discuss references and how they work in Rust.