Rust Cheatsheet
One-page cheatsheet to refresh my memory on Rust. Many examples are copied from Rust by Example.
Tips for Learning Rust
Learn by fixing error:
rustlings is a great example that adopts this method of learning by fixing error.
Think about an error, and figure out how to modify the example code to cause that error. E.g. what line of code do I need to add to make
coerce_static(&lifetime_num)
fail to work in the example below?// Make a constant with `'static` lifetime. static NUM: i32 = 18; // Returns a reference to `NUM` where its `'static` // lifetime is coerced to that of the input argument. fn coerce_static<'a>(_: &'a i32) -> &'a i32 { &NUM } fn main() { { // Make a `string` literal and print it: let static_string = "I'm in read-only memory"; println!("static_string: {}", static_string); // When `static_string` goes out of scope, the reference // can no longer be used, but the data remains in the binary. } { // Make an integer to use for `coerce_static`: let lifetime_num = 9; // Coerce `NUM` to lifetime of `lifetime_num`: let coerced_static = coerce_static(&lifetime_num); println!("coerced_static: {}", coerced_static); } println!("NUM: {} stays accessible!", NUM); }
Read the source code. Seems like many of them are more concise than I expected.
Naming Convention
This is a list of naming conventions. It’s not enforced by the Rust compiler but it’s commonly adopted.
The
new()
method (fn MyStruct::new(arg1: type1, ...) -> Self
) returns an instance of typeMyStruct
.new()
is expected to never fail.The
build()
method (fn MyStruct::build(arg1: type1, ...) -> Result<Self, ...>
) builds an instance of type, and returnResult<MyStruct, ...>
. This function carries out some validation, and may fail if the validation fails. E.g. a nonexistent path to config file is provided.
Cargo
Cargo is Rust’s build systems and package manager. No more messy Make and CMake!
# basics
cargo new <project_name>
cargo build # build for debug mode
cargo build --release # build an optimized ver. of the binary
cargo run
cargo check # fast way to check for compilation errors
# dependency
cargo add <crate_name>
cargo remove <crate_name>
cargo update
# testing & linting
cargo test [<test_name>]
cargo bench # benchmark
cargo clippy # linter
cargo fmt # format code
# misc
cargo tree # show dependency tree
cargo clean # remove `target/` directory
testing
assert!(is_even(3))
assert_eq(triple(3), 9)
A test that should panic
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn zero_div() {
// 3/0 is invalid
let res = div(3, 0);
}
}
Ownership
Lifetime
Trait
Default implementation
trait Licensed {
fn licensing_info(&self) -> String {
"Default license".to_string()
}
}
impl
, Box
, and representing “any type that implement some trait”
Rust enforces that size of type must be known at compile time. This is why you almost never see str
used directly, b/c it’s a variable length string. Instead, the fixed-size &str
reference is used.
The implication of this rule on function call is as follows:
To represent “any type that implements some trait”,
for function argument, we can use
impl Trait
.This is called static dispatch. Rust will determine the concrete type of parameter at each function call, i.e. Rust will monomorphize each function call.
for function return value, we must use
Box<dyn Trait>
.This is called dynamic dispatch. Instead of concrete type, we return a pointer to value of that type. The pointer size is known at compile time, and the actual type is stored on heap.
Technically, we can use
impl Trait
as the returned type if there is only one returned type, for example,fn static_types() -> impl Any { 4 }
This could be useful when we want to hide implementation details, and only add restriction on the behavior of the returned type. However, to have true dynamic dispatch, we must use
Box<dyn Trait>
.
Below is an example showing how dispatch works differently for function parameter and return value.
trait Roll {
fn roll() -> u8;
}
trait Animal {}
fn random_animal(dice: impl Roll) -> Box<dyn Animal> {
if dice.roll() < 3 {
Box::new(Sheep {})
} else {
Box::new(Cow {})
}
}
More about impl
When impl
appears to the left of ->
function signature, it is equivalent to a generic parameter with a bound, like this
fn random_animal(dice: impl Roll) -> Box<dyn Animal> {}
fn random_animal<T: Roll>(dice: T) -> Box<dyn Animal> {}
impl
can also be used to the right of ->
as a returned type. We sometimes prefer this over returning a concrete type b/c we don’t want to define a named type and only want the caller to know the returned type implement some trait. For example,
trait Animal {
fn description() -> String {
format!("This is an animal")
}
}
struct Dog;
impl Animal for Dog;
struct Cat;
impl Animal for Cat;
fn get_fav_animal() -> impl Animal {
// Today my fav animal is Dog
Dog
// Maybe tomorrow my fav animal is Cat
// Cat
}
fn main() {
let animal = get_fav_animal();
println!("Fav animal: {}", anima.description());
}
For the above def of get_fav_animal
, the concrete type of impl Animal
is inferred from the function’s definition.
The benefit of returning an impl Animal
instead of Dog
is that the caller only need to know the returned type implements Animal
trait. When we change the implementation of get_fav_animal()
, the caller doesn’t need to change how they use the returned value of get_fav_animal()
, because they always use it as some type that implements Animal
.
Also, there is no way to write an equivalent signature using generics if impl
appears in function return type.
// This is incorrect
fn get_val_animal<T: Animal> -> T {
Dog
}
This is incorrect because T
is provided by the caller, while the concrete type of impl Animal
is determined by function definition.
Trait Bound
We can require a generic type to implement some traits. This is called “trait bound”.
struct Message<T: Display>(T);
fn print_stuff_1<T: Display>(stuff: T) -> i32 {
println!("#### {} ####", stuff);
42
}
// equivalent to print_stuff_1
fn print_stuff_2<T>(stuff: T) -> i32
where T: Display
{
println!("#### {} ####", stuff);
42
}
Hashmap shenanigan
Insert if not exists
let name2grade: HashMap<&str, Option<u8>> = HashMap::new();
name2grade.entry("Tom").or_insert(None);
Insert default value if not exists
let name2grade: HashMap<&str, Option<u8>> = HashMap::new();
// default of Option<T> is None
name2grade.entry("Tom").or_default();
iterator
Commonly used chain of iter methods
Filter + Count
// a Grade struct representing both numerical score and letter grade
pub struct Grade {
pub score: f64,
pub letter: String,
}
pub fn count_letter_grade(grades: &[Grade], letter: &str) -> usize {
grades.iter().filter(|&grade| grade.letter == letter).count()
}
pub fn count_score_range(grades: &[Grade], score_range: std::ops::Range<f64>) -> usize {
grades.iter().filter(|&grade| score_range.contains(&grade.score)).count()
}
Map
Sum
Map from Result<T, E>
to Result<T, F>
Collecting an impl Iterator<Item = Result<T, E>>
Given an impl Iterator<Item = Result<T, E>>
, for example let res = vec![3, 4, 1, 2].iter().map(|v| checked_sub_1(v, v-1))
, res.collect()
can return two types:
Result<Vec<i32>, Error>
: a result that contains either a vector ofi32
or an errorVec<Result<i32, Error>>
: a vector of results that contains either `i32 or error
A full example is below
use std::fmt::Display;
#[derive(PartialEq, Debug)]
pub enum CheckError {
UnderflowError(i32)
}
pub fn checked_sub_1(val: &i32) -> Result<i32, CheckError> {
match val {
..1 => Err(CheckError::UnderflowError(*val)),
_ => Ok(val - 1)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fn1() -> Result<Vec<i32>, CheckError> {
let values: Vec<i32> = vec![3,2,1,0];
values
.iter()
.map(checked_sub_1)
.collect()
}
#[test]
fn test_error_1() {
assert_eq!(fn1(), Err(CheckError::UnderflowError(0)));
}
fn fn2() -> Vec<Result<i32, CheckError>> {
let values: Vec<i32> = vec![3,2,1,0];
values
.iter()
.map(checked_sub_1)
.collect()
}
#[test]
fn test_error_2() {
assert_eq!(fn2(), [Ok(2), Ok(1), Ok(0), Err(CheckError::UnderflowError(0))]);
}
}
This is because collect()
returns a type that implements FromIterator<Self::Item>
, statically dispatched. The signature of collect
is below
pub trait Iterator {
fn collect<B: FromIterator<Self::Item>>(self) -> B
where
Self: Sized;
}
Here, Item = Result<i32, CheckError>
. Furthermore, both Vec<T>
and Result<T, E>
implements FromIterator
trait. Thus, collect()
can return both types using their FromIterator
implementation respectively.
collect(self) -> Vec<Result<i32, CheckError>>
This one is quite straight forward. Given an iterator of
T
, we return aVec<T>
. Here,T = Result<i32, CheckError>
.collect(self) -> Result<Vec<i32>, CheckError>
This one is more interesting. To return
Result<Vec<i32>, CheckError>
, we consumes values in iterator to build up the vector, and shortcircuit on the first error. That is, we either creates a vector for all values, or return the first error encountered.One catch is that the values are consumed even if an error occur. When an error occur, all values before this error are consumed. For details, see this Rust By Example page
Misc
Create a slice
let a = [1,2,3,4,5];
let slice_a = &a[1..4];
assert_eq!([2,3,4], slice_a);
Destruct a tuple
let point = (3.9, 4.5, "positive");
let (x, y, label) = point;
Function parameter’s mutability
When describing a function’s parameter, we can use the word “mutability”, and its variants, in these situations:
an immutable parameter of
MyStruct
commonly used, input is read-only
an immutable parameter of an immutable reference to
MyStruct
commonly used, need to take ownership of input, i.e. move it inside the function
a mutable parameter of
MyStruct
,rarely used
an immutable parameter of mutable reference to
MyStruct
,commonly used, need to modify input
a mutable parameter of immutable reference to
MyStruct
rarely used
a mutable parameter of mutable reference to
MyStruct
rarely used
Below is a concrete example that uses all the types of parameters above:
struct MyStruct {}
fn wtf(
a: &MyStruct,
// commonly used, need to take ownership of input, i.e. move it inside the function
b: MyStruct,
// rarely used
mut c: MyStruct,
// commonly used, need to modify input
d: &mut MyStruct,
// rarely used
mut e: &MyStruct,
// rarely used
mut f: &mut MyStruct,
) {
println!("WTF???");
}
#[allow(dead_code)]
fn wtf_demo() {
let my_struct_1 = MyStruct {};
let mut my_struct_2 = MyStruct {};
let my_struct_3 = MyStruct {};
let mut my_struct_4 = MyStruct {};
wtf(
&MyStruct{},
MyStruct{},
my_struct_1,
&mut my_struct_2,
&my_struct_3, &mut my_struct_4);
}
Use ..
to destruct part of struct to create another struct
let your_order = Order {
name: String::from("Hacker in Rust"),
count: 1,
..order_template_1
};
while let
and nested pattern matching
let myvec = vec![100, 100, 100, 100]
while let Some(Some(integer)) = myvec.pop() {
assert_eq!(integer, 100);
}
WTF?
while let
will keep iterating untilNone
is encountered;Vec::pop
will returnSome(T)
if vector contains elements, orNone
if vector is empty.Vec::pop
returnsSome(T)
, so we need another pattern matching, thus theSome(Some(integer))
.
Use Test to Structure Tutorial
When reading a tutorial, you may want to write executable codes along with the tutorial. For example, when learning about smart pointer in the rust book, you may want to write code for Box, Drop&Deref, Rc, RefCell respectively. Test is a good way to do this. You can divide each feature into a file, and write executable tests within each file about each feature.
For example, we could have the following folder structure
$ tree .
.
├── Cargo.lock
├── Cargo.toml
└── src
├── main.rs
├── tu_box.rs
└── tu_deref.rs
The file tu_box.rs
showcase the usage of box
/// file: tu_box.rs
#[cfg(test)]
mod tests {
#[test]
fn simple_box() {
let b = Box::new(5);
println!("b = {} ({:?})", b, b);
}
}
and another file tu_deref.rs
, where we define a struct with custom Deref
trait implementation
/// file: tu_deref.rs
use std::ops::Deref;
struct MyStruct {
val: i32,
}
impl Deref for MyStruct {
type Target = i32;
fn deref(&self) -> &Self::Target {
&self.val
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple_deref() {
let val = 31;
let my_var = MyStruct { val };
println!("my_var.val = {}", *my_var.deref());
assert_eq!(val, *my_var);
assert_eq!(val, *my_var.deref());
}
}
Just remember to declare these files as mod in main.rs
.
/// file: main.rs
mod tu_box;
mod tu_deref;
// main is not strictly necessary if we only need to run the test
fn main() {
println!("Hello, world!");
}
This way, we can call specific tests with
// cargo test <test_function_name_keyword> -- --nocapture
cargo test simple_box -- --nocapture
cargo test deref -- --nocapture
cargo test simple -- --nocapture
The -- --nocapture
flag tell rustc to redirect the test output to stdout.
The ?
Operator
TODO: Finish this section
The ?
operator simplifies error handling. It’s a unary postfix operator that can only be applied to Result<T, E>
or Option<T>
.
When applied to a value
res
of typeResult<T, E>
,if its
Ok(x)
,res?
evaluates tox
. For example,if its
Err(e)
,res?
returnsErr(e)
.
As an example,
fn try_to_parse() -> Result<i32, ParseIntError> { let x: i32 = "123".parse()?; // x = 123 let y: i32 = "24a".parse()?; // returns an Err() immediately Ok(x + y) // Doesn't run. } let res = try_to_parse(); println!("{:?}", res);
When applied to a value
opt
of typeOption<T>
,if it’s
Some(x)
,res?
evaluates tox
,if it’s
None
,res?
returnsNone
As an example,
fn try_option_some() -> Option<u8> { let val = Some(1)?; Some(val) } assert_eq!(try_option_some(), Some(1)); fn try_option_none() -> Option<u8> { let val = None?; Some(val) } assert_eq!(try_option_none(), None);
?
operator allows us to chain results / options with minimal boilerplate code.