Podstawy programowania Rust

Każda sekcja to: 📖 pojęcie → 💻 działający przykład z wyjaśnieniem → 📌 pułapka. Wszędzie, gdzie to ma sens, pokazuję analogię z C++ lub Pythona — to języki, które znasz, a Rust wiele rozwiązań "podkradł" z C++, jednocześnie eliminując jej najgorsze bolączki.

Dla kogo? Dla kogoś, kto umie C++ lub Python, ale Rust to nowość. Nie tłumaczę tu, czym jest zmienna albo funkcja — od razu wchodzę w to, co w Rustcie ważne i nietypowe.

Czego nie ma w tym dokumencie — ćwiczenia "zrób sam". Są same materiały: teoria plus praktyczne wyjaśnienia na kodzie.

Stan wiedzy: październik 2026. Aktualny stabilny Rust to wersja 1.99.x, a projekty tworzone dziś używają edycji 2024 (stabilnej od Rust 1.85, luty 2025).


Spis treści

  1. Dlaczego Rust?
  2. Instalacja i pierwszy projekt
  3. Zmienne, typy, funkcje — podstawy w Rust
  4. Łańcuchy znaków: String vs &str
  5. Ownership — serce Rusta
  6. Referencje i pożyczanie (borrowing)
  7. Lifetimes — krótko i bez strachu
  8. Struktury i metody
  9. Enumy i dopasowanie wzorców (pattern matching)
  10. Obsługa błędów: Option, Result, operator ?
  11. Kolekcje: Vec i HashMap
  12. Trait-y — abstrakcja w Rust
  13. Generyki
  14. Zamknięcia (closures) i iteratory — "rustway" programowania
  15. Moduły i zależność na świecie (crates)
  16. Wątki i współbieżność
  17. unsafe — kiedy Rust pozwala Ci sobie pomóc
  18. Rust vs C++ vs Python — tabela porównawcza
  19. Najczęstsze błędy początkującego (C++/Python → Rust)
  20. Narzędzia i zasoby do nauki

1. Dlaczego Rust?

Rust (2010, Graydon Hoare; dziś rozwijany w otwartej społeczności Rust Foundation) powstał, żeby łączyć trzy cechy, których nie da się połączyć w jednym z Twoich znanych języków:

Wymaganie C++ Python Rust
Prędkość bliska metala ✅ ❌ ✅
Bez garbage collector-a ✅ ❌ ✅
Brak use-after-free / double-free bez pracy programisty ❌ (tylko z smart pointerami i dyscypliną) ✅ (GC) ✅ (kompilator, za darmo)
Bez wyścigów danych (data races) ❌ (UB) częściowo ✅ (GIL) ✅ (gwarancje typów Send/Sync)
Zero-cost abstractions ✅ ❌ ✅

Rozwiązanie problemu? System ownership + borrowing + borrow checker: kompilator śledzi, kto "władnie" nad daną i kiedy dana umiera. Wszystko w czasie kompilacji — zero kosztu w runtime. C++ dostaje ten komfort po fakcie (UB = undefined behavior, crash w produkcji). Python płaci za to GIL-em i pamięcią.

Zasada pierwsza Rust: kompilator mówi Ci o błędach pamięci zanim uruchomisz program. Nie "może" być błąd — jest sprawdzony. Cena: uczysz się myśleć o przepływie własności danych.

Gdzie Rust już wygrywa (stan 2026): jądrowe komponenty Linuksa (io_uring, TLS), przeglądarka (Firefox, WebKit, części Chrome), Node.js i Deno, Docker, Nginx (moduły), tooling (ripgrep, fd, bat, fd, exa, alacritty, helix), baza danych (SQLite — FFI, Postgres extensions), WASM, AI inference (tokenizers, llama.cpp-style ekosystem).

🔗 Analogia C++: wyobraź sobie C++, w którym delete/new nie istnieją, każdy obiekt sam się sprząta (RAII na sztywno), a kompilator odmawia zbudowania programu, jeśli delete-owałeś raz za dużo lub odczytujesz zwolnioną pamięć. Bez smart pointerów, bez dyscypliny ekipy — po prostu się nie kompiluje. Tak działa Rust.


2. Instalacja i pierwszy projekt

2.1. rustup

Jedno narzędzie = całe środowisko (kompilator + cargo + dokumentacja offline):

# Windows (PowerShell, w skrócie):
# https://rustup.rs — pobierz rustup-init.exe i uruchom
rustup-init.exe -y

# sprawdzenie
rustc --version
# rustc 1.99.1 (...)

cargo --version
# cargo 1.99.1
# Linux/macOS (skróconie):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

rustup update stable aktualizuje, rustup toolchain list pokazuje wersje (jest też nightly, jeśli kiedyś chcesz eksperymentów).

2.2. Pierwszy projekt — Cargo

Cargo to menedżer pakietów + build system + narzędzie testów. Zastępuje CMake + autotools + apt i pip + setup.py w jednym. Nowy projekt:

cargo init hello
cd hello
cargo run

Struktura, którą dostajesz (Cargo tworzy to za Ciebie — nigdy nie pisać rekompilacji ręcznie):

hello/
├── Cargo.toml         ← manifest projektu (jak package.json / CMakeLists)
├── src/
│   └── main.rs        ← kod źródłowy
└── target/            ← artefakty builda (tutaj trafia binarka)

Cargo.toml (generowany):

[package]
name = "hello"
version = "0.1.0"
edition = "2024"        # domyślnie od Rust 1.85 (luty 2025)

[dependencies]          # puste na start

src/main.rs:

fn main() -> Result<(), std::io::Error> {
    println!("Witaj, Rust!");
    Ok(())
}

Uwagi praktyczne:

  • cargo run kompiluje i uruchamia w jednym kroku.
  • cargo build --release — wersja zoptymalizowana (domyślnie budujesz -dev, szybki build, wolny kod — jak -O0).
  • Do zapamiętania: każdy plik w src/ to osobny moduł, a main.rs to korzeń modułowego drzewa.
  • cargo add serde — dodaje zależność (w 2026 to standard zamiast edycji Cargo.toml ręcznie).

📌 Pułapka: cargo run vs cargo run --release. Rozwijasz debug → optymalizacja. W debug kod bywa 10–100× wolniejszy. Nie strasz się tym — na początku zawsze cargo run.

🔗 Analogia C++: cargo = g++ -O2 ... -o app + ldd + ldconfig + doxygen + vcpkg w jednym narzędziu. I nie ma make do pomylenia z cmake.

🔗 Analogia Python: cargo ≈ pip + python -m build + pytest + black + ruff. crates.io ≈ PyPI. Cargo.toml ≈ pyproject.toml.


3. Zmienne, typy, funkcje — podstawy w Rust

3.1. Zmienne: domyślnie nieruchome

Kluczowa różnica od C++/Python:

fn main() {
    let x = 5;
    // x = 6;             // ❌ COMPILE ERROR: cannot assign to immutable variable

    let mut y = 5;        // "mut" = zmienna (mutable)
    y = 6;                // ✅

    let z = y * 2;
    println!("{x} {y} {z}");   // inline named args, jak C++20
}

🔗 Analogia C++: let x = 5; ≈ const int x = 5; — domyślnie const, a nie domyślnie zmienna jak w Twoich głowach. Jeśli chcesz zmienić, deklarujesz mut. To celowy wybór: większość zmiennych w zdrowym kodzie i tak nie powinna się zmieniać.

