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

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

Одна деталь, которую мы не обсуждали в разделе «Ссылки и заимствование» Главы 4, заключается в том, что каждая ссылка в Rust имеет время жизни — это область видимости, в которой эта ссылка действительна. Большую часть времени время жизни неявно и выводится, так же как и типы выводятся большую часть времени. Мы должны аннотировать типы только когда возможны несколько типов. Подобным образом, мы должны аннотировать время жизни, когда время жизни ссылок может быть связано несколькими разными способами. Rust требует, чтобы мы аннотировали отношения, используя обобщенные параметры времени жизни, чтобы гарантировать, что фактические ссылки, используемые во время выполнения, определенно будут действительными.

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

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

Основная цель времени жизни — предотвратить висячие ссылки (dangling references), которые, если бы им разрешили существовать, заставили бы программу ссылаться на данные, отличные от тех, на которые она предназначена. Рассмотрим программу в Листинге 10-16, которая имеет внешнюю область видимости и внутреннюю область видимости.

fn main() {
    let r;

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

    println!("r: {r}");
}

Примечание: Примеры в Листингах 10-16, 10-17 и 10-23 объявляют переменные без присвоения им начального значения, поэтому имя переменной существует во внешней области видимости. На первый взгляд, это может показаться противоречащим тому, что в Rust нет нулевых значений. Однако, если мы попытаемся использовать переменную до присвоения ей значения, мы получим ошибку компиляции, что показывает, что 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 «не живет достаточно долго». Причина в том, что 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}");   //          |
}                         // ---------+

Здесь мы аннотировали время жизни 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}");   //   |       |
                          // --+       |
}                         // ----------+

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

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

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

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

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

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

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

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

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 }
}

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

$ 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.

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 }
}

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

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

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

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

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

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

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 }
}

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

Далее давайте попробуем пример, который показывает, что время жизни ссылки в result должно быть меньшим временем жизни двух аргументов. Мы переместим объявление переменной result за пределы внутренней области видимости, но оставим присваивание значения переменной result внутри области видимости с string2. Затем мы переместим println!, который использует result, за пределы внутренней области видимости, после того как внутренняя область видимости закончилась. Код в Листинге 10-23 не скомпилируется.

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 }
}

Когда мы пытаемся скомпилировать этот код, мы получаем эту ошибку:

$ 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 еще не вышла из области видимости, ссылка на string1 все еще будет действительна для оператора println!. Однако компилятор не может видеть, что ссылка действительна в этом случае. Мы сказали Rust, что время жизни ссылки, возвращаемой функцией longest, такое же, как меньшее из времен жизни переданных ссылок. Следовательно, проверка заимствований запрещает код в Листинге 10-23 как возможно имеющий недействительную ссылку.

Попробуйте провести больше экспериментов, изменяя значения и время жизни ссылок, передаваемых в функцию longest, и то, как используется возвращаемая ссылка. Выдвигайте гипотезы о том, пройдут ли ваши эксперименты проверку заимствований до компиляции; затем проверьте, правы ли вы!

Отношения

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

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, которая не скомпилируется:

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, которая содержит строковый срез.

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,
    };
}

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

Функция main здесь создает экземпляр структуры ImportantExcerpt, который содержит ссылку на первое предложение String, принадлежащее переменной novel. Данные в novel существуют до создания экземпляра ImportantExcerpt. Кроме того, novel не выходит из области видимости до того, как ImportantExcerpt выйдет из области видимости, поэтому ссылка в экземпляре ImportantExcerpt действительна.

Сокрытие времени жизни

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

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 works on slices of `String`s
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word works on slices of string literals
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

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

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

После написания большого количества кода на Rust команда 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, чтобы вы могли убедиться, что ваш код работает так, как должен.