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

ЗАПОЗИЧЕННЯ І ЗРІЗИ

Borrowing, slices

VTFK · 2026

План лекції

  • Проблема попередньої лекції: постійна передача власності
  • Запозичення через & і &mut
  • Правила запозичень і чому вони існують
  • Зрізи (slices): &str і &[T]
  • Живе демо: перевірка запозичень ловить гонку за даними

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

Два види запозичення

&T — незмінне посилання
  • Можна читати, не можна змінювати
  • Одночасно — скільки завгодно
  • Власність лишається у власника
  • fn len(s: &String) — s можна використати далі
|
&mut T — змінне посилання
  • Можна і читати, і змінювати
  • Одночасно — рівно одне
  • Власність теж не переходить
  • Потребує let mut у власника

Позичити — не означає володіти: посилання нічого не звільняє при виході з області видимості

Immutable borrow

rust
fn calculate_length(s: &String) -> usize {
    s.len()
}   // s виходить з області видимості, але дані НЕ звільняються — s лише позичав

let s1 = String::from("привіт");
let len = calculate_length(&s1);
println!("{}: {}", s1, len);   // s1 і далі валідний — власність не передавалась

& — читати, не володіти

Правила запозичень

  • У будь-який момент: або кілька незмінних &T, або рівно одне змінне &mut T
  • «Багато читачів АБО один письменник» — не обидва одночасно
  • Посилання завжди вказує на дійсні дані: «висяче» посилання (dangling) неможливе
  • Так гонка за даними (data race) ловиться на етапі компіляції, а не в рантаймі

Data race = два потоки звертаються до тих самих даних одночасно, і хоча б один запис

ПРАВИЛА ПОЗИК (BORROW CHECKER) &T &T &T багато читачів одночасно — OK &mut T рівно один письменник і жодного читача & XOR &mut перевіряється під час компіляції → немає гонок даних

Що можна тримати одночасно

Уже єДодати ще &TДодати &mut T
нічого
&T (одне чи багато)✗ error[E0502]
&mut T✗ error[E0502]✗ error[E0499]

Це класична задача «читачі–письменник», розв'язана не блокуванням у рантаймі, а відмовою компілювати

Порушення правил: компілятор ловить конфлікт

rust
let mut s = String::from("привіт");

let r1 = &s;        // незмінне запозичення
let r2 = &mut s;    // error[E0502]: cannot borrow `s` as mutable
                    //   because it is also borrowed as immutable
println!("{} {}", r1, r2);

error[E0502]: конфлікт запозичень виявлено до запуску програми

Живе демо

  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 лише описує наявну область життя даних
У будь-який момент часу можна мати або одне змінне посилання, або будь-яку кількість незмінних — і посилання завжди мають бути дійсними.

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

Підсумок

  • & — незмінне запозичення, &mut — змінне: або багато &, або одне &mut
  • Порушення правил запозичень — помилка компіляції, а не рантайм-баг
  • Зрізи (&str, &[T]) дають доступ до частини даних без володіння й без копіювання
  • Наступна лекція (Л7): що робити, коли значення може бути відсутнім або операція може провалитись

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

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