Understanding Ownership

Stack and the heap

  • Data types that we know the size in advance are allocated on the stack. Examples are the primitive types: ints, floats, chars, bools, tuples containing primitive types,
  • On the other hand, data types that we don't know the size in advance such as a String type or Vec type are allocated on the heap. See stack vs heap for more info

Copying vs Moving

  • Types that implement the Copy trait are copied when you assign them to a variable (or pass it as an argument to a function). For example:
fn main() {
   let number1 = 10;
   println!("number1 is {}", number1);
   
   let mut number2 = number1;
   println!("number2 is {}", number2);
   number2 = 97;
   println!("number2 is now {}", number2);
   
   println!("number1 is still {}", number1);
 }
  • Types that do not implement Copy but instead implement the Drop trait are moved when you re-assign a variable of such type (or pass it as an argument to a function). This means the new variable you have assigned it to is now the new owner of that data. Examples of Rust built-in types that implement the Drop trait are String, Vec, File etc
fn main() {
   let fruit = String::from("apple");
   println!("fruit is {}", fruit);
   
   //fruit is moved into fruit2
   let fruit2 = fruit;
   println!("fruit2 is {}", fruit2);
   
   //cannot use fruit here
   //println!("{} is no longer in scope, it has been dropped", fruit);
}

If you uncomment, the last line in the above snippet and run the snippet, you'll see error messages about moving.

Same thing happens for method calls:

fn main() {
   let fruit = String::from("apple");
   println!("fruit is {}", fruit);
   
   let mut basket:Vec<String> = vec![];
   
   println!("basket is {:#?}", basket);
   
   //fruit is moved into the function
   put_in_basket(fruit, &mut basket);
   
   println!("basket is {:#?}", basket);
   
   //cannot use fruit here
   //println!("{} is no longer in scope, it has been dropped", fruit);
}

fn put_in_basket(theFruit: String, basket: &mut Vec<String>) {
   basket.push(theFruit);
}

If you uncomment, the last line in the above snippet and run the snippet, you'll see error messages about moving.

Ownership rules

There are three rules related to ownership in Rust namely:

  • Every value in Rust has an owner
  • There can be only one owner at a time
  • When the owner goes out of scope, the data/value is dropped (Source: ownership rules )

Who owns what?

In Rust, ownership really only makes sense when you think about types which are allocated on the heap such as String. These types also implement the Drop trait which allows such types to be cleaned up when they go out of scope. On the other hand, primitive types such as integers (signed and unsigned), chars, bools, tuples etc (as seen above ), are simply copied when you reassign variables of these types to a another variable or pass as method argument.

Ownership examples

Whenever you create an object on the heap and assign that object to a variable, the owner of that object is the variable you assigned it to. To understand ownership, we need to use a type like String which is allocated on the heap.

fn main() {
  let name = String::from("Olu Shiyanbade");
}

In the above, we build a new String which is allocated on the heap. The location of that String is assigned to the variable name which is in turn allocated on the stack. In this statement, variable name is said to be the owner of the String.

Changing ownership

fn main() {
  let name = String::from("Olu Shiyanbade");
  
  println!("In main, name is {}", name);
  
  let another_name = name;
  println!("In main, another_name is {}", another_name);
  
  //Can no longer use name because the owner is now variable `another_name`
  //println!("In main, name is {}", name);
  
  print_name(another_name);
 
  //Can no longer use another_name because the ownership was moved into the 
  //function's paramater when it was passed as an argument above.
  // Thus the owner is now the function parameter `name`
  //println!("name is {}", another_name);
}

fn print_name(name: String) {
  println!("In print_name, name is: {}", name);
}

In the above,

  • the String Olu Shiyanbade is initially owned by the name variable.
  • the snippet let another_name = name; assigns name to another_name. At this point, we say name is moved into another_name and another_name becomes the owner of the String Olu Shiyanbade while variable name goes out of scope (i.e. it is dropped).
  • similarly, when we do print_name(another_name, another_name is moved into the print_name function and that function's name parameter becomes the new owner. At this point another_name goes out of scope and can no longer be used beyond that point.