Dev Library ยท Reference Collection

The Dev Reference Library



Last update: 2024-01-15

Rust Language Reference for Developers

Ownership, borrowing, lifetimes, traits, and the compiler model

๐Ÿ”—The Ownership Model

Rust enforces memory safety at compile time through three rules: every value has one owner, ownership can be moved or borrowed, and values are dropped when their owner goes out of scope. No garbage collector, no runtime overhead.

ConceptWhat It MeansCompile Error If You Violate It
OwnershipOne variable owns the data at any timeUse after move
MoveOwnership transfers; old variable is goneValue used after move
CloneDeep copy; both variables own separate data(No error โ€” explicit)
CopyStack-only types (i32, bool, f64) are copied implicitly(No error โ€” cheap bitwise copy)
BorrowReference to data without taking ownershipOutlives owner, or mutated while borrowed
    // Move: ownership transfers to `b`, `a` is no longer valid
    let a = String::from("hello");
    let b = a;              // a is MOVED into b
    // println!("{}", a);  // COMPILE ERROR: value borrowed after move

    // Clone: explicit deep copy, both live independently
    let a = String::from("hello");
    let b = a.clone();      // b is a new independent String
    println!("{} {}", a, b); // both valid

    // Copy types: i32, bool, char, f64, tuples of Copy types
    let x: i32 = 5;
    let y = x;              // x is COPIED (not moved), both valid
    println!("{} {}", x, y);

๐Ÿ”—Borrowing: Shared and Mutable References

References let you use a value without taking ownership. The borrow checker enforces the rule at compile time: you can have many shared references OR one mutable reference โ€” never both at the same time.

    // Shared reference (&T): read-only, many allowed simultaneously
    let s = String::from("hello");
    let r1 = &s;
    let r2 = &s;            // fine โ€” multiple shared refs are OK
    println!("{} {}", r1, r2);

    // Mutable reference (&mut T): exclusive, only one at a time
    let mut s = String::from("hello");
    let r = &mut s;
    r.push_str(", world");
    // let r2 = &mut s;    // COMPILE ERROR: cannot borrow `s` as mutable more than once

    // Cannot mix shared and mutable refs to same data simultaneously
    let r1 = &s;
    // let r2 = &mut s;    // COMPILE ERROR: cannot borrow `s` as mutable because it is also borrowed as immutable

The borrow checker works at the scope level, not the line level. In newer Rust (NLL โ€” Non-Lexical Lifetimes), a borrow ends at its last use, not at the end of its scope. This means you can sometimes take a mutable ref after a shared ref as long as the shared ref is no longer used.

๐Ÿ”—Ownership in Functions

    // Passing by value: ownership moves into the function
    fn take(s: String) {
        println!("{}", s);
    } // s is dropped here

    let s = String::from("hi");
    take(s);
    // println!("{}", s); // COMPILE ERROR: s was moved

    // Passing by reference: borrow, caller retains ownership
    fn borrow(s: &String) {
        println!("{}", s);
    } // borrow ends here, s is not dropped

    let s = String::from("hi");
    borrow(&s);
    println!("{}", s); // still valid โ€” we only borrowed

    // Returning ownership: function gives back what it owns
    fn make() -> String {
        String::from("new string") // returned, not dropped
    }
    let s = make(); // s owns the string

๐Ÿ”—The Stack vs the Heap

StackHeap
AllocationAutomatic (function call frame)Explicit (Box::new, String::from, Vec::new)
SizeMust be known at compile timeCan grow/shrink at runtime
SpeedFast (pointer increment)Slower (allocator call)
Examplesi32, bool, [u8; 4], referencesString, Vec<T>, Box<T>, HashMap
DropAutomatic when frame exitsWhen owning variable is dropped

String vs &str. String is heap-allocated, owned, growable. &str is a borrowed reference to string data (often a string literal in the binary, or a slice of a String). Functions that only read strings should generally take &str โ€” it's more flexible and avoids forcing the caller to allocate.

