Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Проверка ссылок с помощью времён жизни

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

В разделе «Ссылки и заимствование» главы 4 мы не упомянули одну деталь: у каждой ссылки в Rust есть время жизни — та область, в пределах которой ссылка действительна. Чаще всего времена жизни неявные и выводятся сами — так же, как чаще всего сами выводятся типы. Аннотировать типы приходится, только когда подходит сразу несколько. Точно так же и времена жизни приходится аннотировать, только когда времена жизни ссылок могут быть связаны между собой несколькими разными способами. В таких случаях Rust требует описать эту связь с помощью обобщённых параметров времени жизни — чтобы ссылки, которые реально используются во время работы программы, были гарантированно действительны.

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

Висячие ссылки

Главная задача времён жизни — не допустить висячих ссылок, то есть ссылок, которые указывают уже не на те данные, на которые должны. Посмотрите на программу из листинга 10-16: в ней есть внешняя и внутренняя области видимости.

fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {r}");
}
Листинг 10-16: Попытка использовать ссылку, значение которой уже вышло из области видимости

Примечание: в листингах 10-16, 10-17 и 10-23 переменные объявляются без начального значения — чтобы имя переменной существовало во внешней области видимости. На первый взгляд это противоречит тому, что в Rust нет null. Но если попытаться использовать переменную, прежде чем присвоить ей значение, будет ошибка компиляции — так что null в Rust действительно не допускается.

Во внешней области видимости объявлена переменная r без начального значения, а во внутренней — переменная x с начальным значением 5. Внутри внутренней области мы пытаемся сделать r ссылкой на x. Затем внутренняя область заканчивается, а мы пытаемся вывести значение r. Этот код не скомпилируется: значение, на которое ссылается r, вышло из области видимости раньше, чем мы попытались им воспользоваться. Вот сообщение об ошибке:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
5 |         let x = 5;
  |             - binding `x` declared here
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
8 |
9 |     println!("r: {r}");
  |                   - borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

В сообщении сказано, что переменная x «живёт недостаточно долго» (does not live long enough). Дело в том, что x выйдет из области видимости, когда в строке 7 закончится внутренняя область. А r действительна во всей внешней области — её область больше, и говорят, что она «живёт дольше». Если бы Rust пропустил этот код, r ссылалась бы на память, которая освободилась, когда x вышла из области видимости, и всё, что мы попытались бы сделать с r, работало бы неправильно. Как же Rust понимает, что код некорректен? С помощью анализатора заимствований.

Анализатор заимствований

В компиляторе Rust есть анализатор заимствований (borrow checker), который сравнивает области видимости и определяет, все ли заимствования корректны. В листинге 10-17 — тот же код, что в листинге 10-16, но с пометками времён жизни переменных.

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {r}");   //          |
}                         // ---------+
Листинг 10-17: Пометки времён жизни r и x — 'a и 'b соответственно

Здесь время жизни r помечено как 'a, а время жизни x — как 'b. Как видите, внутренний блок 'b гораздо короче внешнего блока времени жизни 'a. При компиляции Rust сравнивает эти два времени жизни и видит: у r время жизни 'a, а ссылается она на память со временем жизни 'b. Программа отвергается, потому что 'b короче 'a: то, на что указывает ссылка, живёт меньше, чем сама ссылка.

В листинге 10-18 код исправлен: висячей ссылки больше нет, и он компилируется без ошибок.

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {r}");   //   |       |
                          // --+       |
}                         // ----------+
Листинг 10-18: Корректная ссылка: данные живут дольше, чем ссылка на них

Здесь у x время жизни 'b, и в этот раз оно больше 'a. Значит, r может ссылаться на x: Rust знает, что ссылка в r будет действительна всё время, пока действительна x.

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

Обобщённые времена жизни в функциях

Напишем функцию, которая возвращает более длинный из двух строковых срезов. Она принимает два строковых среза и возвращает один. Когда мы реализуем функцию longest, код из листинга 10-19 должен вывести The longest string is abcd.

Файл: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}
Листинг 10-19: Функция main вызывает longest, чтобы найти более длинный из двух строковых срезов

Обратите внимание: мы хотим, чтобы функция принимала строковые срезы, то есть ссылки, а не строки, — потому что не хотим, чтобы longest забирала владение параметрами. Почему в листинге 10-19 параметры именно такие, подробнее объясняется в разделе «Строковые срезы как параметры» главы 4.

