Лайфтаймы простыми словами и элизия

Лайфтайм — это не «сколько живёт переменная», а участок кода, на котором ссылка гарантированно указывает на живые данные.

Lifetime — область кода, в течение которой ссылка остаётся валидной; аннотация вида <'a> только описывает связь между лайфтаймами входов и результата и ничего не продлевает.

Вопрос: «Что такое lifetime и зачем аннотации 'a?»

Обычно спрашивают именно так, без кода, и ждут одного-двух предложений. Формулировка «lifetime — это сколько живёт значение» считается неверной: время жизни значения определяется областью видимости и moves, а lifetime — это свойство ссылки, промежуток, на котором borrow checker обязан считать её валидной.

Что на самом деле проверяет интервьюер

  • Понимаете ли вы, что аннотация — это описание, а не действие: она не выделяет память, не продлевает жизнь и вообще исчезает при компиляции.
  • Знаете ли, зачем компилятору эта информация: чтобы проверить функцию по одной только сигнатуре, не заглядывая в её тело и в тела вызывающих.
  • Умеете ли перевести аннотацию в человеческое утверждение: «результат живёт не дольше, чем самое короткое из входных заимствований».

Хороший ход — сказать, что лайфтаймы существуют только на этапе проверки типов: код с <'a> и без него компилируется в одинаковый машинный код, накладных расходов нет.

Вопрос: «Почему этот код требует указать время жизни?»

Здесь дают классическую функцию и просят объяснить ошибку.

fn longest(a: &str, b: &str) -> &str {
    if a.len() >= b.len() { a } else { b }
}

Ошибка компиляции:

error[E0106]: missing lifetime specifier
 --> src/main.rs:1:33
  |
1 | fn longest(a: &str, b: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the
          signature does not say whether it is borrowed from `a` or `b`
help: consider introducing a named lifetime parameter
  |
1 | fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

Компилятор не спорит с логикой функции — он не знает, откуда взялась возвращаемая ссылка. Проверять вызовы он собирается по сигнатуре, а сигнатура молчит. Ответ — связать входы и выход одним параметром:

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() { a } else { b }
}

fn main() {
    let s1 = String::from("абвгд");
    let s2 = String::from("хyz");
    println!("{}", longest(s1.as_str(), s2.as_str()));
}

Вывод:

абвгд

Читается это так: 'a — не конкретный срок, а переменная, которую компилятор на каждом вызове подставляет как пересечение реальных лайфтаймов аргументов.

Что на самом деле проверяет интервьюер

Дальше почти всегда идёт добивка: «а что случится, если один аргумент умрёт раньше?» Именно ради этого случая аннотация и нужна.

fn main() {
    let s1 = String::from("длинная строка");
    let result;
    {
        let s2 = String::from("коротко");
        result = longest(s1.as_str(), s2.as_str());
    }
    println!("{result}");
}

Ошибка компиляции:

error[E0597]: `s2` does not live long enough
 --> src/main.rs:6:39
  |
5 |         let s2 = String::from("коротко");
  |             -- binding `s2` declared here
6 |         result = longest(s1.as_str(), s2.as_str());
  |                                       ^^ borrowed value does not live long enough
7 |     }
  |     - `s2` dropped here while still borrowed
8 |     println!("{result}");
  |               -------- borrow later used here

Обратите внимание: ошибка возникла в вызывающем коде, а не внутри longest. Так и работает контракт из сигнатуры — тело функции проверено один раз, а каждый вызов проверяется отдельно.

Вопрос: «Почему в большинстве функций лайфтаймы писать не нужно?»

Потому что компилятор расставляет их сам по трём правилам элизии. Их полезно назвать вслух — это выглядит как реальное знание, а не заученная фраза.

Что на самом деле проверяет интервьюер