๐Ÿ”—Scalar Types

TypeSizeNotes
i8 / i16 / i32 / i64 / i1281โ€“16 bytesSigned integers. i32 is default.
u8 / u16 / u32 / u64 / u1281โ€“16 bytesUnsigned integers. u8 is a byte.
isize / usizePlatform widthPointer-sized. Used for indexing.
f32 / f644 / 8 bytesFloating point. f64 is default.
bool1 bytetrue or false.
char4 bytesUnicode scalar value (not a byte).

๐Ÿ”—Compound Types

    // Tuple: fixed-length, can mix types
    let t: (i32, f64, bool) = (1, 2.0, true);
    println!("{}", t.0); // access by index
    let (x, y, z) = t; // destructure

    // Array: fixed-length, same type, stack-allocated
    let arr: [i32; 5] = [1, 2, 3, 4, 5];
    let zeros = [0; 10]; // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    // arr[5];           // RUNTIME PANIC: index out of bounds (checked)

    // Vec: heap-allocated, growable array
    let mut v: Vec<i32> = Vec::new();
    v.push(1);
    v.push(2);
    let v2 = vec![1, 2, 3]; // macro shorthand

    // Slice: view into an array or Vec (doesn't own data)
    let slice: &[i32] = &v[0..2]; // first two elements

    // HashMap
    use std::collections::HashMap;
    let mut map = HashMap::new();
    map.insert("key", 42);
    let val = map.get("key"); // returns Option<&i32>

๐Ÿ”—Structs

    // Named-field struct
    struct User {
        name: String,
        age: u32,
        active: bool,
    }

    let user = User {
        name: String::from("Alice"),
        age: 30,
        active: true,
    };
    println!("{}", user.name);

    // Struct update syntax
    let user2 = User {
        name: String::from("Bob"),
        ..user  // fill remaining fields from `user`
                // NOTE: moves `user` if non-Copy fields are used
    };

    // Tuple struct
    struct Point(f64, f64);
    let p = Point(1.0, 2.0);
    println!("{}", p.0);

    // Unit struct (no fields, used as marker type)
    struct Marker;

๐Ÿ”—impl Blocks: Methods on Structs

    struct Rectangle {
        width: f64,
        height: f64,
    }

    impl Rectangle {
        // Associated function (no `self`): called as Rectangle::new()
        fn new(width: f64, height: f64) -> Self {
            Self { width, height }
        }

        // Method: takes `&self` (shared borrow of the instance)
        fn area(&self) -> f64 {
            self.width * self.height
        }

        // Mutable method: takes `&mut self`
        fn scale(&mut self, factor: f64) {
            self.width *= factor;
            self.height *= factor;
        }
    }

    let mut r = Rectangle::new(4.0, 5.0);
    println!("{}", r.area());  // 20.0
    r.scale(2.0);
    println!("{}", r.area());  // 80.0

self, &self, &mut self. self consumes the value (rare). &self borrows immutably (most common โ€” for reads). &mut self borrows mutably (for mutations). Pick the least powerful you need.

๐Ÿ”—Traits

Traits define shared behaviour across types. They're similar to interfaces in other languages, but more powerful โ€” they can have default method implementations, and you can implement them on types you didn't define.

    // Define a trait
    trait Summary {
        fn summarize(&self) -> String;

        // Default implementation โ€” types can override or use as-is
        fn preview(&self) -> String {
            format!("{}...", &self.summarize()[..50])
        }
    }

    // Implement for a struct
    struct Article { title: String, content: String }

    impl Summary for Article {
        fn summarize(&self) -> String {
            format!("{}: {}", self.title, self.content)
        }
    }

    // Use trait bound in a function (monomorphized at compile time)
    fn print_summary<T: Summary>(item: &T) {
        println!("{}", item.summarize());
    }

    // Equivalent using `impl Trait` syntax (simpler for single params)
    fn print_summary(item: &impl Summary) {
        println!("{}", item.summarize());
    }

    // Dynamic dispatch with `dyn Trait` (runtime cost, allows mixed types)
    fn print_any(item: &dyn Summary) {
        println!("{}", item.summarize());
    }

