Chapter 4 - Understanding Ownership

Heap vs Stack

  • When your code calls a function, the values passed into the function (including, potentially, pointers to data on the heap) and the function’s local variables get pushed onto the stack. When the function is over, those values get popped off the stack.
  • Accessing data in the heap is slower than accessing data on the stack because you have to follow a pointer to get there.

Ownership rules

  • Each value in Rust 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.

The String type

  • This type is stored on heap.
  • Can be mutated.

#![allow(unused)]
fn main() {
{
    let s1 = String::from("hello"); // s1 is valid from this point forward

    // do stuff with s1
}                                  // this scope is now over, and s1 is no
                                   // longer valid
}

When a variable goes out of scope, Rust calls a special function for us. This function is called drop, and it’s where the author of String can put the code to return the memory. Rust calls drop automatically at the closing curly bracket.

Representation of s1 in memory

Representation of s1 in memory

Ways Variable and Data interact


#![allow(unused)]
fn main() {
let x = 5;
let y = x;
}
  • In the above code, we bind the value 5 to x; then make a copy of the value in x and bind it to y.
  • This happens because integers are simple values with a known, fixed size, and these two 5 values are pushed onto the stack.

Now let's look at the String version:


#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1;
}

This looks very similar to the previous code, so we might assume that the way it works would be the same: that is, the second line would make a copy of the value in s1 and bind it to s2. But this isn’t quite what happens.

What happens after s2 = s1

s2=s1

But s1 gets invalidated

s1 gets invalidated

Ownership and functions

The semantics for passing a value to a function are similar to those for assigning a value to a variable. Passing a variable to a function will move or copy, just as assignment does.

fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                    // ... and so is no longer valid here

    let x = 5;                      // x comes into scope

    makes_copy(x);                  // x would move into the function,
                                    // but i32 is Copy, so it’s okay to still
                                    // use x afterward

} // Here, x goes out of scope, then s. But because s's value was moved, nothing
  // special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
  // memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.

References and Borrowing

Example code of References:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

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

fn calculate_length(s: &String) -> usize {
    s.len()
}

These ampersands are references, and they allow you to refer to some value without taking ownership of it.

We call having references as function parameters borrowing.

Just as variables are immutable by default, so are references. We’re not allowed to modify something we have a immutable reference to.

Mutuable references

Example code of mutable reference:

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

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

But mutable references have one big restriction: you can have only one mutable reference to a particular piece of data in a particular scope. This code will fail:


#![allow(unused)]
fn main() {
let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;
}

The benefit of having this restriction is that Rust can prevent data races at compile time. A similar rule exists for combining mutable and immutable references:


#![allow(unused)]
fn main() {
let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
}

The Slice type

Another data type that does not have ownership (the other one is references) is the slice.

String Slices


#![allow(unused)]
fn main() {
let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];
}

We can create slices using a range within brackets by specifying [starting_index..ending_index], where starting_index is the first position in the slice and ending_index is one more than the last position in the slice.

Other examples:


#![allow(unused)]
fn main() {
let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];
}

#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];
}

#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];
}

The type that signifies “string slice” is written as &str:


#![allow(unused)]
fn main() {
fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}
}

Other Slices


#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];
}

This slice has the type &[i32].

Bonus: Explanation of String and it's variation from burntsushi