Przejdź do treści
← wszystkie przedmioty

Rust: ownership, borrow checker i systemowa niezawodność bez GC

Głębokie, praktyczne kompendium Rust: ownership, borrow checker, lifetimes, traits, async/await z tokio, unsafe i Cargo, dla inżynierów budujących systemy produkcyjne bez garbage collectora.

TEMATY EGZAMINACYJNE W TYM PRZEDMIOCIE
  • Ownership i move semantics: jeden właściciel, deterministyczny drop, brak GC
  • Borrow checker: jedna referencja mutowalna XOR wiele niemutowalnych, non-lexical lifetimes
  • Lifetimes i trzy reguły elizji
  • Option<T>/Result<T,E> i operator ? do propagacji błędów
  • Traits: dyn (dynamic dispatch, vtable) vs generyki (static dispatch, monomorfizacja), zero-cost abstractions
  • async/await: leniwe Future, executor-agnostic design, tokio jako de facto runtime

1 · Ownership i semantyka przenoszenia (move semantics)

★ egzamin

Ownership to fundament pamięciowego modelu Rust: każda wartość ma dokładnie jednego właściciela, a kompilator statycznie, bez udziału runtime'u, decyduje kiedy pamięć zostanie zwolniona. To zastępuje zarówno ręczne malloc/free, jak i garbage collector.

Trzy zasady ownership
Każda wartość ma dokładnie jednego właściciela w danym momencie. Gdy właściciel wychodzi poza zasięg (scope), wartość jest automatycznie zwalniana (drop). Ownership można przenieść (move) lub pożyczyć (borrow), ale nie zduplikować bez jawnego .clone().
Move dla String, kopiowanie dla i32 rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // move: s1 nie jest już ważne
    // println!("{}", s1); // error[E0382]: borrow of moved value
    println!("{}", s2);

    let n1 = 5; // i32 implementuje Copy
    let n2 = n1;
    println!("{n1} {n2}"); // obie zmienne dalej ważne
}
Termin kluczowy
Move - Przeniesienie ownership przy przypisaniu lub przekazaniu wartości nie-Copy; oryginalna zmienna staje się nieważna, kompilator blokuje jej dalsze użycie statycznie.
Termin kluczowy
Copy - Marker trait dla typów o taniej, bitowej kopii (np. i32, bool, f64, krotki z samych typów Copy); przypisanie takich wartości kopiuje, nie przenosi ownership.
Pułapka
Częsty błąd: próba użycia zmiennej po przekazaniu jej do funkcji lub przypisaniu innej zmiennej, gdy typ nie implementuje Copy. Kompilator zgłasza error[E0382]: borrow of moved value. Rozwiązanie to .clone() (świadomy koszt) albo przeprojektowanie własności, np. przekazanie referencji zamiast wartości.

Move (domyślne dla typów bez Copy)

  • Przenosi ownership przy przypisaniu lub wywołaniu
  • Oryginał staje się nieważny
  • Zero kosztu w runtime, kopiowany jest tylko wskaźnik/metadane
  • Dotyczy np. String, Vec<T>, Box<T>

Copy (dla typów prostych)

  • Tworzy bitową kopię przy przypisaniu
  • Oryginał pozostaje ważny
  • Wymaga, by typ nie implementował Drop
  • Dotyczy np. i32, bool, char, krotek Copy
Częściowy move z pola struktury rust
struct Item {
    name: String,
    qty: u32,
}

fn main() {
    let item = Item { name: String::from("śruba"), qty: 10 };
    let name = item.name; // move: przenosi tylko pole name, nie cały item
    println!("{name} x{}", item.qty); // qty to Copy, wciąż dostępne mimo częściowego move
}
Fakt na test
Dlaczego to działa bez GC: kompilator wie w czasie kompilacji, w którym dokładnie miejscu kodu każda wartość przestaje być używana, więc wstawia wywołanie drop w tym miejscu deterministycznie, bez śledzenia referencji w runtime.
Mnemonik
Jedna wartość, jeden właściciel, jedno wyjście: gdy właściciel znika ze sceny, wartość gaśnie razem z nim.
  • String, Vec<T>, Box<T>, HashMap: typy zarządzające pamięcią na stercie, zawsze move
  • i32, u64, f64, bool, char: typy Copy, zawsze kopiowane
  • Tuple i array są Copy tylko jeśli wszystkie elementy są Copy
  • struct/enum trzeba jawnie oznaczyć #[derive(Copy, Clone)], by uzyskać semantykę Copy

