» Quick Introduction to Rust » 1. Basics » 1.12 Scoping

Scoping

Scopes play an important part in ownership, borrowing, and lifetimes. That is, they indicate to the compiler when borrows are valid, when resources can be freed, and when variables are created or destroyed.

RAII

Variables in Rust do more than just hold data in the stack: they also own resources, e.g. Box<T> owns memory in the heap. Rust enforces RAII (Resource Acquisition Is Initialization), so whenever an object goes out of scope, its destructor is called and its owned resources are freed.

This behavior shields against resource leak bugs, so users will never have to manually free memory or worry about memory leaks.

fn create_box() {
    // Allocate an integer on the heap
    let _1 = Box::new(58i32);
    // `_1` is destroyed here, and memory gets freed
}

fn main() {
    // Allocate an integer on the heap
    let _2 = Box::new(5i32);

    // Creating lots of boxes
    // There's no need to manually free memory!
    for _ in 0..1_000 {
        create_box();
    }

    // `_2` is destroyed here, and memory gets freed
}

Destructor

The notion of a destructor in Rust is provided through the Drop trait. The destructor is called when the resource goes out of scope. This trait is not required to be implemented for every type, only implement it for your type if you require its own destructor logic.

struct HasDrop;

impl Drop for HasDrop {
    fn drop(&mut self) {
        println!("Dropping HasDrop!");
    }
}

struct HasTwoDrops {
    one: HasDrop,
    two: HasDrop,
}

impl Drop for HasTwoDrops {
    fn drop(&mut self) {
        println!("Dropping HasTwoDrops!");
    }
}

fn main() {
    let _x = HasTwoDrops { one: HasDrop, two: HasDrop };
    println!("Running!");
}

Ownership

Because variables are in charge of freeing their own resources, resources can only have one owner. This also prevents resources from being freed more than once.

When doing assignments (let a = b) or passing function arguments by value (func(v)), the ownership of the resources is transferred. In Rust-speak, this is known as a move.

fn use_box(c: Box<i32>) {
    println!("Destroying a box that contains {}", c);
}

fn main() {
    // `a` is a pointer to a heap allocated integer
    let a = Box::new(58i32);
    // Move `a` into `b`
    let b = a;

    // error[E0382]: borrow of moved value: `a`
    println!("a -> {}", a);

    use_box(b); // Destroying a box that contains 58

    // error[E0382]: borrow of moved value: `b`
    println!("b -> {}", b);
}

Borrowing

Most of the time, we'd like to access data without taking ownership over it. To accomplish this, Rust uses a borrowing mechanism. Instead of passing objects by value (T), objects can be passed by reference (&T).

// This function takes ownership of a box and destroys it
fn useup_box_i32(boxed_i32: Box<i32>) {
    println!("Destroying box that contains {}", boxed_i32);
}

// This function borrows an i32
fn borrow_i32(borrowed_i32: &i32) {
    println!("This int is: {}", borrowed_i32);
}

fn main() {
    // Create a boxed i32 in the heap, and a i32 on the stack
    let boxed_i32 = Box::new(5_i32);
    let stacked_i32 = 8_i32;

    // Borrow the contents of the box. Ownership is not taken,
    // so the contents can be borrowed again.
    borrow_i32(&boxed_i32); // This int is: 5
    borrow_i32(&stacked_i32); // This int is: 8

    // `boxed_i32` can now give up ownership to `useup_box_i32` and be destroyed
    useup_box_i32(boxed_i32); // Destroying box that contains 5
}

Mutable reference

Mutable data can be mutably borrowed using &mut T. This is called a mutable reference and gives read/write access to the borrower.

#[derive(Clone, Copy)]
struct Album {
    // `&'static str` is a reference to a string allocated in read only memory
    singer: &'static str,
    title: &'static str,
    year: u32,
}

// This function takes a reference to a album
fn borrow_album(album: &Album) {
    println!("I immutably borrowed {} - {} edition", album.title, album.year);
}

// This function takes a reference to a mutable album and changes `year` to 2014
fn new_edition(album: &mut Album) {
    album.year = 2014;
    println!("I mutably borrowed {} - {} edition", album.title, album.year);
}