3.2. Shadowing — nie mutacja, tylko nowa zmienna

fn main() {
    let s = String::from("krótki");
    let s = s.to_uppercase();        // ✅ nowa zmienna s, inny typ!
    println!("{s}");                // KRÓTKI

    let x = 2;
    let x = x + 5;                  // ✅
    let x = "text";                 // ✅ nawet inny typ
}

Różnica od let mut x — przy shadowingu stara zmienna umiera, nowa żyje. Dzięki temu możesz np. rozszerzyć typ (String → String po to_uppercase()) bez mut.

3.3. Typy podstawowe

Rust ma statyczne, silne typowanie, dużo bliższe C++ niż Python. Co się mieści w jakich bajtach (w 64-bit):

T Rozmiar Analogia C++
i8, i16, i32, i64, i128, isize 1/2/4/8/16/wskr. int8_t, ... ptrdiff_t
u8, u16, u32, u64, u128, usize podobnie uint8_t, ... size_t
f32, f64 4 / 8 float, double
bool, char 1 / 4 bool / char32_t (⚠️ char w Rust = znak Unicode, nie bajt!)

Domyślne typy liczb: i32 i f64 (nie i64/i128 jak C++ long long!).

fn main() {
    let a: i32 = 10;              // jawnie
    let b = 10;                   // wnioskowany: i32
    let c: i64 = 10;              // jawnie
    let pi = 3.14;                // f64

    let tup = (1, "a", 2.0);      // krotka (jak C++ std::tuple)
    let arr = [1, 2, 3, 4, 5];    // tablica: stały rozmiar, wartość na stacku
    let vec = vec![1, 2, 3];      // wektor: dynamiczny, heap — jak std::vector

    println!("{} {} {} {}", arr.len(), vec[0], tup.1, arr[2]);
}

📌 Pułapka: char w Rust to 4 bajty = jeden znak Unicode ('q', '💡'). Nie jest to char z C (1 bajt). Jeśli liczy bajty — to u8. Jeśli znak — to char lub iteracja po str::chars().

3.4. Funkcje

Składnia bliska C++/Python (typy argumentów przed nazwą, zwracany typ po nawiasach):

fn add(a: i32, b: i32) -> i32 {
    a + b                          // ostatni wyrażenie = zwracany wynik
}                                  // "return" niechętne: nie pisz go bez potrzeby

fn greet(name: &str) {            // nic nie zwraca → brak "->"
    println!("Cześć, {name}!");
}

fn main() {
    let s = add(3, 4);            // s: i32 = 7
    greet("świecie");
}

Rusty style: ostatnia linia bez ; = wynik funkcji. return; jest dozwolony, ale idiomatic Rust go unika (zobacz 3.5).

3.5. if to WYRAŻENIE (jak w C++17 switch, nie jak w Pythonie)

fn znak(stopnia: f32) -> char {
    if stopnia >= 5.0 { 'P' } else { 'Z' }        // wartość = znak
}

fn opis(ok: bool) -> String {
    let s = if ok { "udało się" } else { "nie" };   // String
    String::from(s)
}

fn main() {
    let n = 10;
    if n > 0 {         // ✅ bool
        println!("dodatni");
    }
    // if 10 { }       // ❌ if nie przyjmuje liczb (nie ma "prawdziwości" jak w C/Python)
}

📌 Pułapka: if zwraca wartość tylko gdy wszystkie gałęzie zwracają ten sam typ. if x { 1 } else { "a" } = błąd typu.

3.6. Pętle

fn main() {
    let mut suma = 0;
    for i in 0..10 {                    // 0,1,...,9 (10 nie wchodzi, jak Python range)
        suma += i;
    }
    println!("{suma}");                 // 45

    for i in (0..10).rev() {            // 9,8,...,0
        print!("{i} ");
    }

    let mut count = 0;
    while count < 5 {                    // jak C++
        count += 1;
    }

    loop {                               // nieskończona, jak C++/Python "while True"
        println!("raz");
        break;                           // wyjście
    }

    // pętla może ZWRACAĆ WARTOŚĆ (jak C++ goto, ale sensownie):
    let wynik = loop {
        let n = 42;
        if n % 3 == 0 { break n * 2; }   // break z wartością
    };
    println!("wynik={wynik}");           // 84
}

📌 Pułapka: for i in 0..5 to nie iterator nad indeksami z Vec! Działa nad Range<i32> (i innymi Iteratorami — zobacz 14).


4. Łańcuchy znaków: String vs &str

Najczęstszy ból początkującego i jeden z głównych źródeł pytań na Reddit/StackOverflow. Są dwa typy:

Typ Co to Analogia C++
String własny, heap, mutacyjny, UTF-8 std::string
&str slice (podzbiór) str — tylko wskaźnik + długość const char* + size (nie char*)
fn main() {
    let a: String = String::from("Witaj");    // heap, własny
    let b: String = "Witaj".to_string();      // lit → String

    let c: &str = "Witaj";                    // stack/ro, tylko "widok"

    a.push_str("!");                          // ✅ mutacja Stringa
    // c.push_str("!");                        // ❌ &str jest immutable
    println!("{a} {c}");

    // String → &str (darmowo, "spada" do widoku):
    let widok: &str = &a;
    println!("{widok}");
}

Zasada praktyczna (idiom Rust):

  • Funkcje przyjmują &str (niewłaścicielski odczyt) — nie &String.
  • Metody, które zwracają podstring, zwracają &str (np. trim(), split()).
  • Twórca danych (struct) trzyma String.
fn show(s: &str) {   // ✅ przyjmuje &String i &str — uniwersalnie
    println!("{s}");
}
fn main() {
    let a = String::from("hej");
    show(&a);          // ✅
    show("hej");       // ✅
}

📌 Pułapka: str (bez &) to non-Sized type — nie możesz go trzymać bezpośrednio (jak char w C++ bez rozmiaru). Zawsze &str albo String. String w Rust nie ma null-terminatora, nie jest C-string — FFI z C wymaga konwersji (as_c_str()).

🔗 Analogia Python: &str ≈ widok na string (jak memoryview) — tylko "patrzysz", nie "własisz". String ≈ bytes/str, ale możesz zmieniać (push, insert). Pythonowy str jest immutable — w Rust String jest mutable.

🔗 Analogia C++: &str ≈ std::string_view. String ≈ std::string. I dokładnie jak string_view, &str musi żyć na tyle długo, na ile do niego patrzysz (zobacz lifetimes, rozdz. 7).


5. Ownership — serce Rusta

Tu zostawiasz myślenie "wszystko leży w heapie, GC się posprząta". W Rust kompilator śledzi, kto jest właścicielem — i tylko właściciel decyduje o śmierci obiektu.

5.1. trzy zasady (wszystko inne to konsekwencje)

  1. Każda wartość ma dokładnie jednego właściciela (one owner).
  2. Właściciel jest jeden w danym momencie (one at a time).
  3. Kiedy właściciel wychodzi ze scope, wartość jest drop-owana (RAII deterministycznie).

To właśnie RAII z C++, ale egzekwowane na sztywno przez system typów — nie możesz "zapomnieć" o delete, bo nie ma delete.

5.2. Move semantics (domysły dla heap)

fn take(v: Vec<i32>) {        // bierze właściciela (move)
    println!("{:?}", v);
}

fn main() {
    let v = vec![1, 2, 3];
    take(v);                  // ✅ v zostaje przeniesione do take()
    // println!("{:?}", v);   // ❌ ERROR: use of moved value: `v`
}