๐Ÿ”—Common Standard Traits

TraitWhat It ProvidesHow to Derive
Debug{:?} formatting for debugging#[derive(Debug)]
Display{} formatting for user outputMust implement manually
Clone.clone() deep copy#[derive(Clone)]
CopyImplicit bitwise copy (stack only)#[derive(Copy, Clone)]
PartialEq / Eq== and !=#[derive(PartialEq, Eq)]
PartialOrd / Ord<, >, sorting#[derive(PartialOrd, Ord)]
HashUse as HashMap key#[derive(Hash)]
DefaultType::default() zero value#[derive(Default)]
IteratorCustom iteration with .next()Must implement manually
From / IntoType conversionsImplement From, get Into free
// Deriving multiple traits
#[derive(Debug, Clone, PartialEq)]
struct Point { x: f64, y: f64 }

let p1 = Point { x: 1.0, y: 2.0 };
let p2 = p1.clone();
println!("{:?}", p1); // Point { x: 1.0, y: 2.0 }
println!("{}", p1 == p2); // true

// Implementing Display manually
use std::fmt;
impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}
println!("{}", p1); // (1.0, 2.0)

Orphan rule: You can only implement a trait for a type if either the trait or the type is defined in your crate. You cannot implement Display for Vec<T> โ€” neither is yours. This prevents conflicting implementations across crates.

๐Ÿ”—Enums

Rust enums are algebraic data types โ€” each variant can hold different data. Combined with pattern matching, they're the primary tool for modelling states and outcomes.

    // Simple enum
    enum Direction { North, South, East, West }

    // Enum with data in variants
    enum Shape {
        Circle(f64),              // radius
        Rectangle(f64, f64),     // width, height
        Point { x: f64, y: f64 }, // named fields
    }

    let s = Shape::Circle(3.14);

    // Pattern match to extract data
    let area = match s {
        Shape::Circle(r)        => std::f64::consts::PI * r * r,
        Shape::Rectangle(w, h)  => w * h,
        Shape::Point { .. }     => 0.0,
    };

๐Ÿ”—Option

Rust has no null. Instead, optional values use Option<T>. The compiler forces you to handle the None case. No null pointer exceptions.

    enum Option<T> {
        Some(T),
        None,
    }

    let maybe: Option<i32> = Some(42);
    let nothing: Option<i32> = None;

    // Pattern match
    match maybe {
        Some(n) => println!("got {}", n),
        None    => println!("nothing"),
    }

    // Convenient methods
    maybe.unwrap();              // panics if None โ€” use only when certain
    maybe.unwrap_or(0);          // default value if None
    maybe.unwrap_or_else(|| compute()); // lazily computed default
    maybe.map(|n| n * 2);        // transform if Some, pass through None
    maybe.and_then(|n| lookup(n)); // chain operations that may fail
    maybe.is_some();              // bool check
    maybe.is_none();              // bool check

    // if let: concise pattern match for one case
    if let Some(n) = maybe {
        println!("got {}", n);
    }

๐Ÿ”—Pattern Matching

    // match must be exhaustive โ€” compiler enforces all cases are covered
    let n = 7;
    match n {
        1         => println!("one"),
        2 | 3     => println!("two or three"),
        4..=6     => println!("four to six"),
        x if x < 0 => println!("negative: {}", x),
        _          => println!("something else"), // catch-all
    }

    // Destructure structs in match
    let p = Point { x: 3.0, y: 4.0 };
    match p {
        Point { x: 0.0, y } => println!("on y-axis at {}", y),
        Point { x, y }      => println!("({}, {})", x, y),
    }

    // while let: loop while pattern matches
    let mut stack = vec![1, 2, 3];
    while let Some(top) = stack.pop() {
        println!("{}", top);
    }

