+ - 0:00:00
Notes for current slide
Notes for next slide

Lecture 01: Hello, Rust!

Based on: CIS 198 slides

Artyom Pavlov, 2019.

1 / 118

Overview

"Rust is a systems programming language that runs blazingly fast, prevents nearly all segfaults, and guarantees thread safety." – prev.rust-lang.org

"Empowering everyone to build reliable and efficient software. " – rust-lang.org

2 / 118

What is Rust?

Rust is:

  • Fast
  • Safe
  • Functional
  • Zero-cost
  • Excellent tooling
3 / 118

Fast

  • Rust compiles to native code
  • Rust has no garbage collector
  • Most abstractions have zero cost
  • Fine-grained control over lots of things
  • Pay for exactly what you need...
  • ...and pay for most of it at compile time
4 / 118

Safe

  • No null
  • No uninitialized memory
  • No dangling pointers
  • No double free errors
  • No manual memory management!
5 / 118

Functional

  • First-class functions
  • Trait-based generics
  • Algebraic datatypes
  • Pattern matching
6 / 118

Zero-Cost 100% Safe Abstractions

  • Rust's defining feature
  • Strict compile-time checks remove need for runtime
  • Big concept: Ownership
7 / 118

Tooling

  • cargo: one of the best package managers/build systems in class. Say no to dependency hell and makefiles!
  • Built-in benchmarks, tests and docs generator (rustdoc).
  • clippy: A LOT of additionall lints.
  • rustfmt: code formatting utility.
  • Rust Language Server (RLS) for improving IDE experience.
  • Many C/C++ tools are usable with Rust: GDB, LLDB, Valgrind, perf, gprof, etc.
8 / 118

Release Model

  • Rust has a new stable release every six weeks
  • Additionally Rust has 3-year edition releases
  • Nightly builds are available, well, nightly
  • Current stable: Rust 1.31 (2018 edition)
  • Train model:
Date Stable Beta Nightly
2018-09-13 🚂 1.29 🚆 1.30 🚝 1.31
2016-10-25 🚆 1.30 🚝 1.31 🚈 1.32
2016-12-06 🚝 1.31 🚈 1.32 🚅 1.33
9 / 118

Development

10 / 118

Who Uses Rust?

More companies and organizations can be found in the Friends of Rust page.

11 / 118

Administrivia

  • 5 lectures with practical exercises.
  • Attendance: 50% (10% per lecture)
  • Final project: 50%
  • Passing grade: 60%
  • Final project can be done individual or in a group (up to 5 people).
  • You can ask questions in the Canvas chat or personally (ISR Lab, room 151).
  • Class source material generally hosted on GitHub.
    • Corrections are welcomed via pull request/issue!
  • Course is in development - give us feedback!
13 / 118
14 / 118

Let's Dive In!

Hello, Rust!

fn main() {
println!("Hello, world!");
}
  • All code blocks have links to the Rust playpen so you can run them!
15 / 118

Basic Rust Syntax

16 / 118

Variable Bindings

  • Variables are bound with let:

    let x = 17;
  • Bindings are implicitly-typed: the compiler infers based on context.

  • The compiler can't always determine the type of a variable, so sometimes you have to add type annotations.

    let x: i16 = 17;
  • Variables are inherently immutable:

    let x = 5;
    x += 1; // error: re-assignment of immutable variable x
    let mut y = 5;
    y += 1; // OK!
17 / 118

Variable Bindings

  • Bindings may be shadowed:

    let x = 17;
    let y = 53;
    let x = "Shadowed!";
    // x is not mutable, but we're able to re-bind it
  • The shadowed binding for x above lasts until it goes out of scope.

  • Above, we've effectively lost the first binding, since both xs are in the same scope.

  • Patterns may also be used to declare variables:

    let (a, b) = ("foo", 12);
    let [c, d] = [1, 2];
18 / 118

Expressions

  • (Almost!) everything is an expression: something which returns a value.
    • Exception: variable bindings are not expressions.
  • The "nothing" type is called "unit", which is written ().
    • The type () has only one value: ().
    • () is the default return type.
  • Discard an expression's value by appending a semicolon. Now it returns ().
    • Hence, if a function ends in a semicolon, it returns ().
fn foo() -> i32 { 5 }
fn bar() -> () { () }
fn baz() -> () { 5; }
fn qux() { 5; }
19 / 118

Expressions

  • Because everything is an expression, we can bind many things to variable names:

    let x = -5;
    let y = if x > 0 { "greater" } else { "less" };
    println!("x = {} is {} than zero", x, y);
  • Side note: "{}" is Rust's (most basic) string interpolation operator

    • Similar to Python, Ruby, C#, and others; like printf's "%s" in C/C++.
    • "{:?}" can be used for debug formatting
    • More information: doc.rust-lang.org/std/fmt/
20 / 118

Comments

//! Comments like this are for module/crate documentation.
/// Triple-slash comments are docstring comments.
///
/// `rustdoc` uses docstring comments to generate
/// documentation, and supports **Markdown** formatting.
fn foo() {
// Double-slash comments are normal.
/* Block comments
also exist /* and can be nested! */
*/
}
21 / 118

Types

22 / 118

Primitive Types

  • bool: spelled true and false.
  • char: spelled like 'c' or '😺' (chars are Unicode code-points, i.e. 4 bytes long!).

  • Numerics: specify the signedness and size.

    • i8, i16, i32, i64, isize
    • u8, u16, u32, u64, usize
    • f32, f64
    • isize & usize are the size of pointers (and therefore have machine-dependent size)
    • Literals are spelled like 10i8, 10u16, 10.0f32, 10usize.
    • Type inference for non-specific literals default to i32 or f64:
      • e.g. 10 defaults to i32, 10.0 defaults to f64.
  • Arrays, slices, str, tuples.

  • Functions.
