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

RustRustBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Добро пожаловать в Validating References With Lifetimes. Этот лаба является частью Rust Book. Вы можете практиковать свои навыки Rust в LabEx.

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


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") subgraph Lab Skills rust/variable_declarations -.-> lab-100414{{"Проверка ссылок с использованием жизненных циклов"}} rust/integer_types -.-> lab-100414{{"Проверка ссылок с использованием жизненных циклов"}} rust/string_type -.-> lab-100414{{"Проверка ссылок с использованием жизненных циклов"}} rust/function_syntax -.-> lab-100414{{"Проверка ссылок с использованием жизненных циклов"}} rust/expressions_statements -.-> lab-100414{{"Проверка ссылок с использованием жизненных циклов"}} rust/method_syntax -.-> lab-100414{{"Проверка ссылок с использованием жизненных циклов"}} end

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

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

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

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

Предотвращение утечки ссылок с использованием жизненных циклов

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

fn main() {
  1 let r;

    {
      2 let x = 5;
      3 r = &x;
  4 }

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

Listing 10-16: Попытка использовать ссылку, значение которой вышло за пределы области видимости

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

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

error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
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

Сообщение об ошибке говорит, что переменная x "не живет достаточно долго". Причина в том, что x будет вне области видимости, когда внутренняя область видимости заканчивается на строке 7. Но r по-прежнему действителен для внешней области видимости; так как ее область видимости больше, мы говорим, что она "живет дольше". Если Rust разрешил бы этот код работать, r ссылался бы на память, которая была освобождена, когда x вышло за пределы области видимости, и все, что мы бы хотели сделать с r, не работало бы правильно. Так как же Rust определяет, что этот код недействителен? Он использует проверщик заимствований.

Проверщик заимствований

Компилятор Rust имеет проверщик заимствований, который сравнивает области видимости, чтобы определить, все ли заимствования действительны. Listing 10-17 показывает ту же программу, что и Listing 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: субъект ссылки не живет столько времени, сколько ссылка.

Listing 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, код в Listing 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}");
}

Listing 10-19: Функция main, которая вызывает функцию longest, чтобы найти более длинную из двух строковых срезов

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

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

Имя файла: src/main.rs

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

Listing 10-20: Реализация функции longest, которая возвращает более длинную из двух строковых срезов, но еще не компилируется

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

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 {
  |           ++++     ++          ++          ++

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

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

Синтаксис аннотации жизненных циклов

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

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

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

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

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

Аннотации жизненных циклов в сигнатурах функций

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

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

Имя файла: src/main.rs

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 из Listing 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, передавая ссылки с разными конкретными жизненными циклами. Listing 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}");
    }
}

Listing 10-22: Использование функции longest с ссылками на значения String, которые имеют разные конкретные жизненные циклы

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

Далее, давайте попробуем пример, который покажет, что жизненный цикл ссылки в result должен быть меньшим жизненным циклом из двух аргументов. Мы переместите объявление переменной result за пределы внутренней области видимости, но оставим присвоение значения переменной result внутри области видимости с string2. Затем мы переместим println!, которое использует result, за пределы внутренней области видимости, после того, как внутренняя область видимости закончится. Код в Listing 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}");
}

Listing 10-23: Попытка использовать result после того, как string2 вышло за пределы области видимости

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

error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
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

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

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

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

Мышление в терминах жизненных циклов

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

Имя файла: src/main.rs

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

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

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

Имя файла: src/main.rs

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

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

error[E0515]: cannot return reference to local variable `result`
  --> src/main.rs:11:5
   |
11 |     result.as_str()
   |     ^^^^^^^^^^^^^^^ returns a reference to data owned by the
current function

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

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

Аннотации жизненных циклов в определениях структур

До сих пор все структуры, которые мы определяли, содержали типы, владеющие значениями. Мы можем определить структуры, которые будут содержать ссылки, но в этом случае нам нужно добавить аннотацию жизненного цикла для каждой ссылки в определении структуры. Listing 10-24 содержит структуру с именем ImportantExcerpt, которая содержит строковый срез.

Имя файла: src/main.rs

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

fn main() {
  3 let novel = String::from(
        "Call me Ishmael. Some years ago..."
    );
  4 let first_sentence = novel
       .split('.')
       .next()
       .expect("Could not find a '.'");
  5 let i = ImportantExcerpt {
        part: first_sentence,
    };
}

Listing 10-24: Структура, которая содержит ссылку, требующая аннотации жизненного цикла

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

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

Элиминация жизненных циклов

Вы узнали, что каждая ссылка имеет жизненный цикл и что вам нужно указывать параметры жизненных циклов для функций или структур, которые используют ссылки. Однако, в Listing 4-9 была функция, показанная снова в Listing 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[..]
}

Listing 10-25: Функция, которую мы определили в Listing 4-9, которая скомпилировалась без аннотаций жизненных циклов, хотя параметр и возвращаемый тип - ссылки

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

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

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

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

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

Правила элиминации не обеспечивают полного вывода. Если 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 из Listing 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, которая не имела параметров жизненных циклов, когда мы начали работать с ней в Listing 10-20:

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

Применим первое правило: каждый параметр получает свой собственный жизненный цикл. На этот раз у нас два параметра вместо одного, поэтому у нас два жизненных цикла:

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

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

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

Аннотации жизненных циклов в определениях методов

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

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

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

Сначала мы используем метод с именем level, у которого единственный параметр - это ссылка на self, а возвращаемое значение - это i32, которое не является ссылкой на что-либо:

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

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

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

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

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

Жизненный цикл 'static

Одним из особых жизненных циклов, о котором нужно поговорить, является 'static, который означает, что затронутая ссылка может существовать в течение всей длительности программы. Все строковые литералы имеют жизненный цикл 'static, который мы можем аннотировать следующим образом:

let s: &'static str = "I have a static lifetime.";

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

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

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

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

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 из Listing 10-21, которая возвращает более длинный из двух строковых срезов. Но теперь у нее есть дополнительный параметр с именем ann типа T, который может быть заполнен любым типом, реализующим трейт Display, как указано в предложении where. Этот дополнительный параметр будет выведен с использованием {}, поэтому ограничение трейта Display необходимо. Поскольку жизненные циклы - это тип обобщенных, объявления параметра жизненного цикла 'a и обобщенного параметра типа T находятся в одном списке внутри угловых скобок после имени функции.

Резюме

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