Dlaczego tak, a nie copy? Gdyby Rust skopywał zawartość heapu (3 liczby) przy każdym let v2 = v, koszty byłyby nieakceptowalne dla String, Vec, HashMap. Rust mówi: "przenoszę właściciela (wskaźnik+size+capacity), stary właściciel nie istnieje więcej".

Efekt: double-free niemożliwy (bez shared_ptr), use-after-free niemożliwy (stary właściciel nie może być użyty po move).

// ❌ To w C++ byłoby UB / use-after-free:
fn f(x: &Vec<i32>) {
    drop(x);
    println!("{:?}", x);      // Rust: compile error
}

5.3. Copy — małe typy kopiują się "za darmo"

Wartości, które mieszczą się na stacku i mają prostą semantykę, implementują automatycznie Copy (i Clone):

fn show(x: i32) { println!("{x}"); }

fn main() {
    let a = 5;
    show(a);                 // ✅ a i tak żyje — i32 kopiowany
    println!("{a}");         // ✅

    let t = (1, 2, 3);
    let t2 = t;             // ✅ krotka z typami Copy → kopiowana
    println!("{:?}", t);    // ✅

    let v = vec![1, 2];
    let v2 = v;             // ⚠️ MOVE (Vec jest na heap)
    // println!("{:?}", v); // ❌ v już nie żyje
    println!("{:?}", v2);
}

Kto jest Copy: i8…i128, u8…u128, f32, f64, bool, char (dopóki nie trzymasz &String w krotce).

🔗 Analogia C++: i32 ≈ int (kopia = ruch = ten sam koszt), Vec<T> ≈ std::vector (move = steal buffer, drop stary). W C++ musisz pisać std::move; w Rust move jest domyślny dla nie-Copy typów — dokładnie tak, jak C++17 po przeniesieniu std::vector.

🔗 Analogia Python: w Pythonie b = a; zawsze "kopia referencji" (obie zmienne na ten sam obiekt). W Rust b = a; zabija a (dla heap). To najważniejsza intuicyjna różnica!

5.4. Clone — świadoma kopia

Gdy wiesz, że chcesz skopiować heap, rób to explicitnie:

let a = String::from("hej");
let b = a.clone();         // ✅ pełna kopia — droższa, ale oczywista
// let b = a;              // ❌ a umiera

Idiom: kod Rust wygląda tak, że wie, kiedy kopiuje. Brak niespodzianek, brak ukrytych deep-copy.

5.5. Drop — kto sprząta, kiedy

drop = deterministyczny destruktor (jak ~T() w C++), wywoływany przy wychodzeniu ze scope właściciela:

struct Plik {
    nazwa: String,
}

impl Drop for Plik {
    fn drop(&mut self) {
        println!("Zamknięcie pliku {}", self.nazwa);
    }
}

fn otwórz() {
    let p = Plik { nazwa: String::from("log.txt") };
    // ... praca ...
}                        // ← TU wywołuje się p.drop() — deterministycznie
                        //    (nie "kiedyś" jak w C++)

fn main() {
    otwórz();
    println!("main dalej");
}

W C++ destruktor wchodzi "po drodze" (RAII). W Rust: właśnie to, ale bez new/delete, bez UB, bez double-free — bo ownership jest jeden.

📌 Pułapka: drop(p) w środku funkcji = natychmiastowe wywołanie destruktorów, nie "po koniec scope". I nie pisz p = std::mem::forget(p) bez powodu — to "zapomnij o dropie" (leak, świadomy wybór; używany m.in. w FFI).


6. Referencje i pożyczanie (borrowing)

Move byłby morderczy w praktyce (każdy odczyt zabija dane). Dlatego mamy pożyczanie — & (borrow):

fn dl(s: &String) {      // pożyczam — nie biorę właściciela
    println!("{s:len}"); // patrzę
}                        // kończę patrzyć — String wraca do właściciela

fn main() {
    let s = String::from("hello");
    dl(&s);              // ✅ pożyczam
    println!("{s}");     // ✅ nadal mój — nie było move
}

To dokładnie jak const T& w C++: pokazujesz dane, nie zabierasz.

6.1. dwu reguły borrowa (wszystko inne to konsekwencje)

Reguła A: DOWOLNA LICZBA referencji immutable (&T), ALE Reguła B: JEDNA referencja mutable (&mut T) — i żadnych innych. Reguła C (nie zapisana, ale ważna): referencja nie może żyć dłużej niż pożyczane dane.

fn main() {
    let mut v = vec![1, 2, 3];

    // ✅ kilka odczytów:
    let r1 = &v;
    let r2 = &v;
    println!("{r1:?} {r2:?}");
    drop(r1); drop(r2);   // koniec pożyczki (tu widać scope wprost)

    // ✅ jedna mutacja:
    let mut r = &mut v;
    r.push(4);
    println!("{v:?}");    // OK, bo r wyszła ze scope wcześniej

    // ❌ RÓWNOCZEŚNIE odczyt i mutacja:
    // let r = &v;          // odczyt żyje w dół
    // let mut w = &mut v;  // ❌ ERROR: cannot borrow `v` as mutable more than once
    // w.push(5);
}

Dlaczego tak? Gdyby r odczytywał v[0] i w zmieniał v[1], a r patrzyłby jeszcze — data race w single thread, UB w C++, segfault. Rust nie kompiluje.

6.2. Mutacja przez referencję — &mut

fn bump(x: &mut i32) { *x += 1; }      // * = deref (jak w C++)

fn main() {
    let mut n = 0;
    bump(&mut n);
    bump(&mut n);      // ✅ kolejno (nie równolegle)
    println!("{n}");   // 2
}

6.3. Slices — &[T]: "widok na część kolekcji"

fn max<'a>(s: &'a [i32]) -> &'a i32 {  // patrzę na podzbiór
    let mut best = &s[0];
    for &x in s { if x > *best { best = &x; } }
    best
}

fn main() {
    let v = vec![3, 9, 1, 7];
    println!("{} {}", *max(&v[0..3]), *max(&v));
    // 9 (z [3,9,1])   9 (cały)

    let s = "hej, świecie";
    let x: &str = &s[0..3];     // slice bajtów (UTF-8)
    println!("{x}");             // "hej"
}

📌 Pułapka: slicing na &str może być drogi (musi znaleźć granicę UTF-8!), i nie zawsze legalny w środku znaku (panic na złym indeksie bajta). Używaj str::chars() do iteracji znaków.

🔗 Analogia C++: &[T] ≈ std::span<const T> (w skrócie). &str ≈ std::string_view. Z tą różnicą, że Rust zawsze sprawdza, czy zakres mieści się w danych (brak span poza size — ale Vec::as_slice() to dokładnie to co Vec::data() + size).


7. Lifetimes — krótko i bez strachu

'a, 'b, ... — nazwy zakresów życia. W 99% kodu Rust wnioskuje je sam. Wpisujesz je tylko, gdy funkcja zwraca referencję, a kompilator nie wie, do której ze swoich wejściowych należy.

// ❌ Bez lifetimes — kompilator nie wie, czy zwracam z x czy z y:
fn najdłuższy(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

// ✅ Z lifetime 'a — "wynik żyje tak długo jak x LUB y (oba 'a)"
fn najdłuższy<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

Co to gwarantuje: wynik nie będzie dangling — jeśli x umrze, i y umrze, wynik nie istnieje. Jak C++ string_view z wyjątkiem UB: w C++ dangling string_view to crash; w Rust compile error.

Kiedy je pisało (z doświadczeniem):

  1. Struktury z referencjami: struct Widok<'a> { src: &'a [u8] } — 'a w struct.
  2. Funkcje zwracające referencje z argów.
  3. Trait'y z referencjami (zwykle wnioskowane).
