Rust Pro Tips (collection)

Level up your Rust skills.

This is a collection of Rust “pro tips” that I’ve collected, most of which have been posted on Twitter. I’ll keep updating this post as I write more. Tips are ordered in reverse chronological order, with the most recent ones at the top.

Declaratively create HashMaps from iterables

Tweet

HashMaps can be built from iterators of key-value tuples:

use std::collections::HashMap;

let map: HashMap<&str, i32> = [
    ("a", 1),
    ("b", 2),
    ("c", 3),
    ("d", 4),
]
.into();

Playground
Docs

impl vs. dyn in assembler

Tweet

Use impl for generic monomorphization, dyn for dynamic dispatch.

fn with_impl<'a>(v: &'a impl AsRef<[u8]>) -> &'a str {
    std::str::from_utf8(v.as_ref()).unwrap()
}

fn with_dyn<'a>(v: &'a dyn AsRef<[u8]>) -> &'a str {
    std::str::from_utf8(v.as_ref()).unwrap()
}

pub fn main() {
    let array = [72, 101, 108, 108, 111];
    let vector = vec![72, 101, 108, 108, 111];
    assert_eq!("Hello", with_impl(&array));
    assert_eq!("Hello", with_dyn(&array));
    assert_eq!("Hello", with_impl(&vector));
    assert_eq!("Hello", with_dyn(&vector));
}

See the effect in the generated assembly. with_impl is generated once for each parameterization. with_dyn is generated once, but a vtable is required at runtime.

; with_impl(&[u8; 5]) implementation (monomorphized)
_ZN7example9with_impl17h38cd8c9aec305c25E:
        sub     rsp, 40
        ; constant lookup
        mov     rax, qword ptr [rip + _ZN4core5array92_$LT$impl$u20$core..convert..AsRef$LT$$u5b$T$u5d$$GT$$u20$for$u20$$u5b$T$u3b$$u20$N$u5d$$GT$6as_ref17h86d8b288e84fcf8aE@GOTPCREL]
        call    rax
        mov     rsi, rax
        ; [[snip]]

; with_impl(&Vec<u8>) implementation (monomorphized)
_ZN7example9with_impl17hd50582b2776df596E:
        sub     rsp, 40
        ; constant lookup
        mov     rax, qword ptr [rip + _ZN88_$LT$alloc..vec..Vec$LT$T$C$A$GT$$u20$as$u20$core..convert..AsRef$LT$$u5b$T$u5d$$GT$$GT$6as_ref17h56f6b2151f0bc49cE@GOTPCREL]
        call    rax
        mov     rsi, rax
        ; [[snip]]

; with_dyn(&dyn AsRef<[u8]>) fat pointer implementation
_ZN7example8with_dyn17h88123f4b1a0b56e4E:
        sub     rsp, 40
        ; get vtable entry
        mov     rax, qword ptr [rsi + 24]
        call    rax
        mov     rsi, rax
        ; [[snip]]

When invoking with_dyn, the computer must construct the vtable at runtime.

        ; [[snip]]
        ; with_dyn(&Vec<u8>) invocation
        ; load the vtable
        lea     rsi, [rip + .L__unnamed_14]
        lea     rdi, [rsp + 176]
        call    _ZN7example8with_dyn17h88123f4b1a0b56e4E
        ; [[snip]]

.L__unnamed_14:
        .quad   _ZN4core3ptr46drop_in_place$LT$alloc..vec..Vec$LT$u8$GT$$GT$17hec70dc68d599b27eE
        .asciz  "\030\000\000\000\000\000\000\000\b\000\000\000\000\000\000"
        .quad   _ZN88_$LT$alloc..vec..Vec$LT$T$C$A$GT$$u20$as$u20$core..convert..AsRef$LT$$u5b$T$u5d$$GT$$GT$6as_ref17h56f6b2151f0bc49cE

Compare this to the invocation of a monomorphized function:

        ; [[snip]]
        ; with_impl(&Vec<u8>) invocation
        lea     rdi, [rsp + 176]
        call    _ZN7example9with_impl17hd50582b2776df596E
        ; [[snip]]

No vtable required!

dyn docs
impl docs

Closure traits

Tweet

Rule of thumb: the more a closure does with captured variables, the fewer traits it implements.

Only reads? Fn + FnMut + FnOnce.
Mutates? FnMut + FnOnce.
Moves? FnOnce.

No captures at all? Function pointer coercion, too!

fn impl_fn_once(_: &impl FnOnce() -> ()) {}
fn impl_fn_mut(_: &impl FnMut() -> ()) {}
fn impl_fn(_: &impl Fn() -> ()) {}
fn fn_pointer(_: fn() -> ()) {}

#[derive(Debug)]
struct Var(i32);

{ // A
    let mut var = Var(0);
    let f = || drop(var);
    impl_fn_once(&f);
    impl_fn_mut(&f);
    impl_fn(&f);
    fn_pointer(f);
}

