Rust: ownership

·

4 min read

Best Video Material

[YouTube] 300 seconds of Rust - 5. Rust Ownership System

Best Text Material

The Rust Programming Language

Comprehensive Rust: Ownership

Additional Read / Watch

[YouTube] The Rust Borrow Checker - A Deep Dive - Nell Shamrell-Harrington, Microsoft

tl;dr

Ownership in Rust refers to a set of rules that govern how a Rust program manages memory.

Content

Ownership Rules

  • 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.

Stack and Heap

The stack is an organized store where every data chunk stored needs to be of a known, fixed size. It operates on the lifo (last in, first out) principle.

💡 Data with an unknown size at compile time or size that might change must be stored on the heap instead

The heap is an unorganized store. Once data is put on the heap (also called allocated), the memory allocator finds a memory chunk that is big enough and returns a pointer (the memory address of the stored data).

Ownership

When a value stored on the heap is reassigned to another variable, the initial variable is no longer valid. The heap location is reused if a variable's value is moved to another variable.

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1); // 's1' is no longer valid because the value stored at its memory address was moved to 's2'

You can also copy the value instead of moving it by using the .clone() method.

let s1 = String::from("hello");
let s2 = s1.clone();

println!("{}, world! Oh, {}", s1, s2); // both values are valid because the heap data was copied from 's1' into 's2'

For data types stored on the stack, the default behavior is to copy the value into the variable while keeping the initial variable intact. This behavior is used because the value size is known at compile time, so the copy is trivial.

let x = 5;
let y = x;

println!("x = {}, y = {}", x, y); // both are valid because '5' was copied into variable 'y'

Ownership in functions works in the same way as in scopes. Once a variable is passed into a function, by default, the value is moved into the function's scope and is no longer valid in the initial scope for data stored on the heap. For values stored on the stack, the value is copied into the function.

References and Borrowing

Borrowing, at its core, is passing a pointer to a function so that it can make changes to data stored at a memory location without taking ownership.

// Default borrowing syntax
fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

// 'calculate_length' borrows a string
fn calculate_length(s: &String) -> usize {
    s.len()
}

& - reference operator

* - dereference operator

The reference (&some_variable) is immutable by default. This can be changed by making the data source mutable and changing the function to accept a mutable pointer instead.

// Example with a mutable pointer
fn extend_string(s: &mut String) -> usize {
    s.push_str(" is a bit rusty")
}

There can be only one mutable reference to a value at all times. You also can't have both mutable and immutable references at the same time (while having multiple immutable ones is just fine). Having mutable references in one scope and immutable ones in another scope is also fine.

let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// Variables r1 and r2 will not be used after this point

let r3 = &mut s; // no problem
println!("{}", r3);

Reference Rules

  • At any given time, you can have either one mutable reference or any number of immutable references.

  • References must always be valid.