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.
37. Block labels
Implicit returns from blocks aren’t always flexible enough to describe the desired logic. Separate functions aside, the problem can be resolved inline by introducing a block label.
let my_nonzero_u24: Option<u32> = 'extract: {
if buf.len() < 3 {
break 'extract None;
}
let mut bytes = [0u8; 4];
bytes[1..].copy_from_slice(&buf[..3]);
let value = u32::from_be_bytes(bytes);
if value == 0 {
None
} else {
Some(value)
}
};
Note: Loops can also be annotated with labels.
36. Specify the type of self
in method signatures
While the three most common types of the self
parameter have useful shorthands (self
, &self
, &mut self
), the explicit syntax can also include standard library types that deref to Self
, including Box<T>
, Rc<T>
, Arc<T>
.
use std::sync::Arc;
struct MyStruct;
impl MyStruct {
fn only_when_wrapped(self: &Arc<Self>) {
println!("I'm in an Arc!");
}
}
let s = MyStruct;
// let s = Arc::new(s); // Try uncommenting this line.
s.only_when_wrapped(); // ERROR!
Without wrapping the value in an Arc
, the Rust compiler produces this error:
error[E0599]: no method named `only_when_wrapped` found for struct `MyStruct` in the current scope
--> src/main.rs:14:3
|
4 | struct MyStruct;
| --------------- method `only_when_wrapped` not found for this struct
...
7 | fn only_when_wrapped(self: &Arc<Self>) {
| ----------------- the method is available for `Arc<MyStruct>` here
...
14 | s.only_when_wrapped();
| ^^^^^^^^^^^^^^^^^ method not found in `MyStruct`
|
help: consider wrapping the receiver expression with the appropriate type
|
14 | Arc::new(s).only_when_wrapped();
| +++++++++ +
The arbitrary_self_types
feature allows types to implement a new trait std::ops::Receiver
and appear in the type declaration of self
. (tracking issue #44874)
35. Force compilation failure
The compile_error!(...)
macro forces compilation failure. This can be useful to indicate unsupported feature flag combinations or invalid macro arguments.
compile_error!("Boo!");
fn main() {
println!("Hello, world!");
}
Compiler output:
Compiling playground v0.0.1 (/playground)
error: Boo!
--> src/main.rs:1:1
|
1 | compile_error!("Boo!");
| ^^^^^^^^^^^^^^^^^^^^^^
error: could not compile `playground` (bin "playground") due to 1 previous error
Bonus tip #1: Emit compiler warnings using build.rs
:
fn main() {
#[cfg(feature = "funky-time")]
println!("cargo::warning=Funky mode is enabled!");
}
Bonus tip #2: Emit structured diagnostics from proc macros using the nightly Diagnostic
API. (tracking issue #54140)
34. Enable optional dependency features with a feature
Use the ?
syntax in Cargo.toml
to activate features on optional dependencies only when those dependencies are enabled.
[dependencies]
backend-a = { version = "1", optional = true }
backend-b = { version = "1", optional = true }
[features]
default = ["backend-a"]
unstable = ["backend-a?/unstable", "backend-b?/unstable"]
# Enabling the "unstable" feature won't implicitly enable either backend.
33. Use tuple struct initializers as function pointers
Tuple struct initializers can be cast to function pointers. This can help to avoid creating unnecessary lambda functions, e.g. when calling Iterator::map
.
#[derive(Debug, PartialEq, Eq)]
struct Point(i32, i32);
fn zeroes<T>(f: fn(i32, i32) -> T) -> T {
f(0, 0)
}
assert_eq!(zeroes(Point), Point(0, 0));
32. Absolute import paths
Use a leading double colon (::path
) to indicate a path relative to the root of the compilation unit. This can help to avoid name collisions when writing macros.
#[cfg(ignore)] // Remove to cause a name collision.
pub mod std {
pub mod cmp {
pub enum Ordering {
Equal,
}
impl Ordering {
pub fn is_eq(self) -> bool {
panic!("Bamboozled!");
}
}
}
}
macro_rules! create_function {
() => {
// Try replacing the next line with the one below it.
fn print_is_equal(o: std::cmp::Ordering) {
// fn print_is_equal(o: ::std::cmp::Ordering) {
println!("is equal? {}", o.is_eq());
}
}
}
create_function!();
print_is_equal(std::cmp::Ordering::Equal);
31. Use indirection in enums to save memory
All variants of an enum are the same size. This can become a problem when variants have drastically different memory requirements. Use indirection (&
-reference, Box
, etc.) to resolve.
enum WithoutIndirection {
Unit,
Kilobyte([u8; 1024]),
}
enum WithIndirection<'a> {
Unit,
Kilobyte(&'a [u8; 1024]),
}
println!("{}", std::mem::size_of_val(&WithoutIndirection::Unit));
// => 1025
println!("{}", std::mem::size_of_val(&WithIndirection::Unit));
// => 8
30. Create a slice from a reference without copying
If you have a reference to some data and you want to pass it to a function that takes a slice, you can use std::array::from_ref
to cheaply create a slice without copying.
struct Thing;
fn takes_slice(_: &[Thing]) {
// ...
}
fn my_function(arg: &Thing) {
takes_slice(std::array::from_ref(arg));
}
fn main() {
my_function(&Thing);
}
29. Include README in documentation
Insert the README into a crate’s documentation by putting #![doc = include_str!("../README.md")]
in lib.rs
. This can help avoid duplicating information between the README and the documentation, and will also test README examples as doctests.
// lib.rs
#![doc = include_str!("../README.md")]
Docs
Documentation from a crate that does this
28. Workspace dependencies
Easily specify a uniform dependency version for all crates in a workspace with the [workspace.dependencies]
table in Cargo.toml
.
# Cargo.toml
[workspace]
members = ["my_crate"]
[workspace.dependencies]
serde = "1.0.183"
# my_crate/Cargo.toml
[dependencies]
serde.workspace = true # -> 1.0.183
27. Testing for compilation failure
Create tests intended to fail compilation with the compile_fail
attribute on documentation tests.
/// ```compile_fail
/// my_function("hello");
/// ```
pub fn my_function(value: u8) {}
The compilation error is error[E0308]: mismatched types
, which you can check for specifically:
/// ```compile_fail,E0308
/// my_function("hello");
/// ```
pub fn my_function(value: u8) {}
26. Sealed traits
If a public trait requires the implementation of a private trait, the public trait is “sealed” and can only be implemented within the crate that defines it.
// my_crate/src/lib.rs
mod private {
pub trait Sealed {}
impl Sealed for u8 {}
}
pub trait PublicTrait: private::Sealed {}
impl PublicTrait for u8 {}
// another_crate/src/lib.rs
use my_crate::PublicTrait;
// error: requires private::Sealed implementation
impl PublicTrait for String {}
// error: my_crate::private::Sealed is private
impl my_crate::private::Sealed for String {}
25. Static type size assertion
Use std::mem::transmute::<S, D>
to assert that two types have the same size at compile time.
use std::mem::transmute;
struct TwoBytes(u8, u8);
let _ = transmute::<TwoBytes, u16>; // ok
let _ = transmute::<TwoBytes, u32>; // error!
The compiler will complain if the types are not the same size:
error[E0512]: cannot transmute between types of different sizes, or dependently-sized types
Warning: Memory layout, alignment, etc. is often not guaranteed, so be careful!
24. Conditional compilation
Use the cfg
and cfg_attr
attributes to compile different code based on the build environment. This is useful for feature-gating, platform-specific code, etc.
// Implements Serialize if the crate is built with the "serde" feature enabled
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
struct Point2(f64, f64);
fn main() {
#[cfg(target_os = "linux")]
let person = "Linus Torvalds";
#[cfg(target_os = "windows")]
let person = "Bill Gates";
#[cfg(target_os = "macos")]
let person = "Tim Apple";
// Approval-seeking!
println!("Hi there, {person}!");
}
23. Declaratively create HashMap
s from iterables
HashMap
s 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();
22. impl
vs. dyn
in assembler
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!
21. Closure traits
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
20. Write better tests with #[should_panic]
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);
}
19. 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"),
}
18. Union types
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!)
17. Struct update operator ..
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());
16. The @
operator
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
15. Create your own iterators
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:
- What collection we’re iterating over.
- 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.
14. Use longer names for lifetimes and generic parameters
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],
}
13. PhantomData
👻
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.
12. String
s are collections
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");
11. Enforce Clippy lints in a workspace
Update: As of Rust 1.74, Clippy lints can now be configured in Cargo.toml
. While the method originally described in this tip is still available, Cargo.toml
is likely a more convenient way to configure lints.
[lints.clippy]
large_digit_groups = "warn"
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.)
10. The never type !
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 documentationInfallible
documentation
Nothing in Rust
9. Recursive declarative macros
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));
8. Rustdoc link shorthand
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.
7. More precise access control
Specify item visibility with the pub(restricted)
syntax:
Syntax | Visible in… |
---|---|
pub(crate) | current crate |
pub(super) | parent module |
pub(self) | current module |
pub(in <path>) | ancestor module |
6. Reuse code in build.rs
with #[path]
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
5. Reduce unnecessary allocations with cows 🐮
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());
4. Additional trait bounds per-function
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
}
}
3. Use the AsRef
trait to convert references
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]);
2. Use syn
and quote
when writing procedural macros
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.
1. Use the dbg!(...)
macro for better debugging
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));
Jacob Lindahl is a graduate student at the Tokyo Institute of Technology.