2 · Borrow checker: reguły referencji i non-lexical lifetimes

★ egzamin

Borrow checker to statyczny analizator, który przed uruchomieniem programu udowadnia brak data races i dangling references. Cała jego logika sprowadza się do jednej reguły aliasowania, egzekwowanej wyjątkowo rygorystycznie.

Reguła XOR
W danym momencie może istnieć albo dokładnie jedna referencja mutowalna (&mut T), albo dowolna liczba referencji niemutowalnych (&T) do tej samej wartości, nigdy oba typy naraz.
Fakt na test
Druga, równie ważna reguła: referencje muszą zawsze wskazywać na ważne dane. Kompilator nie pozwoli na dangling reference, czyli referencję przeżywającą wskazywaną przez nią wartość.
Non-lexical lifetimes w praktyce rust
fn main() {
    let mut v = vec![1, 2, 3];

    let r1 = &v;
    let r2 = &v;
    println!("{r1:?} {r2:?}"); // ostatnie użycie r1, r2 tutaj

    let r3 = &mut v; // OK: dzięki NLL zasięg r1/r2 już się skończył
    r3.push(4);
    println!("{r3:?}");
}
Termin kluczowy
Non-lexical lifetimes (NLL) - Od edycji 2018, borrow checker liczy zasięg pożyczenia do ostatniego realnego użycia referencji, a nie do końca bloku leksykalnego, co pozwala na wcześniejsze ponowne pożyczenie tej samej wartości.
Pułapka
Przed NLL powyższy kod nie kompilował się, mimo że r1 i r2 nie są już używane w momencie tworzenia r3: kod semantycznie bezpieczny bywał odrzucany przez zbyt konserwatywną analizę opartą na granicach bloków. NLL naprawił to bez zmiany reguł bezpieczeństwa, tylko precyzję analizy.

Przed NLL (do edycji 2018)

  • Zasięg pożyczenia = cały blok leksykalny
  • Referencja 'żyła' aż do zamykającej klamry
  • Częste fałszywe alarmy kompilatora

Po NLL (2018+)

  • Zasięg pożyczenia = do ostatniego realnego użycia
  • Wcześniejsze zakończenie pożyczenia = szybsze ponowne pożyczenie
  • Ten sam poziom bezpieczeństwa, mniej tarcia w codziennym kodzie
Typ referencjiSymbolLiczba narazModyfikacja danych
Niemutowalna (shared)&Tdowolnie wielenie
Mutowalna (exclusive)&mut Tdokładnie jednatak
Fakt na test
Reguła XOR eliminuje data races statycznie: nie da się mieć jednocześnie czytelnika i pisarza tej samej pamięci bez synchronizacji, więc klasyczne wyścigi danych znane z C/C++ są niemożliwe do skompilowania.
Mnemonik
Jeden pisarz i zero czytelników, albo zero pisarzy i wielu czytelników, nigdy pisarz z czytelnikiem naraz.
  • Referencje nie mogą przeżyć wartości, na którą wskazują (brak dangling references)
  • Mutowalna referencja daje wyłączny dostęp, wyklucza wszystkie inne referencje w tym samym czasie
  • Wiele referencji niemutowalnych może współistnieć bezpiecznie
  • NLL analizuje faktyczny przepływ sterowania, nie tylko strukturę bloków kodu

3 · Lifetimes i reguły elizji

★ egzamin

Lifetime to nie mechanizm wydłużający czas życia wartości. To adnotacja opisująca relacje między czasem życia różnych referencji, którą kompilator wykorzystuje do wykrywania dangling references w typach zwracanych z funkcji.

Termin kluczowy
Lifetime ('a) - Nazwany parametr opisujący, że dana referencja musi żyć co najmniej tak długo jak inna powiązana z nią referencja; nie wpływa na realny czas życia wartości w runtime, istnieje wyłącznie na etapie kompilacji.
Jawny lifetime wiążący dwa parametry wejściowe z wynikiem rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Fakt na test
Reguły elizji pozwalają pominąć jawne adnotacje lifetime w większości sygnatur funkcji, kompilator wnioskuje je automatycznie według trzech deterministycznych reguł.
  1. 1 Reguła 1 Każdy elidowany parametr wejściowy referencyjny dostaje własny, świeży parametr lifetime.
  2. 2 Reguła 2 Jeśli istnieje dokładnie jeden parametr wejściowy z lifetime, ten sam lifetime jest przypisywany wszystkim elidowanym lifetime'om wyjściowym.
  3. 3 Reguła 3 Jeśli wśród parametrów jest &self lub &mut self, lifetime self jest przypisywany wszystkim elidowanym lifetime'om wyjściowym.
Elizja reguły 3: lifetime self przenosi się na wynik rust
struct Parser<'a> {
    input: &'a str,
}

