← LOGBOOK LOG-454
COMPLETE · SOFTWARE ·
RUSTSYSTEMS-PROGRAMMINGOWNERSHIPBORROWINGCARGOTYPESERROR-HANDLING

Basics of Rust: Getting Started

Rust programs combine explicit types, ownership, borrowing, enums, pattern matching, and Result with Cargo's standard project workflow.

Rust gives every value one owner and checks how that value is moved, borrowed, mutated, and destroyed. The same model provides deterministic resource cleanup and prevents dangling references without a garbage collector.

create value → owner holds it → borrow or move it → owner leaves scope → drop

Syntax becomes easier once this lifetime model is established. Most compiler errors are explanations of a violated ownership, type, or thread-safety invariant rather than arbitrary restrictions.

Installing the Toolchain

rustup manages Rust toolchains and targets. Its default installation includes rustc, the compiler; Cargo, the build and package tool; and the standard library:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustc --version
cargo --version

Rust has stable, beta, and nightly release channels. Stable is the normal project default. Nightly is required only for explicitly unstable features.

rustup update stable
rustup default stable

Cargo Projects

Cargo creates a package with a manifest and a source directory:

cargo new rust-basics
cd rust-basics
cargo run
rust-basics/
├── Cargo.toml
└── src/
    └── main.rs

Cargo.toml contains package metadata and dependencies. cargo check type-checks without producing the final executable, so it is the fastest command during ordinary editing. cargo build compiles, cargo run compiles and executes, cargo test runs tests, and cargo fmt formats the source.

Bindings, Mutability, and Types

Bindings are immutable by default:

fn main() {
    let language = "Rust";
    let mut lessons = 1;
    lessons += 1;

    println!("{language}: {lessons} lessons");
}

mut permits reassignment while preserving the binding’s type. Rust often infers types, but function boundaries normally state them explicitly:

fn square(value: i32) -> i32 {
    value * value
}

The final expression has no semicolon because it is the function’s return value. Adding a semicolon turns it into a statement whose value is ().

Rust distinguishes stack-sized scalar values such as i32, bool, and char from growable owned values such as String and Vec<T>. The distinction matters because owned heap data must have a clear lifetime.

Ownership and Moves

A String owns a heap allocation. Assigning it to another variable moves that ownership:

let first = String::from("ownership");
let second = first;

println!("{second}");
// println!("{first}"); // error: first was moved

Only second will release the allocation. Invalidating first prevents both variables from freeing the same memory. Types with the Copy trait, including most small scalar values, are copied instead because duplicating them is cheap and unambiguous.

Functions follow the same rule:

fn consume(text: String) {
    println!("{text}");
}

let message = String::from("moved into the function");
consume(message);

Passing message transfers ownership. Returning it could transfer ownership back, but borrowing is usually the clearer operation when a function only needs temporary access.

Borrowing

A reference borrows a value without owning it:

fn word_count(text: &str) -> usize {
    text.split_whitespace().count()
}

let message = String::from("borrow this string");
let count = word_count(&message);
println!("{message}: {count}");

&str is a borrowed string slice. It accepts both &String and string literals without taking ownership.

Mutable borrowing is exclusive:

fn add_period(text: &mut String) {
    text.push('.');
}

let mut message = String::from("safe mutation");
add_period(&mut message);

At a given point, a value can have many immutable references or one mutable reference. This prevents one part of a program from changing data while another part assumes it is stable. The compiler ends most borrows at their last use rather than at the closing brace.

Structs and Methods

A struct gives related fields one type and one owner:

struct Note {
    title: String,
    words: usize,
    complete: bool,
}

impl Note {
    fn summary(&self) -> String {
        format!("{} — {} words", self.title, self.words)
    }

    fn finish(&mut self) {
        self.complete = true;
    }
}

&self borrows an instance immutably. &mut self borrows it mutably. A method that consumes the instance would take self by value. The receiver states the method’s ownership effect directly.

Enums and Pattern Matching

An enum represents a value that can be in one of several states:

enum Progress {
    NotStarted,
    Reading { page: u32 },
    Finished,
}

fn label(progress: &Progress) -> String {
    match progress {
        Progress::NotStarted => String::from("not started"),
        Progress::Reading { page } => format!("page {page}"),
        Progress::Finished => String::from("finished"),
    }
}

match must cover every variant. Adding a new state produces compiler errors at matches that have not decided what the state means.

Option<T> uses the same mechanism for absence:

fn first_word(words: &[String]) -> Option<&str> {
    words.first().map(String::as_str)
}

The return value is either Some(&str) or None; it cannot be dereferenced as if a word definitely exists.

Result and Propagating Errors

Recoverable operations return Result<T, E>:

use std::{fs, io};

fn load_note(path: &str) -> Result<String, io::Error> {
    let text = fs::read_to_string(path)?;
    Ok(text)
}

? returns the error to the caller or unwraps the successful value and continues. unwrap and expect instead terminate the program on an error; they are appropriate for tests and impossible states, not ordinary input failures.

A main function can return a result too:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let text = load_note("README.md")?;
    println!("{} words", word_count(&text));
    Ok(())
}

Collections and Iterators

Vec<T> owns an ordered growable sequence. Iterator adapters borrow or consume collections and build transformation pipelines:

let scores = vec![12, 7, 21, 4];

let doubled: Vec<i32> = scores
    .iter()
    .filter(|score| **score >= 10)
    .map(|score| score * 2)
    .collect();

.iter() borrows the vector, so scores remains usable. .into_iter() consumes it and yields owned values. The choice is an ownership decision, not only an iteration style.

A Complete Program

The fragments compose into a small file inspector. It borrows the path, propagates I/O errors, transforms lines through an iterator, and owns the returned report:

use std::{env, fs, io};

struct Report {
    lines: usize,
    words: usize,
    longest_line: Option<String>,
}

fn inspect(path: &str) -> Result<Report, io::Error> {
    let text = fs::read_to_string(path)?;
    let longest_line = text
        .lines()
        .max_by_key(|line| line.len())
        .map(str::to_owned);

    Ok(Report {
        lines: text.lines().count(),
        words: text.split_whitespace().count(),
        longest_line,
    })
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = env::args()
        .nth(1)
        .ok_or_else(|| {
            io::Error::new(io::ErrorKind::InvalidInput, "usage: cargo run -- <file>")
        })?;

    let report = inspect(&path)?;
    println!("lines: {}", report.lines);
    println!("words: {}", report.words);

    match report.longest_line {
        Some(line) => println!("longest: {line}"),
        None => println!("longest: <empty file>"),
    }

    Ok(())
}
cargo run -- Cargo.toml
cargo fmt --check
cargo clippy
cargo test

The next layer is traits and generics, modules, lifetimes in returned references, closures, smart pointers, and concurrency. They extend the same invariant: types record who owns data, which access is permitted, and what failure states must be handled.

Sources