DEV Community

Nobuo Miura
Nobuo Miura

Posted on Originally published at docs.nobuo-miura.dev

Rust CheatSheet

This cheat sheet is based on Rust 1.98 and the Rust 2024 Edition.

Table of Contents


Installation and updates

Official Rust installation guide

The recommended way to manage a Rust toolchain is with the official rustup tool. On macOS, Linux, WSL, and other Unix-like systems, run the command published on the official website and follow the prompts:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Enter fullscreen mode Exit fullscreen mode

On Windows, download and run rustup-init.exe from the official installation page. Depending on your environment, you may also need the Visual Studio C++ Build Tools.

Restart your terminal after installation, then check the installed versions:

rustc --version
cargo --version
rustup --version
Enter fullscreen mode Exit fullscreen mode

Update the stable toolchain and install the formatter and linter with:

rustup update stable
rustup component add rustfmt clippy
Enter fullscreen mode Exit fullscreen mode

If an existing project contains rust-toolchain.toml or rust-toolchain, follow that project-level toolchain declaration. Libraries can declare their minimum supported Rust version (MSRV) with rust-version in Cargo.toml.


Program structure

fn main() {
    println!("Hello, Rust 1.98!");
}
Enter fullscreen mode Exit fullscreen mode
  • An executable starts at the main function.
  • Statements usually end with ;.
  • A name ending in !, such as println!, is a macro invocation.

Comments

Syntax Purpose
// ... Regular comment through the end of the line
/* ... */ Regular block comment; block comments can be nested
/// ..., /** ... */ Outer documentation comment for the following item
//! ..., /*! ... */ Inner documentation comment for the containing crate or module
// A line comment
let value = 42;

/* A block comment can
   span multiple lines. */
assert_eq!(value, 42);
Enter fullscreen mode Exit fullscreen mode

Documentation comments are parsed as Markdown and included in the API documentation generated by cargo doc. Their code blocks are normally run as documentation tests by cargo test.

mod math {
    //! Basic arithmetic helpers.

    /// Adds two integers.
    ///
    /// # Examples
    ///
    /// ```
{% endraw %}

    /// assert_eq!(2 + 3, 5);
    ///
{% raw %}
Enter fullscreen mode Exit fullscreen mode
pub fn add(left: i32, right: i32) -> i32 {
    left + right
}
Enter fullscreen mode Exit fullscreen mode

}

assert_eq!(math::add(2, 3), 5);




Create and run a Cargo project with:



```bash
cargo new hello-rust
cd hello-rust
cargo run
Enter fullscreen mode Exit fullscreen mode

New Cargo projects use edition = "2024" in Cargo.toml.

Keywords

The Rust Reference: Keywords

Rust divides keywords into strict keywords, reserved keywords kept for future language features, and weak keywords that are special only in specific contexts.

Strict keywords

Keyword Primary use
_ Ignore a value in a pattern, or ask the compiler to infer a type or value
as Primitive casts, aliases in use, and qualified paths
async Define a function, block, or closure that produces a Future
await Suspend the current async computation until a Future is ready
break Exit a loop, optionally returning a value
const Declare compile-time constants, const functions, blocks, or generics
continue Skip the rest of the current loop iteration
crate Refer to the current crate or its root
dyn Form a dynamically dispatched trait object
else Handle the branch where an if or if let condition does not match
enum Define a type with multiple variants
extern Work with external ABIs, functions, statics, or crates
false The false bool literal
fn Define a function or function-pointer type
for Iterate over a value or implement a trait for a type
if Branch on a Boolean condition
impl Define inherent methods or implement a trait
in Specify the value iterated by a for loop
let Bind a value to a pattern
loop Repeat until explicitly stopped
match Perform exhaustive pattern matching
mod Declare or define a module
move Move captured values into a closure or async block
mut Make a binding, reference, or pattern mutable
pub Expose an item outside its default visibility boundary
ref Bind by reference inside a pattern
return Return from a function early
self Refer to a method receiver or the current module
Self Refer to the type currently being defined or implemented
static Declare an item with a fixed storage location
struct Define a named-field, tuple, or unit struct
super Refer to the parent module
trait Define shared behavior for types
true The true bool literal
type Define a type alias or associated type
unsafe Mark operations or contracts the compiler cannot fully verify
use Bring a path into scope or re-export an item
where Write generic and lifetime bounds
while Repeat while a Boolean condition remains true

Reserved keywords

abstract, become, box, do, final, gen, macro, override, priv, try, typeof, unsized, virtual, yield

These are reserved for possible future language features. In Rust 1.98, they have no keyword behavior and cannot be used as ordinary identifiers. In particular, typeof is reserved but is not implemented as a type-inspection operator.

Weak keywords

Keyword Context in which it is special
'static A lifetime valid for the entire program
macro_rules Declare a declarative macro
raw Create raw pointers with &raw const or &raw mut
safe Mark a function or static as safe inside an unsafe extern block
union Define a type whose fields share storage

When an identifier must match a keyword, a raw identifier such as r#type can be used where the grammar permits it.


Variables, constants, and primitive types

Variable declarations

Bindings are immutable by default. Add mut when the value must change.

let name = "Ferris";       // Inferred as &str.
let age: u32 = 10;         // Explicit type.
let mut count = 0;         // Mutable binding.
count += 1;

// Destructure a value with a pattern.
let (x, y) = (10, 20);

// A declaration may omit its initializer if it is definitely initialized before use.
let status: String;
status = String::from("ready");
println!("{status}");
Enter fullscreen mode Exit fullscreen mode

Shadowing

let can declare a new binding with the same name. Unlike mutation, shadowing may also change the type.

let spaces = "   ";
let spaces = spaces.len(); // &str to usize
Enter fullscreen mode Exit fullscreen mode

Constants and statics

const MAX_RETRIES: u32 = 3;
static APPLICATION_NAME: &str = "example";
Enter fullscreen mode Exit fullscreen mode
  • A const is evaluated at compile time and requires an explicit type.
  • A static has a fixed memory address for the duration of the program.
  • Accessing mutable static mut state requires unsafe; prefer synchronization primitives where appropriate.

An atomic type works well for a simple counter shared across threads:

use std::sync::atomic::{AtomicU64, Ordering};

static REQUEST_COUNT: AtomicU64 = AtomicU64::new(0);

fn next_request_count() -> u64 {
    REQUEST_COUNT.fetch_add(1, Ordering::Relaxed) + 1
}

assert_eq!(next_request_count(), 1);
assert_eq!(next_request_count(), 2);
Enter fullscreen mode Exit fullscreen mode

Relaxed is appropriate here because only the atomicity of the counter matters; this counter does not synchronize the ordering of other memory accesses. Choose an ordering or a lock that matches the actual synchronization requirements.

Values and literals

Literals are values written directly in source code. Numeric literals may have a type suffix, and _ can be used as a visual separator.

let integer = 42;              // i32
let unsigned = 42_u64;         // u64
let floating = 3.14_f32;       // f32
let enabled = true;            // bool
let character = 'R';           // char
let text = "Rust";             // &str
let byte = b'R';               // u8
let bytes = b"Rust";           // &[u8; 4]
let raw = r#"A raw string does not escape \"#;
let million = 1_000_000;

assert_eq!(integer, 42);
assert_eq!(bytes, &[82, 117, 115, 116]);
assert!(raw.contains('\\'));
assert_eq!(million, 1_000_000);
Enter fullscreen mode Exit fullscreen mode

Values can also be produced by calls such as String::from("Rust") and by expressions such as left + right. Many Rust constructs are expressions: blocks, if, match, and loop can all produce values.

Scalar types

Category Types Notes
Signed integers i8, i16, i32, i64, i128, isize Integer literals default to i32
Unsigned integers u8, u16, u32, u64, u128, usize usize is commonly used for sizes and indexes
Floating point f32, f64 Defaults to f64
Boolean bool true or false
Character char A four-byte Unicode scalar value
Unit () Represents the absence of another meaningful value
let decimal = 98_222;
let hex = 0xff;
let octal = 0o77;
let binary = 0b1111_0000;
let byte = b'A';
let ratio = 3.5_f64;
let enabled = true;
let crab = '🦀';
Enter fullscreen mode Exit fullscreen mode

Debug builds detect integer overflow at runtime. Use explicit methods when wraparound or saturation is intentional.

let value = u8::MAX;
assert_eq!(value.wrapping_add(1), 0);
assert_eq!(value.saturating_add(1), u8::MAX);
assert_eq!(value.checked_add(1), None);
Enter fullscreen mode Exit fullscreen mode

Compound types

// A tuple can contain values of different types.
let user = ("Ferris", 10, true);
let (name, age, active) = user;
println!("{name}: {age}, active={active}");

// An array has a fixed length that is part of its type.
let numbers: [i32; 3] = [10, 20, 30];
let zeros = [0; 5];
println!("{} {}", numbers[0], zeros.len());
Enter fullscreen mode Exit fullscreen mode

Array indexing with [] panics when the index is out of bounds. Use get when absence is possible.

let values = [10, 20, 30];
match values.get(5) {
    Some(value) => println!("{value}"),
    None => println!("out of bounds"),
}
Enter fullscreen mode Exit fullscreen mode

Type conversion

Rust does not perform implicit numeric conversion. Use as, From, or TryFrom explicitly.

let small: u8 = 42;
let large = u32::from(small); // Lossless conversion.

let integer = 42_i32;
let floating = integer as f64;
assert_eq!(floating, 42.0);

let value: i32 = 300;
let byte = u8::try_from(value); // Result<u8, TryFromIntError>
assert!(byte.is_err());

assert_eq!(char::from_u32(65), Some('A'));
Enter fullscreen mode Exit fullscreen mode

as can truncate a value. Prefer TryFrom when the input range is not guaranteed.

let value = 300_u16;
assert_eq!(value as u8, 44);
assert!(u8::try_from(value).is_err());
Enter fullscreen mode Exit fullscreen mode

Type aliases and newtypes

A type alias gives an existing type another name; it does not create a distinct type.

type UserId = u64;
type IoResult<T> = Result<T, std::io::Error>;

fn load_user(id: UserId) -> IoResult<String> {
    Ok(format!("user-{id}"))
}

let id: UserId = 42;
let raw_id: u64 = id;
assert_eq!(load_user(raw_id).unwrap(), "user-42");
Enter fullscreen mode Exit fullscreen mode

Use the newtype pattern when values must not be mixed accidentally.

struct UserId(u64);
struct ProductId(u64);

fn find_user(id: UserId) {
    println!("user={}", id.0);
}

find_user(UserId(42));
// find_user(ProductId(42)); // Different type: does not compile.
Enter fullscreen mode Exit fullscreen mode

Inspecting a type

Rust has no built-in typeof operator. Use std::any::type_name::<T>() for a known type or std::any::type_name_of_val for a value.

fn type_of<T: ?Sized>(_: &T) -> &'static str {
    std::any::type_name::<T>()
}

let number = 123;
println!("{}", type_of(&number));
println!("{}", std::any::type_name_of_val(&number));
Enter fullscreen mode Exit fullscreen mode

A helper that accepts its argument by value also works:

fn type_of<T>(_: T) -> &'static str {
    std::any::type_name::<T>()
}

let number = 123;
println!("{}", type_of(number));
Enter fullscreen mode Exit fullscreen mode

The by-value version moves non-Copy values such as String, so the borrowed form is usually more convenient. The exact output of type_name is not guaranteed and may change between compiler versions. Use it for diagnostics, not as a stable type identifier or application branching key.

Operators

Category Operators Notes
Arithmetic +, -, *, /, % Integer division discards the fractional part
Unary -, ! Numeric negation; Boolean negation or integer bitwise complement
Comparison ==, !=, <, <=, >, >= Uses traits such as PartialEq and PartialOrd
Logical &&, `\ \
Bitwise {% raw %}&, `\ , ^, <<, >>`
Assignment =, +=, -=, *=, /=, %=, &=, `\ =, ^=, <<=, >>=`
Range .., ..= Exclusive and inclusive upper bounds
Borrow and dereference &, &mut, * Create references and access their targets
Error propagation ? Return early from Result, Option, and similar types
assert_eq!(7 / 2, 3);
assert_eq!(7.0 / 2.0, 3.5);
assert_eq!(-5_i32, 0 - 5);
assert_eq!(!true, false);