impl<'a> Parser<'a> {
    fn remaining(&self) -> &str {
        self.input
    }
}
Termin kluczowy
'static - Specjalny lifetime oznaczający, że referencja (lub dane) może żyć przez cały czas trwania programu. Literały napisowe &'static str mają go domyślnie, ale 'static nie oznacza 'utworzone na starcie programu', tylko 'zdolne przeżyć wszystko'.
Pułapka
Częsty błąd początkujących: dodanie 'static jako szybkiej łatki na błąd lifetime zamiast zrozumienia realnej relacji czasów życia. To zwykle tylko przesuwa problem albo wymusza niepotrzebne klonowanie lub przeciek pamięci.
Fakt na test
Jawne lifetime są wymagane, gdy struktura przechowuje referencję: struct nie może istnieć dłużej niż dane, do których się odwołuje, więc kompilator wymaga adnotacji w definicji struct.
  • Elizja działa tylko dla funkcji i metod, nie dla definicji struct
  • Kompilator nigdy nie zgaduje, gdy wynik jest niejednoznaczny, wtedy wymaga jawnej adnotacji
  • Lifetime to relacja, nie wartość, nie istnieje w skompilowanym kodzie maszynowym
Mnemonik
Lifetime nie przedłuża życia, tylko udowadnia, że jedno życie mieści się w drugim.

4 · Option<T>, Result<T,E> i operator ?

★ egzamin

Rust nie ma null. Brak wartości i błędy są częścią systemu typów, nie wyjątkiem w runtime, dzięki dwóm enumom z biblioteki standardowej: Option<T> i Result<T, E>.

Definicje
enum Option<T> { Some(T), None } koduje obecność lub brak wartości. enum Result<T, E> { Ok(T), Err(E) } koduje sukces z wartością T lub błąd z wartością E.
? propagujący Result rust
use std::num::ParseIntError;

fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
    let n: i32 = s.parse()?; // propaguje ParseIntError wcześnie
    Ok(n * 2)
}
? propagujący Option rust
fn first_char_upper(s: &str) -> Option<char> {
    let c = s.chars().next()?; // propaguje None wcześnie
    Some(c.to_ascii_uppercase())
}
Termin kluczowy
Operator ? - Składniowy skrót propagacji błędu: przy Err(e) natychmiast zwraca Err(From::from(e)) z bieżącej funkcji, przy Ok(v) wyłuskuje v. Działa analogicznie dla Option, gdzie None propaguje None.
Pułapka
? działa tylko wewnątrz funkcji zwracającej Result (lub Option, lub inny typ implementujący Try). fn main() domyślnie zwraca (), więc użycie ? w main wymaga zmiany sygnatury na np. fn main() -> Result<(), Box<dyn std::error::Error>>.
  • map/and_then: transformacja wartości wewnątrz Some/Ok bez ręcznego rozpakowywania
  • unwrap_or/unwrap_or_else: wartość domyślna przy None/Err
  • ok_or: konwersja Option na Result z podanym błędem
  • unwrap/expect: wyłuskanie z panic! przy None/Err, tylko do prototypowania

unwrap() / expect()

  • Panikuje (crash) przy None/Err
  • Szybkie do prototypów i testów
  • W kodzie produkcyjnym to code smell

Operator ?

  • Propaguje błąd w górę stosu wywołań
  • Wymaga typu zwracanego Result/Option
  • Idiomatyczny sposób obsługi błędów w produkcji
Fakt na test
Brak null eliminuje całą klasę błędów runtime (null pointer dereference, tzw. billion-dollar mistake Tony'ego Hoare'a), przenosząc obsługę braku wartości do czasu kompilacji.
Mnemonik
Some albo None, Ok albo Err, nigdy cichy null w tle.

5 · Traits: dyn vs generyki, dispatch i zero-cost abstractions

★ egzamin

