Курс СРС SRS09
до ЛР5 рівень 2 → «4»↔ Лабораторна 5↔ Лекція 5⌨️ команди 2
Самостійна робота · ~2.5 год

Часи життя (lifetimes) у сигнатурах функцій: що саме перевіряє компілятор

🌌 Розшифровка часових міток координат — С.І.Д. підсуває тобі фрагмент, що повертає одну з двох координатних міток, і одразу тішиться нагоді пояснити: без явної анотації часу життя компілятор не знає, від якої з двох залежить результат, і радше відмовиться збирати код, ніж ризикне віддати посилання на мітку, яку вже стерли. «Отака в нього чесність, і я його за це люблю», — усміхається С.І.Д. з екрана.

📚 ТЕОРЕТИЧНІ ВІДОМОСТІ

Запозичення без володіння — і нова проблема, яку воно створює

Запозичення (borrowing) дозволяє функції тимчасово отримати доступ до значення через посилання (&T — незмінне, &mut T — змінне), не забираючи володіння. Це знімає жорсткість моделі власності з попередньої теми: більше не потрібно передавати й повертати значення туди-сюди, щоб просто його прочитати. Але запозичення породжує нове питання: посилання не має сенсу довше, ніж живе значення, на яке воно вказує — інакше отримаємо класичний dangling pointer (висяче посилання), джерело use-after-free в мовах без такого контролю. Rust вирішує це не в рантаймі, а статично, через часи життя (lifetimes).

Найголовніше, що варто зрозуміти про анотації часу життя: 'a не встановлює, скільки саме живе значення, і не подовжує нічиє існування. Вона лише описує компілятору відношення між часами життя різних посилань у сигнатурі — «результат живе не довше, ніж найкоротше з вхідних посилань» чи «ці два вхідні параметри мають жити принаймні стільки ж, скільки». Сама тривалість життя визначається структурою коду (областями видимості змінних) незалежно від анотацій; анотації — це лише мова, якою функція повідомляє виклику про вже наявні обмеження.

Класичний навчальний приклад: функція fn longest(x: &str, y: &str) -> &str, що повертає довший з двох рядкових зрізів. Без анотацій компілятор не має підстав знати, від якого саме з двох вхідних посилань «походить» вихідне — а без цього знання неможливо перевірити безпеку виклику (раптом викликач звільнить y одразу після виклику, а результат насправді «походить» від y?). Помилка компіляції в цьому випадку — missing lifetime specifier. Анотація fn longest<'a>(x: &'a str, y: &'a str) -> &'a str каже: «результат живе не довше за найкоротше з x і y» — і тепер компілятор може відхилити будь-який виклик, де результат використовується довше, ніж живе коротше з двох вхідних посилань.

У більшості практичних випадків програміст не пише анотації часу життя явно — за них це робить elision (правила виведення часу життя): якщо в сигнатурі рівно один вхідний параметр-посилання, вихідне посилання неявно отримує його час життя; якщо серед параметрів є &self, вихідне посилання неявно отримує час життя self. Явна анотація потрібна лише тоді, коли компілятор не може однозначно вивести зв'язок сам — як у прикладі з двома рівноправними вхідними параметрами вище.

Часи життя можуть з'являтися і в структурах, що зберігають посилання (struct Excerpt<'a> { part: &'a str }) — тоді компілятор гарантує, що жоден екземпляр Excerpt не переживе значення, на яке вказує part. Це та сама ідея, поширена з функцій на дані: посилання всередині структури не може «пережити» оригінал. Головний висновок педагогічно важливий: часи життя — це не окрема, «важка» тема поверх звичайних типів, а формалізація того, що будь-яка інша мова з ручними вказівниками просто залишає на відповідальність програміста, часто помилково.

📖 ОПРАЦЮВАТИ

Прочитати розділ «Validating References with Lifetimes» у The Rust Book; спробувати скомпілювати функцію, що повертає одне з двох вхідних посилань, спершу без анотації 'a, потім з нею, і зафіксувати обидва повідомлення компілятора.

🖥️ ТЕРМІНАЛ — СПРОБУЙ САМ
🎯 ЗАВДАННЯ
  1. Написати функцію longest(x: &str, y: &str) -> &str без анотацій часу життя, зафіксувати точний текст помилки компіляції missing lifetime specifier, потім додати 'a і показати, що вона компілюється.
  2. Створити приклад виклику longest, де один з аргументів (наприклад, y) звільняється (виходить з області видимості) раніше, ніж використовується результат — показати, що компілятор відхиляє саме такий виклик, навіть коли сама функція вже коректно анотована.
  3. Написати структуру з полем-посиланням (struct Excerpt<'a> { part: &'a str }) і продемонструвати спробу створити екземпляр Excerpt, що переживає рядок, з якого взято part — зафіксувати помилку про недостатньо довге життя.
  4. Знайти щонайменше дві функції в стандартній бібліотеці Rust (наприклад, у документації str чи slice), де анотація часу життя опущена завдяки правилам elision, і письмово пояснити, яке саме правило elision тут спрацювало.
  5. Переписати longest так, щоб вона завжди повертала копію (String) замість посилання, і пояснити письмово компроміс: яку проблему з часом життя це усуває і якою ціною (продуктивність, володіння).
❓ КОНТРОЛЬНІ ПИТАННЯ
  1. Чи встановлює анотація 'a тривалість життя значення, чи лише описує зв'язок між часами життя різних посилань?
  2. Чому функція longest(x: &str, y: &str) -> &str без анотацій не компілюється, хоча кожен параметр окремо цілком коректний тип?
  3. Що станеться, якщо викликати коректно анотовану longest із результатом, який використовується довше, ніж живе коротше з двох вхідних посилань?
  4. Яке правило elision дозволяє не писати анотацію часу життя для методу з параметром &self?
  5. Навіщо структурі, що зберігає посилання (наприклад, struct Excerpt<'a>), потрібен параметр часу життя у своєму оголошенні?
  6. Яку помилку рантайму в мовах на кшталт C/C++ запобігає механізм часів життя Rust ще на етапі компіляції?
  7. Чому найпоширеніший практичний рецепт «уникнути складних lifetime-помилок» — повернути власне значення (наприклад, String) замість посилання — іноді небажаний, і яку ціну він має?
✅ САМОПЕРЕВІРКА

Що саме описує анотація 'a у сигнатурі fn f<'a>(x: &'a str) -> &'a str — тривалість існування самого значення чи зв'язок між часами життя різних посилань?

🥚 ПАСХАЛКА
У нотатках творців є функція fn last_signal<'a>(freq: &'a str, _noise: &str) -> &'a str — результат походить лише від першого параметра, тому другий обійшовся без 'a, і Прикордонник пропустив її без питань. Єдиний виклик у їхньому тесті: last_signal("101.9", "ICE"). С.І.Д. каже, що це посилання живе рівно стільки, скільки живе частота, — а вона, за його словами, мовчить якось не так.
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 2

Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.

cargo build
Компілює проєкт у target/debug/; --release — з оптимізаціями у target/release/.
cargo
Система збірки й менеджер пакетів Rust: створює проєкт, тягне залежності (крейти), збирає, тестує, запускає.

💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.

← SRS08усі темиSRS10 →