Rust Language Fundamentals

Reading time
15 min read
Word count
2823 words
Diagram count
1 diagram

Source: Victor Bona's Obsidian Compendium snapshot, Knowledge base/Rust/01 Rust Language Fundamentals.md.

Purpose: Build a precise working model of Rust's core language surface so that syntax, types, control flow, pattern matching, containers, iterators, and error values become tools for writing correct systems code.

Rust Language Fundamentals

Related notes: 02 Ownership Borrowing and Lifetimes, 03 Types Traits and Generics, 04 Error Handling and API Design, 07 Concurrency Parallelism and Synchronization, Data Structures/Data Structures, Software testing

Rust is a systems language with a deliberately small runtime, strong static typing, expression-oriented syntax, exhaustive pattern matching, and explicit error values. The central design pressure is that ordinary code should reveal allocation, mutation, ownership transfer, and failure paths without requiring a garbage collector.

This note focuses on the core language surface. The deeper memory model is in 02 Ownership Borrowing and Lifetimes.

Mental Model

Rendering diagram...

Important consequences:

  • Types are checked before code runs.
  • Most abstractions compile away when they are monomorphized and optimized.
  • Option<T> and Result<T, E> model absence and failure as values, not side channels.
  • Pattern matching is the normal way to deconstruct data.
  • Mutability belongs to bindings, references, and interior mutability wrappers, not to objects in a universal sense.

Variables, Bindings, and Mutability

let creates a binding. Bindings are immutable by default.

fn main() {
    let port = 8080;
    let mut retries = 3;

    retries -= 1;

    println!("port={port}, retries={retries}");
}

Immutable bindings prevent accidental reassignment, not all change in the world. A value behind Mutex<T>, RefCell<T>, or atomics may still change through controlled APIs. See 02 Ownership Borrowing and Lifetimes for interior mutability.

Shadowing

Shadowing creates a new binding with the same name. It is useful for validation pipelines and type refinement.

fn parse_port(raw: &str) -> Result<u16, std::num::ParseIntError> {
    let raw = raw.trim();
    let port: u16 = raw.parse()?;
    Ok(port)
}

Shadowing differs from mut:

TechniqueWhat changesType can changeCommon useRisk
let mut xSame binding's valueNoCounters, buffers, state machinesAccidental mutation scope can grow
let x = ...; let x = ...;New bindingYesParsing, normalization, validationName reuse can hide too much context
New descriptive nameNew bindingYesComplex transformsMore names to maintain

Production guidance:

  • Prefer immutable bindings until mutation is the clearest local model.
  • Keep mut scopes small.
  • Use shadowing for narrow type refinement, not for long functions where the same name means several unrelated things.

Scalar Types

Rust scalar types represent single values.

FamilyExamplesNotes
Integersi8, u8, i32, u64, usize, isizeDefault integer literal is often inferred as i32; use usize for indexing and sizes
Floating pointf32, f64Default is f64; floats have NaN and are not total-order friendly by default
BooleanboolValues are true and false
CharactercharA Unicode scalar value, not a byte and not a grapheme cluster

Integer overflow behavior:

  • Debug builds panic on arithmetic overflow for primitive integer operations.
  • Release builds wrap for primitive integer operations unless using checked APIs or compiler options.
  • Production code should use checked_add, saturating_add, wrapping_add, or overflowing_add when overflow semantics matter.
fn add_quota(current: u64, increment: u64, limit: u64) -> Result<u64, &'static str> {
    let next = current.checked_add(increment).ok_or("quota overflow")?;
    if next > limit {
        return Err("quota exceeded");
    }
    Ok(next)
}

Compound Types

Tuples

Tuples group heterogeneous values by position.

fn split_host_port(endpoint: &str) -> Option<(&str, &str)> {
    endpoint.rsplit_once(':')
}

fn main() {
    let parsed = split_host_port("localhost:8080");

    if let Some((host, port)) = parsed {
        println!("host={host}, port={port}");
    }
}

Use tuples for local, obvious groupings. Prefer structs once fields have durable names or cross function boundaries.

Arrays and Slices

Arrays have fixed length known at compile time. Slices are borrowed views over contiguous data.

fn first_two(bytes: &[u8]) -> Option<[u8; 2]> {
    let [a, b, ..] = bytes else {
        return None;
    };
    Some([*a, *b])
}

Use &[T] in APIs when the caller should be able to pass a Vec&lt;T&gt;, array, or other contiguous buffer without transferring ownership.

