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

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

Времена жизни – еще один вид обобщений, который мы уже использовали. Вместо того чтобы гарантировать, что тип обладает нужным нам поведением, времена жизни гарантируют, что ссылки действительны настолько долго, насколько нам нужно.

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

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

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

Главная цель времен жизни – предотвращать висячие ссылки, которые, если бы их разрешили, заставляли бы программу ссылаться на данные, отличные от тех, на которые она должна ссылаться. Рассмотрим программу в листинге 10-16, где есть внешняя и внутренняя области видимости.

fn main() {
    let r;

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

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

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

Во внешней области видимости объявляется переменная с именем 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}");   //          |
}                         // ---------+
Listing 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}");   //   |       |
                          // --+       |
}                         // ----------+
Listing 10-18: Действительная ссылка, потому что данные имеют более длинное время жизни, чем ссылка

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

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

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

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

Filename: 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}");
}
Listing 10-19: Функция main, которая вызывает функцию longest, чтобы найти более длинный из двух строковых срезов

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

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

Filename: 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 }
}
Listing 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.

Filename: 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 }
}
Listing 10-21: Определение функции longest, указывающее, что все ссылки в сигнатуре должны иметь одно и то же время жизни 'a

Этот код должен скомпилироваться и дать нужный нам результат, когда мы используем его с функцией 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 – простой пример.

Filename: 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 }
}
Listing 10-22: Использование функции longest со ссылками на значения String, имеющие разные конкретные времена жизни

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

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

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

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

Связи

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

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

Filename: 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, которая хранит строковый срез.

Filename: 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,
    };
}
Listing 10-24: Структура, которая хранит ссылку и требует аннотации времени жизни

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

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

Опущение времен жизни

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

Filename: 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 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);
}
Listing 10-25: Функция, которую мы определили в листинге 4-9 и которая скомпилировалась без аннотаций времен жизни, хотя параметр и возвращаемый тип являются ссылками

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

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

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

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

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

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

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

Компилятор использует три правила, чтобы определить времена жизни ссылок, когда явных аннотаций нет. Первое правило применяется к входным временам жизни, а второе и третье – к выходным временам жизни. Если компилятор дойдет до конца трех правил и все еще останутся ссылки, для которых он не сможет определить времена жизни, компилятор остановится с ошибкой. Эти правила применяются как к определениям 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 Reference. Но дальше вы узнаете, как писать тесты в Rust, чтобы убедиться, что ваш код работает так, как должен.