Валидация ссылок при помощи времён жизни
Сроки (времена) жизни - ещё один вид обобщений, с которыми мы уже встречались. Если раньше мы использовали обобщения, чтобы убедиться, что тип обладает нужным нам поведением, теперь мы будем использовать сроки жизни для того, чтобы быть уверенными, что ссылки действительны как минимум столько времени в процессе исполнения программы, сколько нам требуется.
В разделе "Ссылки и заимствование" главы 4, мы кое о чём умолчали: у каждой ссылки в Rust есть своё время жизни — область кода, на протяжении которого данная ссылка действительна (valid). В большинстве случаев сроки жизни выводятся неявно — так же, как у типов (нам требуется явно объявлять типы лишь в тех случаях, когда при автоматическом выведении типа возможны варианты). Точно так же мы должны явно объявлять сроки жизни тех ссылок, для которых времена жизни могут быть определены компилятором по-разному. Rust требует от нас объявлять взаимосвязи посредством обобщённых параметров сроков жизни, чтобы убедиться в том, что во время исполнения все действующие ссылки будут корректными.
Аннотирование времени жизни — это концепция, отсутствующая в большинстве других языков программирования, так что она может показаться незнакомой. Хотя в этой главе мы не будем рассматривать времена жизни во всех деталях, тем не менее, мы обсудим основные ситуации, в которых вы можете столкнуться с синтаксисом времени жизни, что позволит вам получше ознакомиться с этой концепцией.
Времена жизни предотвращают появление "повисших" ссылок
Основное предназначение сроков жизни — предотвращать появление так называемых "повисших ссылок" (dangling references), из-за которых программа обращается не к тем данным, к которым она собиралась обратиться. Рассмотрим программу из листинга 10-16, имеющую внешнюю и внутреннюю области видимости.
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {r}");
}
Примечание: примеры в листингах 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 определяет, что этот код некорректен? Он использует для этого анализатор заимствований (borrow checker).
Анализатор заимствований
Компилятор Rust имеет в своём составе анализатор заимствований, который сравнивает области видимости для определения, являются ли все заимствования действительными. В листинге 10-17 показан тот же код, что и в листинге 10-16, но с аннотациями, показывающими времена жизни переменных.
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
Здесь мы описали время жизни для r
с помощью 'a
и время жизни x
с помощью 'b
. Как видите, время жизни 'b
внутреннего блока гораздо меньше, чем время жизни 'a
внешнего блока. Во время компиляции Rust сравнивает продолжительность двух времён жизни и видит, что r
имеет время жизни 'a
, но ссылается на память со временем жизни 'b
. Программа отклоняется, потому что 'b
короче, чем 'a
: объект ссылки не живёт так же долго, как сама ссылка.
Листинг 10-18 исправляет код, чтобы в нём не было повисшей ссылки, и компилируется без ошибок.
fn main() { let x = 5; // ----------+-- 'b // | let r = &x; // --+-- 'a | // | | println!("r: {r}"); // | | // --+ | } // ----------+
Здесь переменная x
имеет время жизни 'b
, которое больше, чем время жизни 'a
. Это означает, что переменная r
может ссылаться на переменную x
потому что Rust знает, что ссылка в переменной r
будет всегда действительной до тех пор, пока переменная x
является валидной.
После того, как мы на примерах рассмотрели времена жизни ссылок и обсудили как Rust их анализирует, давайте поговорим об обобщённых временах жизни входных параметров и возвращаемых значений функций.
Обобщённые времена жизни в функциях
Напишем функцию, которая возвращает более длинный из двух срезов строки. Эта функция принимает два среза строки и возвращает один срез строки. После того как мы реализовали функцию longest
, код в листинге 10-19 должен вывести The longest string is abcd
.
Файл: 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}");
}
Обратите внимание, что мы хотим чтобы функция принимала строковые срезы, которые являются ссылками, а не строки, потому что мы не хотим, чтобы функция longest
забирала во владение свои параметры. Обратитесь к разделу "Строковые срезы как параметры" Главы 4 для более подробного обсуждения того, почему параметры используемые в листинге 10-19 выбраны именно таким образом.
Если мы попробуем реализовать функцию longest
так, как это показано в листинге 10-20, программа не скомпилируется:
Файл: src/main.rs
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
Вместо этого мы получим следующую ошибку, говорящую о временах жизни:
$ 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 reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
Одна аннотация времени жизни сама по себе не имеет большого значения, поскольку аннотации предназначены для того, чтобы проинформировать Rust о том, как времена жизни нескольких ссылок соотносятся между собой. Давайте рассмотрим, как аннотации времени жизни связаны друг с другом в контексте функции longest
.
Аннотации времени жизни в сигнатурах функций
Чтобы использовать аннотации времени жизни в сигнатурах функций, нам нужно объявить параметры обобщённого времени жизни внутри угловых скобок между именем функции и списком параметров, как мы это делали с параметрами обобщённого типа .
Мы хотим, чтобы сигнатура отражала следующее ограничение: возвращаемая ссылка будет действительна до тех пор, пока валидны оба параметра. Это связь между временами жизни параметров и возвращаемого значения. Мы назовём это время жизни 'a
, а затем добавим его к каждой ссылке, как показано в листинге 10-21.
Файл: src/main.rs
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {result}"); } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
Этот код должен компилироваться и давать желаемый результат, когда мы вызовем его в функции 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 является очевидным примером.
Файл: 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 } }
В этом примере переменная string1
действительна до конца внешней области, string2
действует до конца внутренней области видимости и result
ссылается на что-то, что является действительным до конца внутренней области видимости. Запустите этот код, и вы увидите что анализатор заимствований разрешает такой код; он скомпилирует и напечатает The longest string is long string is long
.
Теперь, давайте попробуем пример, который показывает, что время жизни ссылки result
должно быть меньшим временем жизни одного из двух аргументов. Мы переместим объявление переменной result
за пределы внутренней области видимости, но оставим присвоение значения переменной result
в области видимости string2
. Затем мы переместим println!
, который использует result
за пределы внутренней области видимости, после того как внутренняя область видимости закончилась. Код в листинге 10-23 не скомпилируется.
Файл: src/main.rs
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {result}");
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
При попытке скомпилировать этот код, мы получим такую ошибку:
$ 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
можно совсем не указывать. Этот код скомпилируется:
Файл: src/main.rs
fn main() { let string1 = String::from("abcd"); let string2 = "efghijklmnopqrstuvwxyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {result}"); } fn longest<'a>(x: &'a str, y: &str) -> &'a str { x }
Мы указали параметр времени жизни 'a
для параметра x
и возвращаемого значения, но не для параметра y
, поскольку время жизни параметра y
никак не соотносится с временем жизни параметра x
или возвращаемого значения.
При возврате ссылки из функции, параметр времени жизни для возвращаемого типа должен соответствовать параметру времени жизни одного из аргументов. Если возвращаемая ссылка не ссылается на один из параметров, она должна ссылаться на значение, созданное внутри функции. Однако, это приведёт к недействительной ссылке, поскольку значение, на которое она ссылается, выйдет из области видимости в конце функции. Посмотрите на попытку реализации функции longest
, которая не скомпилируется:
Файл: src/main.rs
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}
Здесь, несмотря на то, что мы указали параметр времени жизни 'a
для возвращаемого типа, реализация не будет скомпилирована, потому что время жизни возвращаемого значения никак не связано с временем жизни параметров. Получаем сообщение об ошибке:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return value referencing local variable `result`
--> src/main.rs:11:5
|
11 | result.as_str()
| ------^^^^^^^^^
| |
| returns a value referencing data owned by the current function
| `result` is borrowed here
For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
Проблема заключается в том, что result
выходит за область видимости и очищается в конце функции longest
. Мы также пытаемся вернуть ссылку на result
из функции. Мы не можем указать параметры времени жизни, которые могли бы изменить недействительную ссылку, а Rust не позволит нам создать недействительную ссылку. В этом случае лучшим решением будет вернуть владеющий тип данных, а не ссылку: в этом случае вызывающая функция будет нести ответственность за очистку полученного ею значения.
В конечном итоге, синтаксис времён жизни реализует связывание времён жизни различных аргументов и возвращаемых значений функций. Описывая времена жизни, мы даём Rust достаточно информации, чтобы разрешить безопасные операции с памятью и запретить операции, которые могли бы создать недействительные ссылки или иным способом нарушить безопасность памяти.
Определение времён жизни при объявлении структур
До сих пор мы объявляли структуры, которые всегда содержали владеющие типы данных. Структуры могут содержать и ссылки, но при этом необходимо добавить аннотацию времени жизни для каждой ссылки в определении структуры. Листинг 10-24 описывает структуру ImportantExcerpt
, содержащую срез строки:
Файл: src/main.rs
struct ImportantExcerpt<'a> { part: &'a str, } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap(); let i = ImportantExcerpt { part: first_sentence, }; }
У структуры имеется одно поле part
, хранящее срез строки, который сам по себе является ссылкой. Как и в случае с обобщёнными типами данных, мы объявляем имя обобщённого параметра времени жизни внутри угловых скобок после имени структуры, чтобы иметь возможность использовать его внутри определения структуры. Данная аннотация означает, что экземпляр ImportantExcerpt
не может пережить ссылку, которую он содержит в своём поле part
.
Функция main
здесь создаёт экземпляр структуры ImportantExcerpt
, который содержит ссылку на первое предложение типа String
принадлежащее переменной novel
. Данные в novel
существуют до создания экземпляра ImportantExcerpt
. Кроме того, novel
не выходит из области видимости до тех пор, пока ImportantExcerpt
не выйдет за область видимости, поэтому ссылка в внутри экземпляра ImportantExcerpt
является действительной.
Правила неявного выведения времени жизни
Вы изучили, что у каждой ссылки есть время жизни и что нужно указывать параметры времени жизни для функций или структур, которые используют ссылки. Однако в Главе 4 у нас была функция в листинге 4-9, которая затем снова показана в листинге 10-25, в которой код скомпилировался без аннотаций времени жизни.
Файл: src/lib.rs
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let my_string = String::from("hello world"); // first_word works on slices of `String`s let word = first_word(&my_string[..]); let my_string_literal = "hello world"; // first_word works on slices of string literals let word = first_word(&my_string_literal[..]); // Because string literals *are* string slices already, // this works too, without the slice syntax! let word = first_word(my_string_literal); }
Причина, по которой этот код компилируется — историческая. В ранних (до-1.0) версиях Rust этот код не скомпилировался бы, поскольку каждой ссылке нужно было явно назначать время жизни. В те времена, сигнатура функции была бы написана примерно так:
fn first_word<'a>(s: &'a str) -> &'a str {
После написания большого количества кода на Rust разработчики языка обнаружили, что в определённых ситуациях программисты описывают одни и те же аннотации времён жизни снова и снова. Эти ситуации были предсказуемы и следовали нескольким детерминированным шаблонным моделям. Команда Rust решила запрограммировать эти шаблоны в код компилятора Rust, чтобы анализатор заимствований мог вывести времена жизни в таких ситуациях без необходимости явного указания аннотаций программистами.
Мы упоминаем этот фрагмент истории Rust, потому что возможно, что в будущем появится больше шаблонов для автоматического выведения времён жизни, которые будут добавлены в компилятор. Таким образом, в будущем может понадобится ещё меньшее количество аннотаций.
Шаблоны, запрограммированные в анализаторе ссылок языка Rust, называются правилами неявного выведения времени жизни. Это не правила, которым должны следовать программисты; а набор частных случаев, которые рассмотрит компилятор, и, если ваш код попадает в эти случаи, вам не нужно будет указывать время жизни явно.
Правила выведения не предоставляют полного заключения. Если 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
помещаются в один список внутри угловых скобок после имени функции.
Итоги
В этой главе мы рассмотрели много всего! Теперь вы знакомы с параметрами обобщённого типа, типажами и ограничениями типажа, обобщёнными параметрами времени жизни, вы готовы писать код без повторений, который будет работать во множестве различных ситуаций. Параметры обобщённого типа позволяют использовать код для различных типов данных. Типажи и ограничения типажа помогают убедиться, что, хотя типы и обобщённые, они будут вести себя, как этого требует ваш код. Вы изучили, как использовать аннотации времени жизни чтобы убедиться, что этот универсальный код не будет генерировать никаких повисших ссылок. И весь этот анализ происходит в момент компиляции и не влияет на производительность программы во время работы!
Верите или нет, но в рамках этой темы всё есть ещё чему поучиться: в Главе 17 обсуждаются типажи-объекты, которые являются ещё одним способом использования типажей. Существуют также более сложные сценарии с аннотациями времени жизни, которые вам понадобятся только в очень сложных случаях; для этого вам следует прочитать Rust Reference. Далее вы узнаете, как писать тесты на Rust, чтобы убедиться, что ваш код работает так, как задумано.