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 JupyterAlso check the notebook in my own repo!
myRustPlayground
AgainstEntropy • Updated Feb 4, 2024
TOC
Rust Language and Its EcologyRustCargoBasic syntax with code snippetsType and ValuesType InferenceFunctionControl FlowLoopsbreak and continueBlocks and ScopesFunctionsTuples and ArraysArray IterationPattern MatchingDestructuringReferencesShared ReferencesExclusive referencesref keywordas_ref and as_mut User-defined TypesStructsEnumStatic and ConstType AliasesPattern matchingDestructuring Structs and EnumsLet Control FlowMethods and TraitsMethodsTraitsDerivingTrait ObjectsGenericGeneric FunctionsGeneric Data TypesTrait Boundsimpl TraitStandard Library TypesOptionResultStringVecHashMapStandard Library TraitsComparisonsOperatorsFrom and IntoCastingRead and WriteDefaultClosuresMemory ManagementOwnershipMove SemanticsCloneCopyDropSmart PointersBoxRCBorrowingBorrow a ValueBorrow CheckingInterior Mutability | RefCell | CellSlices and LifetimesSlicesString ReferencesLifetime AnnotationsLifetimes in Function Calls | Lifetimes ElisionLifetimes in Data Structures | Struct LifetimesIteratorsIteratorIntoIteratorFromIteratorTestDoctestUnit testDocumentationNotesProjectsReferences
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}"); }
fn main() { for x in 1..5 { println!("x: {x}"); } }
1..5
is a range that yields 1, 2, 3, 4
use 1..=5
syntax for an inclusive rangefn main() { let mut i = 0; loop { i += 1; println!("{i}"); if i > 100 { break; } } }
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 loopsNote 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” formatfn 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
traitSome 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 Run another example but without
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); }
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); }
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 variantsNote 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"), } }
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}"), } }
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); }
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)); }
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}"); } }
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 See memory layout after allocating
Vec<dyn Pet>
in the example above. See memory layout after allocating pets
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 Now
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 typessuch as
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 itimpl 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!
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 reflexivePartialOrd 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 e.g.
Add
for two different typese.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:?}");
A
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
implementsfn 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 testDocumentation
/// 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
CandleReferences
- A 3-day Rust tutorial from Google Android team
- The Rust Programming Language Tutorial from official Rust team
- The Rust RFC Book