let mut value = 5;
value *= 2;
assert_eq!(value, 10);

let mut flags = 0b0011_u8;
flags |= 0b0100;
flags <<= 1;
assert_eq!(flags, 0b1110);

assert!((1..=10).contains(&value));
Enter fullscreen mode Exit fullscreen mode

User-defined types can support operators by implementing standard traits such as Add, PartialEq, and Index. Rust does not allow arbitrary new operators or custom operator precedence.


Ownership, borrowing, and slices

Rust manages memory without a garbage collector by checking ownership rules at compile time.

Ownership

The three core rules are:

  1. Every value has an owner.
  2. A value has only one owner at a time.
  3. The value is dropped when its owner leaves scope.
let original = String::from("hello");
let moved = original; // Ownership moves to moved.

println!("{moved}");
// println!("{original}"); // Error: original was moved.
Enter fullscreen mode Exit fullscreen mode

Types that implement Copy, such as i32, are copied on assignment.

let x = 5;
let y = x;
println!("{x} {y}");
Enter fullscreen mode Exit fullscreen mode

Use clone when an independent copy of heap-owned data is actually needed.

let original = String::from("hello");
let cloned = original.clone();
println!("{original} {cloned}");
Enter fullscreen mode Exit fullscreen mode

Borrowing and references

A reference borrows a value without taking ownership.

fn length(text: &str) -> usize {
    text.len()
}

let message = String::from("hello");
let size = length(&message);
println!("{message}: {size}");
Enter fullscreen mode Exit fullscreen mode

The main borrowing rules are:

  • Any number of shared references &T, or one mutable reference &mut T, may be active.
  • Shared and mutable references to the same value cannot be active at the same time.
  • A reference must always point to a valid value.
fn append_world(text: &mut String) {
    text.push_str(", world");
}

let mut message = String::from("hello");
append_world(&mut message);
println!("{message}");
Enter fullscreen mode Exit fullscreen mode

Slices

A slice borrows a contiguous portion of a collection without owning the data.

let numbers = [10, 20, 30, 40];
let middle: &[i32] = &numbers[1..3];
assert_eq!(middle, &[20, 30]);

let text = String::from("hello world");
let hello: &str = &text[..5];
assert_eq!(hello, "hello");
Enter fullscreen mode Exit fullscreen mode

String ranges must fall on UTF-8 character boundaries or they panic. Use chars for Unicode scalar values and bytes for bytes.

let text = "日本語";
let characters: Vec<char> = text.chars().collect();
assert_eq!(characters, vec!['日', '本', '語']);
assert_eq!(text.len(), 9);          // UTF-8 bytes
assert_eq!(text.chars().count(), 3); // Unicode scalar values
Enter fullscreen mode Exit fullscreen mode

