Links

Easy Rust | videos

Rust by example

The Rust Programming Language

rustup docs --book

playground

The Rust Reference

Rust API Docs

Functions

Functions and variables use snake case. Types and traits use PascalCase. All by convention. Order doesn't matter.

#![allow(unused)] fn main() { fn sum(a: i32, b: i32) -> i32 { a + b; } }

Looping

loop {} is an infinite loop. break exits the loop, continue exits the current iteration.

Generics

Generic function with type constrained to implement Debug and PartialOrder traits:

#![allow(unused)] fn main() { fn return_me<T: Debug + PartialOrder>(arg: T) -> T { println!("{:?}", arg); arg } }

or

#![allow(unused)] fn main() { fn return_me<T>(arg: T) -> T where T: Debug + PartialOrder { println!("{:?}", arg); arg } }

Pattern Matching

Match is an expression.

#![allow(unused)] fn main() { let val = match exp1 { exp2 if <match guard expression> => ..., exp3 => ..., _ => ..., } }

Printing

print! and println! macros to write to stdout. There is also format! to write to a string.

Simple types can be printed with display formatting {}. More complex types can be printed with debug formatting {:?}.

Display formatting requires implementing std::fmt::Display.

{:#?} is pretty-printing.

Easy Rust printing

Named substitution

#![allow(unused)] fn main() { println!( "{city1} is in {country} and {city2} is also in {country}, but {city3} is not in {country}.", city1 = "Seoul", city2 = "Busan", city3 = "Tokyo", country = "Korea"); }

Substitution format

{variable:padding alignment minimum.maximum}

e.g. {city1:0<5.15}

More Printing

References

References let you pass a way to access a value to a function without copying the value. &identifier is a reference to the identifier value, &Person is a type that is a reference to the Person type. To go from a reference to the value dereference let bob = *bob_ref, but this happens automatically with 'autoderef'.

References are stack allocated pointers to values in the heap. They represent a borrowed value.

#![allow(unused)] fn main() { let a = 1; let referenceToA: &i32 = &a; let mutableReferenceToA = &mut a; }

Dereferencing

* is the inverse of &.

#![allow(unused)] fn main() { let a = 1; let a_ref = &a; let b = *a_ref; }

The 'dot' operator . dereferences automatically.

Mutable References

#![allow(unused)] fn main() { let mut identifier = 1; let mut_ref = &mut identifier; // this is a mutable reference. Mutable borrow. }

There can be only one mutable reference, and it cannot be in scope with an immutable reference.

More on passing to functions and mutability

  • fn function_name(variable: String) takes a String and owns it. If it doesn't return anything, then the variable dies inside the function.
  • fn function_name(variable: &String) borrows a String and can look at it
  • fn function_name(variable: &mut String) borrows a String and can change it

Stack

Whenever we hold a value (includes function arguments) we must know its size, so that the stack pointer can be moved properly.

#![allow(unused)] fn main() { struct S { data: [u8; 0x1000], } println!("0x{:x}", std::mem::size_of::<S>()); }

Knowing the size of a value of a type is indicated by the marker trait Sized.

fn f<T: Sized>(t: T) {}

Strings

String is the dynamic heap string type. When you need to modify or own the string. str is an immutable sequence of UTF-8 characters. A 'slice' of string data. Used behind a reference as &str.

To make a String from a &str: String::from("foo") or "foo".to_string() or let foo: String = "Hello world".into()

#![allow(unused)] fn main() { let data: String = "hello".into(); let s1: &str = &data; let s2: &str = &data; let s3: &str = &data; }

Literal Strings

#![allow(unused)] fn main() { r#"String content with " and / and \ and whatever"#; }

Modifying Strings

String is mutated via .push_str(&str).

Structs

Types are PascalCase. Structs can be named tuples:

#![allow(unused)] fn main() { #[derive(Debug)] struct Rgb(u8, u8, u8); let colour = Rgb(5,5,5); println!("{:?}", colour); }

or with named fields

#[derive(Debug)] struct Person { name: String, age: u8, } fn main() { let age = 38; let me = Person { name: "Liam".to_string(), age }; println!("{:?}", me); }

Struct update

#![allow(unused)] fn main() { let user2 = User { email: String::from("another@example.com"), ..user1 }; }

Traits

Declared like an implementation, but with no function bodies. Can have a body, which becomes the default implementation. Like C# abstract classes.

#![allow(unused)] fn main() { trait DoAThing { fn do_the_thing(&self) -> String; } impl DoAThing for u8 { fn do_the_thing(&self) -> String { format!("The thing is done for {:?}", &self); } } }

You can implement external traits on internal types, and internal traits on external types, but not external traits on external types.

Traits as function parameters is a bit weird. We have to use &impl DoAThing instead of just the trait name.

#![allow(unused)] fn main() { fn process_doers(thing: &impl DoAThing) -> String { thing.do_the_thing() } }

The above is syntactic sugar for (trait bound syntax):

#![allow(unused)] fn main() { fn process_doers<T: DoAthing>(thing: &T) -> String {} }

I think I prefer the trait bound syntax.

Types

Scalar types integer, float, bool, char

Compound types tuple and array.

Tuple

#![allow(unused)] fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); let five_hundred = x.0; }

Values accessed by zero based indexing.

Array

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

Fixed size. Stack allocated. Zero based indexing without the ..

Can be mutated.

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

Can be sliced (range end is exclusive or inclusive (=i)).

#![allow(unused)] fn main() { let mut a = [1,2,3,4,5]; let b = &a[0..=4]; //whole array }

Vector

Resizable generic collection.

#![allow(unused)] fn main() { let mut my_vec: Vec<String> = Vec::new(); my_vec.push(name1); my_vec.push(name2); }

Can also declare with a macro:

#![allow(unused)] fn main() { let mut some_numbers = vec![4,5,6]; }

Can slice:

#![allow(unused)] fn main() { &some_numbers[0..] }

Or declare with a size:

#![allow(unused)] fn main() { let mut letters: Vec<char> = Vec::with_capacity(8); println!("{}", letters.capacity()); }

Enumerations

#![allow(unused)] fn main() { enum ThingsInTheSky { Sun, Stars, } ThingsInTheSky::Sun }

Values can have values, thus they are discriminated unions.

#![allow(unused)] fn main() { enum Feature { Note(u8), Rest(duration: u8), } let sound = Feature::Note(7); let no_sound = Feature::Rest(250); }

and they can have static functions and methods:

#![allow(unused)] fn main() { impl Feature { fun play(&self) {} fun new_rest(duration: u8) -> Self { Feature::Rest { duration } } } }

Questions

  1. Why does mut seem to do double duty? (re-assignable identifiers and mutable values)