23 / 118

Arrays

  • Arrays are generically of type [T; N].
    • N is a compile-time constant. Arrays cannot be resized.
    • Array access is bounds-checked at runtime.
    • No const generics yet. (planned to be added in 2019)
  • Arrays are indexed with [] like most other languages:
    • arr[3] gives you the 4th element of arr
let arr1 = [1, 2, 3]; // (array of 3 elements)
let arr2 = [2; 32]; // (array of 32 `2`s)
24 / 118

Slices

  • Generically of type &[T]
  • A "view" into an array (or heap allocated data) by reference
  • Not created directly, but are borrowed from other variables
  • Mutable &mut [T] or immutable &[T]
  • How do you know when a slice is still valid? Coming soon...
let arr = [0, 1, 2, 3, 4, 5];
let val = arr[0]; // val = 0
let total_slice = &arr; // Slice all of `arr`
let total_slice = &arr[..]; // Same, but more explicit
let partial_slice = &arr[2..5]; // [2, 3, 4]
25 / 118

Strings

  • Two types of Rust strings: String and &str.
  • String is a heap-allocated, growable vector of characters.
  • &str is a type¹ that's used to slice into Strings.
  • String literals like "foo" are of type &str.
  • Strings are guaranteed to be encoded using UTF-8
let s: &str = "galaxy";
let s2: String = "галактика".to_string();
let s3: String = String::from("銀漢");
let s4: &str = &s3;
let s1 = "foobar";
// You can slice strings:
let s2 = &s1[1..3];
// But you can't index with []
// you can use `s1.chars().nth(1).unwrap()` instead
let s3 = s1[1] // does not work!

¹str is an unsized type, which doesn't have a compile-time known size, and therefore cannot exist by itself.

26 / 118

Tuples

  • Fixed-size, ordered, heterogeneous lists
  • Index into tuples with foo.0, foo.1, etc.
  • Can be destructured for example in let bindings
let foo: (i32, char, f64) = (72, 'H', 5.1);
let (x, y, z) = (72, 'H', 5.1);
let (a, b, c) = foo; // a = 72, b = 'H', c = 5.1
27 / 118

Casting

  • Cast between types with as:
let x: i32 = 100;
let y: u32 = x as u32;
  • Naturally, you can only cast between types that are safe to cast between.
    • No casting [i16; 4] to char! (This is called a "non-scalar" cast)
    • There are unsafe mechanisms to overcome this, if you know what you're doing.
28 / 118

Vec<T>

  • A standard library type: you don't need to import anything.
  • A Vec (read "vector") is a heap-allocated growable array.
    • (cf. Java's ArrayList, C++'s std::vector, etc.)
  • <T> denotes a generic type.
    • The type of a Vec of i32s is Vec<i32>.
  • Create Vecs with Vec::new() or the vec! macro.
    • Vec::new() is an example of namespacing. new is a function defined for the Vec struct.
29 / 118

Vec<T>

// Explicit typing
let v0: Vec<i32> = Vec::new();
// v1 and v2 are equal
let mut v1 = Vec::new();
v1.push(1);
v1.push(2);
v1.push(3);
let v2 = vec![1, 2, 3];
// v3 and v4 are equal
let v3 = vec![0; 4];
let v4 = vec![0, 0, 0, 0];
30 / 118

Vec<T>

let v2 = vec![1, 2, 3];
let x = v2[2]; // 3
  • Like arrays and slices, vectors can be indexed and sliced with [].

    • You can't index a vector with an i32/i64/etc.
    • You must use a usize because usize is guaranteed to be the same size as a pointer.
    • Other integers can be cast to usize:
      let i: i8 = 2;
      let y = v2[i as usize];
  • Vectors has an extensive stdlib method list, which can be found in the offical Rust documentation.

31 / 118

References

  • Reference types are written with an &: &i32.
  • References can be taken with & (like C/C++).
  • References can be dereferenced with * (like C/C++).
  • References are guaranteed to be valid.
    • Validity is enforced through compile-time checks!
  • These are not the same as pointers!
  • Reference lifetimes are pretty complex, as we'll explore later on in the course.
let x = 12;
let ref_x = &x;
println!("{}", *ref_x); // 12
32 / 118

Control Flow

33 / 118

If Statements

if x > 0 {
10
} else if x == 0 {
0
} else {
println!("Not greater than zero!");
-10
}
  • No parens necessary.
  • Entire if statement evaluates to one expression, so every arm must end with an expression of the same type.
    • That type can be unit ():
if x <= 0 {
println!("Too small!");
}
34 / 118

Loops

  • Loops come in three flavors: while, loop, and for.

    • break and continue exist just like in most languages
  • while works just like you'd expect:

let mut x = 0;
while x < 100 {
x += 1;
println!("x: {}", x);
}
35 / 118

Loops

  • loop is equivalent to while true, a common pattern.
    • Plus, the compiler can make optimizations knowing that it's infinite.
let mut x = 0;
loop {
x += 1;
println!("x: {}", x);
}
36 / 118

Loops

  • for is the most different from most C-like languages
    • for loops use an iterator expression:
    • n..m creates an iterator from n to m (exclusive).
    • Some data structures can be used as iterators, like arrays and Vecs.