Lifetime overview

Lifetimes describe how long references remain valid. The compiler infers them in most code.

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() >= y.len() { x } else { y }
}
Enter fullscreen mode Exit fullscreen mode

'a says that the returned reference cannot outlive the shorter-lived input reference. It does not extend the lifetime of either value. A string literal has the 'static lifetime because its data is available for the entire program.


Control flow

if, if let, and let else

if is an expression, so it can produce a value. Its condition must be a bool.

let score = 80;
let grade = if score >= 80 { "A" } else { "B" };

let name = Some("Ferris");
if let Some(value) = name {
    println!("{value}");
}

fn print_name(name: Option<&str>) {
    let Some(name) = name else {
        println!("No name was provided");
        return;
    };
    println!("{name}");
}
Enter fullscreen mode Exit fullscreen mode

loop, while, and for

let mut count = 0;
let result = loop {
    count += 1;
    if count == 3 {
        break count * 10; // break can return a value from loop.
    }
};
assert_eq!(result, 30);

while count > 0 {
    count -= 1;
}

for value in [10, 20, 30] {
    println!("{value}");
}

for number in 1..=5 {
    if number % 2 == 0 {
        continue;
    }
    println!("odd: {number}");
}
Enter fullscreen mode Exit fullscreen mode

for calls into_iter. Passing a collection by value may consume it.

let mut names = vec![String::from("Alice"), String::from("Bob")];

for name in &names {        // iter(): &String
    println!("{name}");
}
for name in &mut names {    // iter_mut(): &mut String
    name.push('!');
}
assert_eq!(names, vec!["Alice!", "Bob!"]);

// for name in names { }    // into_iter(): String; consumes names.
Enter fullscreen mode Exit fullscreen mode

Loop labels