{ // B
    let f = || println!("{:?}", Var(0));
    impl_fn_once(&f);
    impl_fn_mut(&f);
    impl_fn(&f);
    fn_pointer(f);
}

{ // C
    let mut var = Var(0);
    let f = || println!("{:?}", var);
    impl_fn_once(&f);
    impl_fn_mut(&f);
    impl_fn(&f);
    fn_pointer(f);
}

{ // D
    let mut var = Var(0);
    let f = || var.0 += 1;
    impl_fn_once(&f);
    impl_fn_mut(&f);
    impl_fn(&f);
    fn_pointer(f);
}

Rust Book
Rust Reference
Playground

Write better tests with #[should_panic]

Tweet

Require tests to panic with #[should_panic]. This is useful for testing unhappy paths. Optionally include a substring to match against the panic message.

#[test]
#[should_panic = "attempt to divide by zero"]
fn div_zero() {
    let val = 1i32.div(0);
}

Docs

Use #[non_exhaustive] to prevent breaking changes

Use the #[non_exhaustive] attribute to prevent breaking changes when adding new fields to a struct or enum variant.

#[non_exhaustive]
pub enum Error {
    Io(IoError),
    ParseInt(ParseIntError),
    ParseFloat(ParseFloatError),
}

This doesn’t make much a difference in the crate where the type is defined, but it’s useful for downstream crates (those that depend on your crate), since it prevents breaking changes when new variants are added.

So, this is fine, but only within the crate that defines the type:

match e {
    Error::Io(_) => println!("IO error"),
    Error::ParseInt(_) => println!("Parse int error"),
    Error::ParseFloat(_) => println!("Parse float error"),
}

However, a crate that depends on your crate must include a wildcard pattern to handle new variants:

match e {
    Error::Io(_) => println!("IO error"),
    Error::ParseInt(_) => println!("Parse int error"),
    Error::ParseFloat(_) => println!("Parse float error"),
    _ => println!("Other error"),
}

Docs

Union types

Tweet

One of Rust’s lesser-known composite types is the union. While they might look like structs at first, each field is actually the same piece of memory, allowing you to reinterpret bytes as a different type. Of course, this requires unsafe code.

union Onion {
    uint: u32,
    tuple: (u16, u16),
    list: [u8; 4],
}

let mut o = Onion { uint: 0xAD0000 };

unsafe {
    o.tuple.0 = 0xBEEF;
    o.list[3] = 0xDE;
}

assert_eq!(unsafe { o.uint }, 0xDEADBEEF);

(Extra pro tip: you can do pattern matching on unions too!)

Playground
Docs

Struct update operator ..

Tweet

Use the struct update operator .. to easily copy a struct with a few minor modifications. This can be useful when a struct implements Default.

#[derive(Default)]
struct Post {
    title: String,
    body: String,
    image_url: Option<String>,
    view_count: u32,
    tags: Vec<String>,
}

let p = Post {
    title: "How to make $1M posting coding tips on Twitter".into(),
    body: "1. Post coding tips on Twitter.\n2. ???\n3. Profit!".into(),
    ..Default::default()
};

assert_eq!(p.image_url, None);
assert_eq!(p.view_count, 0);
assert_eq!(p.tags, Vec::<String>::new());

Playground

The @ operator

Tweet

Use the @ operator to bind an identifier to a value that matches a pattern.

struct User {
    age: u8,
    name: String,
}

let u = User { age: 25, name: "John".into() };

if let User { age: a @ ..=35, .. } = u {
    println!("Become POTUS in {} years", 35 - a);
}

Playground
Documentation
Rust by Example

Create your own iterators

Tweet

Rust’s Iterator trait is super useful! Here’s how to implement it:

We’ll start with a data structure we want to iterate over:

struct Pair<T> {
    a: T,
    b: T,
}

And a function that returns the iterator:

impl<T> Pair<T> {
    pub fn iter(&self) -> PairIter<T> {
        // ...
    }
}

Here’s our iterator struct. It keeps track of:

  1. What collection we’re iterating over.
  2. The current location within the collection.
struct PairIter<'a, T> {
    pair: &'a Pair<T>,
    next: u8,
}

Let’s implement Iterator. Note that the type of the items emitted by the iterator is defined by an associated type.

impl<'a, T> Iterator for PairIter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        // ...
    }
}

The next() function simply needs to update the iterator and return the current item.

match self.next {
    0 => {
        self.next += 1;
        Some(&self.pair.a)
    }
    1 => {
        self.next += 1;
        Some(&self.pair.b)
    }
    _ => None,
}

And that’s it! Now you can easily implement Iterator on your own types.

Playground
Docs

Use longer names for lifetimes and generic parameters

Tweet

Lifetimes and generic parameters don’t have to be just one character long. When you’re deep in the weeds, working your Rust magic, keep your code comprehensible.

struct View<'source, Element: Deserialize> {
    label: &'source Element,
    value: &'source [u8],
}

PhantomData 👻

Tweet