Expressions vs Statements

Rust is expression-oriented. Expressions produce values. Statements perform actions and usually end with semicolons.

fn classify_latency(ms: u64) -> &'static str {
    if ms < 100 {
        "fast"
    } else if ms < 500 {
        "normal"
    } else {
        "slow"
    }
}

The last expression in a block becomes the block value if there is no semicolon.

fn cost(units: u32) -> u32 {
    let base = {
        let minimum = 10;
        minimum + units
    };

    base * 2
}

Common mistake:

fn wrong() -> u32 {
    42;
    0
}

The 42; statement discards the value. The function returns 0.

Functions

Functions use explicit parameter and return types.

fn clamp(value: i64, min: i64, max: i64) -> i64 {
    assert!(min <= max);

    if value < min {
        min
    } else if value > max {
        max
    } else {
        value
    }
}

Guidance:

  • Keep fallible functions explicit with Result&lt;T, E&gt;.
  • Prefer borrowed inputs like &str and &[T] when the function only reads.
  • Return owned values when the result is newly constructed or must outlive the input.
  • Use generic bounds only when they buy real reuse. See 03 Types Traits and Generics.

Control Flow

if

if conditions must be bool. Rust does not coerce integers to truthy or falsey values.

fn is_valid_port(port: u16) -> bool {
    port != 0
}

loop

loop can return a value using break value.

fn next_power_of_two_at_least(mut n: u64) -> u64 {
    let mut power = 1;

    loop {
        if power >= n {
            break power;
        }
        power *= 2;
    }
}

while

Use while for stateful loops with a condition.

fn drain_prefix_spaces(input: &str) -> &str {
    let mut start = 0;

    while input[start..].starts_with(' ') {
        start += 1;
        if start == input.len() {
            break;
        }
    }

    &input[start..]
}

for

Use for for iterating over anything that implements IntoIterator.

fn total(values: &[i64]) -> i64 {
    let mut sum = 0;

    for value in values {
        sum += value;
    }

    sum
}

Prefer iterator methods when they express the transformation directly.

fn total_positive(values: &[i64]) -> i64 {
    values.iter().copied().filter(|value| *value > 0).sum()
}

Pattern Matching

Patterns appear in match, let, function parameters, if let, while let, and let else.

enum Command {
    Put { key: String, value: String },
    Get(String),
    Delete(String),
}

fn command_name(command: &Command) -> &'static str {
    match command {
        Command::Put { .. } => "put",
        Command::Get(_) => "get",
        Command::Delete(_) => "delete",
    }
}

match is exhaustive. This is one of Rust's strongest maintainability features: adding a new enum variant forces callers to revisit behavior.

Pattern tools:

PatternExampleUse
Wildcard_Ignore a value
BindingnameBind matched value
Tuple(a, b)Destructure tuple
StructUser { id, .. }Extract named fields
EnumSome(value)Branch by variant
Slice[first, rest @ ..]Match contiguous sequences
Guardx if x &gt; 0Add boolean condition
OrA | BShare a branch
fn describe_status(code: u16) -> &'static str {
    match code {
        200..=299 => "success",
        300..=399 => "redirect",
        400 | 401 | 403 | 404 => "client error",
        500..=599 => "server error",
        _ => "unknown",
    }
}

if let

Use if let when one pattern matters and the rest can be ignored or handled generically.

fn print_debug_flag(flag: Option<&str>) {
    if let Some(value) = flag {
        println!("debug={value}");
    }
}

let else

Use let else for early exits that keep the happy path unindented.

fn require_token(header: Option<&str>) -> Result<&str, &'static str> {
    let Some(header) = header else {
        return Err("missing authorization header");
    };

    let Some(token) = header.strip_prefix("Bearer ") else {
        return Err("invalid authorization scheme");
    };

    Ok(token)
}

while let

Use while let to process values until a pattern no longer matches.

fn drain_stack(stack: &mut Vec<String>) {
    while let Some(item) = stack.pop() {
        println!("processed {item}");
    }
}

Option

Option&lt;T&gt; means either a value exists or it does not.

fn extension(path: &str) -> Option<&str> {
    path.rsplit_once('.').map(|(_, ext)| ext)
}

Core operations:

MethodMeaningExample
mapTransform present valueopt.map(parse)
and_thenChain optional computationopt.and_then(find)
filterKeep value only if predicate passesopt.filter(|s| !s.is_empty())
unwrap_orSupply a default valueopt.unwrap_or("default")
ok_orConvert to Resultopt.ok_or(Error::Missing)
as_refBorrow inside the optionopt.as_ref()

