Links
rustup docs --book
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.
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}
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 itfn 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
- Why does
mut
seem to do double duty? (re-assignable identifiers and mutable values)