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.
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)
}
};
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 compile_error!(...) macro forces compilation failure. This can be useful to indicate unsupported feature flag combinations or invalid macro arguments.
Tuple struct initializers can be cast to function pointers. This can help to avoid creating unnecessary lambda functions, e.g. when calling Iterator::map.
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);
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.
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.
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.
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 {}
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}!");
}
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.
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);
}
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"),
}
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.
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);
}
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.
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:
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.
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:
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());
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
}
}
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!().