struct Widok<'a> {
    dane: &'a [u8],
}
impl<'a> Widok<'a> {
    fn first(&self) -> u8 { self.dane[0] }
}
fn main() {
    let buf = [1, 2, 3];
    let w = Widok { dane: &buf };
    println!("{}", w.first());  // buf żyje dłużej niż w ✅
}

📌 Pułapka: 'static ≠ "trwały" — oznacza "żyje tak długo jak cały program" (literały, Box na heap). Drobna subtelność; na start: "hej": &'static str, &mut x (gdy x jest w main()) to nie 'static.

🔗 Analogia C++: lifetime ≈ "kto żyje dłużej: właściciel czy widok?" W C++ pytasz się sam (i UB, jeśli pytasz źle). W Rust kompilator odpowiada i blokuje zły kod.


8. Struktury i metody

struct Koto {
    imię: String,
    wiek: u8,
}

// --- konstruktor "new" (konwencja, nie magia) ---
impl Koto {
    fn new(imie: &str) -> Self {
        Koto { imię: imie.to_string(), wiek: 0 }
    }
}

// --- metody ---
impl Koto {
    fn przedstawię(&self) {                    // &self — pożyczanie (nie move)
        println!("Mam na imię {}, mam {} lat", self.imię, self.wiek);
    }
    fn urodziny(&mut self) {                   // &mut self — mutacja
        if self.wiek < 255 { self.wiek += 1; }
    }
    fn zasną(mut self) -> String {             // self — bierze właściciela (move!)
        self.urodziny();                       // mutacja, bo jestem ownerem
        std::mem::forget(self);                // unikam double-print? (tu: drop na końcu)
        String::from("zzz")
    }
}

fn main() {
    let mut k = Koto::new("Rufus");
    k.przedstawię();       // "Rufus, 0 lat"
    k.urodziny();          // 1
    k.przedstawię();       // "Rufus, 1 lat"

    let k2 = Koto { imię: String::from("Nala"), wiek: 3 };
    k2.przedstawię();
}

Wzór Rust (odpowiednik C++ this/self):

Deklaracja C++ analog Co robi
fn f(&self) void f() const odczyt, non-exclusively
fn f(&mut self) void f() mutacje
fn f(self) void f() && (move) przejmuje, obiekt "umiera"
fn f(Self::new(...)) (statyczny) konstruktor

📌 Pułapka: fn f(self) przenosi obiekt — nie użyjesz go po wywołaniu. W C++ T::f() i T&& f() to różne rzeczy; w Rust wybór jest jawny w sygnaturze.

8.1. #[derive] — kompilator dopisuje za Ciebie

#[derive(Debug, Clone, Default)]   // kompilator generuje te trait'y!
struct Konf {
    host: String,        // "debug" = println w ładnej postaci
    port: u16,            // "default" = puste wartości
}
fn main() {
    let k = Konf::default();   // host: "", port: 0
    let mut k2 = k.clone();
    k2.port = 8080;
    println!("{k:?} {k2:?}");  // host:"",port:0 }  host:"",port:8080 }
}

#[derive] = "kompilator, dopisz za mnie te implementacje". Zawsze Clone, Debug na start (oraz częściej PartialEq, Eq, Hash).


9. Enumy i dopasowanie wzorców

Enumy w Rust ≠ C enum (nie są "liczby"). To algebraic data types — jak std::variant + std::optional + std::expected w jednym, bez tagów w runtime.

9.1. Podstawy

enum Kolor { Czerwony, Zielony, Niebieski }
enum Pkt {
    Punkt(u32, u32),                 // variante z danymi
    Prosta { x1: u32, x2: u32, y: u32 }, // "struct variant"
}
fn opis(k: Kolor) -> &'static str {
    match k {
        Kolor::Czerwony => "czerwony",
        Kolor::Zielony => "zielony",
        Kolor::Niebieski => "niebieski",
    }
}
fn main() {
    let k = Pkt::Punkt(3, 4);
    match k {
        Pkt::Punkt(x, y) => println!("punkt ({x},{y})", x, y),
        Pkt::Prosta { x1, x2, y } => println!("prosta"),
    }
    println!("{}", opis(Kolor::Niebieski));
}

9.2. match — wyczerpujący (nie jak C++ switch)

fn stopień(n: i32) -> char {
    match n {
        5    => '5',
        4    => '4',
        0..=3 => '2',        // range pattern (0, 1, 2, 3)
        _    => 'Z',         // wszystko inne (wildcard)
    }
}
// ❌ brak _ = błąd: non-exhaustive match

Dzięki temu kompilator wymaga obsłużenia każdego przypadku — C++ switch bez default = UB. Rust = compile error.

9.3. if let i while let — skróty match

fn main() {
    let x: Option<i32> = Some(5);

    if let Some(n) = x {            // == match { Some(n) => ..., None => {} }
        println!("{n}");
    }

    // 2024: let-chains (stabilne w edition 2024):
    if let Some(a) = x && a > 3 {
        println!("duże!");
    }
}

9.4. Enum + match = kontrola przepływu w Rust

Typowy idiom (zamknięcie / parserzy / maszyny stanów):

enum Komenda { Czytaj(String), Napisz(String), Wyjście }

fn procesuj(c: Komenda) {
    match c {
        Komenda::Czytaj(plik) => println!("czytam {plik}"),
        Komenda::Napisz(plik) => println!("piszę {plik}"),
        Komenda::Wyjście => println!("bye"),
    }
}
fn main() { procesuj(Komenda::Czytaj("a.txt".into())); }

🔗 Analogia C++: enum + dane ≈ std::variant<std::monostate, std::pair<u32,u32>, Prosta>. match ≈ std::visit. Option<T> ≈ std::optional<T>, Result<T,E> ≈ std::expected<T,E> (C++23). W C++ variant + visit to 50 linii boilerplate; w Rust enum + match to zero — kompilator pilnuje exhaustiveness.


10. Obsługa błędów: Option, Result, operator ?

10.1. Option<T> — "może nie być"

fn max<T: std::cmp::PartialOrd>(s: &[T]) -> Option<&T> {
    let mut best = None;
    for &x in s { if best.is_none_or(|b| x > b) { best = Some(&x); } }
    best
}
fn main() {
    let v = [3, 9, 1];
    println!("{:?}", max(&v));              // Some(9)
    println!("{:?}", max(&[] as &[i32]));   // None

    // wydobycie (od najbezpieczniejszego do "niech ginie"):
    let n = max(&v);
    let n = n.expect("musi być");            // panic z wiadomością
    let n = n.unwrap_or(0);                  // wartość domyślna, gdy nie ma
    let n = n.unwrap();                      // panic, gdy nie ma
}

🔗 Analogia C++: Option<T> = std::optional<T>; .unwrap() ≈ .* na optional (UB / exception w C++, panic w Rust). expect(msg) ≈ .* z kontekstem. unwrap_or(x) ≈ value_or(x).

🔗 Analogia Python: Optional[int] = Option<i32>; None = None. Ale Python nie wymusza sprawdzenia (możesz + dodać do None i dostajesz TypeError w runtime); Rust wymusza w kompilatorze (musisz match / if let / ?).