๐Ÿ”—Result<T, E>

Operations that can fail return Result<T, E>. The compiler forces you to handle failures โ€” you cannot accidentally ignore an error.

    enum Result<T, E> {
        Ok(T),
        Err(E),
    }

    // Parsing a string: can succeed (Ok) or fail (Err)
    let r: Result<i32, _> = "42".parse();
    match r {
        Ok(n)  => println!("parsed: {}", n),
        Err(e) => println!("failed: {}", e),
    }

    // Convenient Result methods (mirror Option)
    r.unwrap();              // panic on Err โ€” use in tests or when certain
    r.unwrap_or(0);          // default on Err
    r.expect("parse failed"); // panic with message on Err (better than unwrap)
    r.map(|n| n * 2);        // transform Ok value
    r.map_err(|e| MyErr(e)); // transform Err value
    r.and_then(|n| next(n)); // chain fallible operations
    r.is_ok(); r.is_err();   // bool checks

๐Ÿ”—The ? Operator

The ? operator is shorthand for: if Ok, unwrap and continue; if Err, return the error immediately from the current function. It eliminates boilerplate match chains for error propagation.

    use std::fs;
    use std::io;

    // Without ?: manual propagation
    fn read_file_verbose() -> Result<String, io::Error> {
        let content = match fs::read_to_string("file.txt") {
            Ok(s)  => s,
            Err(e) => return Err(e),
        };
        Ok(content)
    }

    // With ?: concise propagation
    fn read_file() -> Result<String, io::Error> {
        let content = fs::read_to_string("file.txt")?; // returns Err if it fails
        Ok(content)
    }

    // Chaining with ?
    fn parse_config() -> Result<Config, MyError> {
        let raw = fs::read_to_string("config.toml")?;
        let config: Config = toml::from_str(&raw)?;
        Ok(config)
    }

? works on both Result and Option. In a function returning Option<T>, ? returns None early if applied to a None value. In a function returning Result, it returns the Err. The function's return type determines behavior.

๐Ÿ”—Custom Error Types

    // Simple custom error with thiserror crate (recommended)
    use thiserror::Error;

    #[derive(Error, Debug)]
    enum AppError {
        #[error("IO error: {0}")]
        Io(#[from] std::io::Error),

        #[error("parse failed: {msg}")]
        Parse { msg: String },

        #[error("not found: {0}")]
        NotFound(String),
    }

    // anyhow crate: for applications where you don't need typed errors
    use anyhow::Result;
    fn run() -> Result<()> {   // Result<(), anyhow::Error>
        let s = fs::read_to_string("file")?; // any error works
        Ok(())
    }

thiserror for libraries, anyhow for applications. thiserror gives you typed errors callers can match on. anyhow is ergonomic for binaries where you just want to propagate and display errors. Use Box<dyn Error> if you want no deps but accept the ergonomic cost.

๐Ÿ”—Lifetimes

Lifetimes are the compiler's way of tracking how long references are valid. Most of the time the compiler infers them (lifetime elision). You only write them explicitly when the compiler can't figure it out โ€” usually when a function returns a reference and the compiler can't determine which input it came from.

    // The problem: which input does the returned reference point to?
    // The compiler needs to know to ensure the output doesn't outlive the input.
    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
        if x.len() > y.len() { x } else { y }
    }
    // 'a means: "the returned reference lives as long as the shorter of x and y"

    // Lifetime in a struct: struct holds a reference, must not outlive what it points to
    struct Excerpt<'a> {
        text: &'a str,
    }

    let novel = String::from("Call me Ishmael...");
    let first = novel.split('.').next().unwrap();
    let excerpt = Excerpt { text: first };
    // `excerpt` cannot outlive `novel` โ€” compiler enforces this

๐Ÿ”—Lifetime Elision Rules