Production rule: reserve unwrap and expect for invariants that are impossible to violate if surrounding code is correct, tests, quick scripts, or process startup where panic is acceptable. In libraries, return Option or Result.

Result

Result&lt;T, E&gt; represents success or failure.

#[derive(Debug)]
enum ConfigError {
    MissingPort,
    InvalidPort(std::num::ParseIntError),
    PortZero,
}

fn load_port(raw: Option<&str>) -> Result<u16, ConfigError> {
    let raw = raw.ok_or(ConfigError::MissingPort)?;
    let port = raw.parse::<u16>().map_err(ConfigError::InvalidPort)?;

    if port == 0 {
        return Err(ConfigError::PortZero);
    }

    Ok(port)
}

The ? operator returns early on Err and unwraps Ok. It also works on Option in functions that return Option.

Error design tradeoffs:

Error styleProsConsUse when
Box&lt;dyn Error&gt;Quick compositionWeak typed handlingCLI boundary, prototypes
Custom enumExhaustive and matchableMore boilerplateLibrary and service domain errors
String errorEasy to createLoses structureSmall scripts, test helpers
anyhow::ErrorRich context with low frictionApplication-level, not ideal for public librariesBinary crates
thiserror enumTyped errors with clean formattingDependency and derive useStable crate APIs

See 04 Error Handling and API Design for application and library error boundaries.

Structs

Structs group named fields and encode domain shape.

#[derive(Debug, Clone, PartialEq, Eq)]
struct UserId(String);

#[derive(Debug, Clone)]
struct User {
    id: UserId,
    email: String,
    enabled: bool,
}

impl User {
    fn new(id: UserId, email: String) -> Self {
        Self {
            id,
            email,
            enabled: true,
        }
    }

    fn disable(&mut self) {
        self.enabled = false;
    }
}

Struct choices:

ShapeExampleUse
Named-field structstruct User { id: UserId }Durable domain objects
Tuple structstruct UserId(String);Newtypes with little field naming value
Unit structstruct Marker;Type-level marker or zero-sized value

Production guidance:

  • Use newtypes for identifiers, validated strings, and units to avoid mixing plain primitives.
  • Keep constructors responsible for invariants.
  • Derive traits intentionally. Clone and Copy change API ergonomics and costs.
  • Consider privacy: private fields plus public methods protect invariants.

Enums

Enums model one of several known alternatives.

#[derive(Debug, Clone)]
enum JobState {
    Queued,
    Running { attempt: u32 },
    Succeeded,
    Failed { reason: String },
}

fn can_retry(state: &JobState) -> bool {
    match state {
        JobState::Failed { .. } => true,
        JobState::Queued | JobState::Running { .. } | JobState::Succeeded => false,
    }
}

Use enums instead of nullable fields or stringly typed states. Invalid combinations become unrepresentable.

Bad shape:

struct WeakJobState {
    status: String,
    failure_reason: Option<String>,
    attempt: Option<u32>,
}

Better shape:

enum StrongJobState {
    Queued,
    Running { attempt: u32 },
    Succeeded,
    Failed { reason: String },
}

Slices and Strings

String vs str

Rust strings are UTF-8.

TypeOwnedGrowableCommon role
StringYesYesStore or build text
strNo, dynamically sizedNoText slice type, usually seen as &str
&strNoNoBorrowed text view
&StringNoNoRarely needed in APIs

Prefer &str for read-only string parameters.

fn normalize_name(name: &str) -> String {
    name.trim().to_ascii_lowercase()
}

Be careful with indexing. String byte indices must land on UTF-8 boundaries, and direct numeric indexing is not allowed.

fn first_char(input: &str) -> Option<char> {
    input.chars().next()
}

fn prefix_bytes(input: &str, len: usize) -> Option<&str> {
    input.get(..len)
}

String vs &str API guidance:

Function needParameter typeReturn type
Read text only&strAny
Store text beyond callimpl Into&lt;String&gt; or StringOwned type
Return borrowed substring&str tied to input lifetime&str
Return transformed text&strString

Containers

Containers are in the standard library and are usually generic over element types.

Vec

Vec&lt;T&gt; is a growable contiguous array. It is the default sequence container.

fn active_names(users: &[User]) -> Vec<&str> {
    users
        .iter()
        .filter(|user| user.enabled)
        .map(|user| user.email.as_str())
        .collect()
}

