I learned Rust long time back in a haphazard manner, and then I keep getting out of touch when I haven’t had a chance to use it continuously. So here I have put together a quick cheat sheet of things to remember.
It might help someone either refresh their Rust knowledge, or learn the basics for the first time.
Special thanks to https://fasterthanli.me/articles/a-half-hour-to-learn-rust for putting together such a nice concise list. Many of the nuggets below are adapted from there.
Resources
- https://doc.rust-lang.org/book/
- https://doc.rust-lang.org/rust-by-example/
- https://www.rust-lang.org/
- https://docs.rs/
- https://this-week-in-rust.org/
- https://readrust.net/
Videos
- No Boilerplate - playlist
- The Rust Lang Book - playlist
- Rust Tutorial by Derek Banas - 2.5 hrs
- Brad Traversy - 2 hrs
Channels
- https://www.youtube.com/@codetothemoon
- https://www.youtube.com/@JeremyChone
- https://www.youtube.com/@chrisbiscardi
- https://www.youtube.com/@letsgetrusty
- https://www.youtube.com/@RyanLevicksVideos
- https://www.youtube.com/@rusting-inc
- https://www.youtube.com/@dailydoseofrustlang
- https://www.youtube.com/@HelloRust
- https://www.youtube.com/@rustbeltrust
- https://www.youtube.com/@rustlabconference3671
- Search Query: Rust Programming channels on YouTube
Nuggets
Nuggets (tidbits) of useful knowledge gathered.
- create project with
cargo new myproject
myproject/
├── Cargo.toml
└── src
└── main.rs
- Two types of strings
String
owned, growable string (is it always on the heap?)&str
borrowed primitive string slice ref- There is a
Deref
trait that allows String refs to be borrwed into&str
…
let s1: String = String::from("hi");
let s2: &str = &s1;
#[cfg(test)]
mod tests {
#[test]
fn mytestfn() {
let result = ...;
assert_eq!(result, expected)
}
}
- There’s no ternary operator. Use
if/else
with tail expressions
let x = if(age >= 18) { "adult" } else { "minor" }
Or a match
…
match {
age < 18 => "minor",
age < 65 => "adult",
age >= 65 => "senior",
}
- Units of code
- crates are like libraries (modules in Go)
- modules are like source files/file-groups (packages in Go)
use
statement brings stuff into scope
use std::cmp::min;
let least = min(7, 1); // this is 1
- curly braces can contain “glob patterns”
use std::cmp::{min, max};
use std::cmp::*; // bring everything into scope
- methods that take
self
can be called as methods or functions
let l1 = "foo".len();
let l2 = str::len("foo");
- Rust implicitly includes the following at the start of every module,
which brings
Vec
,String
,Option
,Result
, etc. into scope.
use std::prelude::v1::*;
- Structs …
struct Vec2 {
x: f64, // 64-bit floating point, aka "double precision"
y: f64,
}
let v1 = Vec2 { x: 1.0, y: 3.0 };
let v2 = Vec2 { y: 2.0, x: 4.0 };
let v3 = Vec2 {
x: 14.0,
..v2 // spread .. must be last, and does not override previous fields
};
let v4 = Vec2 { ..v3 }; // spread .. all fields
let Vec2 {x, y} = v2; // destructuring: x = 2.0, y = 4.0
let Vec2 {x, ..} = v2; // destructuring: discard y
let v5 = Vec2{x, y:6} // same as Vec2{x:x, y:6}
- Methods … can be implemented only on your types
impl Vec2 {
fn diag(&self) -> f64 {
f64::sqrt(self.x * self.x + self.y * self.y)
}
}
- Traits … can be implemented on anyone’s types (as long as the trait or the type is yours)
trait Signed {
fn is_strictly_negative(self) -> bool;
}
// ok, even if Number is not your type
impl Signed for Number {
// self if of type that is the target of impl-for
fn is_strictly_negative(self) -> bool {
self.value < 0
}
}
// even a primitive type
impl Signed for i32 {
fn is_strictly_negative(self) -> bool {
self < 0
}
}
- marker traits (e.g.
Copy
) have no methods, but they indicate that a type has properties (e.g. copy-itself).
let a:i32 = 5;
let b = a; // copied!
let c: Number = ...;
let d = c; // moved!
- Some traits can be implemented with
#[derive]
attribute.
#[derive(Clone, Copy)]
struct Number {
odd: bool,
value: i32,
}
- Generics …
// trait Display is a constraint of T
fn print<T: Display>(value: T) {
println!("value = {}", value);
}
// trait Debug is a constraint of T
fn print<T: Debug>(value: T) {
println!("value = {:?}", value);
}
fn print<T>(value: T)
where
T: Display + Debug, // multiple constraints
{
// ...
}
- compile time reflection? (
::<>
is called turbofish syntax)
fn main() {
use std::any::type_name;
println!("{}", type_name::<i32>()); // prints "i32"
println!("{}", type_name::<(f64, char)>()); // prints "(f64, char)"
}
Option
is an enum, with two variants. So isResult
. Both can beunwrap
d. But better yet, useexpect
to fail with a custom message.
enum Option<T> {
None,
Some(T),
}
fn main() {
let o1: Option<i32> = Some(128);
o1.unwrap(); // this is fine
let o2: Option<i32> = None;
o2.unwrap(); // this panics!
}
enum Result<T, E> {
Ok(T),
Err(E),
}
- Function reference parameter lifetimes … Named lifetimes allow returning references whose lifetime depend on the lifetime of the arguments. There is a special lifetime, named
'static
, which is valid for the entire program’s lifetime.
// elided (non-named) lifetimes:
fn print(x: &i32) {}
// named lifetimes:
fn print<'a>(x: &'a i32) -> &'a i32{}
// not really needed in this case, since this is default.
- Structs can also be generic over lifetimes, which allows them to hold references
struct NumRef<'a> {
x: &'a i32,
}
fn main() {
let x: i32 = 99;
let x_ref = NumRef { x: &x };
// `x_ref` cannot outlive `x`, etc.
}
- For many types in Rust, there are owned and non-owned variants:
- Strings:
String
is owned,&str
is a reference - Paths:
PathBuf
is owned,&Path
is a reference - Collections:
Vec<T>
is owned,&[T]
is a reference
- Strings:
Pearls
Pearls (real gems) of wisdom gathered.
- If you want to do some complex computation and use the result, you don’t have to write a function or IIFE, etc. Just use the “tail” of a block.
let x = {
let y = 1; // first statement
let z = 2; // second statement
y + z // this is the *tail* - what the whole block will evaluate to
};
let
(destructuring) patterns can be used as conditions inif
. And so canmatch
arms (though amatch
has to be exhaustive; use_
as the catch-all arm):
if let Number { odd: true, value } = n {
println!("Odd number: {}", value);
} else if let Number { odd: false, value } = n {
println!("Even number: {}", value);
}
match n {
Number { odd: true, value } => println!("Odd number: {}", value),
Number { odd: false, value } => println!("Even number: {}", value),
}
- The generic part of a type can be inferred from usage …
fn main() {
let mut v1 = Vec::new();
v1.push(1); // v1 is now Vec<i32>
let mut v2 = Vec::new();
v2.push(false); // v2 is now Vec<bool>
}
- While borrowed, a variable binding cannot be mutated:
fn main() {
let mut x = 42;
let x_ref = &x;
x = 13;
println!("x_ref = {}", x_ref);
// error: cannot assign to `x` because it is borrowed
}
- While immutably borrowed, a variable cannot be mutably borrowed:
fn main() {
let mut x = 42;
let x_ref1 = &x;
let x_ref2 = &mut x;
// error: cannot borrow `x` as mutable because it is also borrowed as immutable
println!("x_ref1 = {}", x_ref1);
}
- Use
?
to forward error …
fn foo() -> Result<i32, std::str::Utf8Error> {
let x = some_fn_that_could_err()?;
Ok(x)
}