Перейти к содержимому

Времена жизни в Rust

Времена жизни (lifetimes) — обычно то место, где начинающие разработчики на Rust впервые упираются в настоящую стену. Эта страница не торопится: цель — просто научиться узнавать, что такое время жизни и почему компилятор о нём просит, а не освоить все связанные правила.

Из урока про заимствование ты уже знаешь, что ссылка (&) позволяет пользоваться значением, не владея им. Но ссылка действительна, только пока существует значение, на которое она указывает. Rust должен убедиться, что у тебя никогда не окажется ссылки на то, что уже удалено. Такая ошибка называется висячей ссылкой (dangling reference), и в языках вроде C это классическая причина падений.

Чаще всего Rust разбирается в этом сам, глядя на области видимости в твоём коде, и тебе не нужно об этом думать (именно это ты видел на странице Область видимости). Но иногда, особенно с функциями, компилятор не может разобраться сам и просит тебя всё явно описать. Это и есть аннотация времени жизни.

Вот функция, которая возвращает более длинный из двух строковых срезов:

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

Она не скомпилируется. Ошибка выглядит так:

error[E0106]: missing lifetime specifier

Это значит «не указан спецификатор времени жизни». Проблема в том, что возвращаемая ссылка может быть заимствована из a или из b, и по одной функции Rust не может сказать, из какой. Значит, он не может гарантировать, что полученная ссылка всё ещё действительна, когда ты ей пользуешься.

Исправляется это так: ты даёшь времени жизни имя (по соглашению — строчную букву вроде 'a) и говоришь Rust, что время жизни результата связано с обоими параметрами:

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("hello");
let s2 = String::from("hi");
let result = longest(&s1, &s2);
println!("Самая длинная: {result}");
}

Вывод:

Самая длинная: hello
▶ Попробуй сам

Читай 'a как метку, а не как значение: она не задаёт никакой реальной продолжительности. Она просто говорит Rust: «все эти ссылки должны оставаться действительными как минимум на одном и том же отрезке кода». Потом компилятор проверяет каждое место вызова и убеждается, что это действительно так. (Строки в примерах на этой странице оставлены на английском, потому что код меряет их через .len() и режет по байтам, а русская буква занимает два байта.)

Без этой аннотации компилятор действительно не может понять, не переживёт ли ссылка, которую возвращает longest, одну из строк, из которых она взята. Именно аннотация времени жизни позволяет ему доказать — при компиляции, без затрат во время работы, — что полученной ссылкой всегда можно безопасно пользоваться.

Следующее место, где ты встретишь времена жизни, — структуры. До сих пор каждое поле структуры владело своими данными, как String. Поле может быть и ссылкой, но тогда структура действительна, только пока живы данные, на которые она указывает. Rust заставляет записать это через время жизни:

struct Highlight<'a> {
text: &'a str, // заимствует из какой-то String, которая живёт в другом месте
}
fn main() {
let book = String::from("Call me Ishmael. Some years ago...");
let first = Highlight { text: &book[..15] };
println!("Цитата: {}", first.text);
println!("Длина всей книги: {} байта", book.len());
}

Вывод:

Цитата: Call me Ishmael
Длина всей книги: 34 байта
▶ Попробуй сам

Читай Highlight<'a> как «Highlight, который что-то заимствует на время жизни 'a». Часть &book[..15] — это срез: он заимствует первые 15 байт book, не копируя их. Поскольку first только заимствует, book можно использовать и дальше.

Именно время жизни позволяет Rust поймать вот такую ошибку, где String удаляется, пока структура всё ещё указывает внутрь неё:

struct Highlight<'a> {
text: &'a str,
}
fn main() {
let highlight;
{
let book = String::from("Call me Ishmael. Some years ago...");
highlight = Highlight { text: &book[..15] };
} // book is dropped here...
println!("{}", highlight.text); // ...but highlight still points into it
}
error[E0597]: `book` does not live long enough
--> src/main.rs:9:40
|
8 | let book = String::from("Call me Ishmael. Some years ago...");
| ---- binding `book` declared here
9 | highlight = Highlight { text: &book[..15] };
| ^^^^ borrowed value does not live long enough
10 | } // book is dropped here...
| - `book` dropped here while still borrowed
11 |
12 | println!("{}", highlight.text); // ...but highlight still points into it
| -------------- borrow later used here

Комментарии в коде говорят: «здесь book удаляется…» и «…но highlight всё ещё указывает внутрь неё». Компилятор сообщает, что book живёт недостаточно долго: она удаляется в строке 10, пока ещё заимствована, а заимствование используется позже, в строке 12.

Практический совет: если времена жизни в структуре тебе мешают, храни владеющую String вместо &str. Текст скопируется, зато структура больше ни от чего не зависит, и время жизни исчезает. Многие опытные программисты на Rust так и начинают, а на ссылки переходят, только когда для этого есть причина.

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

Напиши функцию shortest, которая принимает два строковых среза и возвращает более короткий. Если длина одинаковая, верни первый. Вызови её с "elephant" и "cat".

Подсказка: у неё та же форма, что у longest выше. Нужен <'a> после имени и 'a у обоих параметров и у возвращаемого типа.

Показать решение
fn shortest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() <= b.len() { a } else { b }
}
fn main() {
let big = String::from("elephant");
let small = String::from("cat");
println!("Самая короткая: {}", shortest(&big, &small));
println!("При равенстве побеждает первая: {}", shortest("dog", "cow"));
}

Вывод:

Самая короткая: cat
При равенстве побеждает первая: dog
▶ Попробуй сам

Возвращаемый срез может прийти из a или из b, поэтому Rust нужно 'a, чтобы знать: результат действителен, только пока живы оба входа. <= вместо < означает, что при равенстве выбирается a.

Создай структуру Quote с двумя полями &str: text и author. Создай экземпляр, где text заимствует первые 20 байт строки String "Fearless concurrency is a goal of Rust", а author — литерал "The Rust Book". Выведи его как Fearless concurrency (The Rust Book).

Подсказка: как и Highlight выше, структуре нужно время жизни: struct Quote<'a> с &'a str у обоих полей. &speech[..20] — срез первых 20 байт.

Показать решение
struct Quote<'a> {
text: &'a str,
author: &'a str,
}
fn main() {
let speech = String::from("Fearless concurrency is a goal of Rust");
let quote = Quote {
text: &speech[..20],
author: "The Rust Book",
};
println!("{} ({})", quote.text, quote.author);
}

Вывод:

Fearless concurrency (The Rust Book)
▶ Попробуй сам

quote заимствует из speech, поэтому не может её пережить. Литерал "The Rust Book" вшит в программу и живёт всё время её работы, так что его с запасом хватает на 'a. Попробуй создать speech во внутреннем блоке { } и вывести цитату после конца блока, как в примере с ошибкой выше, — и посмотри, как компилятор тебя остановит.

  • Время жизни говорит компилятору, как ссылки связаны друг с другом, чтобы он мог исключить висячие ссылки.
  • Писать его нужно, только когда компилятор не может вывести его сам, — чаще всего в функциях, которые возвращают ссылку.
  • 'a не задаёт продолжительность — оно связывает времена жизни разных ссылок, чтобы компилятор мог их проверить.
  • Структуре, которая хранит ссылку, тоже нужно время жизни (struct Highlight<'a>), и она не может пережить данные, которые заимствует. Владеющая String позволяет этого избежать.
  • Это проверка только во время компиляции; во время работы она ничего не стоит.

Проверь себя

1. Какую проблему решают времена жизни?

2. Почему fn longest(a: &str, b: &str) -> &str не компилировалась без аннотации времени жизни?

3. Стоит ли что-нибудь аннотация времени жизни во время работы программы?

4. Когда, согласно этой странице, чаще всего нужно писать явное время жизни?

Результат: 0 / 4