Learn Rust
🦀

Learn Rust

Tags
Rust
Public
Public
Published
November 29, 2023
Last Updated
Last updated November 7, 2024
To have the best experience of following this tutorial, I would highly recommend checking out the following note, which provides an interactive coding experience!
🦀
Play with Rust interactively in Jupyter
 
Also check the notebook in my own repo!
myRustPlayground
AgainstEntropyUpdated Feb 4, 2024
 

TOC

Rust Language and Its Ecology

Rust

Cargo

 
 

Basic syntax with code snippets

Type and Values

Type Inference

fn takes_u32(x: u32) { println!("u32: {x}"); } fn takes_i8(y: i8) { println!("i8: {y}"); } fn main() { let x = 10; let y = 20; takes_u32(x); takes_i8(y); // takes_u32(y); }
📖
The compiler infers the type of the variable from the value assigned to it. Such declaration (let x = 10;) is identical to the explicit declaration of a type (let x : u32= 10;), instead of dynamic “any type”

Function

fn fib(n: u32) -> u32 { if n <= 2 { // The base case. // unimplemented!("Implement this"); return 1; } else { // The recursive case. // todo!("Implement this") return fib(n - 1) + fib(n - 2); } } fn main() { let n = 20; println!("fib(n) = {}", fib(n)); }
💡
use macros of unimplemented! and todo! to indicates unfinished code. The difference between them is, todo! conveys an intent of implementing the functionality later and the message is "not yet implemented", while unimplemented! makes no such claims, and its message is merely “not implemented”
 

Control Flow

Loops

fn main() { let mut x = 200; while x >= 10 { x = x / 2; } println!("Final x: {x}"); }
while
fn main() { for x in 1..5 { println!("x: {x}"); } }
for
📖
1..5 is a range that yields 1, 2, 3, 4 use 1..=5 syntax for an inclusive range
fn main() { let mut i = 0; loop { i += 1; println!("{i}"); if i > 100 { break; } } }
loop
⚠️
The loop statement just loops forever, until a break

break and continue

fn main() { 'outer: for x in 1..5 { println!("x: {x}"); let mut i = 0; let result = 'inner: loop { println!("x: {x}, i: {i}"); i += 1; if i >= x { break 'inner i; } if i == 3 { break 'outer; } }; println!("result: {result}"); } }
📖
Both continue and break can optionally take a label argument, such as 'inner and 'outer here, which is used to break out of nested loops
💡
Note that loop is the only looping construct which returns a non-trivial value. This is because it’s guaranteed to be entered at least once (unlike while and for loops).
 

Blocks and Scopes

fn main() { let a = 10; println!("before: {a}"); { let a = "hello"; println!("inner scope: {a}"); let a = true; println!("shadowed in inner scope: {a}"); } println!("after: {a}"); }
📖
A variable’s scope is limited to the enclosing block. Re-declare a variable with an existing name is called “shadowing”, which is different from mutation because more memory is taken.
💡
Shadowing is convenient for holding on to values after .unwrap().
 
fn main() { let z = 13; let x = { let y = 10; println!("y: {y}"); z - y }; println!("x: {x}"); }
📖
block has a value and a type, which are those of the last expression of the block If the last expression ends with ;, then the resulting value and type is (), i.e., the “unit type”.
 

Functions

fn gcd(a: u32, b: u32) -> u32 { if b > 0 { gcd(b, a % b) } else { a // identical to "return a;" } } fn main() { println!("gcd: {}", gcd(143, 52)); }
📖
Declaration parameters are followed by a type, then a return type.
⚠️
It is similar to type hint in Python (after version 3.5), but it’s a must in Rust.
💡
Functions have no return value will return the ‘unit type’, (). The compiler will infer this if the return type is omitted.
⚠️
Overloading is not supported – each function has a single implementation.
⚠️
Rust functions always take a fixed number of parameters. (Macros are sort of special cases) Default arguments are not supported.
 

Tuples and Arrays

fn main() { // a is an array let mut a: [i8; 10] = [42; 10]; a[5] = 0; println!("a: {a:?}"); }
📖
A value of the array type [T; N] holds N (a compile-time constant) elements of the same type T.
⚠️
Note that the length of the array is part of its type, which means that [u8; 3]  and [u8; 4] are considered two different types.
⚠️
The {} gives the default output in println! macro, while {:?} gives the debug output. Types such as integers and strings implement the default output, but arrays only implement the debug output. This means that we must use debug output here.
💡
Adding #, e.g., {a:#?}, invokes a “pretty printing” format
 
fn main() { // t is a tuple let t: (i8, bool) = (7, true); println!("t.0: {}", t.0); println!("t.1: {}", t.1); }
📖
Tuples have a fixed length and group together values of different types into a compound type.
The empty tuple () is also known as the “unit type”. It is both a type, and the only valid value of that type. Analogous to void in C++
 

Array Iteration

fn main() { let primes = [2, 3, 5, 7, 11, 13, 17, 19]; for prime in primes { for i in 2..prime { assert_ne!(prime % i, 0); } } }
💡
The for ... in ... syntax actually uses the IntoIterator trait
💡
Some macros have debug-only variants like debug_assert_ne!, which compile to nothing in release builds.
 

Pattern Matching

fn main() { let input = 'x'; match input { 'q' => println!("Quitting"), 'a' | 's' | 'w' | 'd' => println!("Moving around"), '0'..='9' => println!("Number input"), key if key.is_lowercase() => println!("Lowercase: {key}"), _ => println!("Something else"), } }
📖
The _ pattern is a wildcard pattern which matches any value.
⚠️
The expressions must be irrefutable, meaning that it covers every possibility, so _ is often used as the final catch-all case.
 

Destructuring

fn main() { describe_point((1, 0)); } fn describe_point(point: (i32, i32)) { match point { (0, _) => println!("on Y axis"), (_, 0) => println!("on X axis"), (x, _) if x < 0 => println!("left of Y axis"), (_, y) if y < 0 => println!("below X axis"), _ => println!("first quadrant"), } }
#[rustfmt::skip] fn main() { let triple = [0, -2, 3, 4, 5]; println!("Tell me about {triple:?}"); match triple { // [0, y, z] => println!("First is 0, y = {y}, and z = {z}"), [1, ..] => println!("First is 1 and the rest were ignored"), [_, .., 4] => println!("First is ignored, the last is 4"), [a@.., b] => println!("the last is {b}, a = {a:?}"), [.., b] => println!("the last is {b}, whatever the previous elements are"), _ => println!("All elements were ignored"), } }
📖
 .. will expand to account for different number of elements.
With patterns [a@.., b], all elements except for the last one will be matched into a list called a

References

Shared References

fn main() { let a = 'A'; let b = 'B'; let mut r: &char = &a; println!("r: {}", *r); r = &b; let s = &b; println!("r: {}", *r); println!("s: {}", *r); }
📖
A shared reference to a type T has type &T. A reference value is made with the & operator. The * operator “dereferences” a reference, yielding its value.
⚠️
In this example, r is mutable so that it can be reassigned (r = &b). Note that this re-binds r, so that it refers to something else. This is different from C++, where assignment to a reference changes the referenced value.
💡
Note Shared References here is relative to Exclusive references. The reason why the syntax (let mut a = &x;) is called a shared reference is because there could be other references sharing the accessibility of x, even if a is already there.

Exclusive references

fn main() { let mut point = (1, 2); let x_coord_ref = &mut point.0; let x_coord = point.0; // !panic here!: use of borrowed `point.0` // cannot borrow `point.0` as immutable because it is also borrowed as mutable let mut x_coord_ref_2 = &point.0; // !panic here!: immutable borrow occurs here // cannot borrow `point.0` as immutable because it is also borrowed as mutable let x_coord_ref_3 = &mut point.0; // !panic here!: second mutable borrow occurs here // cannot borrow `point.0` as mutable more than once at a time let y_coord_ref = &point.1; // this is ok *x_coord_ref = 20; println!("point: {point:?}"); }
📖
“Exclusive” means that no other references (shared or exclusive) nor variables can access the borrowed value while the exclusive reference exists.
⚠️
Note the difference between let mut x: &i32 and let x: &mut i32.
 

ref keyword

The ref keyword in Rust is used in pattern matching to create a reference to a value.
#[derive(Debug)] struct Person { name: &'static str, age: u8, } fn main() { let some_person = Some(Person { name: "Alice", age: 25, }); match some_person { Some(ref x) => println!("Got a reference to: {:?}", x), None => println!("Got nothing"), } println!("some_person: {:?}", some_person); }
💡
In the example above, we can still use some_person after match because we borrowed the value of x by reference.
Run another example but without ref to see what will happen:
#[derive(Debug)] struct Person { name: &'static str, age: u8, } fn main() { let some_person = Some(Person { name: "Alice", age: 25, }); match some_person { Some(x) => println!("Got a reference to: {:?}", x), None => println!("Got nothing"), } println!("some_person: {:?}", some_person); }
💡
However, if the value is of copyable type, we can still use the value after match even if there is no ref , as shown below
#[derive(Debug, Clone, Copy)] struct PersonCopyable { name: &'static str, age: u8, } fn main() { let some_copyable_person = Some(PersonCopyable { name: "Alice", age: 25, }); match some_copyable_person { Some(x) => println!("Got a reference to: {:?}", x), None => println!("Got nothing"), } println!("some_copyable_person: {:?}", some_copyable_person); }

as_ref and as_mut

let text: Option<String> = Some("Hello, world!".to_string()); let text_length: Option<usize> = text .as_ref() // First, cast `Option<String>` to `Option<&String>` with `as_ref`, .map(|s| s.len()); // then consume *that* with `map`, leaving `text` on the stack. println!("still can print text: {text:?}");
let mut x = Some(2); match x.as_mut() { Some(v) => *v = 42, None => {}, } assert_eq!(x, Some(42));

User-defined Types

Structs

struct Person { name: String, age: u8, } fn describe(person: &Person) { println!("{} is {} years old", person.name, person.age); } fn main() { let mut peter = Person { name: String::from("Peter"), age: 27, }; describe(&peter); peter.age = 28; describe(&peter); let name = String::from("Avery"); let age = 39; let avery = Person { name, age }; describe(&avery); let jackie = Person { name: String::from("Jackie"), ..avery }; describe(&jackie); }
Named Structs
📖
The struct update syntax .. allows us to explicitly copy fields from another struct. Note that this syntax must always be the last element.
⚠️
Unlike in C++, there is no inheritance between structs.
 
struct Point(i32, i32); struct PoundsOfForce(f64); struct Newtons(f64); fn compute_thruster_force() -> PoundsOfForce { todo!("Ask a rocket scientist at NASA") } fn set_thruster_force(force: Newtons) { // ... } fn main() { let p = Point(17, 23); println!("({}, {})", p.0, p.1); let force = compute_thruster_force(); set_thruster_force(force); }
Tuple Structs
💡
Newtypes are a great way to encode additional information about the value in a primitive type, such as a number measured in some units
 

Enum

#[derive(Debug)] enum Direction { Left, Right, } #[derive(Debug)] enum PlayerMove { Pass, // Simple variant Run(Direction), // Tuple variant Teleport { x: u32, y: u32 }, // Struct variant } fn main() { let mut m = PlayerMove::Run(Direction::Left); println!("On this turn: {:?}", m); m = PlayerMove::Teleport { x: 1, y: 2 }; println!("On this turn: {:?}", m); }
📖
The enum keyword allows the creation of a type which has a few different variants
💡
Note these variants can have different primitive types, although all of these variants are of the same Enum type
 
#[repr(u32)] #[derive(Debug)] enum Bar { A, // 0 B = 10000, C, // 10001 } fn main() { let a = Bar::A; println!("a: {:?}", a); println!("a(u32): {:?}", a as u32); let b = Bar::B; println!("b: {:?}", b as u16); println!("C(u32): {}", Bar::C as u32); println!("C(u8): {}", Bar::C as u8); }
💡
Rust uses minimal space to store the discriminant, unless specified
 

Static and Const

const DIGEST_SIZE: usize = 3; static ZERO: Option<u8> = Some(42); fn main() { let mut digest = [ZERO.unwrap_or(0); DIGEST_SIZE]; println!("digest: {digest:?}"); let text: &str = "Hello"; for (idx, &b) in text.as_bytes().iter().enumerate() { digest[idx % DIGEST_SIZE] = digest[idx % DIGEST_SIZE].wrapping_add(b); println!("digest: {digest:?}"); } }
Property
Static
Constant
Has an address in memory
Yes
No (inlined)
Lives for the entire duration of the program
Yes
No
Can be mutable
Yes (unsafe)
No
Evaluated at compile time
Yes (initialised at compile time)
Yes
Inlined wherever it is used
No
Yes
📖
Divide global declarations into two categories:
  • constants declare constant values. These represent a value, not a memory address. This is the most common thing one would reach for and would replace static as we know it today in almost all cases.
  • statics declare global variables. These represent a memory address. They would be rarely used: the primary use cases are global locks, global atomic counters, and interfacing with legacy C libraries.
 

Type Aliases

fn main() { type Name = String; type Age = u32; type Person = (Name, Age); fn new_person(name: Name, age: Age) -> Person { (name, age) } let john = new_person(String::from("John"), 42); println!("john: {:?}", john); }
📖
A type alias creates a name for another type.
⚠️
Note the difference between a tuple struct and a tuple type alias
Person as a tuple struct
fn main() { type Name = String; type Age = u32; #[derive(Debug)] struct Person(Name, Age); fn new_person(name: Name, age: Age) -> Person { Person(name, age) } let john = new_person(String::from("John"), 42); println!("john: {:?}", john); }
 

Pattern matching

Destructuring Structs and Enums

struct Foo { x: (u32, u32), y: u32, } #[test] fn destruct_struct() { let foo = Foo { x: (2, 2), y: 2 }; match foo { Foo { x: (1, b), y } => println!("x.0 = 1, b = {b}, y = {y}"), Foo { y: 2, x: i } => println!("y = 2, x = {i:?}"), Foo { y, .. } => println!("y = {y}, other fields were ignored"), } }
struct
enum Result { Ok(i32), Err(String), } fn divide_in_two(n: i32) -> Result { if n % 2 == 0 { Result::Ok(n / 2) } else { Result::Err(format!("cannot divide {n} into two equal parts")) } } #[test] fn destruct_enum() { let n = 99; match divide_in_two(n) { Result::Ok(half) => println!("{n} divided in two is {half}"), Result::Err(msg) => println!("sorry, an error happened: {msg}"), } }
enum

Let Control Flow

fn sleep_for(secs: f32) { let dur = if let Ok(dur_) = std::time::Duration::try_from_secs_f32(secs) { dur_ } else { std::time::Duration::from_millis(500) }; std::thread::sleep(dur); println!("slept for {:?}", dur); } #[test] fn if_let() { sleep_for(-10.0); sleep_for(0.8); sleep_for(0.5); }
if-let
💡
To be clear, in this case the if-let expression tries to match the value returned from std::time::Duration::try_from_secs_f32 to the pattern Ok(dur_)
#[derive(Debug)] enum Result { Ok(u32), Err(String), } fn hex_or_die_trying(maybe_string: Option<String>) -> Result { let s = if let Some(s_) = maybe_string { s_ } else { return Result::Err(String::from("got None")); }; let first_byte_char = if let Some(first_byte_char_) = s.chars().next() { first_byte_char_ } else { return Result::Err(String::from("got empty string")); }; if let Some(digit_) = first_byte_char.to_digit(16) { Result::Ok(digit_) } else { Result::Err(String::from("not a hex digit")) } } #[test] fn let_else() { println!("result: {:?}", hex_or_die_trying(Some(String::from("foo")))); println!("result: {:?}", hex_or_die_trying(Some(String::from("Aoo")))); println!("result: {:?}", hex_or_die_trying(Some(String::from("")))); println!("result: {:?}", hex_or_die_trying(None)); }
let-else
📖
The let-else construct can flatten the cumbersome if-let expressions. So we can rewrite the above hex_or_die_trying function into a neat version:
fn hex_or_die_trying_flatten(maybe_string: Option<String>) -> Result { let Some(s) = maybe_string else { return Result::Err(String::from("got None")); }; let Some(first_byte_char) = s.chars().next() else { return Result::Err(String::from("got empty string")); }; let Some(digit) = first_byte_char.to_digit(16) else { return Result::Err(String::from("not a hex digit")); }; Result::Ok(digit) }
#[test] fn while_let() { let mut name = String::from("Comprehensive Rust 🦀"); while let Some(c) = name.pop() { println!("character: {c}"); } }
while-let

Methods and Traits

Methods

#[derive(Debug)] struct Race { name: String, laps: Vec<i32>, } impl Race { // No receiver, a static method fn new(name: &str) -> Self { Self { name: String::from(name), laps: Vec::new() } } // Exclusive borrowed read-write access to self fn add_lap(&mut self, lap: i32) { self.laps.push(lap); } // Shared and read-only borrowed access to self fn print_laps(&self, index: bool) { println!("Recorded {} laps for {}:", self.laps.len(), self.name); if index { for (idx, lap) in self.laps.iter().enumerate() { println!("Lap {idx}: {lap} sec"); } } else { for lap in self.laps.iter() { println!("{lap} sec"); } } } // Exclusive ownership of self fn finish(self) { let total: i32 = self.laps.iter().sum(); println!("Race {} is finished, total lap time: {}", self.name, total); } } let mut race = Race::new("Monaco Grand Prix"); race.add_lap(70); race.add_lap(68); race.print_laps(false); race.add_lap(71); race.print_laps(true); race.finish(); // race.finish(); // You cannot call finish twice
📖
The self arguments specify the “receiver” - the object the method acts on. There are several common receivers for a method:
  • &self: borrows the object from the caller using a shared and immutable reference. The object can be used again afterwards.
  • &mut self: borrows the object from the caller using a unique and mutable reference. The object can be used again afterwards.
  • self: takes ownership of the object and moves it away from the caller. The method becomes the owner of the object. The object will be dropped (deallocated) when the method returns, unless its ownership is explicitly transmitted. Complete ownership does not automatically mean mutability.
  • mut self: same as above, but the method can mutate the object.
  • No receiver: this becomes a static method on the struct. Typically used to create constructors which are called new by convention.
💡
Note that Self is actually a type alias for the type the impl block is in (in this case, Race) and can be used elsewhere in the block. And the self in previous codes is an abbreviated term for self: Self.
 

Traits

struct Dog { name: String, age: i8, } struct Cat { lives: i8, } trait Pet { fn talk(&self) -> String; fn greet(&self) { println!("Oh you're a cutie! What's your name? {}", self.talk()); } } impl Pet for Dog { fn talk(&self) -> String { format!("Woof, my name is {}!", self.name) } } impl Pet for Cat { fn talk(&self) -> String { String::from("Miau!") } } let captain_floof = Cat { lives: 9 }; let fido = Dog { name: String::from("Fido"), age: 5 }; captain_floof.greet(); fido.greet();

Deriving

#[derive(Debug, Clone, Default)] struct Player { name: String, strength: u8, hit_points: u8, } let p1 = Player::default(); // Default trait adds `default` constructor. let mut p2 = p1.clone(); // Clone trait adds `clone` method. p2.name = String::from("EldurScrollz"); // Debug trait adds support for printing with `{:?}`. println!("{:?} vs. {:?}", p1, p2);

Trait Objects

struct Dog { name: String, age: i8, } struct Cat { lives: i8, } trait Pet { fn talk(&self) -> String; } impl Pet for Dog { fn talk(&self) -> String { format!("Woof, my name is {}!", self.name) } } impl Pet for Cat { fn talk(&self) -> String { String::from("Miau!") } } let pets: Vec<Box<dyn Pet>> = vec![ Box::new(Cat { lives: 9 }), Box::new(Dog { name: String::from("Fido"), age: 5 }), ]; for pet in pets { println!("Hello, who are you? {}", pet.talk()); }
💡
Types that implement a given trait may be of different sizes. This makes it impossible to have things like Vec<dyn Pet> in the example above.
See memory layout after allocating pets
notion image
📖
dyn Pet is a way to tell the compiler about a dynamically sized type that implements Pet. Run following codes for verification:
println!("{} {}", std::mem::size_of::<Dog>(), std::mem::size_of::<Cat>()); println!("{} {}", std::mem::size_of::<&Dog>(), std::mem::size_of::<&Cat>()); println!("{}", std::mem::size_of::<&dyn Pet>()); println!("{}", std::mem::size_of::<Box<dyn Pet>>());
 

Generic

Generic Functions

/// Pick `even` or `odd` depending on the value of `n`. fn pick<T>(n: i32, even: T, odd: T) -> T { if n % 2 == 0 { even } else { odd } } println!("picked a number: {:?}", pick(97, 222, 333)); println!("picked a tuple: {:?}", pick(28, ("dog", 1), ("cat", 2)));
💡
Rust infers a type for T based on the types of the arguments and return value, and turns generic code into non-generic code accordingly. Therefore, this is a zero-cost abstraction: you get exactly the same result as if you had hand-coded the data structures without the abstraction.

Generic Data Types

#[derive(Debug)] struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn coords(&self) -> (&T, &T) { (&self.x, &self.y) } } let integer = Point { x: 5, y: 10 }; let float = Point { x: 1.0, y: 4.0 }; println!("{integer:?} and {float:?}"); println!("coords: {:?}", integer.coords()); println!("coords: {:?}", float.coords());
📖
T is specified twice in impl<T> Point<T> {} because it is a generic implementation section for generic type.
💡
It is possible to write impl Point<u32> { .. }.
Now Point is still generic and you can use Point<f64>, but methods in this block will only be available for Point<u32>.
#[derive(Debug)] struct Point<T> { x: T, y: T, } impl Point<u32> { fn coords(&self) -> (&u32, &u32) { (&self.x, &self.y) } } impl Point<f64> { fn coords(&self) -> (&f64, u32, &f64) { (&self.x, 0, &self.y) } } let integer = Point { x: 5, y: 10 }; let float = Point { x: 1.0, y: 4.0 }; println!("{integer:?} and {float:?}"); println!("coords(u32): {:?}", integer.coords()); println!("coords(f64): {:?}", float.coords());
💡
We can also handle different types by using multiple generic types
such as T and U
#[derive(Debug)] struct Point<T, U> { x: T, y: U, } impl<T, U> Point<T, U> { fn coords(&self) -> (&T, &U) { (&self.x, &self.y) } } let mix = Point { x: 2, y: 3.0 }; println!("{mix:?}"); println!("coords: {:?}", mix.coords());

Trait Bounds

fn duplicate<T: Clone>(a: T) -> (T, T) { (a.clone(), a.clone()) } #[derive(Debug)] // #[derive(Clone)] // try to uncomment this line struct NotClonable { name: String, age: i32, } let foo = String::from("foo"); let foo_pair = duplicate(foo); println!("{foo_pair:?}"); let me = NotClonable { name: String::from("Ethan"), age: 23 }; let me_pair = duplicate(me); println!("{bar_pair:?}");
💡
The struct NotClonable is not clonable because the trait Clone is not implemented for it

impl Trait

fn add_42_millions(x: impl Into<i32>) -> i32 { x.into() + 42_000_000 } fn pair_of(x: u32) -> impl std::fmt::Debug { (x + 1, x - 1) } let many = add_42_millions(42_i8); println!("{many}"); let many_more = add_42_millions(10_000_000); println!("{many_more}"); let debuggable = pair_of(27); println!("debuggable: {debuggable:?}");
📖
fn add_42_millions(x: impl Into<i32>) can be seen as a syntactic sugar for fn add_42_millions<T: Into<i32>>(x: T)
💡
Using impl Trait as return type can be useful when you don’t want to expose the concrete type in a public API.

Standard Library Types

Option

let name = "Löwe 老虎 Léopard Gepardi"; let mut position: Option<usize> = name.find('é'); println!("find returned {position:?}"); assert_eq!(position.unwrap(), 14); position = name.find('Z'); println!("find returned {position:?}"); assert_eq!(position.expect("Character not found"), 0);
📖
unwrap will return the value in an Option, or panic. expect is similar but takes an error message. Option<T> often has the same size in memory as T.
📖
take

Result

use std::fs::File; use std::io::Write; use std::io::Read; let mut file = File::create("diary.txt").unwrap(); file.write_all(b"When did I write something here?\nI don't remember!").expect("failed to write message"); let file: Result<File, std::io::Error> = File::open("diary.txt"); match file { Ok(mut file) => { let mut contents = String::new(); if let Ok(bytes) = file.read_to_string(&mut contents) { println!("Dear diary: \n{contents} \n({bytes} bytes)"); } else { println!("Could not read file content"); } } Err(err) => { println!("The diary could not be opened: {err}"); } }
When did I write something here? I don't remember!
diary.txt
📖
Result is defined as below (see Result document)
enum Result<T, E> { Ok(T), Err(E), }
It is an generic enum with the variants, Ok(T), representing success and containing a value, and Err(E), representing error and containing an error value.

String

let mut s1 = String::new(); s1.push_str("Hello"); println!("s1: {s1} len = {}, capacity = {}", s1.len(), s1.capacity()); let mut s2 = String::with_capacity(s1.len() + 1); s2.push_str(&s1); s2.push('!'); println!("s2: {s2} len = {}, capacity = {}", s2.len(), s2.capacity()); s2.push('!'); println!("s2: {s2} len = {}, capacity = {}", s2.len(), s2.capacity()); let s3 = String::from("🇨🇭"); println!("s3: {s3} len = {}, capacity = {}, number of chars = {}", s3.len(), s3.capacity(), s3.chars().count()); for (i, c) in s3.chars().enumerate() { println!("char {i}: {c}"); }
💡
When a type implements Deref<Target = T>, the compiler will let you transparently call methods from T. String implements Deref<Target = str>, so we can call all str methods on a String.
⚠️
Note that a char can be different from what a human will consider a “character” due to grapheme clusters.

Vec

let mut v1 = Vec::new(); v1.push(42); println!("v1: {:?}, len = {}, capacity = {}", v1, v1.len(), v1.capacity()); let mut v2 = Vec::with_capacity(v1.len() + 1); v2.extend(v1.iter()); v2.push(9999); println!("v2: {:?}, len = {}, capacity = {}", v2, v2.len(), v2.capacity()); v2.push(1026); println!("v2: {:?}, len = {}, capacity = {}", v2, v2.len(), v2.capacity()); // Canonical macro to initialize a vector with elements. let mut v3 = vec![0, 0, 1, 2, 3, 4]; // Retain only the even elements. v3.retain(|x| x % 2 == 0); println!("v3: {v3:?}"); // Remove consecutive duplicates. v3.dedup(); println!("v3: {v3:?}"); v3.push(0); println!("v3: {v3:?}"); v3.dedup(); println!("v3: {v3:?}"); // Use vector to store UTF-8 encoded strings. let mut v4 = Vec::new(); v4.extend(String::from("Hello 🦀").chars()); println!("v4: {v4:?}");
💡
vec![...] is a canonical macro to use instead of Vec::new() , which supports adding initial elements to the vector.
📖
To index the vector you use [], but they will panic if out of bounds. Alternatively, using get will return an Option. The pop function will remove the last element.

HashMap

use std::collections::HashMap; let mut page_counts = HashMap::new(); page_counts.insert("Adventures of Huckleberry Finn".to_string(), 207); page_counts.insert("Grimms' Fairy Tales".to_string(), 751); page_counts.insert("Pride and Prejudice".to_string(), 303); if !page_counts.contains_key("Les Misérables") { println!( "We know about {} books, but not Les Misérables.", page_counts.len() ); } for book in ["Pride and Prejudice", "Alice's Adventure in Wonderland"] { match page_counts.get(book) { Some(count) => println!("{book}: {count} pages"), None => println!("{book} is unknown."), } } // Use the .entry() method to insert a value if nothing is found. for book in ["Pride and Prejudice", "Alice's Adventure in Wonderland"] { let page_count: &mut i32 = page_counts.entry(book.to_string()).or_insert(0); *page_count += 1; } { let pc1 = page_counts.get("Harry Potter and the Sorcerer's Stone").unwrap_or(&336); println!("Harry Potter has {} pages", pc1); } println!("page_counts:\n{page_counts:#?}");
💡
Unlike vec!, there is no standard hashmap! macro. But since Rust 1.56, HashMap implements From<[(K, V); N]>, which allows us to easily initialize a hash map from a literal array:
let page_counts = HashMap::from([ ("Harry Potter and the Sorcerer's Stone".to_string(), 336), ("The Hunger Games".to_string(), 374), ]);

Standard Library Traits

Comparisons

PartialEq and Eq
struct Key { id: u32, metadata: Option<String>, } impl PartialEq for Key { fn eq(&self, other: &Self) -> bool { self.id == other.id } } let key1 = Key { id: 5, metadata: None }; let key2 = Key { metadata: Some("color=red".to_string()), ..key1 }; println!("Equal? {}", key1 == key2);
📖
The == and != operators will call required method eq and provided method ne in PartialEq trait.
💡
The “Partial” here implies only a portion of elements of the given type can be compared. For instance, float only implements PartialEq but not Eq because NaN is also a float number but cannot be compared. See Rust course for details.
⚠️
PartialEq can be implemented between different types, but Eq cannot, because it is reflexive
PartialOrd and Ord
use std::cmp::Ordering; #[derive(Eq, PartialEq)] struct Citation { author: String, year: u32, } impl PartialOrd for Citation { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { match self.author.partial_cmp(&other.author) { Some(Ordering::Equal) => self.year.partial_cmp(&other.year), author_ord => author_ord, } } } let cite1 = Citation { author: "Einstein".to_string(), year: 1905 }; let cite2 = Citation { author: "Ethan Wang".to_string(), year: 2023 }; println!("cite1 < cite2? {}", cite1 < cite2);
📖
The <<=>=, and > operators will call partial_cmp method in PartialOrd trait.
💡
In practice, it’s common to derive these traits, but uncommon to implement them.

Operators

#[derive(Debug, Copy, Clone)] struct Point { x: i32, y: i32, } impl std::ops::Add for Point { type Output = Self; fn add(self, other: Self) -> Self { Self { x: self.x + other.x, y: self.y + other.y } } } let p1 = Point { x: 10, y: 20 }; let p2 = Point { x: 100, y: 200 }; println!("{:?} + {:?} = {:?}", p1, p2, p1 + p2);
💡
We could implement Add for two different types
e.g. impl Add<(i32, i32)> for Point would add a tuple to a Point.
impl std::ops::Add<(i32, i32)> for Point { type Output = (i32, i32); fn add(self, other: (i32, i32)) -> Self::Output { (self.x + other.0, self.y + other.1) } } let p3 = (1, 2); let p4 = p1 + p3; println!("{:?} + {:?} = {:?}", p1, p3, p4);

From and Into

// From let s = String::from("hello"); let addr = std::net::Ipv4Addr::from([127, 0, 0, 1]); let one = i16::from(true); let bigger = i32::from(123_i16); println!("{s}, {addr}, {one}, {bigger}"); // Into let s: String = "hello".into(); let addr: std::net::Ipv4Addr = [127, 0, 0, 1].into(); let one: i16 = true.into(); let bigger: i32 = 123_i16.into(); println!("{s}, {addr}, {one}, {bigger}");

Casting

let value: i64 = 1000; println!("as u16: {}", value as u16); println!("as i16: {}", value as i16); println!("as u8: {}", value as u8);
📖
Rust has no implicit type conversions, but does support explicit casts with as.
⚠️
Casts are best used only when the intent is to indicate unconditional truncation (e.g. selecting the bottom 32 bits of a u64 with as u32, regardless of what was in the high bits).

Read and Write

// Read use std::io::{BufRead, BufReader, Read, Result}; fn count_lines<R: Read>(reader: R) -> usize { let buf_reader = BufReader::new(reader); buf_reader.lines().count() } let slice: &[u8] = b"foo\nbar\nbaz\n"; println!("lines in slice: {}", count_lines(slice)); let file = std::fs::File::open(std::env::current_exe()?)?; println!("lines in file: {}", count_lines(file)); // Write use std::io::{Result, Write}; fn log<W: Write>(writer: &mut W, msg: &str) -> Result<()> { writer.write_all(msg.as_bytes())?; writer.write_all("\n".as_bytes()) } let mut buffer = Vec::new(); log(&mut buffer, "Hello")?; log(&mut buffer, "World")?; println!("Logged: {:?}", buffer);

Default

#[derive(Debug, Default)] struct Derived { x: u32, y: String, z: Implemented, } #[derive(Debug)] struct Implemented(String); impl Default for Implemented { fn default() -> Self { Self("John Smith".into()) } } let default_struct = Derived::default(); println!("Default struct:\n{:#?}\n", default_struct); let almost_default_struct = Derived { y: "Y is set!".into(), ..Derived::default() }; println!("Default struct (with y set):\n{almost_default_struct:#?}\n"); let nothing: Option<Derived> = None; println!("Fill Nothing with Default:\n{:#?}\n", nothing.unwrap_or_default());

Closures

fn apply_with_log(func: impl FnOnce(i32) -> i32, input: i32) -> i32 { println!("Calling function on {input}"); func(input) } { let add_3 = |x| x + 3; println!("add_3: {}", apply_with_log(add_3, 10)); println!("add_3: {}", apply_with_log(add_3, 20)); let mut v = Vec::new(); let mut accumulate = |x: i32| { v.push(x); v.iter().sum::<i32>() }; println!("accumulate: {}", apply_with_log(&mut accumulate, 4)); println!("accumulate: {}", apply_with_log(&mut accumulate, 5)); let multiply_sum = |x| x * v.into_iter().sum::<i32>(); println!("multiply_sum: {}", apply_with_log(multiply_sum, 3)); };
fn make_greeter(prefix: String) -> impl Fn(&str) { return move |name| println!("{} {}", prefix, name); } { let hi = make_greeter("Hi".to_string()); hi("there"); };

Memory Management

Ownership

struct Point(i32, i32); { let p = Point(3, 4); println!("x: {}", p.0); } println!("y: {}", p.1);

Move Semantics

let s1: String = String::from("Hello!"); let s2: String = s1; println!("s2: {s2}"); // println!("s1: {s1}");
fn say_hello(name: String) { println!("Hello {name}") } fn say_hello_ref(name: &String) { println!("Hello {name}") } let name = String::from("Alice"); say_hello_ref(&name); say_hello_ref(&name); say_hello(name.clone()); say_hello(name); // say_hello(name)

Clone

#[derive(Default)] struct Backends { hostnames: Vec<String>, weights: Vec<f64>, } impl Backends { fn set_hostnames(&mut self, hostnames: &Vec<String>) { self.hostnames = hostnames.clone(); self.weights = hostnames.iter().map(|_| 1.0).collect(); } }

Copy

let x = 42; let y = x; println!("x: {x}"); // would not be accessible if not Copy println!("y: {y}");
#[derive(Copy, Clone, Debug)] struct Point(i32, i32); let p1 = Point(3, 4); let p2 = p1; println!("p1: {p1:?}"); println!("p2: {p2:?}");

Drop

struct Droppable { name: &'static str, } impl Drop for Droppable { fn drop(&mut self) { println!("Dropping {}", self.name); } } { let a = Droppable { name: "a" }; { let b = Droppable { name: "b" }; { let c = Droppable { name: "c" }; let d = Droppable { name: "d" }; println!("Exiting block B"); } println!("Exiting block A"); } drop(a); println!("Exiting main"); }
 

Smart Pointers

Box

Box in Rust is like std::unique_ptr in C++, except that it’s guaranteed to be not null.
let five = Box::new(5); println!("five: {}", *five);
📖
Box<T> implements Deref<Target = T>, which means that you can call methods from T directly on a Box<T>.
#[derive(Debug)] enum List<T> { /// A non-empty list: first element and the rest of the list. Element(T, Box<List<T>>), /// An empty list. Nil, } let list: List<i32> = List::Element(1, Box::new(List::Element(2, Box::new(List::Nil)))); println!("{list:?}");
💡
Box can be useful when you want to transfer ownership of a large amount of data. To avoid copying large amounts of data on the stack, instead store the data on the heap in a Box so only the pointer is moved.
 

RC

Rc in Rust is like std::shared_ptr in C++.
use std::rc::Rc; let a = Rc::new(10); println!("a: {a}"); println!("a strong: {}", Rc::strong_count(&a)); let b = Rc::clone(&a); println!("b: {b}"); println!("a strong: {}", Rc::strong_count(&a)); println!("b strong: {}", Rc::strong_count(&b));
💡
Use Rc::strong_count to check the reference count.
 

Borrowing

Borrow a Value

#[derive(Debug)] struct Point(i32, i32); fn add(p1: &Point, p2: &Point) -> Point { let p = Point(p1.0 + p2.0, p1.1 + p2.1); println!("&p.0: {:p}", &p.0); p } let p1 = Point(3, 4); let p2 = Point(10, 20); let p3 = add(&p1, &p2); println!("&p3.0: {:p}", &p3.0); println!("{p1:?} + {p2:?} = {p3:?}");

Borrow Checking

let _ = { let mut a: i32 = 10; let b: &i32 = &a; { // let c: &mut i32 = &mut a; // *c = 20; let c: &i32 = &a; println!("c: {c}"); } println!("a: {a}"); println!("b: {b}"); };
To make the above code compile
let _ = { let mut a: i32 = 10; let b: &i32 = &a; println!("b: {b}"); { let c: &mut i32 = &mut a; *c = 20; // let c: &i32 = &a; println!("c: {c}"); } println!("a: {a}"); };

Interior Mutability | RefCell | Cell

use std::cell::RefCell; use std::rc::Rc; #[derive(Debug, Default)] struct Node { value: i64, children: Vec<Rc<RefCell<Node>>>, } impl Node { fn new(value: i64) -> Rc<RefCell<Node>> { Rc::new(RefCell::new(Node { value, ..Node::default() })) } fn sum(&self) -> i64 { self.value + self.children.iter().map(|c| c.borrow().sum()).sum::<i64>() } fn inc(&mut self) { self.value += 1; for n in &self.children { n.borrow_mut().inc(); } } } let root = Node::new(1); root.borrow_mut().children.push(Node::new(5)); let subtree = Node::new(10); subtree.borrow_mut().children.push(Node::new(11)); subtree.borrow_mut().children.push(Node::new(12)); root.borrow_mut().children.push(subtree); println!("graph: {root:#?}"); println!("graph sum: {}", root.borrow().sum()); root.borrow_mut().inc(); println!("graph: {root:#?}"); println!("graph sum: {}", root.borrow().sum());
📖
Cell wraps a value and allows getting or setting the value, even with a shared reference to the Cell. However, it does not allow any references to the value. Rc only allows shared (read-only) access to its contents, since its purpose is to allow (and count) many references.
 

Slices and Lifetimes

Slices

{ let mut a: [i32; 6] = [10, 20, 30, 40, 50, 60]; println!("a: {a:?}"); let s: &[i32] = &a[2..4]; println!("s: {s:?}"); };
📖
A slice is created by borrowing another object and specifying the starting and ending indexes in brackets.
💡
Rust’s range syntax allows us to drop the starting index or the ending index
  • &a[0..a.len()] and &a[..a.len()] are identical
  • &a[2..a.len()] and &a[2..] are identical
  • &a[..] is a slice of the full array

String References

{ let s1: &str = "World"; println!("s1: {s1}"); let mut s2: String = String::from("Hello "); println!("s2: {s2}"); s2.push_str(s1); println!("s2: {s2}"); let s3: &str = &s2[6..]; println!("s3: {s3}"); };
📖
&str is an immutable reference to a string slice.
String is a mutable string buffer.
💡
Rust’s range syntax allows us to drop the starting index or the ending index
  • &a[0..a.len()] and &a[..a.len()] are identical
  • &a[2..a.len()] and &a[2..] are identical
  • &a[..] is a slice of the full array
 

Lifetime Annotations

#[derive(Debug)] struct Point(i32, i32); // fn left_most(p1: &Point, p2: &Point) -> &Point { fn left_most<'a>(p1: &'a Point, p2: &'a Point) -> &'a Point { if p1.0 < p2.0 { p1 } else { p2 } } { let p1: Point = Point(10, 10); let p2: Point = Point(20, 20); let p3 = left_most(&p1, &p2); // What is the lifetime of p3? println!("p3: {p3:?}"); };
📖
Read &'a Point as “a borrowed Point which is valid for at least the lifetime a”.
💡
This says, “given p1 and p2 which both outlive 'a, the return value lives for at least 'a.
 

Lifetimes in Function Calls | Lifetimes Elision

#[derive(Debug)] struct Point(i32, i32); fn cab_distance(p1: &Point, p2: &Point) -> i32 { (p1.0 - p2.0).abs() + (p1.1 - p2.1).abs() } fn nearest<'a>(points: &'a [Point], query: &Point) -> Option<&'a Point> { let mut nearest = None; for p in points { if let Some((_, nearest_dist)) = nearest { let dist = cab_distance(p, query); if dist < nearest_dist { nearest = Some((p, dist)); } } else { nearest = Some((p, cab_distance(p, query))); }; } nearest.map(|(p, _)| p) } println!( "{:?}", nearest( &[Point(1, 0), Point(1, 0), Point(-1, 0), Point(0, -1),], &Point(0, 2) ) );
 

Lifetimes in Data Structures | Struct Lifetimes

#[derive(Debug)] struct Highlight<'doc>(&'doc str); fn erase(text: String) { println!("Bye {text}!"); } { let text = String::from("The quick brown fox jumps over the lazy dog."); let fox = Highlight(&text[4..19]); let dog = Highlight(&text[35..43]); // erase(text); println!("{fox:?}"); println!("{dog:?}"); };
 

Iterators

Iterator

struct Fibonacci { curr: u32, next: u32, } impl Iterator for Fibonacci { type Item = u32; fn next(&mut self) -> Option<Self::Item> { let new_next = self.curr + self.next; self.curr = self.next; self.next = new_next; Some(self.curr) } } { let fib = Fibonacci { curr: 0, next: 1 }; for (i, n) in fib.enumerate().take(5) { println!("fib({i}): {n}"); } };
 

IntoIterator

struct Grid { x_coords: Vec<u32>, y_coords: Vec<u32>, } impl IntoIterator for Grid { type Item = (u32, u32); type IntoIter = GridIter; fn into_iter(self) -> GridIter { GridIter { grid: self, i: 0, j: 0 } } } struct GridIter { grid: Grid, i: usize, j: usize, } impl Iterator for GridIter { type Item = (u32, u32); fn next(&mut self) -> Option<(u32, u32)> { if self.i >= self.grid.x_coords.len() { self.i = 0; self.j += 1; if self.j >= self.grid.y_coords.len() { return None; } } let res = Some((self.grid.x_coords[self.i], self.grid.y_coords[self.j])); self.i += 1; res } } { let grid = Grid { x_coords: vec![3, 5, 7, 9], y_coords: vec![10, 20, 30, 40] }; for (x, y) in grid { println!("point = {x}, {y}"); } };
💡
The Iterator trait tells you how to iterate once you have created an iterator. The trait IntoIterator defines how to create an iterator for a type. It is used automatically by the for loop.
 

FromIterator

let primes = vec![2, 3, 5, 7]; let prime_squares = primes.into_iter().map(|p| p * p).collect::<Vec<_>>(); println!("prime_squares: {prime_squares:?}");
💡
The above code works because the trait Iterator implements
fn collect<B>(self) -> B where B: FromIterator<Self::Item>, Self: Sized
 

Test

Doctest

//! Colorized output utilities for the terminal using ANSI escape codes. //! # Examples: //! ``` //! use cli_utils::colors::*; //! println!("{}{}{}", red("Red"), green("Green"), blue("Blue")); //! ``` /// Returns a string with the ANSI escape code for red. /// # Examples: /// ``` /// use cli_utils::colors::*; /// println!("{}", red("Red")); /// ``` pub fn red(s: &str) -> String { format!("\x1b[31m{}\x1b[0m", s) }
# test the color module cargo test --doc --package cli-utils -- colors --nocapture # test the function red in the color module cargo test --doc --package cli-utils -- colors::red --nocapture

Unit test

#[test] fn test_fib() { assert_eq!(fib(1), 1); assert_eq!(fib(2), 1); assert_eq!(fib(4), 3); assert_eq!(fib(6), 8); } fn fib(n: u32) -> u32 { if n <= 2 { // The base case. return 1; } else { // The recursive case. return fib(n - 1) + fib(n - 2); } }
💡
Decorate a function with #[test] and run cargo test to do unit test
 

Documentation

/// Returns a string with the ANSI escape code for red. /// # Examples: /// ``` /// use cli_utils::colors::*; /// println!("{}", red("Red")); /// ``` pub fn red(s: &str) -> String { format!("\x1b[31m{}\x1b[0m", s) }
# generate documentations at ./target/doc/ cargo doc
 

Notes

 

Projects

Candle
 

References