☰ Конспект ← Курс
ЛЕКЦІЯ 6

ЗРІЗИ, ЧАСИ ЖИТТЯ І ТИПИ БЕЗ NULL

Дивитися в дані, не володіючи ними — і описувати їхню відсутність явно

ВТФК · Системне програмування · 2026

План лекції

  • Зріз (slice): &str і &[T] — вікно в буфер без володіння
  • NLL: де насправді закінчується запозичення
  • Часи життя і синтаксис 'a — коротко
  • Проблема null: «помилка на мільярд доларів»
  • enum Option<T>: Some(T) або None
  • match над Option — компілятор вимагає повноти
  • Живе демо: match над Option

Фокус: позичити — не означає володіти

Живе демо

  1. Пишемо функцію, яка одночасно тримає &Vec<i32> (читає елемент) і &mut Vec<i32> (додає елемент) у тій самій області видимості
  2. Запускаємо `cargo build`, читаємо повідомлення error[E0502] з поясненням, яке саме запозичення конфліктує
  3. Розводимо запозичення по різних, не перетинних областях видимості (закриваємо {} блок)
  4. Переконуємось, що код збирається і поведінка передбачувана

Очікувано: Перша версія: помилка компіляції з точним рядком конфлікту. Друга: успішна збірка

Власник проти зрізу

ВласникЗрізЩо містить зрізПриклад
String&strвказівник на початок + довжина"літерал", s.as_str(), &s[..i]
Vec<T>&[T]вказівник + довжина&v[1..3]
[T; N]&[T]вказівник + довжина&arr[..]

Зріз — посилання на неперервну ділянку колекції: не володіє даними, нічого не копіює і не звільняє

Зріз — вікно в буфер, а не володіння

ЗРІЗ — ЦЕ ВІКНО, А НЕ ВОЛОДІННЯ VEC<I32> · 6 ЕЛЕМЕНТІВ У КУПІ &v[2..5] ptr → v[2] · len = 3 10 0 20 1 30 2 40 3 50 4 60 5 зріз не копіює дані — це вказівник + довжина ЧОМУ &MUT ЗАБОРОНЕНИЙ, ПОКИ ЗРІЗ ЖИВИЙ let s = &v[2..5] позика жива v.push(60) потрібне &mut v push перевиділить буфер — і зріз вказував би в нікуди NLL: жива до останнього вжитку error[E0502] зріз не володіє даними — він лише вікно, тому й правила позик такі суворі

`&v[2..5]` — це вказівник на v[2] плюс довжина 3. Поки цей зріз живий, `v.push(60)` потребує `&mut v` і не компілюється: error[E0502]

&str і &[T] на практиці

rust
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &b) in bytes.iter().enumerate() {
        if b == b' ' { return &s[..i]; }
    }
    s
}

let v = vec![10, 20, 30, 40];
let mid = &v[1..3];   // зріз [20, 30], без копіювання даних

Зрізи дозволяють працювати з частиною даних без володіння й без копіювання

largest: типова функція із зрізом

rust
fn largest(list: &[i32]) -> i32 {
    let mut max = list[0];
    for &item in list {
        if item > max { max = item; }
    }
    max
}

let numbers = vec![34, 50, 25, 3101, 65];
println!("{}", largest(&numbers));   // 3101, без копіювання вектора

Функція приймає зріз — працює і з Vec<i32>, і з масивом [i32; N]

Що виключають правила запозичень

🧷
Висяче посиланняпосилання на вже знищені дані — не компілюється
⚡
Гонка за даними&mut виключає будь-який інший доступ до тих самих даних
🔁
Інвалідація ітератораpush у Vec під час for по ньому — error[E0502]
🆓
Без рантайм-витратжодних lock, GC чи підрахунку посилань — усе статично

Ціна — явно продумувати, хто й коли позичає дані; це і є «суворість», яку ставлять Rust у докір

Лексична перевірка проти NLL

Rust 2015: лексичні області
  • Посилання живе до закриваючої дужки блоку
  • Потрібні штучні блоки {} для завершення запозичення
  • Хибні спрацювання на коректному коді
→
Rust 2018+: Non-Lexical Lifetimes
  • Посилання живе до останнього фактичного використання
  • Природний код без зайвих блоків
  • Ті самі гарантії безпеки — жодна помилка не пропускається

NLL аналізує потік керування, а не текстову область видимості змінної

NLL: запозичення завершується там, де закінчилось реальне використання

rust
let mut v = vec![1, 2, 3];

let first = &v[0];       // незмінне запозичення починається
println!("{}", first);   // останнє використання first — тут

v.push(4);                // ОК під NLL: first більше не використовується

До появи NLL цей код відмовлявся компілюватись — first текстово "жила" до кінця блоку

Часи життя (lifetimes) і синтаксис 'a

  • Кожне посилання має час життя — інтервал, протягом якого воно дійсне
  • Найчастіше компілятор виводить його сам (lifetime elision)
  • 'a не подовжує життя даних — це лише опис контракту для компілятора
  • Потрібен явно, коли повернене посилання могло походити з кількох джерел
  • Правило: результат живе не довше за коротше з входів із тим самим 'a