Три правила элизии

  1. Каждый параметр-ссылка получает собственный лайфтайм: fn f(a: &str, b: &str) превращается в fn f<'a, 'b>(a: &'a str, b: &'b str).
  2. Если входной лайфтайм ровно один, он уходит во все выходные ссылки.
  3. Если среди параметров есть &self или &mut self, то в возврат уходит лайфтайм self, независимо от остальных аргументов.

Поэтому вот это компилируется без единой аннотации: сработало правило 2.

fn first_word(s: &str) -> &str {
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

А longest не компилировался, потому что входных лайфтаймов два, правило 2 не применяется, self нет — правило 3 тоже. Правил кончились, аннотацию надо писать руками.

Лайфтайм в структуре

Если структура хранит ссылку, она обязана объявить параметр времени жизни: экземпляр не может пережить данные, на которые смотрит.

struct Excerpt<'a> {
    part: &'a str,
}

impl<'a> Excerpt<'a> {
    fn announce(&self, note: &str) -> &str {
        println!("Внимание: {note}");
        self.part
    }
}

У метода announce два параметра-ссылки, но аннотации не нужны: по правилу 3 возвращается лайфтайм self.

'static — не «живёт вечно»

&'static str означает, что данные доступны всё время работы программы: строковые литералы лежат прямо в бинарнике. Типичное заблуждение — думать, что добавление 'static «чинит» ошибку заимствования. Наоборот, это самое строгое требование, и добавляя его в сигнатуру, вы обычно просто переносите ту же ошибку на вызывающий код.

let motto: &'static str = "ссылка на данные внутри бинарника";

Типичные ошибки кандидатов

  • Говорить «lifetime — это сколько живёт переменная». Живут значения, а lifetime — характеристика ссылки.
  • Считать, что аннотация продлевает жизнь данным или что-то делает в рантайме.
  • Лепить <'a> во все сигнатуры «на всякий случай» вместо опоры на элизию.
  • Ставить 'static как средство «заткнуть» borrow checker.
  • Не уметь объяснить, почему E0597 указывает на вызывающий код, а не на тело функции.
  • Забывать про третье правило и удивляться, что методы с &self почти никогда не требуют аннотаций.

Как ответить кратко

Lifetime — это область, на которой ссылка гарантированно валидна. Аннотация <'a> ничего не меняет во время выполнения: она описывает связь между лайфтаймами аргументов и результата, чтобы компилятор мог проверять каждый вызов по одной сигнатуре. В longest два входа, и компилятор не знает, откуда взялся выход, — отсюда E0106; связываем оба входа и возврат общим 'a, и правило становится «результат живёт не дольше короткого из аргументов». В большинстве функций писать это не нужно из-за трёх правил элизии: свой лайфтайм каждому параметру, единственный вход уходит в возврат, а при наличии &self в возврат идёт лайфтайм self.

Проверьте себя
1. Что означает аннотация 'a в сигнатуре fn longest<'a>(a: &'a str, b: &'a str) -> &'a str?
AЧто обе входные строки будут скопированы в новый буфер
BЧто возвращённая ссылка валидна не дольше, чем самое короткое из входных заимствований
CЧто аргументы будут жить до конца программы
DЧто функция сама продлит время жизни аргументов до конца main
2. Почему fn first_word(s: &str) -> &str компилируется без аннотаций, а longest с двумя аргументами — нет?
AПотому что &str всегда 'static, а String — нет
BПотому что компилятор смотрит в тело функции и выводит лайфтайм оттуда
CПотому что при единственном входном лайфтайме второе правило элизии отдаёт его в возврат, а при двух входах правил не хватает
DПотому что функции с одним аргументом не проверяются borrow checker
3. Что на самом деле означает тип &'static str у строкового литерала?
AЧто переменная никогда не будет освобождена, даже если выйдет из области видимости
BЧто данные доступны всё время работы программы, потому что лежат в бинарнике
CЧто строку можно изменять из любого места программы
DЧто компилятор отключит для неё проверку заимствований