Лайфтаймы простыми словами и элизия
Лайфтайм — это не «сколько живёт переменная», а участок кода, на котором ссылка гарантированно указывает на живые данные.
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. Так и работает контракт из сигнатуры — тело функции проверено один раз, а каждый вызов проверяется отдельно.
Вопрос: «Почему в большинстве функций лайфтаймы писать не нужно?»
Потому что компилятор расставляет их сам по трём правилам элизии. Их полезно назвать вслух — это выглядит как реальное знание, а не заученная фраза.
Что на самом деле проверяет интервьюер
Три правила элизии
- Каждый параметр-ссылка получает собственный лайфтайм:
fn f(a: &str, b: &str)превращается вfn f<'a, 'b>(a: &'a str, b: &'b str). - Если входной лайфтайм ровно один, он уходит во все выходные ссылки.
- Если среди параметров есть
&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.