You don't write lifetimes in most function signatures because the compiler applies three elision rules automatically:

RuleWhat It Says
Rule 1Each reference parameter gets its own lifetime: fn f(x: &T, y: &U) โ†’ fn f<'a,'b>(x: &'a T, y: &'b U)
Rule 2If there's exactly one input lifetime, all outputs get that lifetime.
Rule 3If one of the inputs is &self or &mut self, all output lifetimes get self's lifetime.
    // These three signatures are equivalent after elision:
    fn first_word(s: &str) -> &str
    // is the same as:
    fn first_word<'a>(s: &'a str) -> &'a str

    // Method on a struct: rule 3 applies
    impl Excerpt<'_> {
        fn announce(&self, msg: &str) -> &str {
            println!("Attention: {}", msg);
            self.text  // lifetime of return = lifetime of &self (rule 3)
        }
    }

๐Ÿ”—The 'static Lifetime

    // 'static: lives for the entire program duration
    let s: &'static str = "I live forever"; // string literals are 'static

    // Common in trait objects and thread-spawning
    fn spawn_worker(f: impl Fn() + Send + 'static) {
        std::thread::spawn(f);
    }
    // `'static` here means the closure must not borrow from the current stack frame
    // (the thread might outlive the current function call)

Don't reach for lifetimes first. If you find yourself writing complex lifetime annotations, consider whether you could own the data (use String instead of &str, Vec<T> instead of &[T]) or restructure the code. Explicit lifetimes are sometimes unavoidable but often a design smell.

๐Ÿ”—Threads

Rust's ownership system makes data races impossible to compile. The Send and Sync traits mark what's safe to share across thread boundaries โ€” the compiler enforces them automatically.

    use std::thread;

    // Spawn a thread, move data into it
    let data = String::from("hello");
    let handle = thread::spawn(move || {
        println!("in thread: {}", data); // `move` transfers ownership
    });
    handle.join().unwrap(); // wait for thread to finish

    // Spawn many threads and collect results
    let handles: Vec<_> = (0..10)
        .map(|i| thread::spawn(move || i * i))
        .collect();

    let results: Vec<_> = handles.into_iter()
        .map(|h| h.join().unwrap())
        .collect();

๐Ÿ”—Sharing State: Arc and Mutex

    use std::sync::{Arc, Mutex};
    use std::thread;

    // Arc = Atomically Reference Counted: shared ownership across threads
    // Mutex = Mutual Exclusion: only one thread can access data at a time
    let counter = Arc::new(Mutex::new(0));

    let handles: Vec<_> = (0..10).map(|_| {
        let c = Arc::clone(&counter);
        thread::spawn(move || {
            let mut num = c.lock().unwrap(); // lock โ€” blocks until available
            *num += 1;
        }) // lock released automatically when `num` goes out of scope
    }).collect();

    handles.into_iter().for_each(|h| h.join().unwrap());
    println!("count: {}", *counter.lock().unwrap()); // 10

๐Ÿ”—Channels: Message Passing

    use std::sync::mpsc; // multi-producer, single-consumer
    use std::thread;

    let (tx, rx) = mpsc::channel();

    // Sender can be cloned for multiple producers
    let tx2 = tx.clone();

    thread::spawn(move || {
        tx.send(String::from("from thread 1")).unwrap();
    });

    thread::spawn(move || {
        tx2.send(String::from("from thread 2")).unwrap();
    });

    // Receive (blocks until a message arrives)
    for msg in rx {
        println!("{}", msg);
    }

๐Ÿ”—Async / Await

    // Async functions return a Future โ€” they don't run until polled
    // Requires a runtime like tokio or async-std

    // Cargo.toml: tokio = { version = "1", features = ["full"] }

    use tokio;

    #[tokio::main]
    async fn main() {
        let result = fetch_data().await;
        println!("{:?}", result);
    }

    async fn fetch_data() -> Result<String, reqwest::Error> {
        let body = reqwest::get("https://httpbin.org/get")
            .await?
            .text()
            .await?;
        Ok(body)
    }

    // Spawn concurrent async tasks
    let (a, b) = tokio::join!(task_a(), task_b()); // run concurrently, wait for both
    let handle = tokio::task::spawn(task_c());        // fire and forget (or .await later)