Strengths:

  • Cache-friendly iteration.
  • Fast append at the end.
  • O(1) indexing by usize.
  • Works naturally with slices.

Costs:

  • Inserting or removing near the front shifts elements.
  • Reallocation can move the buffer.
  • Holding references into a Vec while mutating it is restricted because mutation may invalidate references.

HashMap

HashMap&lt;K, V&gt; stores key-value pairs with hash-based lookup.

use std::collections::HashMap;

fn count_words(input: &str) -> HashMap<&str, usize> {
    let mut counts = HashMap::new();

    for word in input.split_whitespace() {
        *counts.entry(word).or_insert(0) += 1;
    }

    counts
}

Use entry to avoid duplicate lookups.

use std::collections::HashMap;

fn push_group<'a>(groups: &mut HashMap<&'a str, Vec<&'a str>>, key: &'a str, value: &'a str) {
    groups.entry(key).or_default().push(value);
}

BTreeMap

BTreeMap&lt;K, V&gt; stores sorted keys.

use std::collections::BTreeMap;

fn sorted_counts(counts: impl IntoIterator<Item= (String, usize)>) -> BTreeMap<String, usize> {
    counts.into_iter().collect()
}

Use when deterministic order, range queries, or ordered iteration matter.

HashSet

HashSet&lt;T&gt; stores unique values with hash-based membership.

use std::collections::HashSet;

fn unique_tags(tags: impl IntoIterator<Item= String>) -> HashSet<String> {
    tags.into_iter().filter(|tag| !tag.is_empty()).collect()
}

VecDeque

VecDeque&lt;T&gt; is a growable ring buffer with efficient push and pop at both ends.

use std::collections::VecDeque;

fn process_queue(mut queue: VecDeque<String>) {
    while let Some(job) = queue.pop_front() {
        println!("job={job}");
    }
}

Container tradeoffs:

ContainerOrderingLookupInsert patternBest fit
Vec&lt;T&gt;Insertion orderO(1) index, O(n) searchFast push at endDense sequences
VecDeque&lt;T&gt;Queue orderO(1) front and backFast both endsQueues, sliding windows
HashMap&lt;K, V&gt;Not stableAverage O(1) by keyKey-value updatesCaches, indexes
BTreeMap&lt;K, V&gt;Sorted by keyO(log n)Ordered key-value updatesDeterministic maps, ranges
HashSet&lt;T&gt;Not stableAverage O(1) membershipUnique insertsDeduplication, membership

Iterators

Iterators are lazy value producers. They do nothing until consumed.

fn even_squares(values: &[u32]) -> Vec<u32> {
    values
        .iter()
        .copied()
        .filter(|value| value % 2 == 0)
        .map(|value| value * value)
        .collect()
}

Important forms:

FormConsumes collectionItem type for Vec&lt;T&gt;Use
iter()No&TRead borrowed values
iter_mut()No&mut TMutate in place
into_iter()Yes for owned collectionTMove values out
fn uppercase_all(values: &mut [String]) {
    for value in values.iter_mut() {
        value.make_ascii_uppercase();
    }
}

Common iterator adapters:

AdapterPurpose
mapTransform each item
filterKeep matching items
filter_mapFilter and transform optional results
flat_mapExpand each item into many items
takeLimit item count
skipIgnore first items
chainConcatenate iterators
zipPair two iterators
enumerateAdd index
inspectObserve items, usually for logging
collectMaterialize into a collection
foldAccumulate manually
try_foldAccumulate with early failure
fn parse_numbers(input: &str) -> Result<Vec<i64>, std::num::ParseIntError> {
    input
        .split(',')
        .map(str::trim)
        .filter(|part| !part.is_empty())
        .map(str::parse)
        .collect()
}

collect can collect Iterator&lt;Item = Result&lt;T, E&gt;&gt; into Result&lt;Vec&lt;T&gt;, E&gt;, stopping at the first error.

Closures

Closures are anonymous functions that can capture their environment.

fn filter_by_prefix<'a>(items: &'a [String], prefix: &str) -> Vec<&'a str> {
    items
        .iter()
        .filter(|item| item.starts_with(prefix))
        .map(String::as_str)
        .collect()
}

Closure capture modes are inferred:

TraitCapture behaviorExample use
FnShared borrow, callable many timesPredicates, pure callbacks
FnMutMutable borrow, callable many timesStateful callbacks
FnOnceConsumes captured values, callable onceThread spawn, one-shot actions
fn apply_twice<F>(mut f: F) -> i32
where
    F: FnMut(i32) -> i32,
{
    let first = f(1);
    f(first)
}