Traits definiują współdzielone zachowanie, podobnie do interfejsów, ale Rust daje dwa fundamentalnie różne sposoby ich użycia w czasie wykonania: monomorfizowane generyki i dynamiczne dyn Trait.

Termin kluczowy
Trait - Zbiór sygnatur metod definiujących zachowanie, które typ może zaimplementować przez impl Trait for Type; odpowiednik interfejsu, ale z możliwością domyślnych implementacji metod.
Definicja traitu i implementacja rust
trait Shape {
    fn area(&self) -> f64;
}

struct Circle { radius: f64 }
impl Shape for Circle {
    fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
}
Static dispatch: monomorfizacja per konkretny typ rust
fn print_area<T: Shape>(shape: &T) {
    println!("{}", shape.area());
}
Dynamic dispatch: jedna kopia kodu, wywołanie przez vtable rust
fn print_area_dyn(shape: &dyn Shape) {
    println!("{}", shape.area());
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle { radius: 2.0 })];
    for s in &shapes {
        print_area_dyn(s.as_ref());
    }
}

Generyki (static dispatch)

  • Kompilator generuje osobną kopię kodu per konkretny typ (monomorfizacja)
  • Zero narzutu w runtime, pełny inlining
  • Większy rozmiar binarki przy wielu instancjacjach
  • Typ musi być znany w czasie kompilacji

dyn Trait (dynamic dispatch)

  • Jedna kopia skompilowanego kodu, wywołanie przez vtable
  • Mały, stały narzut jednego wskaźnikowego skoku, brak inliningu
  • Mniejsza binarka, pozwala na heterogeniczne kolekcje jak Vec<Box<dyn Trait>>
  • Konkretny typ może być nieznany w czasie kompilacji
Zero-cost abstraction
Zasada przyjęta z C++ (Bjarne Stroustrup): czego nie używasz, za to nie płacisz; czego używasz, nie napiszesz ręcznie wydajniej. Generyki, iteratory i closures w Rust kompilują się do kodu porównywalnego z ręcznie pisanym imperatywnym odpowiednikiem.
Pułapka
Nie każdy trait można użyć jako dyn Trait. Wymóg object safety: metody nie mogą mieć parametrów generycznych ani zwracać Self (poza wyjątkami jak Box<Self>), inaczej kompilator odrzuci dyn Trait błędem 'trait cannot be made into an object'.
Fakt na test
Monomorfizacja dzieje się w czasie kompilacji: każda konkretna instancjacja generyku (np. print_area::<Circle> i osobno dla innego typu) generuje osobny fragment kodu maszynowego, co umożliwia agresywny inlining kosztem rozmiaru binarki.
  • Wybierz generyki, gdy typ jest znany statycznie i liczy się maksymalna wydajność
  • Wybierz dyn Trait, gdy potrzebujesz heterogenicznej kolekcji lub chcesz ograniczyć rozrost binarki
  • Można łączyć oba podejścia w jednym programie bez konfliktu

6 · Enumy, pattern matching, RAII i brak garbage collectora

Enumy w Rust to algebraiczne typy danych: każdy wariant może nieść własne dane, a match wymusza wyczerpującą obsługę wszystkich przypadków. To samo ownership, które zarządza pamięcią, napędza też deterministyczne czyszczenie zasobów przez trait Drop.

Termin kluczowy
Enum z danymi (algebraic data type) - Typ, którego wartość jest dokładnie jednym z kilku wariantów, z których każdy może przechowywać różne, nazwane lub pozycyjne dane, np. Message::Move { x, y } albo Message::Write(String).
match z guardem na enumie z danymi rust
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(u8, u8, u8),
}

fn process(msg: &Message) {
    match msg {
        Message::Quit => println!("quit"),
        Message::Move { x, y } if *x == *y => println!("diagonal move to {x},{y}"),
        Message::Move { x, y } => println!("move to {x},{y}"),
        Message::Write(text) => println!("write: {text}"),
        Message::ChangeColor(r, g, b) => println!("color: #{r:02x}{g:02x}{b:02x}"),
    }
}
Fakt na test
match musi pokryć wszystkie warianty typu (lub jawny wildcard `_`), inaczej kompilator zgłasza błąd 'non-exhaustive patterns'. To eliminuje klasę błędów 'zapomniałem obsłużyć przypadek', częstą przy switch w innych językach.
  • if let Some(x) = opt { ... }: dopasowanie jednego wariantu bez pełnego match
  • while let Some(x) = stack.pop() { ... }: pętla dopóki wzorzec pasuje
  • wzorzec_a | wzorzec_b: dopasowanie alternatywy (OR-pattern)
  • n @ 1..=5: związanie zmiennej z jednoczesnym sprawdzeniem zakresu
