← Свитки rust

Времена жизни в Rust - не длительность, а зависимость

Аннотация `'a` не управляет тем, как долго живут данные - она фиксирует зависимость: ссылка действительна пока живы оба аргумента

Вы писали функции, принимающие ссылки. В какой-то момент вы попробовали вернуть ссылку - и Rust остановил вас с ошибкой про lifetime specifier. Вы добавили 'a в сигнатуру, и либо стало лучше, либо нет, но в обоих случаях вы не понимали почему. Распространённый вывод: 'a управляет тем, как долго что-то живёт. Это не так.

Договор аренды

Представьте: вы организуете мероприятие. Вы арендовали зал с семи до полуночи. На основании этого договора вы выдаёте пропуска. Каждый пропуск действителен ровно на срок аренды - не дольше. Когда аренда заканчивается и зал возвращается владельцу, все пропуска становятся недействительными. Договор не определяет, как долго существует здание; он определяет, как долго действует ваше право на него. Пропуска не продлевают аренду. Они на неё ссылаются.

Теперь представьте два зала - основной и резервный. Оба доступны с начала мероприятия, но основной арендован до десяти, резервный - до полуночи. Какой использовать - решат по числу гостей. Вы выдаёте пропуск команде обслуживания. Пропуск должен работать в любом случае - значит, он действителен только пока открыты оба варианта, то есть до десяти. Не потому что так написано на пропуске, а потому что это единственный период, когда результат гарантированно верен.

Именно это делает аннотация 'a в Rust: фиксирует, что возвращаемая ссылка действительна ровно столько, сколько живут оба аргумента. Не дольше.

Дата на пропуске

Распространённое заблуждение: 'a - это длительность, и написание её в коде заставляет Rust держать что-то живым дольше. Это неверно. Оно формируется потому, что добавление 'a иногда убирает ошибку компилятора - и это ощущается как подтверждение. Но это совпадение, а не причина.

fn active_label(current: &str, default: &str) -> &str {
    if current.is_empty() { default } else { current }
}
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:46
  |
1 | fn active_label(current: &str, default: &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 `current` or `default`

Компилятор не спрашивает, как долго что-то должно жить. Он спрашивает: до какого момента действителен этот пропуск? Возвращаемый &str указывает на данные одного из двух аргументов - но без аннотации компилятор не знает какого, а значит не может определить срок действия пропуска и проверить что он не истечёт раньше чем будет использован.

Пометка на пропуске

fn active_label<'a>(
    current: &'a str,  // основной зал - до десяти
    default: &'a str,  // резервный зал - до полуночи
) -> &'a str {         // пропуск действителен пока оба открыты
    if current.is_empty() { default } else { current }
}

'a не продлевает жизнь current или default. Это пометка: возвращаемая ссылка действительна в пределах пересечения их сроков - пока живы оба. Реальный срок определяется областями видимости в вызывающем коде.

Теперь компилятор может проверить:

fn main() {
    let label;
    {
        let current = String::from("beta");          // основной зал открывается
        label = active_label(&current, "stable");    // пропуск выдан - действителен пока оба открыты
    }                                                // основной зал закрылся - пропуск истёк
    println!("{}", label);                           // пропуск уже недействителен
}
error[E0597]: `current` does not live long enough
  --> src/main.rs:9:28
   |
9  |         label = active_label(&current, "stable");
   |                              ^^^^^^^^ borrowed value does not live long enough
10 |     }
   |     - `current` dropped here while still borrowed
11 |     println!("{}", label);
   |                    ----- borrow later used here

Аннотация сделала зависимость видимой. Компилятор нашёл нарушение. Исправление - не менять аннотацию, а использовать label пока оба зала ещё открыты:

fn main() {
    let current = String::from("beta");           // основной зал открывается
    let label = active_label(&current, "stable"); // пропуск выдан - оба зала ещё открыты
    println!("{}", label);                        // используем пока оба открыты
}                                                 // залы закрываются - пропуск истекает, но уже не нужен

Что теперь читается иначе

Когда видите 'a в сигнатуре функции - читайте не как таймер, а как условие: возвращаемая ссылка действительна пока живы оба аргумента.


Стоит знать ещё одно: если нужно вернуть ссылку только из одного конкретного аргумента - можно дать им разные аннотации: 'a и 'b. Тогда возвращаемая ссылка будет привязана именно к тому аргументу, от которого зависит. Это уже точечный контроль, а не пересечение.