Use move to force capture by value.

use std::thread;

fn spawn_worker(name: String) -> thread::JoinHandle<()> {
    thread::spawn(move || {
        println!("worker={name}");
    })
}

Practical Design Patterns

Parse, Validate, Then Store

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct ServiceName(String);

impl ServiceName {
    fn parse(raw: &str) -> Result<Self, &'static str> {
        let normalized = raw.trim().to_ascii_lowercase();

        if normalized.is_empty() {
            return Err("service name is empty");
        }

        if !normalized
            .chars()
            .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-')
        {
            return Err("service name contains invalid characters");
        }

        Ok(Self(normalized))
    }

    fn as_str(&self) -> &str {
        &self.0
    }
}

This pattern keeps validation close to construction. Downstream code can accept ServiceName instead of repeatedly validating String.

Exhaustive Domain State

enum Deployment {
    Pending,
    Running { version: String },
    Failed { version: String, message: String },
    RolledBack { from: String, to: String },
}

fn is_terminal(deployment: &Deployment) -> bool {
    matches!(
        deployment,
        Deployment::Failed { .. } | Deployment::RolledBack { .. }
    )
}

matches! is useful for boolean checks over patterns.

Deterministic Output

Use BTreeMap when stable output matters for tests, config generation, or snapshot files.

use std::collections::BTreeMap;

fn render_env(env: &BTreeMap<String, String>) -> String {
    env.iter()
        .map(|(key, value)| format!("{key}={value}"))
        .collect::<Vec<_>>()
        .join("\n")
}

Places, Values, and Evaluation Contexts

Rust expressions are evaluated in either a place-expression context or a value-expression context. This distinction explains many moves that otherwise look arbitrary.

  • A place expression identifies a memory location. Local variables, statics, dereferences, indexing, and field access are common places.
  • A value expression produces a value. Literals, arithmetic, calls, and most control-flow expressions are common values.
  • Reading a place in a value context copies the value when its type implements Copy; otherwise it moves the value when moving is legal.
  • Borrowing a place with &place or &mut place creates a reference without moving the referent.
  • Basic assignment evaluates the right operand first and the assignee place second. Most ordinary multi-operand expressions evaluate left to right, while lazy boolean and control-flow expressions are conditional. Compound assignment has type-dependent ordering and should never carry order-sensitive side effects.
let mut pair = (String::from("left"), String::from("right"));

let first = pair.0;        // Moves one field out of a local place.
let second = &pair.1;      // Borrows the remaining initialized field.
pair.1.push('!');          // Error while `second` is still used later.

println!("{first} {second}");

Partial moves are available for structs and tuples that do not implement Drop. The compiler tracks which fields remain initialized. Moving a non-Copy element directly out of values[index] is rejected because Index::index yields a borrowed place. Use remove, swap_remove, pop, into_iter, or replace the element.

Temporaries and drop scopes

A temporary normally lives until the end of its enclosing statement. Certain let initializers extend temporary lifetimes so a reference remains valid. Do not infer lifetime extension from visual similarity: it is a syntactic rule, not general escape analysis.

let borrowed: &str = &String::from("extended");
// The temporary String is lifetime-extended to the enclosing block.

let length = String::from("statement temporary").len();
// That String is dropped at the semicolon.

Edition 2024 narrows several temporary scopes. In particular, an if let scrutinee temporary is dropped before the else branch, and a block tail-expression temporary is dropped before local variables in that block. These rules reduce surprising borrow and destructor interactions.

Divergence and the Never Type

The never type ! describes computation that cannot produce a value. return, break with no reachable continuation, continue, panic!, and infinite loops diverge. A diverging expression can coerce to another type, which is why a match arm that returns early can coexist with an arm that yields a value.

fn parse_port(text: Option<&str>) -> Result<u16, std::num::ParseIntError> {
    let Some(text) = text else {
        return Ok(8080); // A let-else `else` block must diverge.
    };

    text.parse()
}

Use divergence to make control flow explicit, but avoid hiding ordinary recoverable failure behind panic! merely because ! type-checks conveniently.

Edition 2024 Language Baseline

An edition is selected per crate and changes parsing, lint migrations, and a small set of language rules. It does not select a compiler version and it does not prevent crates from different editions from linking together.