Termin kluczowy
RAII (Resource Acquisition Is Initialization) - Wzorzec, w którym pozyskanie zasobu (pamięć, plik, blokada) wiąże się z inicjalizacją obiektu, a zwolnienie zasobu następuje automatycznie w destruktorze przy końcu jego zasięgu.
RAII przez trait Drop rust
struct FileGuard {
    name: String,
}

impl Drop for FileGuard {
    fn drop(&mut self) {
        println!("closing {}", self.name); // automatyczne, bez GC
    }
}

fn main() {
    let _a = FileGuard { name: "a.txt".into() };
    let _b = FileGuard { name: "b.txt".into() };
    println!("working...");
} // kolejność drop: najpierw _b, potem _a
Fakt na test
Wartości są zwalniane (drop) w kolejności odwrotnej do deklaracji w danym zasięgu: ostatnia zadeklarowana wartość jest niszczona jako pierwsza, analogicznie do stosu wywołań.
Pułapka
Nie można wywołać x.drop() bezpośrednio, to błąd kompilacji E0040 (explicit use of destructor method). Trzeba użyć std::mem::drop(x), zwykłej funkcji przejmującej ownership i kończącej zasięg wartości natychmiast.
Fakt na test
Brak garbage collectora wynika wprost z ownership: kompilator statycznie wylicza dokładny punkt w kodzie, w którym każda wartość przestaje być używana, więc zwolnienie pamięci jest deterministyczne i nie wymaga żadnego mechanizmu trasującego działającego w runtime.
Mnemonik
Ostatni wszedł, pierwszy wyjdzie: drop działa jak stos, nie jak kolejka.

7 · async/await i tokio: wykonawcy niezależne od języka

★ egzamin

async/await w Rust to składnia nad leniwym trait Future: sam język i standardowa biblioteka nie narzucają, jak i gdzie future zostanie wykonany, tę odpowiedzialność przejmuje zewnętrzny executor, w praktyce niemal zawsze tokio.

Fakt na test
Funkcja oznaczona async fn nie wykonuje swojego ciała od razu, zwraca wartość implementującą impl Future<Output = T>. Future jest odpytywane (polled) przez executor, dopóki nie zwróci Poll::Ready.
async fn, #[tokio::main] i równoległe oczekiwanie rust
use tokio::time::{sleep, Duration};

async fn fetch(id: u32) -> u32 {
    sleep(Duration::from_millis(10)).await;
    id * 2
}

#[tokio::main]
async fn main() {
    // futures są leniwe: nic się nie dzieje, dopóki nie zostaną odpytane
    let (a, b) = tokio::join!(fetch(1), fetch(2));
    println!("{a} {b}");
}
Termin kluczowy
Executor (runtime) - Komponent odpowiedzialny za odpytywanie (poll) futures aż do ukończenia, planowanie zadań (tasks) na wątkach oraz dostarczanie asynchronicznego I/O i timerów; std nie dostarcza własnego executora.
Fakt na test
Ten podział na 'język definiuje Future i .await' oraz 'crate dostarcza executor' nazywany jest projektem executor-agnostic: ten sam kod async może teoretycznie działać pod tokio, async-std czy smol, choć w praktyce ekosystem skonsolidował się wokół tokio.
Termin kluczowy
tokio::spawn - Funkcja planująca nowy, niezależny task na executorze tokio; task wykonuje się współbieżnie z innymi, zwraca JoinHandle do odebrania wyniku.
Pułapka
Wykonanie długiej, blokującej operacji synchronicznej (np. ciężkie obliczenia CPU-bound albo blokujące I/O) wewnątrz async fn bez tokio::task::spawn_blocking zatrzymuje cały wątek workera, wstrzymując wszystkie inne taski współdzielące ten wątek. Kooperatywne planowanie zakłada, że taski oddają kontrolę na .await.

multi_thread (domyślny)

  • Wielowątkowy scheduler typu work-stealing
  • Taski mogą migrować między wątkami
  • Lepsze wykorzystanie wielu rdzeni