10.2. Result<T, E> — "udało się albo nie"

use std::fs;

fn czytaj(plik: &str) -> String {
    // fs::read_to_string zwraca Result<String, std::io::Error>
    // `?` = "jeśli Err, to zwróć ten Err od razu, jeśli Ok — kontynuuj"
    let zawartość = fs::read_to_string(plik)?;   // ✅
    zawartość.to_uppercase()
}

// funkcja zwracająca Result:
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let s = czytaj("notatka.txt");
    println!("{s}");
    Ok(())
}

Idiom Rust (odpowiednik try/catch, ale jawny, typowany):

fn procesuj() -> Result<(), Box<dyn std::error::Error>> {
    let x = 10;
    let y = x.checked_div(100);        // Option<i32> = None przy /0
    let _ = y.ok_or("dzielenie przez zero")?;
    Ok(())
}

10.3. panic! — "to jest BŁĄD PROGRAMISTY"

fn main() {
    let v = vec![1, 2, 3];
    // let a = v[100];   // ❌ PANIC: index out of bounds (jak C++ UB, ale z "zwalaniem się")
                            //    ✅ v.get(100) = Some(3)

    panic!("tu jest BUG, nie błąd użytkownika");    // abort + backtrace
}

Konwencja Rust:

Sytuacja Mechanizm
Błąd użytkownika (pliku nie ma, dzielenie przez 0) Result<T, E> + ?
Nieoczekiwany błąd programisty (panic, UB — np. zły invariant) panic! / unwrap
"Może nie być" Option<T>
FFI / C library (C "błędnie" — NULL, errno) ręcznie zamieniaj na Result

📌 Pułapka: unwrap() w kodzie produkcyjnym = bomba. Kompiluje bez problemu, ale w runtime panikuje. Clippy (zob. 20) ostrzeże.

🔗 Analogia C++: Result<T,E> ≈ std::expected<T,E> (C++23) — ale w C++ throw może przelecieć przez 50 layerów, w Rust ? to jedna linia, jawna w sygnaturze (od razu wiesz, co funkcja może rzucić — jak -> Result<...> w C++).

🔗 Analogia Python: Result<T,E> ≈ return (None, err) + with context manager. Ale w Rust nie da się zignorować Result — kompilator wymaga obsłużenia (must_use attribute). Python if err: return to konwencja; Rust to typ.


11. Kolekcje: Vec i HashMap

11.1. Vec<T> — std::vector

fn main() {
    let mut v: Vec<i32> = Vec::new();     // pusty (jak std::vector)
    v.push(1); v.push(2); v.push(3);
    v[1] = 20;                            // mutacja
    println!("{:?}", v);                  // [1, 20, 3]

    // iteracja:
    for x in &v { println!("{x}"); }               // &i32 — widok, zero-copy
    for (i, x) in v.iter().enumerate() {            // (0, 1), (1, 20), ...
        println!("{i}: {x}");
    }
    for x in v { println!("{x}"); }                    // CONSUME (move) — v nie istnieje!

    // borrowing:
    let first = &v[0];                              // &i32
    println!("{first}");

    // slicing:
    let sub: &[i32] = &v[1..];                      // widok od v[1]
    println!("{sub:?}");
}

Różnice od std::vector:

std::vector<T> Vec<T>
v[i] = x copy-assign move (i x nie żyje), o ile T nie-Copy
v[2] (po push) UB (po resize) panic (w debug: assert, w release: undefined)
iteracja .begin() iterator, move by default .iter() (borrow), .iter_mut(), into_iter() (move)

11.2. HashMap<K, V> — std::unordered_map

use std::collections::HashMap;

fn main() {
    let mut konta = HashMap::new();
    konta.insert("anna", 100);
    konta.insert("bartek", 200);

    // odczyt (borrowing, Option):
    if let Some(b) = konta.get("anna") {
        println!("anna: {b}");
    }

    // modyfikacja (entry API — idiomatic):
    *konta.entry("cynia").or_insert(0) += 50;
    println!("{:?}", konta);

    // iteracja:
    for (k, v) in &konta {
        println!("{k}={v}");
    }
}

11.3. String, BTreeMap, Box, Rc, Arc — rodzinie

Ty C++ analog Kiedy używasz
String std::string zawsze, jak "własny string"
Vec<T> std::vector<T> dynamiczny, heap
HashMap<K,V> std::unordered_map hash map (domyślne)
BTreeMap<K,V> std::map sortowanie, range()
Box<T> std::unique_ptr<T> heap, rekurencyjne typy
Rc<T> std::shared_ptr<T> współdzielona własność (single-thread)
Arc<T> std::shared_ptr<T> (atomic) shared + wielowątkowa

🔗 Analogia C++: w C++ unique_ptr jest domyślny (move-only). W Rust wszystko jest "unique_ptr" — musisz świadomie wybrać, czy używasz Rc/Arc (shared). To celowe: współdzielona własność = potencjalna data race, więc Rust wymaga decji.


12. Trait-y — abstrakcja w Rust

Trait = kontrakt "ktoś może to robić". Odpowiednik C++ class z virtual + częściowo concept (C++20), ale bez vtable-kosztu (static dispatch domyślnie).

trait Zwierze {
    const RODZAJ: &'static str;          // static member (jak C++ static constexpr)
    fn głos(&self) -> String;            // metoda (wymuszenie)
    fn przywitaj(&self) -> String {      // default implementation!
        format!("Ja jestem {} — {}", Self::RODZAJ, self.głos())
    }
}
struct Kot;
struct Pies;

impl Zwierze for Kot {
    const RODZAJ: &'static str = "kot";
    fn głos(&self) -> String { "miiiaau".into() }
}
impl Zwierze for Pies {
    const RODZAJ: &'static str = "pies";
    fn głos(&self) -> String { "hau!".into() }
}

// --- dispatch statyczny (domyślny): osobna, zoptymalizowana koda pod każdy typ ---
fn zaprzyjaźnij(z: impl Zwierze) {        // "generic" na trait (C++ T: zwiazane)
    println!("{}", z.przywitaj());
}
fn main() {
    let k = Kot;
    let p = Pies;
    zaprzyjaźnij(k);       // monomorfizacja: kod Kot::przyw
    zaprzyjaźnij(p);       // i Pies::przyw
}

12.1. Dwa style dispatchu

Statyczny (impl Trait / generyki) Dynamiczny (dyn Trait)
Kiedy decyduje kompilacja runtime
Koszt zero (monomorph) vtable (1–2 indykcje)
C++ analog template <class T> virtual (vtable)
Kiedy znasz typy kolekcja różnych typów
fn main() {
    // dynamiczny: vec z różnymi typami (jak C++ vector<unique_ptr<base>>)
    let stado: Vec<Box<dyn Zwierze>> = vec![Box::new(Kot), Box::new(Pies)];
    for z in &stado {
        println!("{}", z.głos());
    }
}

12.2. Trait-y wbudowane (implementowane automatycznie)

Trait Co robi C++ analog
Debug println!("{:?}") operator<< (stream)
Display println!("{}", x) operator< (string)
Clone .clone() copy constructor
PartialEq / Eq == / != operator==
Hash klucze w HashMap std::hash<T>
Ord / PartialOrd sort, min, max operator< etc.
From / Into konwersje std::from (C++23)
Deref *x = x operator* (deref)
Iter for x in s begin()/end()
Send / Sync thread safety (brak — Rust typy!)

