Tenzan Academy
Motyw
  • 8 sekcji
  • 24 pytania
  • 22 fiszki

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

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

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

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

Quiz

Ownership i semantyka przenoszenia (move semantics)

na egzaminie

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. Wstawia tam wywołanie drop 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
Dalej: 02 Borrow checker: reguły referencji i non-lexical lifetimes

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

na egzaminie

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. Dzięki temu można wcześniej pożyczyć tę samą wartość ponownie.

Pułapka

Przed NLL powyższy kod nie kompilował się, choć r1 i r2 nie są już używane przy tworzeniu r3. Zbyt konserwatywna analiza, oparta na granicach bloków, czasem odrzucała semantycznie bezpieczny kod. NLL poprawił precyzję analizy, a reguły bezpieczeństwa zostały bez zmian.

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 klasycznych wyścigów danych znanych z C/C++ nie da się skompilować.

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
Dalej: 03 Lifetimes i reguły elizji

Lifetimes i reguły elizji

na egzaminie

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. Reguła 1 Każdy elidowany parametr wejściowy referencyjny dostaje własny, świeży parametr lifetime.
  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. 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.

Dalej: 04 Option<T>, Result<T,E> i operator ?

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

na egzaminie

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. Przykład: 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.

Dalej: 05 Traits: dyn vs generyki, dispatch i zero-cost abstractions

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

na egzaminie

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. Umożliwia to 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
Dalej: 06 Enumy, pattern matching, RAII i brak garbage collectora

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. Każdy wariant może przechowywać inne, 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. Zasób zwalnia się automatycznie w destruktorze, na końcu zasięgu obiektu.

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. Zwolnienie pamięci jest więc deterministyczne i nie wymaga mechanizmu trasującego w runtime.

Mnemonik

Ostatni wszedł, pierwszy wyjdzie: drop działa jak stos, nie jak kolejka.

Dalej: 07 async/await i tokio: wykonawcy niezależne od języka

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

na egzaminie

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. Przejmuje to 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 odpytujący (poll) futures aż do ukończenia, planujący zadania (tasks) na wątkach i dostarczający asynchroniczne I/O oraz timery. Std nie dostarcza własnego executora.

Fakt na test

Ten podział na 'język definiuje Future i .await' oraz 'crate dostarcza executor' nazywa się projektem executor-agnostic. Ten sam kod async może teoretycznie działać pod tokio, async-std czy smol. 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

Długa, blokująca operacja synchroniczna (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. Wstrzymuje 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
Dalej: 08 Cargo workspaces, feature flags i bloki unsafe

Cargo workspaces, feature flags i bloki unsafe

na egzaminie

Produkcyjny Rust to nie tylko semantyka języka. To także 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. Wymaga jej też wiele lintów (np. clippy::undocumented_unsafe_blocks). Komentarz dokumentuje, dlaczego dany niezmiennik jest zachowany, i ułatwia review oraz przyszłe refaktoryzacje.

Mnemonik

unsafe nie znaczy 'wyłącz mózg', znaczy 'kompilator ci teraz ufa, uzasadnij to'.

Sprawdź się

20 z 24 pytań, losowo, z wyjaśnieniem po każdym. Wynik zostaje na tym urządzeniu.

Rozpocznij quiz

Fiszki: Rust

Pytanie

O tym demo

Działające demo kursów napisanych przez Tenzan Logic. Wyniki zostają na tym urządzeniu, bez konta.

Każdy kurs, pytanie i fiszkę napisał Tenzan Logic, po polsku i od zera. Jeśli chcesz taki zestaw dla swojego zespołu, z Twoimi procedurami i Twoim słownictwem, napisz.

Napisz do Tenzan Logic

Jak to działa

100% czas (dni) bez powtórek 1 3 7 21 z powtórkami
Schemat idei, nie dane pomiarowe (Ebbinghaus 1885; Cepeda i in. 2006).
  • Odstępy

    1·3·7·21 dni do powtórki

    Fiszka z „Umiem” wraca po 1, 3, 7 i 21 dniach.

    Badanie

    Powtórki z przerwami dają trwalszą pamięć niż jedna po drugiej. Cepeda i in. 2006, Psychological Bulletin 132(3): 354-380

  • Przypominanie

    ukryta odpowiedź fiszki

    Najpierw odpowiedz z pamięci, potem odwróć.

    Badanie

    Próba przypomnienia dała lepszą pamięć po dwóch dniach i po tygodniu niż ponowne czytanie. Roediger i Karpicke 2006, Psychological Science 17(3): 249-255

  • Dwa kody

    4 kolory bloków

    Fakt, termin, mnemonik i pułapka mają swój kolor.

    Badanie

    Teoria: słowo i obraz zapisują się osobno. To teoria, nie gwarancja wyniku. Paivio 1991, Canadian Journal of Psychology 45(3): 255-287

  • Porcje

    167 krótkich sekcji

    Jedna sekcja to jedna porcja do zapamiętania.

    Badanie

    Pamięć robocza mieści około czterech porcji (Miller 1956: siedem, Cowan 2001: cztery). Cowan 2001, Behavioral and Brain Sciences 24(1): 87-114

  • Skróty

    134 zielone bloki

    Mnemonik zbiera kilka elementów w jeden, np. „SUR”.

    Badanie

    Skrót zbiera kilka elementów w jeden; przykład „SUR” jest w kursie o podstawach AI. Opis funkcji aplikacji, bez cytowanego badania

  • Testy

    20 z 24 pytań w quizie

    Po każdej odpowiedzi widzisz wyjaśnienie.

    Badanie

    Ćwiczenie, nie sprawdzian: pytania są w losowej kolejności i uzupełniają fiszki, nie zastępują ich. Efekt testowania: zob. „Przypominanie”

Szukaj

Postęp

Brak wyników. Rozwiąż pierwszy quiz.

Tu zbiera się Twój postęp: rekord i medal każdego quizu, seria dni i pytania do powtórki.

Gdzie jest zapis
Tylko w pamięci tej przeglądarki. Konta nie ma i wyniki nigdzie nie są wysyłane. Inna przeglądarka lub inne urządzenie zaczyna od zera.
Jak wyczyścić
W quizie: Historia, potem Wyczyść historię. Fiszki i serię dni usuniesz razem z danymi witryny w ustawieniach przeglądarki.
Wybierz quiz

Progi medali

Medal liczy się z najlepszego wyniku quizu: Brąz od 60%, Srebro od 75%, Złoto od 90%, Platyna przy 100%.