Important Edition 2024 review points include:

  • extern blocks must be written unsafe extern, making the declaration audit boundary explicit.
  • Attributes whose contracts can cause undefined behavior, including no_mangle, export_name, and link_section, use #[unsafe(...)].
  • Return-position impl Trait captures in-scope generic parameters by default. Use a precise use&lt;...&gt; bound when a public API must capture less.
  • Match ergonomics reject some previously confusing combinations of ref, ref mut, and mut under an inherited reference binding mode.
  • Cargo workspaces using Edition 2024 default to resolver version 3, which considers dependency rust-version declarations during selection.
  • cargo fix --edition performs mechanical migrations, but tests and manual semantic review are still required.

Constants, Statics, and Compile-time Evaluation

const defines a value expression that can be evaluated at compile time. static defines one storage location for the entire program. The distinction affects address identity, interior mutability, initialization, and thread safety.

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

const fn kibibytes(value: usize) -> usize {
    match value.checked_mul(1024) {
        Some(bytes) => bytes,
        None => panic!("size overflow"),
    }
}

const MAX_BODY_BYTES: usize = kibibytes(64);
static REQUESTS: AtomicU64 = AtomicU64::new(0);
static INSTANCE_NAME: OnceLock<String> = OnceLock::new();

fn record_request() {
    REQUESTS.fetch_add(1, Ordering::Relaxed);
}

Compile-time function evaluation, or CTFE, executes eligible constant expressions in the compiler. A const fn is still callable at runtime; the const qualifier means it may also run in a constant context when its operations are permitted there. A panic, overflow, out-of-bounds access, or other invalid operation reached during required constant evaluation is a compile error.

Key rules:

  • Constant evaluation cannot perform arbitrary I/O, inspect the host environment, or depend on runtime clocks.
  • A const block, const { expression }, requests compile-time evaluation and can capture in-scope generic parameters where the language permits it.
  • A shared immutable static T must be Sync because every thread can access the same location.
  • Avoid static mut. Its global aliasing obligations are difficult to uphold, and Edition 2024 denies creation of references to mutable statics by default. Use atomics, Mutex, OnceLock, or LazyLock according to the invariant.
  • Promotion can place certain constant expressions in static storage, but public APIs should not depend on incidental pointer identity.
  • Stable const generics support many array-length and dimension use cases, while more general const expressions can still face compiler restrictions. Keep compile-time arithmetic small and covered by target-version tests.

Use a build script only when generation needs filesystem, environment, native-tool, or target-discovery access. Use constants and const fn for deterministic language-level invariants.

Common Mistakes

MistakeSymptomBetter approach
Passing String when only readingCallers must allocate or give up ownershipAccept &str
Passing &Vec&lt;T&gt;API is unnecessarily specificAccept &[T]
Calling unwrap in library codePanics instead of typed failureReturn Result or Option
Using string statesInvalid combinations at runtimeUse enums
Overusing mutHarder local reasoningUse shadowing or smaller mutable scopes
Indexing strings by bytesCompile error or boundary bugsUse chars, bytes, or get
Choosing HashMap for generated filesUnstable output orderUse BTreeMap
Collecting too earlyExtra allocationKeep iterator chain lazy until needed
Hiding parsing in business logicRepeated validation and weaker invariantsUse newtypes and constructors

Production Guidance

  • Model domain states with enums instead of combinations of booleans and options.
  • Use Result for recoverable errors and panic only for violated invariants.
  • Prefer borrowed parameter types in APIs: &str, &[T], and trait bounds such as impl AsRef&lt;Path&gt;.
  • Use Vec by default for ordered sequences, then switch only when access patterns justify it.
  • Use deterministic containers when output must be reproducible.
  • Keep iterator chains readable. Split into named intermediate values when debugging or domain meaning improves.
  • Treat Clone as an explicit cost. Clone at API boundaries only when ownership needs are real.
  • Add tests for edge cases around parsing, empty input, duplicate keys, Unicode text, and error conversion.

Review Checklist

  • Are fallible operations represented as Option or Result instead of panics?
  • Are API inputs borrowed where ownership is unnecessary?
  • Are container choices justified by access pattern and ordering requirements?
  • Are enum matches exhaustive and intentional?
  • Are string operations UTF-8 safe?
  • Are integer overflow semantics explicit where values come from untrusted input or external systems?
  • Are iterator chains lazy until the final consumer?
  • Are unwrap and expect limited to tests, startup invariants, or clearly impossible states?
  • Are structs protecting invariants through constructors, private fields, or newtypes?
  • Are shadowed variables easy to follow and local to a narrow transformation?

Primary Sources