Module A2 - Advanced Syntax, Ownership, references
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
.