PhantomData has a dead-simple definition, but fascinating use-cases. PhantomData makes it look like your type contains another type even if it really doesn’t. It’s zero-sized, so it costs you nothing to use!

PhantomData can be useful when interfacing with external resources:

struct ExternalResource<T> {
    _marker: PhantomData<T>,
}

impl<T> ExternalResource<T> {
    fn fetch(&self) -> T { /* ... */ }
    fn update(&self, v: T) { /* ... */ }
}

If you’re working with FFI or otherwise need to manually restrict lifetimes:

struct FP<'a>(usize, *const u8, PhantomData<&'a ()>);

impl<'a> From<&'a Vec<u8>> for FP<'a> {
    fn from(v: &'a Vec<u8>) -> Self {
        FP(v.len(), v.as_ptr(), PhantomData)
    }
}

Playground
More in Nomicon.

Strings are collections

Tweet

A String is a collection that can be built from an iterator:

let my_string = vec![1, 2, 3, 4]
    .iter()
    .map(|i| (i * 2).to_string())
    .collect::<String>();

assert_eq!(my_string, "2468");

Enforce Clippy lints in a workspace

Tweet

Enforce a consistent set of Clippy lints across multiple crates in one workspace by adding to .cargo/config.toml at the project root.

[target.'cfg(all())']
rustflags = [
  "-Wclippy::large_digit_groups",
]

(Otherwise you’d need to have a #![warn(...)] directive in every crate.)

The never type !

Tweet

The never type in Rust (denoted by !) represents a type that will never exist. Called the “bottom type” in type theory, it’s used as the type of expressions that never resolve (e.g. panics or infinite loops) or the type of a return or break statement.

The never type can be coerced to any type. For example:

let x: u32 = panic!();

! type documentation
Standard library documentation
Infallible documentation
Nothing in Rust

Recursive declarative macros

Tweet

Build quick-and-dirty parsers with recursive declarative macros.

macro_rules! m {
    (y) => { true };
    (n) => { false };
    ($a:tt xor $($b:tt)+) => { m!($a) != m!($($b)+) };
}

assert!(m!(y xor n xor n));

Tweet

Link to a module, struct, or enum directly in rustdoc comments with the shorthand syntax:

/// Link to [`MyStruct`]

Works for external crates too!

/// Link to [`serde::Serialize`]

(Links to https://docs.rs/serde/latest/serde/ser/trait.Serialize.html.) Backticks are optional, they just format it nicely.

More precise access control

Tweet

Specify item visibility with the pub(restricted) syntax:

SyntaxVisible in…
pub(crate)current crate
pub(super)parent module
pub(self)current module
pub(in <path>)ancestor module

Docs

Reuse code in build.rs with #[path]

Tweet

Easily reuse code from your project in the build.rs file by using the #[path] directive:

#[path = "./src/parse.rs"]
mod parse;

fn main() {
    parse::om_nom(/* ... */);
}

Feature flags work in build.rs too!

#[cfg(feature = "parser")]
let feature_gated = /* ... */;

build.rs documentation
The #[path] attribute

Reduce unnecessary allocations with cows 🐮

Tweet

The clone-on-write smart pointer std::borrow::Cow creates owned values only when necessary. It’s useful when you want to work with both owned and borrowed values, and it can help you dynamically prevent unnecessary allocations.

fn f(v: &mut Cow<str>, m: bool) {
    if m {
        v.to_mut().push_str(", world");
    }
}

let mut v: Cow<str> = "hi".into();
f(&mut v, false);
assert!(v.is_borrowed()); // note: is_borrowed() is only available on nightly
f(&mut v, true);
assert!(v.is_owned());

Playground

Additional trait bounds per-function

Tweet

Did you know you can add additional trait bounds to a type parameter for a single function when writing traits or impl blocks?

trait MyTrait<T: Debug> {
    fn action(&self) {
        // Implemented when T is Debug
    }

    fn print(&self) where T: Display {
        // Only implemented when T is Debug + Display
    }
}

Playground

Use the AsRef trait to convert references

Tweet

Vec<T>? 😢
AsRef<[T]>? 😄

fn sum_is_even(v: impl AsRef<[u32]>) -> bool {
    v.as_ref().iter().fold(true, |e, i| e == (i & 1 == 0))
}

// both work!
sum_is_even(vec![0, 2, 4]));
sum_is_even([1]);

Playground
Docs

Use syn and quote when writing procedural macros

Tweet

The crates syn and quote make parsing and generating streams of Rust tokens a breeze—super handy when writing procedural macros!

More about writing procedural macros in Rust.

Use the dbg!(...) macro for better debugging

Tweet

Still using println!() debugging in Rust?

Use dbg!() instead. It prints the filename, line number, expression, and value and returns the value it was given. It’s in the standard library, and it’s shorter than println!().

println!("{value:?}");
my_function(&value);

vs.

my_function(dbg!(&value));

I’m a software engineer for NEAR Protocol, head of education at the Blockchain Acceleration Foundation, and a graduate student at the Tokyo Institute of Technology.

Connect with me on Twitter.