// Loops from 0 to 9.
for x in 0..10 {
println!("{}", x);
}
let xs = [0, 1, 2, 3, 4];
// Loop through elements in a slice of `xs`.
for x in &xs {
println!("{}", x);
}
37 / 118

Functions

fn foo(x: T, y: U, z: V) -> T {
// ...
}
  • foo is a function that takes three parameters:
    • x of type T
    • y of type U
    • z of type V
  • foo returns a T.

  • Must explicitly define argument and return types.

    • The compiler is actually smart enough to figure this out for you, but Rust's designers decided it was better practice to force explicit function typing.
38 / 118

Functions

  • The final expression in a function is its return value.
    • Use return for early returns from a function.
fn square(n: i32) -> i32 {
n * n
}
fn squareish(n: i32) -> i32 {
if n < 5 { return n; }
n * n
}
fn square_bad(n: i32) -> i32 {
n * n;
}
  • The last one won't even compile!
    • Why? It ends in a semicolon, so it evaluates to ().
39 / 118

Function Objects

  • Several things can be used as function objects:
    • Function pointers (a reference to a normal function)
    • Closures (covered later)
  • Much more straightforward than C function pointers:
let x: fn(i32) -> i32 = square;
  • Can be passed by reference:
fn apply_twice(f: &Fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
// ...
let y = apply_twice(&square, 5);
40 / 118

Macros!

  • Macros are like functions, but they're named with ! at the end.
  • Can do generally very powerful stuff.
    • They actually generate code at compile time!
  • Call and use macros like functions.
  • You can define your own with macro_rules! macro_name blocks.
    • These are very complicated (especially procedural macros).
    • We will not cover writing custom macros in this course.
  • Because they're so powerful, a lot of common utilities are defined as macros.
41 / 118

print! & println!

  • Print stuff out. Yay.
  • Use {} for general string interpolation, and {:?} for debug printing.
    • Some types can only be printed with {:?}, like arrays and Vecs.
print!("{}, {}, {}", "foo", 3, true);
// => foo, 3, true
println!("{:?}, {:?}", "foo", [1, 2, 3]);
// => "foo", [1, 2, 3]
42 / 118

format!

  • Uses println!-style string interpolation to create formatted Strings.
let fmted = format!("{}, {:x}, {:?}", 12, 155, Some("Hello"));
// fmted == "12, 9b, Some("Hello")"
43 / 118

panic!(msg)

  • Exits current task with given message.
  • Don't do this lightly! It is better to handle and report errors explicitly.
if x < 0 {
panic!("Oh noes!");
}
44 / 118

assert! & assert_eq!

  • assert!(condition) panics if condition is false.
  • assert_eq!(left, right) panics if left != right.
  • debug_assert!(condition) and debug_assert_eq!(left, right) work in debug build, but omitted in release build.
  • Useful for testing and catching illegal conditions.
#[test]
fn test_something() {
let actual = 1 + 2;
assert!(actual == 3);
assert_eq!(3, actual);
}
45 / 118

unreachable!()

  • Used to indicate that some code should not be reached.
  • panic!s when reached.
  • Can be useful to track down unexpected bugs (e.g. optimization bugs).
if false {
unreachable!();
}
46 / 118

unimplemented!()

  • Shorthand for panic!("not yet implemented").
fn sum(x: Vec<i32>) -> i32 {
// TODO
unimplemented!();
}
47 / 118

dbg!()

  • A macro for quick and dirty debugging with which you can inspect the value of a given expression.
  • Was added in Rust 1.32.
let a = 2;
let b = dbg!(a * 2) + 1;
// ^-- prints: [src/main.rs:2] a * 2 = 4
assert_eq!(b, 5);
48 / 118

Match statements

  • switch on steroids.
let x = 3;
match x {
1 => println!("one fish"), // <- comma required
2 => {
println!("two fish");
println!("two fish");
}, // <- comma optional when using braces
// we can match several patterns in one arm
3 | 4 => println!("three or four, dunno"),
_ => println!("no fish for you"), // "otherwise" case
}
  • match takes an expression (x) and branches on a list of value => expression statements.
  • The entire match evaluates to one expression.
    • Like if, all arms must evaluate to the same type.
  • _ is commonly used as a catch-all (cf. Haskell, OCaml).
49 / 118

Match statements

let x = 3;
let y = -3;
let q = 10;
let s = match (x, y) {
(1, 1) => "one".to_string(),
(2, j) => format!("two, {}", j),
(_, 3) => "three".to_string(),
(i, j) if i > 5 && j < 0 => "On guard!".to_string(),
// NB: note that we do not test x == 10 here!
(q, k) => format!("???: {} {}", q, k),
};
println!("{}", s);
  • The matched expression can be any expression (l-value), including tuples and function calls.
    • Matches can bind variables. _ is a throw-away variable name.
  • You must write an exhaustive match in order to compile.
  • Use if-guards to constrain a match to certain conditions.
  • Patterns can get very complex.
50 / 118

Exercise

  • Write two functions for calculating Fibonacci numbers.
  • The first function should use for loop without recursion.
  • And the second one should use recursion and match.
  • Reminder: Fibonacci numbers are calculated as F(0) = 0; F(1) = 1; F(n) = F(n - 1) + F(n - 2)
  • You can use Rust Playground for this exercise.
51 / 118

Exercise: non-recursive solution

fn fibonacci(n: u32) -> u64 {
if n == 0 { return 0; }
let mut sum = 1;
let mut last = 0;
let mut curr = 1;
for _ in 1..n {
sum = last + curr;
last = curr;
curr = sum;
}
sum
}
52 / 118

Exercise: recursive solution

fn fibonacci(n: u32) -> u64 {
match n {
0 => 0,
1 | 2 => 1,
3 => 2,
// 50 => 12586269025,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
53 / 118

Rust Environment & Tools

54 / 118

Rustc

  • Rust's compiler is rustc.
  • Run rustc your_program.rs to compile into an executable your_program.
    • Things like warnings are enabled by default.
    • Read all of the output! It may be verbose but it is very useful.
  • rustc doesn't need to be called once for each file like in C.
    • The build dependency tree is inferred from module declarations in the Rust code (starting at main.rs or lib.rs).
  • Typically, you'll instead use cargo, Rust's package manager and build system.
55 / 118

Cargo

  • Rust's package manager & build tool
  • Create a new project:
    • cargo new project_name (executable)
    • cargo new project_name --lib (library)
  • Build your project: cargo build
  • Run your project: cargo run
  • Use --release flag to enable release build profile (longer compilation times, but more performant binaries)
  • Typecheck code without building it: cargo check (much faster)
  • Run your benchmarks if you have any: cargo bench (requires Nightly toolchain)
  • Run your tests: cargo test
56 / 118

Cargo.toml

  • Cargo uses the Cargo.toml file to declare and manage dependencies and project metadata.
    • TOML is a simple format similar to INI.
[package]
name = "my_cool_project"
version = "0.1.0"
authors = ["My name"]
edition = "2018"
[dependencies]
uuid = "0.1"
rand = "0.3"
[profile.release]
opt-level = 3
debug = false
57 / 118

cargo test

  • A test is any function annotated with #[test].
  • cargo test will run all annotated functions in your project.
  • Any function which executes without crashing (panic!ing) succeeds.
  • Use assert! (or assert_eq!) to check conditions (and panic! on failure)
#[test]
fn it_works() {
// ...
}
58 / 118

Ownership & Borrowing

  • Explicit ownership is the biggest new feature that Rust brings to the table!
  • Ownership is all (well, mostly) checked at compile time!
  • Newcomers to Rust often find themselves "fighting with the borrow checker" trying to get their code to compile
  • The ownership model is the biggest thing that Rust brings to the table, its claim to fame.
  • Ownership is something that's checked at compile time and has as little runtime cost as possible.
  • So it's zero (or very little) runtime cost, but you pay for it with a longer compilation time and learning curve. Which is where the phrase "fighitng with the borrow checker" comes from, when you have to work around the compiler's restrictions to figure out how to do what you want.
59 / 118

Ownership

  • A variable binding takes ownership of its data.
    • A piece of data can only have one owner at a time.
  • When a binding goes out of scope, the bound data is released automatically.
    • For heap-allocated data, this means de-allocation.
  • Data must be guaranteed to outlive its references.
fn foo() {
// Creates a Vec object.
// Gives ownership of the Vec object to v1.
let mut v1 = vec![1, 2, 3];
v1.pop();
v1.push(4);
// At the end of the scope, v1 goes out of scope.
// v1 still owns the Vec object, so it can be cleaned up.
}
60 / 118

Ownership

So here are the basics.

  • When you introduce a variable binding, it takes ownership of its data. And a piece of data can only have one owner at a time.
  • When a variable binding goes out of scope, nothing has access to the data anymore, so it can be released. Which means, if it's on the heap, it can be de-allocated.
  • And data must be guaranteed to outlive its references. Or, all references are guaranteed to be valid.
61 / 118

Move Semantics

let v1 = vec![1, 2, 3];
// Ownership of the Vec object moves to v2.
let v2 = v1;
println!("{}", v1[2]); // error: use of moved value `v1`
  • let v2 = v1;
    • We don't want to copy the data, since that's expensive.
    • The data cannot have multiple owners.
    • Solution: move the Vec's ownership into v2, and declare v1 invalid.
  • println!("{}", v1[2]);
    • We know that v1 is no longer a valid variable binding, so this is an error.
  • Rust can reason about this at compile time, so it throws a compiler error.
62 / 118

Move Semantics

  • Moving ownership copies data. BUT:
    • Often moves are optimized out by compiler.
    • When we move ownership of a heap allocated data (e.g. Vec<T>), we do not touch data on heap, just few bytes allocated on stack are copied (pointer to heap, length and capacity; 24 bytes on 64-bit machine)
  • Moves are automatic (via assignments); no need to use something like C++'s std::move.
    • However, there are functions like std::mem::replace in Rust to provide advanced ownership management.
  • For more finer-grained control see standrard library functions: std::mem::replace, std::mem::swap and others.
63 / 118

Ownership

  • Ownership does not always have to be moved.
  • What would happen if it did? Rust would get very tedious to write:
fn vector_length(v: Vec<i32>) -> Vec<i32> {
// Do whatever here,
// then return ownership of `v` back to the caller
}
  • You could imagine that this does not scale well either.
    • The more variables you had to hand back, the longer your return type would be!
    • Imagine having to pass ownership around for 5+ variables at a time :(
64 / 118

Borrowing

  • Instead of transferring ownership, we can borrow data.
  • A variable's data can be borrowed by taking a reference to the variable; ownership doesn't change.
    • When a reference goes out of scope, the borrow is over.
    • The original variable retains ownership throughout.
let v = vec![1, 2, 3];
// v_ref is a reference to v.
let v_ref = &v;
// use v_ref to access the data in the vector v.
assert_eq!(v[1], v_ref[1]);
65 / 118

Borrowing

  • Caveat: this adds restrictions to the original variable.
  • Ownership cannot be transferred from a variable while references to it exist.
    • That would invalidate the reference.
let v = vec![1, 2, 3];
// v_ref is a reference to v.
let v_ref = &v;
// Moving ownership to v_new would invalidate v_ref.
// error: cannot move out of `v` because it is borrowed
let v_new = v;
66 / 118

Borrowing

/// `length` only needs `vector` temporarily, so it is borrowed.
fn length(vec_ref: &Vec<i32>) -> usize {
// vec_ref is auto-dereferenced when you call methods on it.
vec_ref.len()
// you can also explicitly dereference.
// (*vec_ref).len()
}
fn main() {
let vector = vec![];
length(&vector);
println!("{:?}", vector); // this is fine
}
  • Note the type of length: vec_ref is passed by reference, so it's now an &Vec<i32>.
  • References, like bindings, are immutable by default.
  • The borrow is over after the reference goes out of scope (at the end of length).
67 / 118

Borrowing

/// `push` needs to modify `vector` so it is borrowed mutably.
fn push(vec_ref: &mut Vec<i32>, x: i32) {
vec_ref[0] = 100;
vec_ref.push(x);
}
fn main() {
let mut vector: Vec<i32> = vec![1, 2];
let vector_ref: &mut Vec<i32> = &mut vector;
push(vector_ref, 4);
assert_eq!(vector_ref, &[100, 2, 4]);
}
  • Variables can be borrowed by mutable reference: &mut vec_ref.
    • vec_ref is a reference to a mutable Vec.
    • The type is &mut Vec<i32>, not &Vec<i32>.
  • Different from a reference which is variable.
68 / 118

Borrowing

/// `push` needs to modify `vector` so it is borrowed mutably.
fn push2(vec_ref: &mut Vec<i32>, x: i32) {
// error: cannot move out of borrowed content.
let vector = *vec_ref;
vector.push(x);
}
fn main() {
let mut vector = vec![];
push2(&mut vector, 4);
}
  • Error! You can't dereference vec_ref into a variable binding because that would change the ownership of the data.
69 / 118

Borrowing

  • Rust will auto-dereference variables...
    • When making method calls on a reference.
    • When passing a reference as a function argument.
/// `length` only needs `vector` temporarily, so it is borrowed.
fn length(vec_ref: &&Vec<i32>) -> usize {
// vec_ref is auto-dereferenced when you call methods on it.
vec_ref.len()
}
fn main() {
let vector = vec![];
length(&&&&&&&&&&&&vector);
}
70 / 118

Borrowing

  • You will have to dereference variables...
    • When writing into them.
    • And other times that usage may be ambiguous.
let mut a = 5;
let ref_a = &mut a;
*ref_a = 4;
println!("{}", *ref_a + 4);
// ==> 8
71 / 118

Copy Types

  • Rust defines a trait¹ named Copy that signifies that a type may be copied instead whenever it would be moved.
  • Most primitive types are Copy (i32, f64, char, bool, etc.)
  • Types that contain references may not be Copy (e.g. Vec, String).
    let x: i32 = 12;
    let y = x; // `i32` is `Copy`, so it's not moved :D
    println!("x still works: {}, and so does y: {}", x, y);

¹ Like a Java/Go interface or Haskell typeclass

72 / 118

Borrowing Rules

The Holy Grail of Rust

Learn these rules, and they will serve you well.

  • You can't keep borrowing something after it stops existing.
  • One object may have many immutable references to it (&T).
  • OR exactly one mutable reference (&mut T) (not both).
  • That's it!

73 / 118

Borrowing Prevents...

  • Iterator invalidation due to mutating a collection you're iterating over.
  • This pattern can be written in C, C++, Java, Python, Javascript...
    • But may result in, e.g, ConcurrentModificationException (at runtime!)
let mut vs = vec![1,2,3,4];
for v in &vs {
vs.pop();
// ERROR: cannot borrow `vs` as mutable because
// it is also borrowed as immutable
}
  • pop needs to borrow vs as mutable in order to modify the data.
  • But vs is being borrowed as immutable by the loop!
74 / 118

Borrowing Prevents...

  • Use-after-free
  • Valid in C, C++...
let y: &i32;
{
let x = 5;
y = &x; // error: `x` does not live long enough
}
println!("{}", *y);
  • The full error message:
error: `x` does not live long enough
note: reference must be valid for the block suffix following statement
0 at 1:16
...but borrowed value is only valid for the block suffix
following statement 0 at 4:18
  • This eliminates a huge number of memory safety bugs at compile time.
  • As a side note, this technique of creating a block to limit the scope of a variable (in this case x) is pretty useful.
75 / 118

Borrowing Prevents...

  • Data races in multithreaded environments.
  • It checks at compile time if it's safe to share or send a given piece of data.
  • Compiler ensures that programm uses necessary synchronization using various primitives: mutexes, atomics, channels, etc.
  • NB: data races != race condition.
  • Check out TRPL section of "Fearless Concurrency"
76 / 118

Example: Vectors

  • You can iterate over Vecs in three different ways:
let mut vs = vec![0,1,2,3,4,5,6];
// Borrow immutably
for v in &vs { // Can also write `for v in vs.iter()`
println!("I'm borrowing {}.", v);
}
// Borrow mutably
for v in &mut vs { // Can also write `for v in vs.iter_mut()`
*v = *v + 1;
println!("I'm mutably borrowing {}.", v);
}
// Take ownership of the whole vector
for v in vs { // Can also write `for v in vs.into_iter()`
println!("I now own {}! AHAHAHAHA!", v);
}
// `vs` is no longer valid
77 / 118

Structured Data

  • Rust has two simple ways of creating structured data types:

    • Structs: C-like structs to hold data.
    • Enums: OCaml-like; data that can be one of several types.
  • Structs and enums may have one or more implementation blocks (impls) which define methods for the data type.

78 / 118

Structs

  • A struct declaration:
    • Fields are declared with name: type.
struct Point {
x: i32,
y: i32,
}
  • By convention, structs have CamelCase names, and their fields have snake_case names.
  • Structs may be instantiated with fields assigned in braces.
let origin = Point { x: 0, y: 0 };
79 / 118

Structs

  • Struct fields may be accessed with dot notation.
  • Structs may not be partially-initialized.
    • You must assign all fields upon creation, or declare an uninitialized struct that you initialize later.
let mut p = Point { x: 19, y: 8 };
p.x += 1;
p.y -= 1;
80 / 118

Structs

  • Structs do not have field-level mutability.
  • Mutability is a property of the variable binding, not the type.
  • Field-level mutability (interior mutability) can be achieved via Cell types.
    • More on these very soon.
struct Point {
x: i32,
mut y: i32, // Illegal!
}
81 / 118

Structs

  • Structs are namespaced with their module name.
    • The fully qualified name of Point is foo::Point.
  • Struct fields are private by default.
    • They may be made public with the pub keyword.
  • Private fields may only be accessed from within the module where the struct is declared.
mod foo {
pub struct Point {
pub x: i32,
y: i32,
}
}
fn main() {
let b = foo::Point { x: 12, y: 12 };
// ^~~~~~~~~~~~~~~~~~~~~~~~~~~
// error: field `y` of struct `foo::Point` is private
}
82 / 118

Structs

mod foo {
pub struct Point {
pub x: i32,
y: i32,
}
// Creates and returns a new point
pub fn new(x: i32, y: i32) -> Point {
Point { x: x, y: y }
}
}
  • new is inside the same module as Point, so accessing private fields is allowed.
83 / 118

Struct matching

  • Destructure structs with match statements.
pub struct Point {
x: i32,
y: i32,
}
match p {
Point { x, y } => println!("({}, {})", x, y)
}
84 / 118

Struct matching

  • Some other tricks for struct matches:
match p {
Point { y: y1, x: x1 } => println!("({}, {})", x1, y1)
}
match p {
Point { y, .. } => println!("{}", y)
}
  • Fields do not need to be in order.
  • List fields inside braces to bind struct members to those variable names.
    • Use struct_field: new_var_binding to change the variable it's bound to.
  • Omit fields: use .. to ignore all unnamed fields.
85 / 118

Struct Update Syntax

  • A struct initializer can contain .. s to copy some or all fields from s.
  • Any fields you don't specify in the initializer get copied over from the target struct.
  • The struct used must be of the same type as the target struct.
    • No copying same-type fields from different-type structs!
struct Foo { a: i32, b: i32, c: i32, d: i32, e: i32 }
let mut x = Foo { a: 1, b: 1, c: 2, d: 2, e: 3 };
let x2 = Foo { e: 4, .. x };
// Useful to update multiple fields of the same struct:
x = Foo { a: 2, b: 2, e: 2, .. x };
86 / 118

Tuple Structs

  • Variant on structs that has a name, but no named fields.
  • Have numbered field accessors, like tuples (e.g. x.0, x.1, etc).
  • Can also match these.
struct Color(i32, i32, i32);
let mut c = Color(0, 255, 255);
c.0 = 255;
match c {
Color(r, g, b) => println!("({}, {}, {})", r, g, b)
}
87 / 118

Tuple Structs

  • Helpful if you want to create a new type that's not just an alias.
    • This is often referred to as the "newtype" pattern.
  • These two types are structurally identical, but not equatable.
// Not equatable
struct Meters(i32);
struct Yards(i32);
// May be compared using `==`, added with `+`, etc.
type MetersAlias = i32;
type YardsAlias = i32;
88 / 118

Unit Structs (Zero-Sized Types)

  • Structs can be declared to have zero size.
    • This struct has no fields!
  • We can still instantiate it.
  • It can be used as a "marker" type on other data structures.
    • Useful to indicate, e.g., the type of data a container is storing.
struct Unit;
let u = Unit;
89 / 118

Enums

  • An enum, or "sum type", is a way to express some data that may be one of several things.
  • Much more powerful than in Java, C, C++, C#...
  • Each enum variant can have:
    • no data (unit variant)
    • named data (struct variant)
    • unnamed ordered data (tuple variant)
enum Resultish {
Ok,
Warning { code: i32, message: String },
Err(String)
}
90 / 118

Enums

  • Enum variants are namespaced by their enum type: Resultish::Ok.
    • You can import all variants with use Resultish::*.
  • Enums, much as you'd expect, can be matched on like any other data type.
match make_request() {
Resultish::Ok =>
println!("Success!"),
Resultish::Warning { code, message } =>
println!("Warning: {}!", message),
Resultish::Err(s) =>
println!("Failed with error: {}", s),
}
91 / 118

Enums

  • Enum constructors like Resultish::Ok and the like can be used as functions.
  • This is not currently very useful, but will become so when we cover closures & iterators.
92 / 118

Recursive Types

  • You might think to create a nice functional-style List type:
enum List {
Nil,
Cons(i32, List),
}
93 / 118

Recursive Types

  • Such a definition would have infinite size at compile time!
  • Structs & enums are stored inline by default, so they may not be recursive.
    • i.e. elements are not stored by reference, unless explicitly specified.
  • The compiler tells us how to fix this, but what's a box?
enum List {
Nil,
Cons(i32, List),
}
// error: invalid recursive enum type
// help: wrap the inner value in a box to make it representable
94 / 118

Boxes, Briefly

  • A box (lowercase) is a general term for one of Rust's ways of allocating data on the heap.
  • A Box<T> (uppercase) is a heap pointer with exactly one owner.
    • A Box owns its data (the T) uniquely-- it can't be aliased.
  • Boxes are automatically destructed when they go out of scope.
  • Create a Box with Box::new():
let boxed_five = Box::new(5);
enum List {
Nil,
Cons(i32, Box<List>), // OK!
}
  • We'll cover these in greater detail when we talk more about pointers.
95 / 118

Methods

impl Point {
pub fn distance(&self, other: Point) -> f32 {
let (dx, dy) = (self.x - other.x, self.y - other.y);
((dx.pow(2) + dy.pow(2)) as f32).sqrt()
}
}
fn main() {
let p = Point { x: 1, y: 2 };
p.distance();
}
  • Methods can be implemented for structs and enums in an impl block.
  • Like fields, methods may be accessed via dot notation.
  • Can be made public with pub.
    • impl blocks themselves don't need to be made pub.
  • Work for enums in exactly the same way they do for structs.
96 / 118

Methods

  • The first argument to a method, named self, determines what kind of ownership the method requires.
  • &self: the method borrows the value.
    • Use this unless you need a different ownership model.
  • &mut self: the method mutably borrows the value.
    • The function needs to modify the struct it's called on.
  • self: the method takes ownership.
    • The function consumes the value and may return something else.
97 / 118

Methods

impl Point {
fn distance(&self, other: Point) -> f32 {
let (dx, dy) = (self.x - other.x, self.y - other.y);
((dx.pow(2) + dy.pow(2)) as f32).sqrt()
}
fn translate(&mut self, x: i32, y: i32) {
self.x += x;
self.y += y;
}
fn mirror_y(self) -> Point {
Point { x: -self.x, y: self.y }
}
}
  • distance needs to access but not modify fields.
  • translate modifies the struct fields.
  • mirror_y returns an entirely new struct, consuming the old one.
98 / 118

Associated Functions

impl Point {
fn new(x: i32, y: i32) -> Point {
Point { x: x, y: y }
}
}
fn main() {
let p = Point::new(1, 2);
}
  • Associated function: like a method, but does not take self.
    • This is called with namespacing syntax: Point::new().
      • Not Point.new().
    • Like a "static" method in Java.
  • A constructor-like function is usually named new.
    • No inherent notion of constructors, no automatic construction.
99 / 118

Implementations

  • Methods, associated functions, and functions in general may not be overloaded.
    • e.g. Vec::new() and Vec::with_capacity(capacity: usize) are both constructors for Vec
  • Methods may not be inherited.
    • Rust structs & enums must be composed instead.
    • However, traits (coming soon) cover some of the inheritence functionality.
100 / 118

Patterns

  • Use ..= to specify a range of values. Useful for numerics and chars.
  • You can encounter ... which is deprecated variant of ..=.
  • Use _ to bind against any value (like any variable binding) and discard the binding.
let x = 17;
match x {
0 ..= 5 => println!("zero through five (inclusive)"),
_ => println!("You still lose the game."),
}
101 / 118

match: References

  • Get a reference to a variable by asking for it with ref.
let x = 17;
match x {
ref r => println!("Of type &i32: {}", r),
}
  • And get a mutable reference with ref mut.
    • Only if the variable was declared mut.
  • Note: with introduction of "match ergonomics" ref patterns become much less common.
let mut x = 17;
match x {
ref r if x == 5 => println!("{}", r),
ref mut r => *r = 5
}
  • Similar to let ref.
102 / 118

if-let Statements

  • If you only need a single match arm, it often makes more sense to use Rust's if-let construct.
  • For example, given the Resultish type we defined earlier:
enum Resultish {
Ok,
Warning { code: i32, message: String },
Err(String),
}
103 / 118

if-let Statements

  • Suppose we want to report an error but do nothing on Warnings and Oks.
match make_request() {
Resultish::Err(_) => println!("Total and utter failure."),
_ => println!("ok."),
}
  • We can simplify this statement with an if-let binding:
let result = make_request();
if let Resultish::Err(s) = result {
println!("Total and utter failure: {}", s);
} else {
println!("ok.");
}
104 / 118

while-let Statement

  • There's also a similar while-let statement, which works like an if-let, but iterates until the condition fails to match.
while let Resultish::Err(s) = make_request() {
println!("Total and utter failure: {}", s);
}
105 / 118

Inner Bindings

  • With more complicated data structures, use @ to create variable bindings for inner elements.
#[derive(Debug)]
enum A { None, Some(B) }
#[derive(Debug)]
enum B { None, Some(i32) }
fn foo(x: A) {
match x {
a @ A::None => println!("a is A::{:?}", a),
ref a @ A::Some(B::None) => println!("a is A::{:?}", *a),
A::Some(b @ B::Some(_)) => println!("b is B::{:?}", b),
}
}
foo(A::None); // ==> x is A::None
foo(A::Some(B::None)); // ==> a is A::Some(None)
foo(A::Some(B::Some(5))); // ==> b is B::Some(5)
106 / 118

Lifetimes

  • There's one more piece to the ownership puzzle: Lifetimes.
  • Lifetimes generally have a pretty steep learning curve.
    • We may cover them again later on in the course under a broader scope if necessary.
  • Don't worry if you don't understand these right away.
107 / 118

Lifetimes

  • Imagine This:
    1. I acquire a resource.
    2. I lend you a reference to my resource.
    3. I decide that I'm done with the resource, so I deallocate it.
    4. You still hold a reference to the resource, and decide to use it.
    5. You crash 😿.
  • We've already said that Rust makes this scenario impossible, but glossed over how.
  • We need to prove to the compiler that step 3 will never happen before step 4.
108 / 118

Lifetimes

  • Ordinarily, references have an implicit lifetime that we don't need to care about:
    fn foo(x: &i32) {
    // ...
    }
  • However, we can explicitly provide one instead:

    fn bar<'a>(x: &'a i32) {
    // ...
    }
  • 'a, pronounced "tick-a" or "the lifetime a" is a named lifetime parameter.

    • <'a> declares generic parameters, including lifetime parameters.
    • The type &'a i32 is a reference to an i32 that lives at least as long as the lifetime 'a.
109 / 118

Stop here briefly to discuss

Lifetimes

  • The compiler is smart enough not to need 'a above, but this isn't always the case.
  • Scenarios that involve multiple references or returning references often require explicit lifetimes.
    • Speaking of which...
110 / 118

Multiple Lifetime Parameters

fn borrow_x_or_y<'a>(x: &'a str, y: &'a str) -> &'a str;
  • In borrow_x_or_y, all input/output references all have the same lifetime.
    • x and y are borrowed (the reference is alive) as long as the returned reference exists.
fn borrow_p<'a, 'b>(p: &'a str, q: &'b str) -> &'a str;
  • In borrow_p, the output reference has the same lifetime as p.
    • q has a separate lifetime with no constrained relationship to p.
    • p is borrowed as long as the returned reference exists.
111 / 118

Lifetimes

  • Okay, great, but what does this all mean?
    • If a reference R has a lifetime 'a, it is guaranteed that it will not outlive the owner of its underlying data (the value at *R)
    • If a reference R has a lifetime of 'a, anything else with the lifetime 'a is guaranteed to live as long R.
  • This will probably become more clear the more you use lifetimes yourself.
112 / 118

Lifetimes - structs

  • Structs (and struct members) can have lifetime parameters.
struct Pizza(Vec<i32>);
struct PizzaSlice<'a> {
pizza: &'a Pizza, // <- references in structs must
index: u32, // ALWAYS have explicit lifetimes
}
let p1 = Pizza(vec![1, 2, 3, 4]);
{
let s1 = PizzaSlice { pizza: &p1, index: 2 }; // this is okay
}
let s2;
{
let p2 = Pizza(vec![1, 2, 3, 4]);
s2 = PizzaSlice { pizza: &p2, index: 2 };
// no good - why?
}
  • Currently Rust does not support self-referential structs out-of-box.
113 / 118

Lifetimes - structs

  • Lifetimes can be constrained to "outlive" others.
    • Same syntax as type constraint: <'b: 'a>.
struct Pizza(Vec<i32>);
struct PizzaSlice<'a> { pizza: &'a Pizza, index: u32 }
struct PizzaConsumer<'a, 'b: 'a> { // says "b outlives a"
slice: PizzaSlice<'a>, // <- currently eating this one
pizza: &'b Pizza, // <- so we can get more pizza
}
fn get_another_slice(c: &mut PizzaConsumer, index: u32) {
c.slice = PizzaSlice { pizza: c.pizza, index: index };
}
let p = Pizza(vec![1, 2, 3, 4]);
{
let s = PizzaSlice { pizza: &p, index: 1 };
let mut c = PizzaConsumer { slice: s, pizza: &p };
get_another_slice(&mut c, 2);
}
114 / 118