📌 Pułapka: println!("{}", x) wymaga Display (nie Debug). println!("{:?}", x) wymaga Debug. Różne typy, różne traity — kompilator powie, którego brakuje (przeczytaj listę traitów w błędzie, zazwyczaj jedno kliknięcie w IDE).

🔗 Analogia C++: trait ≈ abstract class + template<class T> requires, ale bez kosztu (domyślnie) i z darmową implementacją (impl Clone for ...). C++: pisać sam, albo walczyć z SFINAE / concepts w template. Rust: #[derive(Clone)] — i gotowe.


13. Generyki

fn max<T: std::cmp::PartialOrd>(s: &[T]) -> Option<&T> {
    let mut best = None;
    for &x in s { if best.is_none_or(|b| x > b) { best = Some(&x); } }
    best
}
fn main() {
    let ci = [3, 9, 1];
    let sl = ["b", "a", "c"];
    println!("{:?}", max(&ci));      // Some(9)
    println!("{:?}", max(&sl));      // Some("c")
}

Trait bound (T: PartialOrd) — wymuszenie w generyku, jak C++ requires / SFINAE. Ale Rust: wyraźne, krótkie, bez SFINAE-magic.

13.1. Generyczne struktury

struct Para<L, P> { lewa: L, prawa: P }
impl<L: std::fmt::Debug, P: std::fmt::Debug> Para<L, P> {
    fn pokaż(&self) { println!("{:?}", (self.lewa, self.prawa)); }
}
fn main() {
    let p = Para { lewa: 1, prawa: "hej" };
    p.pokaż();    // (1, "hej")
}

13.2. impl Trait — skróty (2024)

// wejściowe: "przyjął coś, co ma ten trait" (nie nazywam):
fn zaprzyjaźnij(z: impl Zwierze) { ... }

// wejściowe (w return — od 2024 można też w params):
fn losuj() -> impl Zwierze {
    if 0 == 0 { Kot } else { Pies }       // zwracany typ jest konkretny (np. Kot)
}

📌 Pułapka: impl Trait w parametrach = alias dla generyka (darmowy). W zwracanej wartości → concrete type (nie "trait object" Box<dyn ...>).


14. Zamknięcia (closures) i iteratory

14.1. Closures — jak Python lambda, ale z borrow

fn main() {
    let s = String::from("hej");
    let f = || println!("{s}");      // borrow (nie move) s
    f();
    println!("{s}");                  // ✅ s nadal żyje

    let mut v = vec![1, 2, 3];
    let g = |x: i32| { v.push(x); x };  // mut borrow v
    let wynik = g(4);
    println!("{v:?} {wynik}");          // [1, 2, 3, 4]  4

    let h = |x: i32| x * 2;                // move (nie używamy niczego z zewnątrz)
    println!("{}", h(10));
}

🔗 Analogia C++: [](...) { ... } capture, ale Rust wymusza move jawnie (domyślnie: borrow). C++: [] = capture by value, [&] = by ref — ty decydujesz. Rust: kompilator wie, co jest potrzebne, a Ty możesz wymusić move (dokładnie jak C++ [=]/[&]).

14.2. Iteratory — "rustway" programowania

Rusty kod preferuje iteratory, nie for i in 0..n (jak C++/Python):

fn main() {
    let v = vec![1, 2, 3, 4];

    // 1) suma kwadratów (Pipelines — zero pętli!):
    let suma = v.iter()
        .map(|x| x * x)
        .sum::<i32>();
    println!("{suma}");     // 30

    // 2) filtr + zbierz:
    let parzyste: Vec<i32> = v.iter()
        .filter(|x| x % 2 == 0)
        .cloned()            // &i32 → i32 (bo Vec<i32>)
        .collect();
    println!("{parzyste:?}"); // [2, 4]

    // 3) fold (reduce):
    let iloczyn = v.iter().fold(1, |acc, x| acc * x);
    println!("{iloczyn}");      // 24
}

Wzorzec Rust (odpowiednik C++ algorithm / Python itertools):

Idiom C++ analog Python analog
v.iter().map(...) std::transform map(...)
.filter(...) std::remove_if filter(...)
.collect() (brak; vec + push) list(...), dict(...)
.fold(...) std::accumulate functools.reduce
.zip(...) std::zip (C++23) zip(...)
.enumerate() (brak) enumerate(...)

🔗 Analogia C++: iterator + range-for + std::transform — ale lazy (jak C++ views C++23). Zero alokacji pośrednich (zero-copy dla iter()). C++: v.begin(), v.end() = 2 iterator; Rust: jedno iterator + trait.

📌 Pułapka: iteratory są lazy — v.iter().map(...) nie robi nic, dopóki nie wejdzie w nie pętla lub collect(). I każdy iterator konsumuje się — po jednym przejsciu nie da się z niego korzystać ponownie (w C++ views można pójść jeszcze raz; w Rust Iterator jest jednokierunkowy).


15. Moduły i zależność na świecie (crates)

15.1. Moduły w projekcie

src/
├── main.rs            // korzeń (main + mod tree)
├── lib.rs             // (opcjonalnie) biblioteta
├── db/
│   ├── mod.rs         // mod mod.rs (domyślny)
│   ├── user.rs        // mod user (auto-detected)
│   └── user/          // (albo) mod user/
│       └── id.rs

Deklaracja w main.rs:

mod db;                 // ← tworzy "drzewo" modów z pliku db/mod.rs
use db::user::Uzytkownik;            // import (jak C++ using, ale bez namespace)

fn main() {
    let u = Uzytkownik::nowy("anna");
    u.zapisz();
}

15.2. Widoczność: pub (jak C++ public, ale domyślnie private)

// db/user.rs
pub struct Uzytkownik {           // ✅ pub = widoczny poza mod
    imie: String,                 // ⚠️ private (domyślnie)
}
impl Uzytkownik {
    pub fn nowy(imie: &str) -> Self {    // ✅ publiczna konstrukcja
        Uzytkownik { imie: imie.into() }
    }
    fn wewnętrzny(&self) { ... }         // ⚠️ dostępny tylko w tym module!
}

Zasada: domyślnie private — jak C++ class (nie struct). Ekspozycja jest jawna (pub), nie domyślna.

15.3. Zależności (crates.io)

# Cargo.toml
[dependencies]
serde = { version = "1", features = ["derive"] }   # serializacja JSON
tokio = { version = "1", features = ["full"] }      # async runtime
cargo add serde --features derive    # (nowoczesny sposób, edycja Cargo.toml)
cargo update                        # aktualizacja
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct User { id: u32, name: String }
fn main() {
    let u = User { id: 1, name: "A".into() };
    let json = serde_json::to_string(&u).unwrap();     // {"id":1,"name":"A"}
    let u2: User = serde_json::from_str(&json).unwrap();
    println!("{u2:?}");
}

🔗 Analogia C++: crates ≈ vcpkg + CMakeLists + pkg-config, ale w jednym pliku i z jednou wersją (Cargo rozwiązuje wersje globalnie, nie w każdym projekcie osobno). C++: 50 CMakeLists.txt z find_package; Rust: jeden cargo add — i gotowe.

🔗 Analogia Python: cargo ≈ pip + Poetry + uv (100% w jednym). crates.io ≈ PyPI. serde ≈ dataclasses + json. tokio ≈ asyncio (ale z threadpool, GIL-free).


16. Wątki i współbieżność

Rusty gwarancje: dane nie mogą być współdzielone między wątkami, chyba że są Send; stan współdzielony jest Sync. W C++ (i Python!) to problem programisty — w Rust typów (brak UB / data race).

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