'outer: for x in 0..3 {
    for y in 0..3 {
        if x + y == 3 {
            break 'outer;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

match

match must cover every possible pattern.

let number = 7;

let description = match number {
    0 => "zero",
    1..=9 => "one digit",
    n if n % 2 == 0 => "even",
    _ => "other",
};

println!("{description}");
Enter fullscreen mode Exit fullscreen mode
let point = (0, 5);
match point {
    (0, y) => println!("on the y-axis: {y}"),
    (x, 0) => println!("on the x-axis: {x}"),
    (x, y) => println!("({x}, {y})"),
}
Enter fullscreen mode Exit fullscreen mode

Functions and closures

Function parameter and return types are explicit. The final expression is returned when it has no semicolon.

fn add(left: i32, right: i32) -> i32 {
    left + right
}

fn divide(left: f64, right: f64) -> Option<f64> {
    if right == 0.0 { return None; }
    Some(left / right)
}

let value = { let base = 10; base * 2 };
assert_eq!(value, 20);
Enter fullscreen mode Exit fullscreen mode

Closures can borrow or capture values from their environment. Their types are usually inferred.

let offset = 10;
let add_offset = |value: i32| value + offset;
assert_eq!(add_offset(5), 15);

let message = String::from("hello");
let show = move || println!("{message}");
show();
Enter fullscreen mode Exit fullscreen mode

Closures implement Fn, FnMut, or FnOnce depending on how they use captured values.

fn apply_twice<F>(mut operation: F, value: i32) -> i32
where F: FnMut(i32) -> i32,
{
    let once = operation(value);
    operation(once)
}
assert_eq!(apply_twice(|x| x + 1, 10), 12);
Enter fullscreen mode Exit fullscreen mode

Functions and non-capturing closures can be used as fn pointers.

fn double(value: i32) -> i32 { value * 2 }
fn calculate(operation: fn(i32) -> i32, value: i32) -> i32 { operation(value) }
assert_eq!(calculate(double, 5), 10);
Enter fullscreen mode Exit fullscreen mode

Structs, unions, enums, and patterns

Structs and methods

#[derive(Debug, Clone, PartialEq)]
struct User { name: String, age: u32, active: bool }

let user = User { name: String::from("Ferris"), age: 10, active: true };
let updated = User { age: 11, ..user.clone() };
println!("{updated:?}");
Enter fullscreen mode Exit fullscreen mode

Common derived traits include Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, and Default. Copy cannot be implemented for a type that implements Drop.

struct UserId(u64);          // Tuple struct
struct Point(i32, i32);
struct Marker;               // Unit struct
Enter fullscreen mode Exit fullscreen mode

Methods are defined in impl blocks. &self borrows, &mut self mutably borrows, and self consumes the value.

struct Rectangle { width: u32, height: u32 }

impl Rectangle {
    fn square(size: u32) -> Self { Self { width: size, height: size } }
    fn area(&self) -> u32 { self.width * self.height }
    fn resize(&mut self, width: u32, height: u32) {
        self.width = width; self.height = height;
    }
    fn dimensions(self) -> (u32, u32) { (self.width, self.height) }
}

let mut rectangle = Rectangle::square(10);
rectangle.resize(20, 30);
assert_eq!(rectangle.area(), 600);
assert_eq!(rectangle.dimensions(), (20, 30));
Enter fullscreen mode Exit fullscreen mode

Unions

A union stores all fields in shared storage. It is mainly used for FFI and low-level representations.

#[repr(C)]
union Number { integer: i32, floating: f32 }

let number = Number { integer: 42 };
// SAFETY: number was initialized through integer.
let integer = unsafe { number.integer };
assert_eq!(integer, 42);
Enter fullscreen mode Exit fullscreen mode

Writing a field is safe, but reading is unsafe because the compiler cannot prove that the bits are valid for the selected field. A union does not track an active field. Its fields are restricted to types that do not require ordinary drop glue, references, or wrappers such as ManuallyDrop<T>. Prefer an enum for ordinary safe application data.

Enums and Option

Enum variants can hold different shapes of data.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(u8, u8, u8),
}

fn handle(message: Message) {
    match message {
        Message::Quit => println!("quit"),
        Message::Move { x, y } => println!("move to {x}, {y}"),
        Message::Write(text) => println!("{text}"),
        Message::ChangeColor(r, g, b) => println!("rgb({r}, {g}, {b})"),
    }
}
handle(Message::Write(String::from("hello")));
Enter fullscreen mode Exit fullscreen mode

Rust represents optional values with Option<T> rather than null.

fn find_even(values: &[i32]) -> Option<i32> {
    values.iter().copied().find(|value| value % 2 == 0)
}

let value = find_even(&[1, 3, 4, 5]);
assert_eq!(value, Some(4));
assert_eq!(value.map(|n| n * 2), Some(8));
assert_eq!(None::<i32>.unwrap_or(0), 0);
Enter fullscreen mode Exit fullscreen mode

unwrap and expect panic on None. For expected absence, prefer matching, ?, or fallback methods such as unwrap_or.


Collections and strings

Vec<T>

let mut values = Vec::with_capacity(4);
values.push(10);
values.extend([20, 30]);
assert_eq!(values[0], 10);              // Panics if out of bounds.
assert_eq!(values.get(1), Some(&20));   // Safe optional access.
assert_eq!(values.pop(), Some(30));

let numbers = vec![1, 2, 3];
let zeros = vec![0; 5];
Enter fullscreen mode Exit fullscreen mode

String and &str

String owns a growable UTF-8 buffer; &str borrows UTF-8 text. Read-only function parameters generally prefer &str over &String.

let mut message = String::from("Hello");
message.push(',');
message.push_str(" Rust");

fn greet(name: &str) -> String { format!("Hello, {name}!") }
assert_eq!(greet(&message), "Hello, Hello, Rust!");

let first = String::from("Hello");
let second = String::from("Rust");
let joined = format!("{first}, {second}"); // Does not consume either value.
let combined = first + ", " + &second;     // Consumes first.
Enter fullscreen mode Exit fullscreen mode

Maps, sets, and queues

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Alice"), 10);
scores.entry(String::from("Carol")).or_insert(0);
*scores.entry(String::from("Alice")).or_insert(0) += 5;

if let Some(score) = scores.get("Alice") { println!("{score}"); }
for (name, score) in &scores { println!("{name}: {score}"); }
Enter fullscreen mode Exit fullscreen mode

Do not depend on HashMap iteration order. Use BTreeMap when key order matters. HashSet stores unique values, and VecDeque supports efficient operations at both ends.

use std::collections::{BTreeMap, HashSet, VecDeque};
let unique: HashSet<_> = [1, 2, 2, 3].into_iter().collect();
let mut sorted = BTreeMap::new(); sorted.insert("b", 2); sorted.insert("a", 1);
let mut queue = VecDeque::new(); queue.push_back("first");
assert_eq!(queue.pop_front(), Some("first"));
Enter fullscreen mode Exit fullscreen mode

Error handling

Rust uses Result<T, E> for recoverable errors and panic! for unrecoverable bugs.

use std::{fs, io};
fn read_config() -> Result<String, io::Error> {
    let contents = fs::read_to_string("config.toml")?;
    Ok(contents)
}
Enter fullscreen mode Exit fullscreen mode

? unwraps a success value and returns an error early, converting it through From when applicable. main may also return Result.

use std::{error::Error, fs};
fn main() -> Result<(), Box<dyn Error>> {
    println!("{}", fs::read_to_string("config.toml")?);
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Useful combinators include map, map_err, and_then, unwrap_or, unwrap_or_else, inspect, and inspect_err.

fn parse_number(text: &str) -> Result<i32, String> {
    text.parse::<i32>().map_err(|error| format!("invalid integer: {error}"))
}
assert_eq!(parse_number("42"), Ok(42));
Enter fullscreen mode Exit fullscreen mode

Library APIs usually benefit from a concrete error type callers can inspect. Implement Display, Error, and appropriate From conversions; crates such as thiserror and anyhow may be useful depending on the application.

Use unwrap and expect mainly in tests, examples, or places where failure proves an invariant violation. Use Result or Option for ordinary input failures.


Generics, traits, and lifetimes

fn largest<T: PartialOrd>(values: &[T]) -> Option<&T> {
    let mut iterator = values.iter();
    let mut largest = iterator.next()?;
    for value in iterator { if value > largest { largest = value; } }
    Some(largest)
}
assert_eq!(largest(&[3, 1, 4, 2]), Some(&4));
Enter fullscreen mode Exit fullscreen mode

Traits define shared behavior.

trait Summary {
    fn summarize(&self) -> String;
    fn category(&self) -> &'static str { "general" }
}

struct Article { title: String }
impl Summary for Article {
    fn summarize(&self) -> String { self.title.clone() }
}
Enter fullscreen mode Exit fullscreen mode

Use impl Trait, explicit bounds, or where clauses depending on readability.

use std::fmt::{Debug, Display};
fn print_summary(item: &impl Display) { println!("{item}"); }
fn print_debug<T: Debug>(item: &T) { println!("{item:?}"); }
fn compare_and_print<T, U>(left: &T, right: &U)
where T: Display + PartialOrd<U>, U: Display,
{ println!("{left} / {right}"); }
Enter fullscreen mode Exit fullscreen mode

Associated types select one concrete type per trait implementation.

trait Repository {
    type Item;
    type Error;
    fn find(&self, id: u64) -> Result<Option<Self::Item>, Self::Error>;
}
Enter fullscreen mode Exit fullscreen mode

Generics use static dispatch; dyn Trait uses runtime dispatch through a vtable and allows heterogeneous values.

use std::fmt::Display;
let values: Vec<Box<dyn Display>> = vec![Box::new(42), Box::new(String::from("hello"))];
Enter fullscreen mode Exit fullscreen mode

Lifetime annotations connect the validity of references; they do not extend a value's lifetime.

struct Excerpt<'a> { text: &'a str }
impl<'a> Excerpt<'a> { fn text(&self) -> &str { self.text } }
Enter fullscreen mode Exit fullscreen mode

Owning a String instead of borrowing &str can eliminate lifetime parameters when ownership better matches the design.


Iterators

Iterator adapters are lazy; consumers such as collect, sum, and find drive evaluation.

let result: Vec<i32> = (1..=10)
    .filter(|n| n % 2 == 0)
    .map(|n| n * n)
    .collect();
assert_eq!(result, vec![4, 16, 36, 64, 100]);
Enter fullscreen mode Exit fullscreen mode

iter() yields &T, iter_mut() yields &mut T, and into_iter() usually yields T and consumes the collection.

let mut values = vec![1, 2, 3];
for value in values.iter_mut() { *value *= 2; }
assert_eq!(values, vec![2, 4, 6]);
let total: i32 = values.into_iter().sum();
assert_eq!(total, 12);
Enter fullscreen mode Exit fullscreen mode

Implement Iterator::next to create an iterator.

struct Counter { current: u32, end: u32 }
impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<Self::Item> {
        if self.current >= self.end { return None; }
        self.current += 1; Some(self.current)
    }
}
assert_eq!(Counter { current: 0, end: 3 }.collect::<Vec<_>>(), vec![1, 2, 3]);
Enter fullscreen mode Exit fullscreen mode

