Валидация ссылок при помощи времён жизни

Когда мы говорили о ссылках в разделе "Ссылки и заимствование" Главы 4, мы опустили весьма важную деталь: каждая ссылка в Rust имеет время жизни (lifetime), определяющее область действия, в которой ссылка является действительной. В большинстве случаев, времена жизни выводятся неявно также как у типов. Мы должны явно аннотировать типы, когда возможно выведение нескольких типов. Аналогичным образом, мы должны аннотировать времена жизни, когда времена жизни ссылок могут быть соотнесены несколькими различными способами. Rust требует, чтобы мы аннотировали отношения, используя обобщённые параметры времени жизни для гарантирования того, что реальные ссылки используемые во время выполнения, будут однозначно действительными.

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

Времена жизни предотвращают появление недействительных ссылок

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

fn main() {
    {
        let r;

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

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

Листинг 10-17: Попытка использования ссылки, значение которой вышло из области видимости

Примечание: примеры в листингах 10-17, 10-18 и 10-24 объявляют переменные без предоставления им начального значения, поэтому переменные существуют во внешней области видимости. На первый взгляд может показаться, что это противоречит отсутствию нулевых (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:7:17
   |
7  |             r = &x;
   |                 ^^ borrowed value does not live long enough
8  |         }
   |         - `x` dropped here while still borrowed
9  | 
10 |         println!("r: {}", r);
   |                           - borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10`

To learn more, run the command again with --verbose.

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

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

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

fn main() {
    {
        let r;                // ---------+-- 'a
                              //          |
        {                     //          |
            let x = 5;        // -+-- 'b  |
            r = &x;           //  |       |
        }                     // -+       |
                              //          |
        println!("r: {}", r); //          |
    }                         // ---------+
}

Пример 10-18: Описание времён жизни переменных r и x, с помощью идентификаторов времени жизни 'a и 'b

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

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

fn main() {
    {
        let x = 5;            // ----------+-- 'b
                              //           |
        let r = &x;           // --+-- 'a  |
                              //   |       |
        println!("r: {}", r); //   |       |
                              // --+       |
    }                         // ----------+
}

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

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

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

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

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

Файл: src/main.rs

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

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

Листинг 10-20: Функция main вызывает функцию longest для поиска наибольшей строки

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

Если мы попробуем реализовать функцию longest так, как это показано в листинге 10-22, то программа не будет скомпилирована:

Файл: src/main.rs

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

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

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

Листинг 10-21: Реализация функции 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 {
  |           ^^^^    ^^^^^^^     ^^^^^^^     ^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10`

To learn more, run the command again with --verbose.

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

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

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

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

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

Приведём несколько примеров: у нас есть ссылка на i32 без указания времени жизни, ссылка на i32, с временем жизни имеющим имя 'a и изменяемая ссылка на i32, которая тоже имеет время жизни 'a.

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

Одна аннотация времени жизни сама по себе не имеет большого смысла, потому что эти аннотации призваны сообщить компилятору Rust как соотносятся между собой несколько обобщённых параметров времени жизни. Предположим, что у нас есть функция с параметром first, имеющим ссылочный тип данных &i32 и временем жизни 'a, и вторым параметром second, который также имеет ссылочный тип &i32 со временем жизни 'a. Аннотации времени жизни этих параметров имеют одинаковое имя, что говорит о том, что обе ссылки first и second должны жить одинаково долго.

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

Давайте посмотрим на аннотации времён жизни в контексте функции longest. Обобщённые параметры времени жизни объявляются в угловых скобках между именем функции и списком её параметров. Ограничение, которое мы хотим выразить в этой сигнатуре, говорит о том, что все ссылки во входных параметрах и возвращаемом значении функции должны иметь одинаковое время жизни. Мы назовём время жизни как 'a и добавим его к каждой ссылке, как показано в листинге 10-23.

Файл: src/main.rs

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

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

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

Листинг 10-23: В сигнатуре функции longest указано, что все ссылки должны иметь одинаковое время обозначенное как 'a

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

Сигнатура функции теперь говорит 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-23 является простым примером.

Файл: src/main.rs

fn main() {
    let string1 = String::from("long string is long");

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

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

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

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

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

Файл: src/main.rs

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

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

Листинг 10-24: Попытка использования переменной 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
  |
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

error: aborting due to previous error

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10`

To learn more, run the command again with --verbose.

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

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

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

Мышление в терминах времён жизни

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

Файл: src/main.rs

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

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

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

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

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

Файл: src/main.rs

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

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

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

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

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

error: aborting due to previous error

For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10`

To learn more, run the command again with --verbose.

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

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

Определение времён жизни при объявлении структур

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

Файл: src/main.rs

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

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

Листинг 10-25. Структура, которая содержит ссылку, поэтому её объявление требует аннотации времени жизни

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

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

Правила неявного выведения времени жизни

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

Файл: 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);
}

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

Причина, по которой этот код компилируется — историческая. В первых (pre-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 листинга 10-26. Сигнатура этой функции начинается без объявления времён жизни ссылок:

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-21:

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

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

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

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

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

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

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

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

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

Сначала, воспользуемся методом с именем 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().expect("Could not find a '.'");
    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().expect("Could not find a '.'");
    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.

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

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

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

Итоги

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

Верите или нет, но в рамках этой темы всё есть ещё чему поучиться: в Главе 17 обсуждаются типажи-объекты, что является ещё одним способом использовать типажи. Существуют также более сложные сценарии с аннотациями времени жизни, которые вам понадобятся только в очень сложных случаях; для этого вам следует прочитать Rust Reference. Далее вы узнаете, как писать тесты на Rust, чтобы убедиться, что ваш код работает так, как должен.