Lifetimes - 'static

  • There is one reserved, special lifetime, named 'static.
  • 'static means that a reference may be kept (and will be valid) for the lifetime of the entire program.
    • i.e. the data referred to will never go out of scope.
  • All &str literals have the 'static lifetime.
let s1: &str = "Hello";
let s2: &'static str = "World";
115 / 118

Structured Data With Lifetimes

  • Any struct or enum that contains a reference must have an explicit lifetime.
  • Normal lifetime rules otherwise apply.
struct Foo<'a, 'b> {
v: &'a Vec<i32>,
s: &'b str,
}
116 / 118

Lifetimes in impl Blocks

  • Implementing methods on Foo struct requires lifetime annotations too!
  • You can read this block as "the implementation using the lifetimes 'a and 'b for the struct Foo using the lifetimes 'a and 'b."
impl<'a, 'b> Foo<'a, 'b> {
fn new(v: &'a Vec<i32>, s: &'b str) -> Foo<'a, 'b> {
Foo {
v: v,
s: s,
}
}
}
117 / 118

Exercises/homework

118 / 118

Overview

"Rust is a systems programming language that runs blazingly fast, prevents nearly all segfaults, and guarantees thread safety." – prev.rust-lang.org

"Empowering everyone to build reliable and efficient software. " – rust-lang.org

2 / 118
Paused

Help

Keyboard shortcuts

, , Pg Up, k Go to previous slide
, , Pg Dn, Space, j Go to next slide
Home Go to first slide
End Go to last slide
b / m / f Toggle blackout / mirrored / fullscreen mode
c Clone slideshow
p Toggle presenter mode
t Restart the presentation timer
?, h Toggle this help
Esc Back to slideshow