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)