Rust - Ownership


Ownership is a central part of Rust. Ownership is similar to a unique_ptr in C++, but for all variables, including referenced variables.

  • Each value has a variable that's called its owner
  • There can only be one owner at a time
  • When the owner goes out of scope, the value will be dropped

Clone

To create a deep copy of a variable, using the clone method is required.

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

println!("s1 = {}, s2 = {}", s1, s2);

Copy

The Copy trait copies a variable onto the stack. Scalar types and tuples implement the Copy trait by default.

Copy VS Drop

A type may either have a Copy trait or a Drop trait. If a type or any part of a type has implemented the Drop trait then it cannot have the Copy trait. This is to make it very obvious if the type is to be placed on the stack or the heap.

Functions

Similar to assignment, variables are either moved or copied into a function. Same for return values. For example:

fn main() {
    let s1 = String::from("Daltie");
    takes_ownership(s1); // s1 is no longer valid

    let x = 3;
    makes_copy(x); // x is still valid

    let s2 = gives_ownership(); // The return value from gives_ownership() is moved

    let s3 = takes_and_gives_ownership(s2); // s2 is no longer valid
} 
// s3 is dropped. 
// s1 and s2 were already moved, so nothing happens. 
// x goes out of scope.

fn takes_ownership(some_str: String) {
    println!("{}", some_str);
}

fn makes_copy(some_int: i32) {
    println!("{}", some_int);
}

fn gives_ownership() -> String {
    let some_str = String::from("Daltie");
    some_str
}

fn takes_and_gives_ownership(some_str: String) -> String {
    some_str
}

Move

In Rust, you need to know what variables will be put on the stack and which variables will be added to the heap. Heap and stack variables are treated differently.

Stack
Stack variables consist of basic types with a known size at compile time. Stack variables are valid until they go out of scope. For example:
let x = 3;
let y = x;

Both x and y are valid and contain the value 3 until they go out of scope.

Heap

Heap variables follow similar but more strict rules to shallow copies in Python in the sense that the new variable will point to the data, however, in Rust, the old variable becomes invalid. For example:

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

Line 1 initializes a String variable by creating a string of size 5(ish) on the heap and a pointer to said string on the stack. Line 2 then takes ownership of this heap string. s1 now no longer points to the string and s1 is no longer valid and cannot be used until it is re-assigned. The data is NEVER copied, instead it behaves similarly to C++'s move semantics.

See the Rust Book for fantastic diagrams of this process, specifically Figure 4-4.

References

Functions

Rust lets a function borrow a variable using references.

fn main() {
    let s1 = String::from("Daltie");
    let len = find_length(&s1);

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

fn find_length(s: &String) -> usize {
    s.len()
}
// s is never actually given ownership, it is simply borrowing the String,
// thus nothing happens when s goes out of scope
Mutable References

Rust allows for exactly one mutable reference per scope (as long as the variable is active).

fn main() {
    let mut s = String::from("Daltie");

    change(&mut s);

    let r1 = &mut s;
    r1.push_str(" is ");
    println!("{}", r1); // "Daltie Cole is"
    
    let r2 = &mut s; // Okay because r1 is never used after r2 creation
    r2.push_str(" cool!");
    println!("{}", r2); // "Daltie Cole is cool!"

    // Either unlimited immutable references are allowed at once OR one mutable reference is allowed per scope
    let r3 = &s;
    let r4 = &s;
    //let r5 = &mut s; // BREAKS

    println!("{}, {}", r3, r4);
}

fn change(some_str: &mut String) {
    some_str.push_str(" Cole");
}