Smart pointers

  • Box<T> places a value on the heap with single ownership.
  • Rc<T> provides reference-counted shared ownership within one thread.
  • Arc<T> provides atomic reference-counted ownership across threads.
  • Cell<T> and RefCell<T> provide interior mutability; RefCell checks borrowing at runtime and panics on violations.
enum List { Cons(i32, Box<List>), Nil }
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
Enter fullscreen mode Exit fullscreen mode
use std::{cell::RefCell, rc::Rc};
let shared = Rc::new(String::from("shared"));
let copy = Rc::clone(&shared);
assert_eq!(Rc::strong_count(&shared), 2);

let values = RefCell::new(vec![1, 2]);
values.borrow_mut().push(3);
assert_eq!(*values.borrow(), vec![1, 2, 3]);
Enter fullscreen mode Exit fullscreen mode

Rust uses RAII: resources such as locks and files are released when their owner is dropped. Implement Drop for custom cleanup, and call drop(value) to release a value before the end of its scope.


Modules, packages, and visibility

Term Meaning
Package A Cargo.toml definition containing one or more crates
Crate A compilation unit: a binary or library
Module A namespace and visibility boundary within a crate

Items are private by default. pub, pub(crate), pub(super), and pub(in path) expose progressively scoped APIs.

mod network {
    pub mod client { pub fn connect() { println!("connected"); } }
    fn private_helper() {}
}
use network::client;
fn main() { client::connect(); }
Enter fullscreen mode Exit fullscreen mode

Typical files include src/main.rs for a binary root, src/lib.rs for a library root, and files or subdirectories for modules. Both network.rs plus network/client.rs and the older network/mod.rs layout are supported.

use std::collections::HashMap;
use std::fmt::{self, Display};
use std::io::Result as IoResult;

