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:

fn main() {
    let x = 5; // x is valid from this point
    {
        let y = 10; // y is valid from this point
        println!("The value of y is: {}", y);
    } // y goes out of scope here
    println!("The value of x is: {}", x);
}

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:

fn main() {
    let x = String::from("Hello");
    let y = x;
    println!("The value of x is: {}", x); // Compile error
    println!("The value of y is: {}", y);
}

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 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 and f64.
  • 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:

fn main() {
    let x = 42;
    let y = x;
    println!("The value of x is: {}", x); // No compile error, because the value of `x` is copied, not moved.
    println!("The value of y is: {}", y);
}

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:

fn main(){
  let x = String::from("Hello");
  let y = x; // `x` is moved to `y`
  println!("x is {}", x); // Compile error
  println!("y is {}", y);
}

Let's explain the code above, step by step:

  1. First, x is the owner of the value Hello.

Moving values step 1 Moving values step 1

  1. Then, x is moved to y and now y is the owner of the value Hello.

Moving values step 2 Moving values step 2

  1. When running println!("x is {}", x);, it will give a compile error because x is no longer the owner of the value Hello, it has been moved to y. 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:

fn main(){
  let x = String::from("Hello");
  let y = x.clone(); // Value has been re-allocated (cloned)
  println!("x is {}", x);
  println!("y is {}", y);
}

Cloned values 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:

fn main(){
  let s = String::from("Hello");
  takes_ownership(s);
  println!("s is {}", s); // Compile error
}
 
fn takes_ownership(s: String){
  println!("s is {}", s);
}

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:

fn main(){
  let s = String::from("Hello");
  let s = takes_ownership(s);
  println!("s is {}", s);
}
 
fn takes_ownership(s: String) -> String { // Gives back the ownership
  println!("s is {}", s);
  s
}

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.

Rustfinity.com

Links

  1. Home
  2. Learn Rust
  3. Get Started
  4. Practice Rust
  5. Challenges
  6. Tutorials
  7. Blog
  8. Open source
  9. Learn Gleam

Socials

  1. GitHub
  2. X

Legal

  1. Privacy Policy
  2. Terms of Service