fn main() {
    // --- 1) wątki bez wspólnej danych (Send) ---
    let mut h = vec![];
    for i in 0..4 {
        h.push(thread::spawn(move || {       // move dane DO wątku
            thread::sleep(std::time::Duration::from_secs(1));
            format!("Wątek-{i} gotowy")
        }));
    }
    for x in h {
        println!("{}", x.join().unwrap());
    }

    // --- 2) współdzielona mutacja (Arc + Mutex, jak C++ shared_ptr + mutex) ---
    let shared = Arc::new(Mutex::new(0i32));
    let mut h = vec![];
    for _ in 0..4 {
        let s = Arc::clone(&shared);         // +1 ref (atomic)
        h.push(thread::spawn(move || {
            let mut num = s.lock().unwrap();  // MutexGuard (RAII, jak C++ lock_guard)
            *num += 1;
        }));
    }
    for x in h { x.join().unwrap(); }
    println!("suma={}", *shared.lock().unwrap());    // 4
}

Główne typy (C++ / Rust):

Wątki C++ Rust
Mutex std::mutex + lock_guard std::sync::Mutex + MutexGuard (RAII)
shared_ptr std::shared_ptr Arc<T> (atomic refcount)
channel brak w std (własna implementacja) std::sync::mpsc::channel() / tokio::sync::mpsc
CondVar std::condition_variable parking_lot::Condvar (std: wait/notify na Mutex)
atomic std::atomic<T> std::sync::atomic (jak C++)

🔗 Analogia C++: Arc + Mutex ≈ shared_ptr<mutex_guarded>. Różnica: w C++ dopisujesz [thread: safe] (i zapominasz w 6 miesiącach); w Rust typ mówi — Vec<i32> nie jest Sync, więc nie da się udostępnić go między wątkami bez Mutex (brak UB, brak data race — po prostu się nie kompiluje).

📌 Pułapka: move || w thread::spawn przenosi dane do wątku (jak C++ capture by value). Bez move borrow checker blokuje kod, w którym closure żyłaby dłużej niż dane (w C++: UB; w Rust: compile error).


17. unsafe — kiedy Rust daje Ci swobodę

99% Rust kodu jest safe. unsafe to opt-in do rzeczy, których borrow checker nie może sprawdzić:

fn main() {
    // --- surowe wskaźniki (C-style) ---
    let mut x = 5;
    let ptr: *mut i32 = &mut x;            // &mut → *mut (zero-cost)
    unsafe { *ptr = 7; }                    // deref — "ja wiem co robię"
    println!("{x}");                        // 7

    // --- FFI (C library) ---
    extern "C" {                        // import funkcji z C
        fn strlen(s: *const u8) -> usize;
    }
    let s = b"hej";
    let len = unsafe { strlen(s.as_ptr()) };   // 3 (bez null-terminatora!)

    // --- atomics (jak C++) ---
    use std::sync::atomic::{AtomicUsize, Ordering};
    static COUNTER: AtomicUsize = AtomicUsize::new(0);
    COUNTER.fetch_add(1, Ordering::Relaxed);
}

Kiedy Rust wymaga unsafe:

  1. FFI z C / C++ / Go
  2. Surowe wskaźniki *const T / *mut T
  3. Trait Deref impl (np. Vec::as_ref → &[T])
  4. Atomic operations w std::sync::atomic
  5. #[no_mangle], extern "C" (ABI)
  6. Unsafe block (np. w tokio pod spodem)

Gwarancje: unsafe ≠ "dowolność" — oznacza "ja gwarantuję wariant (invarianty), które borrow checker nie wie". Błąd w unsafe = UB (jak w C++, ale ograniczony do unsafe bloków, reszta kodu safe).

🔗 Analogia C++: unsafe ≈ reinterpret_cast + static_cast + goto, ale zawarte w jednym bloku, reszta kodu nie może ich dotknąć. C++: UB może być gdziekolwiek, jeśli ktoś napisze int* p = (int*)"abc". Rust: unsafe jest jedynym miejscem, gdzie to można zrobić.


18. Rust vs C++ vs Python — tabela porównawcza

Aspekt Rust C++ Python
Type system statyczny, strong statyczny, strong (ale weak w runtime) dynamiczny
Memory management ownership + RAII (darmy, kompilator) RAII + UB (runtime) GC + GIL
Data races nie możliwe przez typy UB (brak gwarancji) ograniczone (GIL)
Zero-cost abstractions ✅ ✅ ❌
Speed ≈ C++ ≈ C++ 10–100× wolniejszy
Startup ~μs ~ms ~10ms (interpret)
GC overhead 0 0 ~10–30% (jemalloc/pymalloc)
Concurrency model threads + async threads + (boost) threads (GIL) + asyncio
Error handling Result<T,E> + ? exceptions (runtime) exceptions
FFI / C interop extern "C" (proste) ABI (native) swig/cffi (bolączne)
WASM ✅ (pierwszorzędny) ✅ (emscripten) ❌
Embedded / BAREMETAL ✅ ✅ ❌
Web front-end (WASM) ✅ ✅ (C++ → JS) ❌ (Pyodide)
AI / Data science w roście (wchodzący) silnik (TensorFlow, PyTorch) dominuje
Standard library std (solidne, małe) <iostream> (solidne, duże) stdlib (duże, dynamiczne)
Build time ~10–30 s (release) ~30 s – min (C++) 0 (interpret)
Binary size ~10–100 KB ~1–100 KB interpreter 10–50 MB
"Crash" na błędach panic + backtrace (deterministic) UB (random) traceback (jawnie)
GIL brak brak ma
"Zero-copy" parsing ✅ (domyślnie) ✅ ❌

Kiedy Rust, kiedy C++, kiedy Python (2026):

Zadanie Najlepszy
AI / Data science / research Python (+ Rust pod spodem, jak numba, rust-numpy)
Silnik gry (CPU) C++ (maturity, GPU drivers)
Silnik gry (multi-platform, WASM) Rust
Serwer / API (throughput) Rust
CLI / tooling (ripgrep-like) Rust
Embedded / baremetal Rust
WASM / Web front-end Rust
Quick script / prototyp Python
Legacy C / Linux kernel C++
AI inference engine Rust (wchodzący) / C++

Zasada: Python = produkcyjny research i user experience. C++ = maturity i performance w GPU. Rust = bezpieczeństwo + performance + concurrency w jednym.


19. Najczęstsze błędy początkującego (C++/Python → Rust)

19.1. "Dlaczego mój String nie istnieje po move?"

let s = String::from("hej");
let t = s;               // MOVE (t = s)
println!("{s}");          // ❌ ERROR: use of moved value

Rozwiązanie: t = s.clone(); jeśli chcesz oba (droższe), albo &s jeśli tylko patrzysz.

19.2. "Dlaczego if 5 nie działa?"

if 5 { }                 // ❌ Rust if wymaga boola
if 5 != 0 { }            // ✅

Rozwiązanie: C/Python "pravdivost" (if 5) w Rust nie istnieje (silny typy).

19.3. "Dlaczego let x = 5; x = 6; nie działa?"

let x = 5;               // immutable (domyślnie)
x = 6;                    // ❌ ERROR
let mut x = 5;            // ✅ mut
x = 6;                    // ✅

Rozwiązanie: mut = "chcę zmienić". Domyślnie: immutable (spokoje).

