Nuggets of Rust
Submitted by Jitesh Doshi
on Tue, 02/28/2023 - 09:02
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;
- loops
// 0..10 is just a case of something that implements std::iter::IntoIterator
for i in 0..10 {
// i iterates from 0 to 9
}
let mut xs = vec![1,2,3];
while let Some(x) = xs.pop() {
// use x
}
#[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 bringsVec
,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 ... Think of it like athrows
clause from Java where you're passing the buck to the caller by declaring what kind of error this function could produce.
fn foo() -> Result<i32, std::str::Utf8Error> {
let x = some_fn_that_could_err()?;
Ok(x)
}