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).
String vs &strOption, Result, operator ?Vec i HashMapunsafe — kiedy Rust pozwala Ci sobie pomócRust (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/newnie istnieją, każdy obiekt sam się sprząta (RAII na sztywno), a kompilator odmawia zbudowania programu, jeślidelete-owałeś raz za dużo lub odczytujesz zwolnioną pamięć. Bezsmart pointerów, bez dyscypliny ekipy — po prostu się nie kompiluje. Tak działa Rust.
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).
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).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 runvscargo run --release. Rozwijasz debug → optymalizacja. W debug kod bywa 10–100× wolniejszy. Nie strasz się tym — na początku zawszecargo run.🔗 Analogia C++:
cargo=g++ -O2 ... -o app+ldd+ldconfig+doxygen+vcpkgw jednym narzędziu. I nie mamakedo pomylenia zcmake.🔗 Analogia Python:
cargo≈pip+python -m build+pytest+black+ruff.crates.io≈PyPI.Cargo.toml≈pyproject.toml.
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ć, deklarujeszmut. To celowy wybór: większość zmiennych w zdrowym kodzie i tak nie powinna się zmieniać.
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.
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:
charw Rust to 4 bajty = jeden znak Unicode ('q','💡'). Nie jest tocharz C (1 bajt). Jeśli liczy bajty — tou8. Jeśli znak — tocharlub iteracja postr::chars().
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).
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:
ifzwraca wartość tylko gdy wszystkie gałęzie zwracają ten sam typ.if x { 1 } else { "a" }= błąd typu.
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..5to nie iterator nad indeksami zVec! Działa nadRange<i32>(i innymi Iteratorami — zobacz 14).
String vs &strNajczę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):
&str (niewłaścicielski odczyt) — nie &String.&str (np. trim(), split()).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 (jakcharw C++ bez rozmiaru). Zawsze&stralboString.Stringw 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). Pythonowystrjest immutable — w RustStringjest mutable.🔗 Analogia C++:
&str≈std::string_view.String≈std::string. I dokładnie jakstring_view,&strmusi żyć na tyle długo, na ile do niego patrzysz (zobacz lifetimes, rozdz. 7).
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.
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.
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
}
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 przeniesieniustd::vector.🔗 Analogia Python: w Pythonie
b = a;zawsze "kopia referencji" (obie zmienne na ten sam obiekt). W Rustb = a;zabijaa(dla heap). To najważniejsza intuicyjna różnica!
Clone — świadoma kopiaGdy 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.
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 piszp = std::mem::forget(p)bez powodu — to "zapomnij o dropie" (leak, świadomy wybór; używany m.in. w FFI).
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.
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.
&mutfn 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
}
&[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
&strmoże być drogi (musi znaleźć granicę UTF-8!), i nie zawsze legalny w środku znaku (panic na złym indeksie bajta). Używajstr::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 (brakspanpozasize— aleVec::as_slice()to dokładnie to coVec::data() + size).
'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):
struct Widok<'a> { src: &'a [u8] } — 'a w struct.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,Boxna heap). Drobna subtelność; na start:"hej": &'static str,&mut x(gdyxjest wmain()) 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.
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()iT&& f()to różne rzeczy; w Rust wybór jest jawny w sygnaturze.
#[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).
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.
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));
}
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.
if let i while let — skróty matchfn 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!");
}
}
match = kontrola przepływu w RustTypowy 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+visitto 50 linii boilerplate; w Rustenum + matchto zero — kompilator pilnuje exhaustiveness.
Option, Result, operator ?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ć doNonei dostajesz TypeError w runtime); Rust wymusza w kompilatorze (musiszmatch/if let/?).
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(())
}
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++throwmoż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)+withcontext manager. Ale w Rust nie da się zignorowaćResult— kompilator wymaga obsłużenia (must_useattribute). Pythonif err: returnto konwencja; Rust to typ.
Vec i HashMapVec<T> — std::vectorfn 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) |
HashMap<K, V> — std::unordered_mapuse 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}");
}
}
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_ptrjest domyślny (move-only). W Rust wszystko jest "unique_ptr" — musisz świadomie wybrać, czy używaszRc/Arc(shared). To celowe: współdzielona własność = potencjalna data race, więc Rust wymaga decji.
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
}
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());
}
}
| 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)wymagaDisplay(nieDebug).println!("{:?}", x)wymagaDebug. 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.
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.
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")
}
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 Traitw parametrach = alias dla generyka (darmowy). W zwracanej wartości → concrete type (nie "trait object"Box<dyn ...>).
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 wymuszamovejawnie (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++[=]/[&]).
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++viewsC++23). Zero alokacji pośrednich (zero-copy dlaiter()). 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 lubcollect(). I każdy iterator konsumuje się — po jednym przejsciu nie da się z niego korzystać ponownie (w C++viewsmożna pójść jeszcze raz; w RustIteratorjest jednokierunkowy).
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();
}
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.
# 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++: 50CMakeLists.txtzfind_package; Rust: jedencargo 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).
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 jestSync, więc nie da się udostępnić go między wątkami bezMutex(brak UB, brak data race — po prostu się nie kompiluje).📌 Pułapka:
move ||wthread::spawnprzenosi dane do wątku (jak C++ capture by value). Bezmoveborrow checker blokuje kod, w którym closure żyłaby dłużej niż dane (w C++: UB; w Rust: compile error).
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:
*const T / *mut TDeref impl (np. Vec::as_ref → &[T])std::sync::atomic#[no_mangle], extern "C" (ABI)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ś napiszeint* p = (int*)"abc". Rust:unsafejest jedynym miejscem, gdzie to można zrobić.
| 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.
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.
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).
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).
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/?).
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).
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.
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).
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".
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.
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).
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.
| 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 |
// .vscode/settings.json
{
"rust-analyzer.checkOnSave": true,
"rust-analyzer.check.command": "clippy"
}
Extensions: rust-analyzer (auto-install), CodeLLDB (debug).
| 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) |
| 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) |
# 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"
}
}
delete.)&T (odczyt) i &mut T (mutacja), ale nie jednocześnie.String, Vec, HashMap nie kopiują się bez .clone().Option<T> — "może nie być" (musisz obsłużyć).Result<T, E> — "udało się albo nie" (musisz obsłużyć, ? = "przepchnij").enum + match — kontrola przepływu + algebraic types (wyczerpujące, zero-UB).class, ale zero-cost).unsafe — 1% kodu, jawne (reszta safe, bez UB).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).