19.4. "Dlaczego Option i ? są tu i ówdzie?"

let x: Option<i32> = 5;
let y = x + 3;            // ❌ ERROR: expected integer, found Option
let y = x.unwrap() + 3;   // ✅ (lub `?` w funkcji Result)

Rozwiązanie: Option i Result to typy, nie "może być" (C NULL). Musisz obsłużyć (match/if let/?).

19.5. "Dlaczego for x in v zjada v?"

let v = vec![1, 2, 3];
for x in v { ... }        // MOVE (v nie istnieje po pętli)
for x in &v { ... }       // ✅ borrowing (v nadal żyje)

Rozwiązanie: for x in &v (borrowing, zero-copy).

19.6. "Dlaczego struct S { x: i32 } nie może być kluczem w HashMap?"

use std::collections::HashMap;
let mut m: HashMap<i32, i32> = HashMap::new();
m.insert(1, 2);           // ✅ i32: Hash + Eq
// let mut m: HashMap<S, i32> = HashMap::new();  // ❌ S nie ma Hash

Rozwiązanie: #[derive(Hash, PartialEq)] na S i #[derive(Eq)] i #[derive(PartialEq)], albo rób mapę przez i32.

19.7. "Dlaczego trait ma Self?"

trait A {
    fn f(&self) -> Self;   // "zwracam swój typ" (jak C++ T -> T)
}

Rozwiązanie: Self = "typ implementujący trait" (jak C++ T w template<class T> class ... { T f(); }). Rust: nie da się użyć Self po Box<dyn A> (dynamiczny dispatch).

19.8. "Dlaczego kod z Arc nie działa w thread::spawn?"

let data = Arc::new(Mutex::new(0i32));
thread::spawn(move || {       // move data DO wątku
    data.lock().unwrap();
}).join().unwrap();

Rozwiązanie: move closure przenosi dane do wątku. Bez move → borrow checker: "closure żyje dłużej niż dane".

19.9. "Dlaczego let x = 5; let x = "text"; działa?"

let x = 5;               // i32
let x = "text";          // &str (shadowing, nowa zmienna)
// x = 5;                // ❌ nie ma mut

Rozwiązanie: shadowing ≠ mutacja. Nowa zmienna, nowy typ, nowy scope.

19.10. "Dlaczego impl Trait w return ≠ dyn Trait?"

// statyczny dispatch (konkretny typ):
fn f() -> impl Trait { Kot }      // "zwracam KOT, ale nie mówię jakiego"

// dynamiczny dispatch (trait object):
fn f() -> Box<dyn Trait> { Box::new(Kot) }   // "zwracam dowolny"

Rozwiązanie: impl Trait = konkretny typ (monomorph, zero-cost). dyn Trait = dowolny typ (vtable, runtime dispatch).

19.11. "Dlaczego unsafe nie jest tak samo niebezpieczne?"

// C++: UB może być GDZIEŚ
int* p = (int*)"abc";  // UB (UB + UB + UB...)

// Rust: UB tylko w unsafe blokach (i tylko jeśli złamałeś invarianty)
unsafe { *ptr = 7; }    // "ja gwarantuję valid, inaczej UB"

Rozwiązanie: unsafe ≠ "brak gwarancji". To "gwarancja, ale ja gwarantuję invarianty". Reszta kodu safe, nawet jeśli unsafe jest obok.


20. Narzędzia i zasoby do nauki

20.1. Stack Rust (2026)

Narzędzie Co robi Kiedy
rustc kompilator (2026: 1.99.x) zawsze
cargo build + pakety + tests zawsze
rustup zarządzanie wersjami (stable/beta/nightly) zawsze
rust-analyzer LSP (IntelliSense, autocomplete, refactor) załóż w IDE (VS Code / Neovim / Helix)
clippy "wyrzucenie błędów" (warning > err) cargo clippy -- -D warnings
rustfmt formatter cargo fmt (darmowy, auto)
cargo doc dokumentacja cargo doc --open
cargo test testy cargo test (inline!)
miri wyłapuje UB (undefined behavior) po fakcie cargo +nightly miri test
ripgrep, fd, bat "grep/find/less" (Rust) CLI (opcjonalnie)
tokio async runtime (dominujący) web / networking
serde serializacja (JSON, YAML, TOML) config / API
axum / actix web framework (Rust) API / web

20.2. IDE setup (VS Code, 2026)

// .vscode/settings.json
{
  "rust-analyzer.checkOnSave": true,
  "rust-analyzer.check.command": "clippy"
}

Extensions: rust-analyzer (auto-install), CodeLLDB (debug).

20.3. Inne IDE / edytory

Edytor Wsparcie
VS Code ✅ rust-analyzer (LSP)
Neovim ✅ rust-analyzer (LSP)
Helix ✅ rust-analyzer (LSP)
Zed ✅ rust-analyzer (LSP)
Sublime / Notepad++ ❌ (tylko składnia, braki autocomplete)

20.4. Gdzie się uczyć (2026, stan)

Zasób Poziom
The Rust Book (doc.rust-lang.org/book) początek (darmy, offline)
Rustlings (github.com/rust-lang/rustlings) ćwiczenia (małe, 1–10 min)
Rust by Example (doc.rust-lang.org/rust-by-example) przykłady (darmy)
The Rustonomicon (doc.rust-lang.org/nomicon) unsafe, FFI, typy
The Cargo Book (doc.rust-lang.org/cargo) pakety
tokio tutorial (tokio.rs/tokio/tutorial) async (przód)
Axum guide (github.com/tokio-rs/axum) web (przód)
Rust Playground (play.rust-lang.org) online IDE (darm)
Rust Discord / Reddit Q&A (aktywne)

20.5. Workflow (darmy, od zera)

# 1. instalacja
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 2. pierwszy projekt
cargo init moja-aplikacja
cd moja-aplikacja

# 3. build + run
cargo run

# 4. formatowanie + kontrola jakości
cargo fmt
cargo clippy -- -D warnings
cargo test

# 5. testy inline (w kodzie!):
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn działa() {
        assert_eq!(add(2, 3), 5);    // jak C++ assert, ale w Rust "panic"
    }
}

21. Podsumowanie — najważniejsze rzeczy (TL;DR)

  1. Ownership — 1 właściciel, 1 raz, deterministyczny drop. (Nie GC, nie delete.)
  2. Borrowing — &T (odczyt) i &mut T (mutacja), ale nie jednocześnie.
  3. Move by default — String, Vec, HashMap nie kopiują się bez .clone().
  4. Option<T> — "może nie być" (musisz obsłużyć).
  5. Result<T, E> — "udało się albo nie" (musisz obsłużyć, ? = "przepchnij").
  6. enum + match — kontrola przepływu + algebraic types (wyczerpujące, zero-UB).
  7. Trait-y — abstrakcja (C++ class, ale zero-cost).
  8. Iteratory — "rustway" programowania (lazy, zero-copy, pipelines).
  9. unsafe — 1% kodu, jawne (reszta safe, bez UB).
  10. Cargo + rust-analyzer + clippy — cały toolchain w jednym narzędziu.

Ostatnia zasada: Rust jest niekompatybilny z C++/Python w myśleniu o pamięci i własności. I tutaj jest trudny: borrow checker (na start). Po miesiącu — luz. Nie walcz z nim — zrozum (ownership + borrowing + lifetimes), a kod Rust stanie się przyjemniejszy niż C++ (brak UB, brak memory leaks, brak data races — to wszystko w typach, bez runtime).