Module A2 - Advanced Syntax, Ownership, references

Slides (or pdf)

A2.0 Borrowing

Fix the two examples in the exercises/A2/0-borrowing crate. Don't forget you can run individual binaries by using cargo run --bin 01 in that directory. Make sure to follow the instructions that are in the comments.

When it comes to borrowing in Rust:

  • If a value is borrowed, it is not moved and the ownership stays with the original owner
  • To borrow in Rust, we create a reference (&):
fn main() {
    let mut s = String::from("hello");
    change(&mut s);
    println!("{s}");
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}
  • A mutable reference can even fully replace the original value. To do this, you can use the dereference operator * to modify the value:
#![allow(unused)]
fn main() {
*some_string = String::from("Goodbye");
}

A2.1 Error Propagation

Follow the instructions in the comments of excercises/A2/1-error-propagating/src/main.rs.

  • A really powerful enum is the Result, very useful when we think about error handling and propagation:
#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}
  • Results are so common that there is a special operator associated with them, the ? operator:
#![allow(unused)]
fn main() {
fn can_fail() -> Result<i64, Error> {
  let intermediate_result = divide(10, 0)?;
  Ok(divide(intermediate_result, 0)? * 2)
}
}
  • The ? operator does an implicit match, if there is an error, that error is then immediately returned and the function returns early
  • If the result is Ok(_) then the value is extracted and we can continue right away

A2.2 Slices

Follow the instructions in the comments of excercises/A2/2-slices/src/main.rs. You will have to implement the well-known merge sort algorithm and fix the existing code so it compiles. Don't take too much time on the extra assignment, instead come back later once you've done the rest of the excercises.

The merge sort algorithm follows a divide-and-conquer approach. It breaks down an unsorted list into smaller sub-lists, sorts each of these sub-lists recursively, and eventually merges them back together in the correct order.

A slice is a dynamically sized view into a contiguous sequence:

  • Contiguous: elements are layed out in memory such that they are evenly spaced
  • Dynamically sized: the size of the slice is not stored in the type, but is determined at runtime
  • View: a slice is never an owned data structure

You can use ranges to create a slice from parts of a vector or array:

fn sum(data: &[i32]) -> i32 { /* ... */ }

fn main() {
    let v = vec![0, 1, 2, 3, 4, 5, 6];
    let all = sum(&v[..]);
    let except_first = sum(&v[1..]);
    let except_last = sum(&v[..5]);
    let except_ends = sum(&v[1..5]);
}

The range start..end contains all values x with start <= x < end.

A2.3 Error Handling

Follow the instructions in the comments of excercises/A2/3-error-handling/src/main.rs.

A panic in Rust is the most basic way to handle errors:

  • A panic is an all or nothing kind of error
  • A panic will immediately stop running the current thread/program

What would you do when there is an error? Use the enum Result like mentioned in another exercise:

#![allow(unused)]
fn main() {
enum Result<T, E> {
  Ok(T),
  Err(E),
}

enum DivideError {
  DivisionByZero,
  CannotDivideOne,
}

fn divide(x: i64, y: i64) -> Result<i64, DivideError> {
  if x == 1 {
    Err(DivideError::CannotDivideOne)
  } else if y == 0 {
    Err(DivideError::DivisionByZero)
  } else {
    Ok(x / y)
  }
}
}

The caller may decide how to handle a possible error:

#![allow(unused)]
fn main() {
fn div_zero_fails() {
  match divide(10, 0) {
    Ok(div) => println!("{div}"),
    Err(e) => panic!("Could not divide by zero"),
  }
}
}
  • The divide function’s signature is explicit in how it can fail
  • The function’s caller can decide what to do, even if it is panicking

A2.4 Boxed Data

Follow the instructions in the comments of excercises/A2/4-boxed-data/src/main.rs.

Boxing something is the way to store values on the heap. A Box uniquely owns that value, there is no one else that also owns the same value. Even if the type inside the box is Copy, the box itself is not, move semantics apply to a box.

fn main() {
    // put an integer on the heap
    let boxed_int = Box::new(10);
}

There are several reasons to box a variable on the heap:

  • When something is too large to move around
  • You need something that is sized dynamically
  • For writing recursive data structures

A2.5 Bonus - Ring Buffer

This is a bonus exercise! Follow the instructions in the comments of excercises/A2/5-bonus-ring-buffer/src/main.rs.