Детально — на ЛР5 і в парному СРС: там це інструмент, а не теорія заради теорії

Функція з явним часом життя

rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

let s1 = String::from("довгий рядок");
let s2 = String::from("короткий");
println!("{}", longest(&s1, &s2));

Без 'a компілятор не знав би, з якого саме аргументу походить повернене посилання

Як компілятор виводить час життя (elision)

  1. 1
    Правило 1кожен параметр-посилання отримує власний час життя
  2. 2
    Правило 2вхідний час життя рівно один → він і у вихідних посилань
  3. 3
    Правило 3є &self / &mut self → його час життя у вихідних посилань
  4. 4
    Інакшеоднозначної відповіді немає — потрібна явна анотація 'a

Саме правило 3 дозволяє писати fn get(&self) -> &str без 'a

Спроба модифікувати Vec під час ітерації

rust
let mut v = vec![1, 2, 3];

for x in &v {
    v.push(*x);   // error[E0502]: cannot borrow `v` as mutable
                  //   because it is also borrowed as immutable
}

Iterator invalidation — класичний рантайм-баг в інших мовах — тут помилка компіляції

Підводні камені

ПасткаЩо станетьсяЯк уникнути
&mut і & до однієї змінної в одній ділянціerror[E0502]розвести запозичення в часі; часто рятує NLL
Повернути &str на локальну Stringвисяче посилання — не компілюєтьсяповертати String або приймати вхідне посилання
v.push() усередині for x in &verror[E0502]: інвалідація ітераторазібрати зміни окремо, застосувати після циклу
Вважати, що 'a «створює» час життяконтракт не збігається з реальністю'a лише описує наявну область життя даних

Проблема null

  • Тоні Гоар, автор null-посилання в ALGOL W (1965): «помилка на мільярд доларів»
  • null має тип T, але нічого не містить: NullPointerException, segfault
  • Компілятор не змушує перевіряти null перед використанням — легко забути
  • Rust радикально: null як значення в безпечному коді взагалі не існує
enum Option<T> — ЗАМІСТЬ null Option<T> два стани Some(value) є значення T None значення немає match дає гілку на кожен стан → компілятор змушує обробити None некоректний стан (null) фізично непредставний

null проти Option<T>

null (Java, C, JS …)
  • Має тип T, але даних типу T не містить
  • Перевірку if (x != null) легко забути
  • Помилка виявляється в рантаймі: NPE, segfault
  • На випадковому вході, у проді
vs
Option<T> (Rust)
  • Окремий тип: Option<i32> ≠ i32
  • Компілятор вимагає обробити Some і None
  • Помилка виявляється при компіляції: error[E0004]
  • До першого запуску програми

Дістати значення безпечно: match або комбінатори (map, unwrap_or, and_then)

enum Option<T>

rust
enum Option<T> {
    Some(T),
    None,
}

let some_number: Option<i32> = Some(3101);   // 0xC1D
let absent_number: Option<i32> = None;

Option<T> — частина стандартної бібліотеки, є завжди в prelude

match над Option

rust
fn describe(x: Option<i32>) -> String {
    match x {
        Some(n) if n < 0 => format!("від'ємне: {}", n),
        Some(n) => format!("значення: {}", n),
        None => String::from("значення відсутнє"),
    }
}

Прибрати гілку None — і код взагалі не скомпілюється: error[E0004] non-exhaustive

Enum робить невалідний стан непредставним

⚪
Disconnectedбез полів — жодних зайвих даних
🟡
Connectingбез полів
🟢
Connected { session_id }ідентифікатор існує лише тут
🔴
Failed { reason }причина існує лише тут

Альтернатива — прапорці is_connected + is_connecting + error: дозволяє «з'єднано» і «помилка» водночас. Option — окремий випадок цього прийому (СРС 12)

У будь-який момент часу можна мати або одне змінне посилання, або будь-яку кількість незмінних — і посилання завжди мають бути дійсними.

— The Rust Programming Language, розд. 4.2 «References and Borrowing»

Підсумок

  • Зрізи (&str, &[T]) дають доступ до частини даних без володіння й без копіювання
  • NLL: запозичення живе доти, доки реально використовується, а не до кінця блоку
  • Час життя 'a у сигнатурі описує, від чого залежить повернуте посилання (глибина — СРС 09)
  • У Rust немає null: відсутність значення — це Option<T>, а не невалідне T
  • match над Option вимагає обробити обидва варіанти — забути None компілятор не дасть
  • Наступного разу (Л7): що робити, коли операція може провалитись — Result, ? і власні типи помилок

🥚 Найбільше число у прикладі largest — 3101, тобто 0xC1D. Функція повертає його копією, не забираючи володіння вектором; сама константа супроводжує курс від першого модуля.

VTFK · TERMLINK · L06← → · space · f — на весь екран