Async โ‰  multithreaded by default. async/await is cooperative concurrency on one (or a pool of) thread(s). tokio::task::spawn can run on a thread pool, but a Future alone doesn't create threads. Use threads for CPU-bound work; use async for I/O-bound work.

๐Ÿ”—Cargo: The Build Tool and Package Manager

    # Create a new project
    cargo new my_project         # binary (has src/main.rs)
    cargo new my_lib --lib       # library (has src/lib.rs)

    # Build and run
    cargo build                  # compile (debug mode, fast compile, slow binary)
    cargo build --release        # optimized (slow compile, fast binary)
    cargo run                    # build + run
    cargo run --release
    cargo run -- arg1 arg2       # pass args to the binary

    # Check and test
    cargo check                  # type-check only, no codegen โ€” very fast
    cargo test                   # run all tests
    cargo test test_name         # run tests matching a name
    cargo test -- --nocapture    # show println! output in tests

    # Dependency management
    cargo add serde              # add crate (requires cargo-edit or Rust 1.62+)
    cargo add serde --features derive
    cargo update                 # update Cargo.lock to latest compatible versions
    cargo outdated               # show outdated deps (requires cargo-outdated)

    # Other
    cargo fmt                    # format all code with rustfmt
    cargo clippy                 # lint: catch common mistakes and style issues
    cargo doc --open             # build and open documentation in browser
    cargo publish                # publish crate to crates.io

๐Ÿ”—Cargo.toml

    [package]
    name = "my_project"
    version = "0.1.0"
    edition = "2021"         # always use 2021

    [dependencies]
    serde = { version = "1", features = ["derive"] }
    serde_json = "1"
    tokio = { version = "1", features = ["full"] }
    reqwest = { version = "0.12", features = ["json"] }
    thiserror = "1"
    anyhow = "1"

    [dev-dependencies]           # only for tests and benchmarks
    mockall = "0.12"

    [features]
    default = []
    my-feature = ["some-optional-dep"]

    [[bin]]                      # multiple binaries in one project
    name = "server"
    path = "src/bin/server.rs"

๐Ÿ”—Essential Crates

CratePurpose
serde + serde_jsonSerialize/deserialize JSON (and many other formats)
tokioAsync runtime โ€” the standard for async Rust
reqwestHTTP client (async, built on tokio)
axumWeb framework (async, built on tokio)
sqlxAsync SQL with compile-time query checking
thiserrorDerive macros for custom error types (libraries)
anyhowErgonomic error handling (applications)
tracingStructured logging and diagnostics
clapCLI argument parsing with derive macros
rayonData parallelism โ€” parallel iterators
randRandom number generation
regexRegular expressions
chronoDate and time handling
uuidUUID generation and parsing

๐Ÿ”—Testing

    // Unit tests: in the same file, inside a `#[cfg(test)]` module
    #[cfg(test)]
    mod tests {
        use super::*; // bring parent module into scope

        #[test]
        fn test_add() {
            assert_eq!(add(2, 3), 5);
        }

        #[test]
        #[should_panic(expected = "divide by zero")]
        fn test_panic() {
            divide(1, 0);
        }

        #[test]
        fn test_result() -> Result<(), String> {
            let n: i32 = "5".parse().map_err(|e: _| e.to_string())?;
            assert_eq!(n, 5);
            Ok(())
        }
    }

    // Integration tests: in tests/ directory (separate crate, public API only)
    // tests/integration_test.rs
    use my_project;

    #[test]
    fn it_works() {
        assert!(my_project::public_fn());
    }