This cheat sheet is based on Rust 1.98 and the Rust 2024 Edition.
Table of Contents
- Installation and updates
- Program structure
- Variables, constants, and primitive types
- Ownership, borrowing, and slices
- Control flow
- Functions and closures
- Structs, unions, enums, and patterns
- Collections and strings
- Error handling
- Generics, traits, and lifetimes
- Iterators
- Smart pointers
- Modules, packages, and visibility
- Concurrency
- Macros and attributes
- Testing
- Standard-library quick reference
- Calling external functions:
externand FFI unsafeRust- Cargo and dependency management
- Tips and common mistakes
- Official references
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
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
Update the stable toolchain and install the formatter and linter with:
rustup update stable
rustup component add rustfmt clippy
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!");
}
- An executable starts at the
mainfunction. - Statements usually end with
;. - A name ending in
!, such asprintln!, 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);
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 %}
pub fn add(left: i32, right: i32) -> i32 {
left + right
}
}
assert_eq!(math::add(2, 3), 5);
Create and run a Cargo project with:
```bash
cargo new hello-rust
cd hello-rust
cargo run
New Cargo projects use edition = "2024" in Cargo.toml.
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}");
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
Constants and statics
const MAX_RETRIES: u32 = 3;
static APPLICATION_NAME: &str = "example";
- A
constis evaluated at compile time and requires an explicit type. - A
statichas a fixed memory address for the duration of the program. - Accessing mutable
static mutstate requiresunsafe; 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);
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);
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 = '🦀';
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);
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());
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"),
}
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'));
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());
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");
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.
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));
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));
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));
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:
- Every value has an owner.
- A value has only one owner at a time.
- 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.
Types that implement Copy, such as i32, are copied on assignment.
let x = 5;
let y = x;
println!("{x} {y}");
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}");
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}");
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}");
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");
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
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 }
}
'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}");
}
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}");
}
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.
Loop labels
'outer: for x in 0..3 {
for y in 0..3 {
if x + y == 3 {
break 'outer;
}
}
}
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}");
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})"),
}
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);
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();
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);
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);
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:?}");
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
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));
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);
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")));
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);
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];
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.
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}"); }
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"));
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)
}
? 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(())
}
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));
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));
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() }
}
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}"); }
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>;
}
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"))];
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 } }
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]);
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);
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]);
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>andRefCell<T>provide interior mutability;RefCellchecks 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))));
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]);
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(); }
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.
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:?}")); });
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}"); }
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);
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 }
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 };
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 }
#[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(())
}
}
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
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");
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(())
}
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);
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);
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);
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);
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
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"
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
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}");
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('🦀'));
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;
}
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
Review Clippy suggestions before applying them; they can affect semantics or performance. Follow project-specific lint policy where one exists.
Official references
- Rust
- The Rust Programming Language
- Rust By Example
- The Rust Reference
- Standard Library
- The Cargo Book
- Clippy Documentation
- The Rust Edition Guide
- Rust 1.98.0 release notes
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)