fn main() {
    let immut_album = Album {
        // string literals have type `&'static str`
        singer: "Taylor Swift",
        title: "reputation",
        year: 2011,
    };

    // Create a mutable copy
    let mut mut_album = immut_album;

    // Immutably borrow an immutable object
    borrow_album(&immut_album);
    // I immutably borrowed reputation - 2011 edition

    // Immutably borrow a mutable object
    borrow_album(&mut_album);
    // I immutably borrowed reputation - 2011 edition
    
    // Borrow a mutable object as mutable
    new_edition(&mut mut_album);
    // I mutably borrowed reputation - 2014 edition

    // error[E0596]: cannot borrow `immut_album` as mutable
    new_edition(&mut immut_album);
}

Lifetime

A lifetime is a construct of the compiler uses to ensure all borrows are valid. Specifically, a variable's lifetime begins when it is created and ends when it is destroyed.

Explicit Annotation

// `succeeded_borrow` takes two references to `i32` which have different
// lifetimes `'a` and `'b`. These two lifetimes must both be at
// least as long as the function `succeeded_borrow`.
fn succeeded_borrow<'a, 'b>(x: &'a i32, y: &'b i32) {
    println!("x is {} and y is {}", x, y);
}

// A function which takes no arguments, but has a lifetime parameter `'a`.
fn failed_borrow<'a>() {
    let _x = 12;

    // error[E0597]: `_x` does not live long enough
    let y: &'a i32 = &_x;
    // Attempting to use the lifetime `'a` as an explicit type annotation 
    // inside the function will fail because the lifetime of `&_x` is shorter
    // than that of `y`. A short lifetime cannot be coerced into a longer one.
}

fn main() {
    let (five, eight) = (5, 8);
    
    // Borrows (`&`) of both variables are passed into the function.
    succeeded_borrow(&five, &eight);
    // Any input which is borrowed must outlive the borrower. 
    // In other words, the lifetime of `five` and `eight` must 
    // be longer than that of `succeeded_borrow`.
    
    failed_borrow();
    // `failed_borrow` contains no references to force `'a` to be 
    // longer than the lifetime of the function, but `'a` is longer.
    // Because the lifetime is never constrained, it defaults to `'static`.
}

Struct Lifetimes

Annotation of lifetimes in structures are similar to functions.

// Both references here must outlive this structure.
#[derive(Debug)]
struct NamedBorrowed<'a> {
    x: &'a i32,
    y: &'a i32,
}

#[derive(Debug)]
enum Either<'a> {
    Num(i32),
    Ref(&'a i32),
}

fn main() {
    let (x, y) = (5, 8);

    let double = NamedBorrowed { x: &x, y: &y };
    let reference = Either::Ref(&x);
    let number    = Either::Num(y);

    println!("x and y are borrowed in {:?}", double);
    println!("x is borrowed in {:?}", reference);
    println!("y is not borrowed in {:?}", number);
}

Static Lifetime

Rust has a few reserved lifetime names. One of those is 'static.

// A reference with 'static lifetime:
let s: &'static str = "hello world";

// 'static as part of a trait bound:
fn generic<T>(x: T) where T: 'static {}

Reference Lifetime

There are two common ways to make a variable with 'static lifetime, and both are stored in the read-only memory of the binary:

  • Make a constant with the static declaration.
  • Make a string literal which has type: &'static str.
static NUM: i32 = 18;
let s = "I'm a string literal";

Trait Bound

As a trait bound, it means the type does not contain any non-static references.

use std::fmt::Debug;

fn print_it( input: impl Debug + 'static ) {
    println!("'static value: {:?}", input);
}

fn main() {
    // i is owned and contains no references, thus it's 'static:
    let i = 58;
    print_it(i); // 'static value: 58

    // &i is not 'static
    // error[E0597]: `i` does not live long enough
    print_it(&i);
}

Some lifetime patterns are overwhelmingly common and so the borrow checker will allow you to omit them to save typing and to improve readability. This is known as lifetime elision.

Code Challenge

Try to modify the code provided in the editor to add proper lifetimes.

Loading...
> code result goes here
Prev
Next