Если реализовать longest так, как в листинге 10-20, код не скомпилируется.

Файл: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}
Листинг 10-20: Реализация функции longest, которая возвращает более длинный из двух строковых срезов, но пока не компилируется

Вместо этого мы получим ошибку, в которой говорится о временах жизни:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &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 `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

Из подсказки видно, что возвращаемому типу нужен обобщённый параметр времени жизни: Rust не может понять, на что указывает возвращаемая ссылка — на x или на y. Да мы и сами этого не знаем: блок if в теле функции возвращает ссылку на x, а блок else — ссылку на y!

Определяя функцию, мы не знаем, какие конкретные значения в неё передадут, а значит, не знаем, выполнится ветка if или ветка else. Не знаем мы и конкретных времён жизни переданных ссылок, так что не можем, как в листингах 10-17 и 10-18, посмотреть на области видимости и понять, будет ли возвращаемая ссылка всегда действительна. Анализатор заимствований тоже не может этого определить: он не знает, как времена жизни x и y связаны со временем жизни возвращаемого значения. Чтобы исправить ошибку, добавим обобщённые параметры времени жизни, которые описывают связь между ссылками, — тогда анализатор заимствований сможет выполнить свою проверку.

Синтаксис аннотаций времени жизни

Аннотации времени жизни не меняют того, сколько живёт та или иная ссылка. Они лишь описывают, как времена жизни нескольких ссылок соотносятся друг с другом, никак на эти времена жизни не влияя. Как функция с обобщённым параметром типа в сигнатуре может принимать значения любого типа, так и функция с обобщённым параметром времени жизни может принимать ссылки с любым временем жизни.

У аннотаций времени жизни немного необычный синтаксис: имена параметров времени жизни начинаются с апострофа (') и обычно состоят из строчных букв и очень коротки — как и обобщённые типы. Чаще всего первую аннотацию времени жизни называют 'a. Аннотацию ставят после & ссылки и отделяют от типа ссылки пробелом.

Вот несколько примеров — ссылка на i32 без параметра времени жизни, ссылка на i32 с параметром времени жизни 'a и изменяемая ссылка на i32 с тем же временем жизни 'a:

&i32        // ссылка
&'a i32     // ссылка с явным временем жизни
&'a mut i32 // изменяемая ссылка с явным временем жизни

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

В сигнатурах функций

Чтобы использовать аннотации времени жизни в сигнатуре функции, обобщённые параметры времени жизни нужно объявить в угловых скобках между именем функции и списком параметров — так же, как обобщённые параметры типа.

Мы хотим, чтобы сигнатура выражала такое ограничение: возвращаемая ссылка действительна, пока действительны оба параметра. Это и есть связь между временами жизни параметров и возвращаемого значения. Назовём время жизни 'a и добавим его к каждой ссылке, как в листинге 10-21.

Файл: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Листинг 10-21: Определение функции longest, где у всех ссылок в сигнатуре одно и то же время жизни 'a

Этот код должен компилироваться и давать нужный результат с функцией main из листинга 10-19.

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

Помните: указывая в сигнатуре параметры времени жизни, мы не меняем время жизни ни одного переданного или возвращаемого значения. Мы лишь говорим анализатору заимствований отвергать любые значения, которые не соблюдают эти ограничения. Заметьте, что функции longest не нужно точно знать, сколько проживут x и y, — достаточно, чтобы вместо 'a можно было подставить какую-то область, удовлетворяющую сигнатуре.

Аннотации времени жизни в функциях пишутся в сигнатуре, а не в теле функции. Они становятся частью договора функции — так же, как типы в сигнатуре. Раз договор о временах жизни записан прямо в сигнатуре, анализ, который выполняет компилятор Rust, получается проще. Если что-то не так с аннотациями функции или с тем, как её вызывают, ошибка компиляции точнее укажет на нужное место кода и на нарушенные ограничения. А если бы компилятор сам угадывал, какие связи между временами жизни мы имели в виду, он мог бы указать лишь на место, где наш код используется, — а оно бывает очень далеко от настоящей причины ошибки.

Когда мы передаём в longest конкретные ссылки, вместо 'a подставляется конкретное время жизни — та часть области видимости x, которая пересекается с областью видимости y. Иначе говоря, обобщённое время жизни 'a получает конкретное время жизни, равное меньшему из времён жизни x и y. А поскольку возвращаемую ссылку мы пометили тем же параметром 'a, она тоже будет действительна в течение меньшего из времён жизни x и y.

Посмотрим, как аннотации времени жизни ограничивают функцию longest, если передавать ей ссылки с разными конкретными временами жизни. Листинг 10-22 — простой пример.

Файл: src/main.rs
fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {result}");
    }
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Листинг 10-22: Вызываем longest со ссылками на значения String с разными конкретными временами жизни

Здесь string1 действительна до конца внешней области, string2 — до конца внутренней, а result ссылается на что-то, действительное до конца внутренней области. Запустите этот код — анализатор заимствований его одобрит: код скомпилируется и выведет The longest string is long string is long.

Теперь попробуем пример, который покажет, что время жизни ссылки в result должно быть меньшим из времён жизни двух аргументов. Вынесем объявление переменной result за пределы внутренней области, но присваивание ей значения оставим внутри — рядом со string2. А println!, который использует result, перенесём наружу, после конца внутренней области. Код в листинге 10-23 не скомпилируется.

Файл: src/main.rs
fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {result}");
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Листинг 10-23: Попытка использовать result после того, как string2 вышла из области видимости

При компиляции получаем ошибку:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
5 |         let string2 = String::from("xyz");
  |             ------- binding `string2` declared here
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {result}");
  |                                      ------ borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

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

Мы, люди, смотрим на этот код и видим, что string1 длиннее string2, а значит, в result окажется ссылка на string1. string1 ещё не вышла из области видимости, так что ссылка на неё в println! по-прежнему действительна. Но компилятор не может увидеть, что в этом случае ссылка корректна. Мы сказали Rust, что время жизни ссылки, которую возвращает longest, равно меньшему из времён жизни переданных ссылок. Поэтому анализатор заимствований не пропускает код из листинга 10-23 — в нём может оказаться недействительная ссылка.

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

Связи между временами жизни

Как именно указывать параметры времени жизни, зависит от того, что делает функция. Например, если бы longest всегда возвращала первый параметр, а не более длинный срез, указывать время жизни для параметра y не понадобилось бы. Следующий код скомпилируется:

Файл: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "efghijklmnopqrstuvwxyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

Мы указали параметр времени жизни 'a для параметра x и возвращаемого типа, но не для y: время жизни y никак не связано ни с временем жизни x, ни с возвращаемым значением.

Когда функция возвращает ссылку, параметр времени жизни возвращаемого типа должен совпадать с параметром времени жизни одного из параметров. Если возвращаемая ссылка не указывает ни на один из параметров, значит, она указывает на значение, созданное внутри функции. Но тогда это будет висячая ссылка: значение выйдет из области видимости в конце функции. Вот попытка реализовать longest, которая не скомпилируется:

Файл: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}

Хотя мы указали для возвращаемого типа параметр времени жизни 'a, эта реализация не скомпилируется: время жизни возвращаемого значения никак не связано со временами жизни параметров. Вот сообщение об ошибке:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return value referencing local variable `result`
  --> src/main.rs:11:5
   |
11 |     result.as_str()
   |     ------^^^^^^^^^
   |     |
   |     returns a value referencing data owned by the current function
   |     `result` is borrowed here

For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

Проблема в том, что result выходит из области видимости и очищается в конце функции longest, а мы пытаемся вернуть из функции ссылку на result. Никакими параметрами времени жизни висячую ссылку не исправить, а создавать висячие ссылки Rust не позволит. Лучшее решение здесь — возвращать не ссылку, а тип-владелец, чтобы за очистку значения отвечала вызывающая функция.

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

В определениях структур

До сих пор все наши структуры хранили типы-владельцы. Структура может хранить и ссылки, но тогда к каждой ссылке в определении структуры нужно добавить аннотацию времени жизни. В листинге 10-24 — структура ImportantExcerpt (важный отрывок), которая хранит строковый срез.

Файл: src/main.rs
struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}
Листинг 10-24: Структура со ссылкой, которой нужна аннотация времени жизни

У этой структуры одно поле — part, в котором хранится строковый срез, то есть ссылка. Как и с обобщёнными типами данных, имя обобщённого параметра времени жизни объявляется в угловых скобках после имени структуры, чтобы его можно было использовать в теле определения. Эта аннотация означает, что экземпляр ImportantExcerpt не может жить дольше ссылки, которая хранится в его поле part.

Функция main создаёт экземпляр ImportantExcerpt, который хранит ссылку на первое предложение String, принадлежащей переменной novel (роман). Переменная novel появляется раньше, чем экземпляр ImportantExcerpt, а выходит из области видимости позже него. Поэтому ссылка внутри ImportantExcerpt всё время остаётся действительной.

Неявные времена жизни

Вы узнали, что у каждой ссылки есть время жизни и что для функций и структур, использующих ссылки, нужно указывать параметры времени жизни. Однако в листинге 4-9 у нас была функция (она снова показана в листинге 10-25), которая компилировалась без всяких аннотаций времени жизни.

Файл: src/lib.rs
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");

    // first_word принимает срезы `String`
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word принимает срезы строковых литералов
    let word = first_word(&my_string_literal[..]);

    // Строковые литералы *уже* являются срезами строк,
    // поэтому сработает и так, без синтаксиса срезов!
    let word = first_word(my_string_literal);
}
Листинг 10-25: Функция из листинга 4-9, которая компилировалась без аннотаций времени жизни, хотя и параметр, и возвращаемый тип — ссылки

Причина, по которой эта функция компилируется без аннотаций, историческая. В ранних версиях Rust (до 1.0) этот код не скомпилировался бы: у каждой ссылки должно было быть явное время жизни. Тогда сигнатуру функции записали бы так:

fn first_word<'a>(s: &'a str) -> &'a str {

Написав много кода на Rust, команда Rust заметила, что в определённых ситуациях программисты раз за разом пишут одни и те же аннотации времени жизни. Эти ситуации были предсказуемы и укладывались в несколько однозначных шаблонов. Разработчики встроили эти шаблоны в компилятор, чтобы анализатор заимствований сам выводил времена жизни в таких ситуациях и явные аннотации были не нужны.

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

Шаблоны, встроенные в анализ ссылок Rust, называются правилами вывода времён жизни (lifetime elision rules, буквально «правила опускания»). Это не правила, которым должны следовать программисты, а набор частных случаев, которые рассматривает компилятор: если ваш код подходит под них, писать времена жизни явно не нужно.

Эти правила не дают полного вывода. Если после их применения остаётся неоднозначность в том, какие времена жизни у ссылок, компилятор не станет гадать, каким должно быть время жизни оставшихся ссылок. Вместо этого он выдаст ошибку, которую можно исправить, добавив аннотации времени жизни.

Времена жизни параметров функции или метода называются входными временами жизни (input lifetimes), а времена жизни возвращаемых значений — выходными (output lifetimes).

Когда явных аннотаций нет, компилятор определяет времена жизни ссылок по трём правилам. Первое правило относится к входным временам жизни, второе и третье — к выходным. Если, пройдя все три правила, компилятор так и не определил времена жизни каких-то ссылок, он остановится с ошибкой. Эти правила действуют и для определений fn, и для блоков impl.

Первое правило: компилятор назначает каждому параметру-ссылке свой параметр времени жизни. То есть функция с одним параметром получает один параметр времени жизни: fn foo<'a>(x: &'a i32); функция с двумя — два разных: fn foo<'a, 'b>(x: &'a i32, y: &'b i32); и так далее.

Второе правило: если входной параметр времени жизни ровно один, это время жизни назначается всем выходным параметрам времени жизни: fn foo<'a>(x: &'a i32) -> &'a i32.

Третье правило: если входных параметров времени жизни несколько, но один из них — &self или &mut self (то есть это метод), всем выходным параметрам времени жизни назначается время жизни self. Благодаря третьему правилу методы гораздо приятнее читать и писать: значков нужно меньше.

Представим, что мы компилятор, и применим эти правила, чтобы определить времена жизни ссылок в сигнатуре функции first_word из листинга 10-25. Сначала у ссылок в сигнатуре нет никаких времён жизни:

fn first_word(s: &str) -> &str {

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

fn first_word<'a>(s: &'a str) -> &str {

Входное время жизни ровно одно, поэтому применяется второе правило: время жизни единственного входного параметра назначается выходному, и сигнатура становится такой:

fn first_word<'a>(s: &'a str) -> &'a str {

Теперь у всех ссылок в сигнатуре есть времена жизни, и компилятор может продолжать анализ, не требуя от программиста аннотаций в этой сигнатуре.

Рассмотрим другой пример — функцию longest, у которой не было параметров времени жизни, когда мы начинали работать с ней в листинге 10-20:

fn longest(x: &str, y: &str) -> &str {

Применим первое правило: у каждого параметра своё время жизни. На этот раз параметров два, значит, и времён жизни два:

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

Второе правило не применяется: входных времён жизни больше одного. Третье тоже: longest — функция, а не метод, и ни один параметр не является self. Пройдя все три правила, мы так и не выяснили, какое время жизни у возвращаемого типа. Вот почему код из листинга 10-20 не компилировался: компилятор прошёл по правилам вывода времён жизни, но так и не смог определить времена жизни всех ссылок в сигнатуре.

Третье правило на самом деле применяется только к сигнатурам методов, поэтому дальше рассмотрим времена жизни именно в методах — и увидим, почему благодаря третьему правилу в сигнатурах методов аннотации времени жизни нужны нечасто.

В определениях методов

Когда мы реализуем методы для структуры с временами жизни, синтаксис тот же, что и для обобщённых параметров типа (см. листинг 10-11). Где объявлять и использовать параметры времени жизни, зависит от того, связаны ли они с полями структуры или с параметрами и возвращаемыми значениями метода.

Имена времён жизни для полей структуры всегда объявляются после ключевого слова impl и используются после имени структуры: эти времена жизни — часть типа структуры.

В сигнатурах методов внутри блока impl ссылки могут быть привязаны к времени жизни ссылок в полях структуры, а могут быть и независимы. Кроме того, благодаря правилам вывода времён жизни в сигнатурах методов аннотации часто не нужны. Посмотрим на примеры со структурой ImportantExcerpt, которую мы определили в листинге 10-24.

Начнём с метода level, единственный параметр которого — ссылка на self, а возвращаемое значение — i32, то есть не ссылка ни на что:

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

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {announcement}");
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

Объявить параметр времени жизни после impl и использовать его после имени типа обязательно, а вот аннотировать время жизни ссылки на self благодаря первому правилу вывода не нужно.

А вот пример, где применяется третье правило:

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

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {announcement}");
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

Входных времён жизни два, поэтому Rust применяет первое правило и даёт и &self, и announcement собственные времена жизни. Затем, поскольку один из параметров — &self, возвращаемый тип получает время жизни &self, и все времена жизни определены.

Статическое время жизни

Есть одно особое время жизни, о котором нужно сказать, — 'static. Оно означает, что ссылка может жить всё время работы программы. У всех строковых литералов время жизни 'static, и его можно указать так:

#![allow(unused)]
fn main() {
let s: &'static str = "I have a static lifetime.";
}

Текст этой строки хранится прямо в бинарном файле программы, а он доступен всегда. Поэтому время жизни всех строковых литералов — 'static.

В сообщениях об ошибках вам могут встретиться советы использовать время жизни 'static. Но прежде чем указывать для ссылки 'static, подумайте, действительно ли ваша ссылка живёт всё время работы программы и нужно ли вам это. Чаще всего сообщение с советом про 'static возникает из-за попытки создать висячую ссылку или из-за несоответствия доступных времён жизни. В таких случаях нужно устранять сами эти проблемы, а не указывать 'static.

Обобщённые параметры типа, ограничения трейтами и времена жизни вместе

Взглянем коротко на синтаксис, в котором в одной функции указаны и обобщённые параметры типа, и ограничения трейтами, и времена жизни!

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest_with_an_announcement(
        string1.as_str(),
        string2,
        "Today is someone's birthday!",
    );
    println!("The longest string is {result}");
}

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {ann}");
    if x.len() > y.len() { x } else { y }
}

Это функция longest из листинга 10-21, которая возвращает более длинный из двух строковых срезов. Но теперь у неё есть дополнительный параметр ann обобщённого типа T, вместо которого можно подставить любой тип, реализующий трейт Display, — так сказано в конструкции where. Этот параметр выводится через {}, поэтому ограничение трейтом Display и нужно. Времена жизни — разновидность обобщений, поэтому объявления параметра времени жизни 'a и обобщённого параметра типа T стоят в одном списке в угловых скобках после имени функции.

Итоги

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

Хотите верьте, хотите нет, но по темам этой главы можно узнать ещё очень многое. В главе 18 речь пойдёт о трейт-объектах — ещё одном способе использовать трейты. Бывают и более сложные сценарии с аннотациями времени жизни, но они понадобятся только в очень продвинутых случаях; о них читайте в справочнике Rust. А дальше вы научитесь писать тесты на Rust, чтобы убеждаться, что код работает так, как должен.