current_thread

  • Jeden wątek dla całego runtime'u
  • Mniejszy narzut dla prostych/CLI programów
  • Wymaga jawnego #[tokio::main(flavor = "current_thread")]
Fakt na test
.await zawiesza wykonanie bieżącego taska bez blokowania wątku systemowego (OS thread): kontrola wraca do executora, który w tym czasie może odpytywać inne taski. To fundament wysokiej gęstości współbieżności, tysięcy tasków na garstce wątków.
  • tokio::time::sleep: nieblokujące oczekiwanie
  • tokio::select!: odpytywanie kilku futures naraz, pierwszy gotowy wygrywa
  • tokio::sync::Mutex: blokada async-aware, nie blokuje wątku podczas oczekiwania
  • tokio::join!: równoległe uruchomienie i oczekiwanie na kilka futures

8 · Cargo workspaces, feature flags i bloki unsafe

★ egzamin

Poza semantyką języka, produkcyjny Rust to też organizacja projektu (workspaces), warunkowa kompilacja (feature flags) i świadome wyjście poza gwarancje bezpieczeństwa (unsafe), gdy jest to jedyny sposób na dany poziom kontroli.

Fakt na test
Workspace grupuje wiele powiązanych pakietów (crates) pod jednym Cargo.lock i wspólnym katalogiem target, deklarowanym w sekcji [workspace] z listą members w głównym Cargo.toml.
Workspace root + opcjonalna zależność jako feature toml
# Cargo.toml (root workspace)
[workspace]
members = ["core", "cli"]
resolver = "2"

# core/Cargo.toml
[package]
name = "core"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1", optional = true, features = ["derive"] }

[features]
default = []
json = ["dep:serde"]
Termin kluczowy
Feature flag - Nazwana, opcjonalna funkcjonalność crate'a włączana w sekcji [features], pozwalająca konsumentom kompilować tylko potrzebne części kodu: mniejsza binarka, mniej zależności tranzytywnych.
Fakt na test
Składnia dep:nazwa pozwala uczynić opcjonalną zależność włącznikiem feature bez automatycznego tworzenia identycznie nazwanego, ukrytego feature'a, co wcześniej było źródłem konfliktów nazw.
Termin kluczowy
Blok unsafe - Fragment kodu odblokowujący dokładnie pięć operacji niemożliwych do statycznego zweryfikowania przez kompilator; reszta reguł języka, w tym borrow checker, nadal obowiązuje wewnątrz unsafe.
  • Dereferencja surowego wskaźnika (*const T / *mut T)
  • Wywołanie unsafe fn lub unsafe metody
  • Odczyt lub modyfikacja mutowalnej zmiennej statycznej (static mut)
  • Implementacja unsafe trait
  • Dostęp do pól unii (union)
unsafe z komentarzem SAFETY dokumentującym niezmiennik rust
fn split_at_mut_manual(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();
    assert!(mid <= len);
    let ptr = slice.as_mut_ptr();
    // SAFETY: mid <= len sprawdzone wyżej; obie połowy [0, mid) i
    // [mid, len) nigdy się nie nakładają, więc dwie mutowalne referencje
    // z jednego wskaźnika są bezpieczne.
    unsafe {
        (
            std::slice::from_raw_parts_mut(ptr, mid),
            std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}
Pułapka
unsafe nie wyłącza borrow checkera ani sprawdzania granic (bounds checking) dla zwykłego indeksowania []. Odblokowuje wyłącznie pięć operacji z listy powyżej. Niezmienniki, np. że wskaźnik jest wyrównany i wskazuje na ważną pamięć, pozostają w całości odpowiedzialnością programisty, kompilator ich nie zweryfikuje.
Fakt na test
Komentarz // SAFETY: tuż przed blokiem unsafe to konwencja społeczności Rust, wymagana też przez wiele lintów (np. clippy::undocumented_unsafe_blocks): dokumentuje, dlaczego dany niezmiennik jest zachowany, ułatwiając review i przyszłe refaktoryzacje.
Mnemonik
unsafe nie znaczy 'wyłącz mózg', znaczy 'kompilator ci teraz ufa, uzasadnij to'.
Fiszki - aktywne przypominanie

Sprawdź się - testowanie to nauka

24 pytań w losowej kolejności. Twoje wyniki zapisują się lokalnie.

Rozpocznij quiz
Zbudowane przez Tenzan Logic