mod models { pub struct User; }
pub use models::User; // Re-export from a crate root.
Enter fullscreen mode Exit fullscreen mode

Concurrency

Ownership plus the Send and Sync traits lets Rust reject many concurrency mistakes at compile time.

use std::thread;
let values = vec![1, 2, 3];
let handle = thread::spawn(move || values.iter().sum::<i32>());
assert_eq!(handle.join().expect("thread panicked"), 6);

let borrowed = vec![1, 2, 3];
thread::scope(|scope| { scope.spawn(|| println!("{borrowed:?}")); });
Enter fullscreen mode Exit fullscreen mode

The standard mpsc channel supports multiple producers and one consumer.

use std::{sync::mpsc, thread};
let (sender, receiver) = mpsc::channel();
for id in 0..2 {
    let sender = sender.clone();
    thread::spawn(move || sender.send(format!("worker {id}")).unwrap());
}
drop(sender);
for message in receiver { println!("{message}"); }
Enter fullscreen mode Exit fullscreen mode

Use Arc<Mutex<T>> for mutable state shared across threads. Lock guards release automatically when dropped; keep critical sections short and acquire multiple locks in a consistent order.

use std::{sync::{Arc, Mutex}, thread};
let counter = Arc::new(Mutex::new(0));
let handles: Vec<_> = (0..10).map(|_| {
    let counter = Arc::clone(&counter);
    thread::spawn(move || *counter.lock().expect("poisoned mutex") += 1)
}).collect();
for handle in handles { handle.join().unwrap(); }
assert_eq!(*counter.lock().unwrap(), 10);
Enter fullscreen mode Exit fullscreen mode

Send means ownership can move to another thread; Sync means &T can be shared across threads. Manual unsafe impl requires the author to uphold the full safety contract.

Rust supplies Future and async syntax, but the standard library does not provide a general-purpose async runtime. Choose a runtime such as Tokio when the I/O requirements call for one.

async fn fetch_value() -> u32 { 42 }
async fn calculate() -> u32 { fetch_value().await * 2 }
Enter fullscreen mode Exit fullscreen mode

Calling an async fn creates a lazy Future; work progresses only when it is awaited or driven by an executor.


Macros and attributes

Common macros include println!, eprintln!, format!, vec!, assert!, assert_eq!, and assert_ne!. todo!, unimplemented!, and unreachable! panic if executed.

macro_rules! hash_map {
    ($($key:expr => $value:expr),* $(,)?) => {{
        let mut map = std::collections::HashMap::new();
        $(map.insert($key, $value);)*
        map
    }};
}
let scores = hash_map! { "Alice" => 10, "Bob" => 20 };
Enter fullscreen mode Exit fullscreen mode

Procedural macros include custom derives, attribute macros, and function-like macros, and are defined in a separate proc-macro crate.

#[derive(Debug, Clone, PartialEq)]
struct User;

#[cfg(target_os = "linux")]
fn platform_name() -> &'static str { "Linux" }

#[must_use]
fn calculate() -> i32 { 42 }
Enter fullscreen mode Exit fullscreen mode

#[cfg(feature = "json")] enables conditional compilation for Cargo features.


Testing

pub fn add(left: i32, right: i32) -> i32 { left + right }

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn adds_numbers() { assert_eq!(add(2, 3), 5); }

    #[test]
    fn parses_number() -> Result<(), Box<dyn std::error::Error>> {
        assert_eq!("42".parse::<i32>()?, 42); Ok(())
    }
}
Enter fullscreen mode Exit fullscreen mode

Files under tests/ are separate crates that test a library's public API. Documentation code blocks attached to /// comments are tested by default.

cargo test
cargo test name_filter
cargo test -- --nocapture
cargo test -- --ignored
cargo test --doc
cargo test --all-features
Enter fullscreen mode Exit fullscreen mode

Standard-library quick reference

let name = "Ferris";
println!("{name:>8}");
println!("hex: {:x}, binary: {:b}", 10, 10);

let text = "  Hello, Rust  ";
assert_eq!(text.trim(), "Hello, Rust");
assert_eq!("a,b,c".split(',').collect::<Vec<_>>(), vec!["a", "b", "c"]);
assert_eq!(["a", "b", "c"].join("-"), "a-b-c");
Enter fullscreen mode Exit fullscreen mode
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let number: i32 = "42".parse()?;
    let hex = i32::from_str_radix("ff", 16)?;
    assert_eq!((number.to_string(), hex), (String::from("42"), 255));
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Use fs::read_to_string and fs::write for small files, or BufReader and BufRead::lines for streaming large files. Build paths with Path and PathBuf. Read arguments with env::args and environment variables with env::var; never print secrets in logs or error messages.

Use Instant for elapsed time, SystemTime for system-clock values, and Duration for spans. Date formatting and time zones generally require a suitable external crate.

OnceLock<T> initializes a global value once; LazyLock<T> initializes it from a closure on first access.

use std::sync::{LazyLock, OnceLock};
static CONFIG: OnceLock<String> = OnceLock::new();
static DEFAULTS: LazyLock<Vec<String>> = LazyLock::new(|| vec![String::from("default")]);
assert_eq!(CONFIG.get_or_init(|| String::from("production")), "production");
assert_eq!(DEFAULTS.len(), 1);
Enter fullscreen mode Exit fullscreen mode

Calling external functions: extern and FFI

extern "C" selects the C ABI. In Edition 2024, external blocks must be declared with unsafe extern because Rust cannot verify that their signatures match the foreign definitions.

use std::ffi::c_int;
unsafe extern "C" { fn abs(input: c_int) -> c_int; }
// SAFETY: -42 is a valid c_int input for C abs.
assert_eq!(unsafe { abs(-42) }, 42);
Enter fullscreen mode Exit fullscreen mode

Export a Rust function with a C ABI as follows:

#[unsafe(no_mangle)]
pub extern "C" fn add(left: i32, right: i32) -> i32 { left + right }
assert_eq!(add(2, 3), 5);
Enter fullscreen mode Exit fullscreen mode

At an FFI boundary, verify the ABI and signatures, #[repr(C)] layouts, pointer validity and alignment, string encoding and termination, allocation ownership, and panic or exception behavior. Generated bindings such as bindgen may reduce transcription mistakes.


unsafe Rust

unsafe does not disable the borrow checker. It permits a small set of operations whose safety the compiler cannot prove: dereferencing raw pointers, calling unsafe functions, accessing mutable statics, implementing unsafe traits, and reading union fields.

unsafe fn first_unchecked(values: &[i32]) -> &i32 {
    // SAFETY: The caller guarantees that values is non-empty.
    unsafe { values.get_unchecked(0) }
}
let values = [10, 20];
// SAFETY: values contains two elements.
assert_eq!(*unsafe { first_unchecked(&values) }, 10);
Enter fullscreen mode Exit fullscreen mode

Edition 2024 still requires an explicit unsafe block inside an unsafe fn. Keep unsafe regions small, document invariants with SAFETY comments, and ensure every safe wrapper preserves those invariants for all safe callers.


Cargo and dependency management

cargo new app
cargo new --lib my-library
cargo check
cargo build --release
cargo run -- arg1 arg2
cargo test
cargo doc --open
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
Enter fullscreen mode Exit fullscreen mode

cargo check is fast because it does not produce a final binary. Still run builds and tests before release.

[package]
name = "example"
version = "0.1.0"
edition = "2024"
rust-version = "1.98"
Enter fullscreen mode Exit fullscreen mode

edition selects language compatibility rules; rust-version declares the MSRV actually supported and tested by the package.

cargo add serde --features derive
cargo add --dev tempfile
cargo update
cargo tree
cargo tree -d
Enter fullscreen mode Exit fullscreen mode

Cargo.lock records an exact dependency resolution. Applications normally commit it. Libraries may also commit a workspace lockfile for reproducible development and CI, while consumers still resolve versions from Cargo.toml constraints.

Features should be additive. Test meaningful combinations with --all-features and --no-default-features. A workspace can share edition, rust-version, and dependency declarations through [workspace.package] and [workspace.dependencies]; Edition 2024 workspaces use resolver = "3".


Tips and common mistakes

Avoid reflexive clone

Borrow when ownership is unnecessary; cloning may hide a design problem and add allocations.

fn print_name(name: &str) { println!("{name}"); }
let name = String::from("Ferris");
print_name(&name);
println!("{name}");
Enter fullscreen mode Exit fullscreen mode

Prefer slices in read-only APIs

Use &str instead of &String, and &[T] instead of &Vec<T>, unless the concrete container is required.

Use checked indexing for uncertain input

values[index] panics out of bounds; values.get(index) returns Option<&T>.

Remember that strings are UTF-8

let text = "🦀Rust";
assert_eq!(text.len(), 8); // Bytes, not visible characters.
assert_eq!(text.chars().next(), Some('🦀'));
Enter fullscreen mode Exit fullscreen mode

Rust strings have no integer indexing. Use an appropriate Unicode crate when grapheme-cluster processing is required.

Do not hold a synchronous mutex guard across .await

use std::sync::Mutex;
async fn async_operation(_value: String) {}
async fn process(shared: &Mutex<String>) {
    let value = { shared.lock().expect("poisoned mutex").clone() };
    async_operation(value).await;
}
Enter fullscreen mode Exit fullscreen mode

Release the guard first, or choose an async-aware mutex when appropriate. Likewise, move blocking I/O or CPU-heavy work off an async executor's worker threads using the runtime's blocking API or dedicated threads.

Document panics in public APIs

Ordinary invalid input should generally return Result or Option. If a public function can panic, document the condition in a # Panics section.

Keep warnings clean

cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
Enter fullscreen mode Exit fullscreen mode

Review Clippy suggestions before applying them; they can affect semantics or performance. Follow project-specific lint policy where one exists.


Official references

Rust works best when ownership, borrowing, and types make invalid states difficult to represent. Compiler errors are not merely restrictions—they are early feedback about the design.

Top comments (0)