Валидация ссылок по времени жизни
Времена жизни — это ещё один вид обобщений, с которыми мы уже встречались. Если раньше мы использовали обобщения, чтобы убедиться, что тип обладает нужным нам поведением, то теперь мы будем их использовать, чтобы убедиться, что ссылки действительны столько, сколько требуется.
В разделе ["Ссылки и заимствование"](ch04-02-references-and-borrowing.html #Ссылки-и-заимствование) Главы 4 мы кое о чём умолчали: у каждой ссылки в Rust есть своё время жизни — промежуток программы, в котором данная ссылка действительна. В большинстве случаев времена жизни выводятся неявно — так же, как и типы. Точно так же мы должны явно объявлять времена жизни тех ссылок, которые комплятор не может самостоятельно однозначно определить. Rust требует от нас объявлять взаимосвязи посредством обобщённых параметров времени жизни, чтобы убедиться в том, что во время исполнения все используемые ссылки будут корректными.
Аннотирование времени жизни — это концепция, отсутствующая в большинстве других языков программирования, так что она может показаться неясной. Хотя в этой главе мы не будем рассматривать времена жизни во всех деталях, тем не менее, мы обсудим основные ситуации, в которых вы можете столкнуться с ручным указанием времени жизни, что позволит вам получше ознакомиться с этой концепцией.
Защита от висячих ссылок с помощью времён жизни
Основное предназначение времён жизни — предотвращать появление так называемых висячих ссылок — ссылок, указывающих не на те данные, на которые предполагалось указывать. Рассмотрим программу из Листинга 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 определяет, что этот код некорректен? Он использует для этого анализатор заимствований.
Анализатор заимствований
Компилятор 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 их анализирует, давайте поговорим об обобщённых временах жизни параметров и возвращаемых значений функций.
Обобщённые времена жизни в функциях
We’ll write a function that returns the longer of two string slices. This function will take two string slices and return a single string slice. After we’ve implemented the longest
function, the code in Listing 10-19 should print The longest string is abcd
.
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("Самая длинная строка: {result}");
}
Обратите внимание, что мы хотим, чтобы функция принимала строковые срезы (которые являются ссылками), а не сами строки, потому что мы не хотим, чтобы функция longest
забирала во владение свои параметры. Обратитесь к разделу "Строковые срезы как параметры" Главы 4, чтобы вспомнить, почему параметры функции в Листинге 10-19 имеют именно такой тип.
Если мы попробуем реализовать функцию longest
так, как это показано в Листинге 10-20, то программа не скомпилируется:
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("Самая длинная строка: {result}");
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
Вместо этого мы получим следующую ошибку, говорящую о временах жизни:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
9 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
Текст ошибки показывает, что возвращаемому типу нужен обобщённый параметр времени жизни, потому что Rust не может определить, на что указывает возвращаемая ссылка — на x
или на y
. На самом деле, мы тоже не знаем! — блок if
в теле функции возвращает ссылку на x
, а блок else
— на y
.
Когда мы определяем эту функцию, мы не знаем конкретных значений, которые будут переданы в эту функцию, поэтому мы не знаем, какая из ветвей (if
или else
) будет исполнена. Мы также не знаем конкретных сроков жизни ссылок, которые будут переданы, поэтому мы не можем просмотреть области видимости, как мы это делали в Листингах 10-17 и 10-18, чтобы определить, всегда ли возвращаемая ссылка будет действительной. Анализатор заимствований тоже бессилен, поскольку он не знает, как времена жизни x
и y
соотносятся с временем жизни возвращаемого значения. Чтобы исправить эту ошибку, мы добавим обобщённые параметры времени жизни, которые указывают анализатору заимствований то, как ссылки относятся друг к другу.
Аннотирование времени жизни
Аннотации времени жизни не меняют время жизни ссылок. Они скорее описывают, как соотносятся между собой времена жизни нескольких ссылок, не влияя на само время жизни. Точно так же, как функции могут принимать любой тип, когда в сигнатуре указан параметр обобщённого типа, функции могут принимать ссылки с любым временем жизни, указанным с помощью параметра обобщённого времени жизни.
Аннотации времени жизни имеют немного необычный синтаксис: имена параметров времени жизни должны начинаться с апострофа ('
), пишутся маленькими буквами, и обычно очень короткие, как и имена обобщённых типов. Большинство людей использует имя 'a
в качестве первой аннотации времени жизни. Аннотации параметров времени жизни следуют после символа &
и отделяются пробелом от типа значения ссылки.
Приведём несколько примеров: у нас есть ссылка на i32
без указания времени жизни, ссылка на i32
с временем жизни 'a
и изменяемая ссылка на i32
, которая также имеет время жизни 'a
.
&i32 // ссылка
&'a i32 // ссылка с явно указанным временем жизни
&'a mut i32 // изменяемая ссылка с явно указанным временем жизни
Одна лишь аннотация времени жизни сама по себе не имеет большого значения, поскольку аннотации предназначены для того, чтобы информировать Rust о том, как соотносятся между собой времена жизни нескольких ссылок. Давайте рассмотрим, как аннотации времени жизни соотносятся друг с другом в функции longest
.
Аннотации времени жизни в сигнатурах функций
Чтобы использовать аннотации времени жизни в сигнатурах функций, нам нужно объявить параметры обобщённого времени жизни внутри угловых скобок между именем функции и списком параметров, как мы это делали с параметрами обобщённого типа.
Мы хотим, чтобы сигнатура отражала следующее ограничение: возвращаемая ссылка будет действительна до тех пор, пока действительны оба параметра. Это и есть связь между временами жизни параметров и возвращаемого значения. Мы назовём это время жизни 'a
, а затем припишем его каждой ссылке, как показано в Листинге 10-21.
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("Самая длинная строка: {result}"); } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
Теперь наша функция будет работать, а код из Листинга 10-19 — скомпилируется.
Сигнатура функции теперь сообщает Rust, что для некоторого времени жизни 'a
функция принимает два параметра, оба из которых являются строковыми срезами, имеющими время жизни не меньшее, чем 'a
. Сигнатура функции также сообщает Rust, что время жизни строкового среза, возвращаемого функцией, будет не меньше, чем 'a
. На практике это означает, что время жизни ссылки, возвращаемой функцией longest
, равно меньшему времени жизни из времён жизней ссылок, передаваемых в неё. Мы хотим, чтобы Rust использовал именно такие отношения времён жизни при анализе этого кода.
Помните, что когда мы указываем параметры времени жизни в этой сигнатуре функции, мы не меняем времена жизни каких-либо передаваемых или возвращаемых значений. Скорее, мы указываем, что анализатор заимствований должен отклонять любые значения, которые не соответствуют этим ограничениям. Обратите внимание, что самой функции longest
не нужно точно знать, как долго будут жить x
и y
, достаточно того, что некоторая область может быть заменена на 'a
, которая будет удовлетворять этой сигнатуре.
При аннотировании времени жизни в функциях, аннотации помещаются в сигнатуру функции, а не в тело функции. Аннотации времени жизни становятся частью контракта функции, так же как и типы в сигнатуре. Наличие в сигнатурах функций аннотаций времён жизни упрощает работу компилятору. Если возникнет проблема с аннотациями функции или тем, как она используется, компилятор сможет более точно и указать на проблемы нашего кода и необходимые ограничения. Если бы вместо этого компилятор Rust пытался самостоятельно выводить, какие времена жизни мы подразумеваем, то это привело бы к тому, что сообщения компилятора стали бы куда более запутанными, и указывали бы на значительно более отдалённые участки кода.
Когда мы передаём longest
конкретные ссылки, конкретное время жизни, которое подставляется вместо 'a
, становится частью области видимости x
, которая перекрывается с областью видимости y
. Другими словами, общее время жизни 'a
получит конкретное время жизни, равное меньшему из времён жизни x
и y
. Поскольку мы указали возвращаемой ссылке тот же параметр времени жизни ('a
), время жизни возвращаемой ссылки будет не меньшим, чем минимальное из времён жизни x
и y
.
Давайте посмотрим, как аннотации времени жизни ограничивают функцию longest
путём передачи в неё ссылок, которые имеют разные конкретные времена жизни. Посмотрите на Листинг 10-22:
fn main() { let string1 = String::from("длинная строка такая длинная"); { let string2 = String::from("xyz"); let result = longest(string1.as_str(), string2.as_str()); println!("Самая длинная строка: {result}"); } } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
В этом примере переменная string1
действительна до конца внешней области видимости string2
действует до конца внутренней области видимости, а result
ссылается на что-то, что является действительным до конца внутренней области видимости. Запустите этот код, и вы увидите что анализатор заимствований разрешает такой код; он скомпилируется и напечатает Самая длинная строка: длинная строка такая длинная
.
Теперь давайте рассмотрим пример, который показывает, что время жизни ссылки в result
должно быть меньшим временем жизни одного из двух аргументов. Мы переместим объявление переменной result
за пределы внутренней области видимости, но оставим присвоение значения переменной result
в области видимости string2
. Затем мы переместим println!
, который использует result
, за пределы внутренней области видимости — после того как внутренняя область видимости закончилась. Код в Листинге 10-23 не скомпилируется.
fn main() {
let string1 = String::from("длинная строка такая длинная");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("Самая длинная строка: {result}");
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
При попытке скомпилировать этот код, мы получим такую ошибку:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
--> src/main.rs:6:44
|
5 | let string2 = String::from("xyz");
| ------- binding `string2` declared here
6 | result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^ borrowed value does not live long enough
7 | }
| - `string2` dropped here while still borrowed
8 | println!("The longest string is {result}");
| -------- borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
Эта ошибка говорит о том, что если мы хотим использовать result
в инструкции println!
, переменная string2
должна быть действительной до конца внешней области видимости. Rust знает об этом, потому что мы аннотировали параметры функции и её возвращаемое значение одинаковым временем жизни 'a
.
Будучи людьми, мы можем посмотреть на этот код и увидеть, что string1
длиннее, чем string2
и, следовательно, result
будет содержать ссылку на string1
. Поскольку string1
ещё не вышла из области видимости, ссылка на string1
будет всё ещё действительной в инструкции println!
. Однако компилятор не видит, что ссылка в этом случае валидна. Мы сказали Rust, что время жизни ссылки, возвращаемой из функции longest
, равняется меньшему из времён жизни переданных в неё ссылок. Таким образом, анализатор заимствований запрещает код в Листинге 10-23, как потенциально имеющий недействительную ссылку.
Попробуйте провести больше экспериментов с различными значениями и временами жизни ссылок, передаваемыми в функцию longest
, а также с тем, как используется возвращаемое значение Перед компиляцией делайте предположения о том, пройдёт ли ваш код анализ заимствований, а затем проверяйте, насколько вы были правы.
Мышление в терминах времён жизни
В зависимости от того, что делает ваша функция, следует использовать разные способы указания параметров времени жизни. Например, если мы изменим реализацию функции longest
таким образом, чтобы она всегда возвращала свой первый аргумент вместо самого длинного среза строки, то время жизни для параметра y
можно совсем не указывать. Этот код скомпилируется:
fn main() { let string1 = String::from("abcd"); let string2 = "efghijklmnopqrstuvwxyz"; let result = longest(string1.as_str(), string2); println!("Самая длинная строка: {result}"); } fn longest<'a>(x: &'a str, y: &str) -> &'a str { x }
Мы указали параметр времени жизни 'a
для параметра x
и возвращаемого значения, но не для параметра y
, поскольку время жизни параметра y
никак не соотносится с временем жизни параметра x
или возвращаемого значения.
При возврате ссылки из функции, параметр времени жизни для возвращаемого типа должен соответствовать параметру времени жизни одного из аргументов. Если возвращаемая ссылка не ссылается на один из параметров, она должна ссылаться на значение, созданное внутри функции. Однако, это приведёт к недействительной ссылке, поскольку значение, на которое она ссылается, выйдет из области видимости в конце функции. Посмотрите на вот эту попытку реализации функции longest
, которая не скомпилируется:
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("Самая длинная строка: {result}");
}
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("очень длинная строка");
result.as_str()
}
Здесь, несмотря на то, что мы указали параметр времени жизни 'a
для возвращаемого типа, реализация не будет пропущена анализатором, потому что время жизни возвращаемого значения никак не связано с временем жизни параметров. Мы получим такое сообщение об ошибке:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return value referencing local variable `result`
--> src/main.rs:11:5
|
11 | result.as_str()
| ------^^^^^^^^^
| |
| returns a value referencing data owned by the current function
| `result` is borrowed here
For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
Проблема заключается в том, что result
выходит за область видимости и очищается в конце функции longest
. Мы также пытаемся вернуть ссылку на result
из функции. Мы не можем указать параметры времени жизни, которые могли бы изменить висячую ссылку, а Rust не позволит нам создать висячую ссылку. В этом случае лучшим решением будет вернуть владеемый тип данных, а не ссылку: в этом случае вызывающая функция будет нести ответственность за очистку полученного ею значения.
В конечном итоге, синтаксис времён жизни реализует связывание времён жизни различных аргументов и возвращаемых значений функций. Описывая времена жизни, мы даём Rust достаточно информации, чтобы разрешить безопасные операции с памятью и запретить операции, которые могли бы создать висячие ссылки или иным способом нарушить безопасность памяти.
Аннотирование времён жизни в определениях структур
До сих пор мы объявляли структуры, которые всегда содержали владеемые типы данных. Структуры могут содержать и ссылки, но при этом необходимо добавлять аннотации времени жизни для каждой ссылки в определении структуры. Посмотрите на Листинг 10-24, описывающий структуру ImportantExcerpt
, содержащую срез строки:
struct ImportantExcerpt<'a> { part: &'a str, } fn main() { let novel = String::from("Зовите меня Измаил. Несколько лет тому назад..."); 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: она компилировалась без аннотаций времени жизни.
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 принимает срезы значений типа `String` let word = first_word(&my_string[..]); let my_string_literal = "hello world"; // first_word принимает срезы строковых литералов let word = first_word(&my_string_literal[..]); // Поскольку строковые литералы *эквивалентны* срезам строк, // это тоже сработает, без необходимости брать срез! let word = first_word(my_string_literal); }
Причина, по которой этот код компилируется — историческая. В ранних (до 1.0) версиях Rust этот код не скомпилировался бы, поскольку каждой ссылке нужно было явно назначать время жизни. В те времена, сигнатура этой функции была бы написана примерно так:
fn first_word<'a>(s: &'a str) -> &'a str {
После написания большого количества кода на 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!("Пожалуйста, обратите внимание: {announcement}"); self.part } } fn main() { let novel = String::from("Зовите меня Измаил. Несколько лет тому назад..."); 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!("Пожалуйста, обратите внимание: {announcement}"); self.part } } fn main() { let novel = String::from("Зовите меня Измаил. Несколько лет тому назад..."); let first_sentence = novel.split('.').next().unwrap(); let i = ImportantExcerpt { part: first_sentence, }; }
В этом методе имеется два входных параметра, поэтому Rust применит первое правило и назначит обоим параметрам &self
и announcement
собственные времена жизни. Далее, поскольку один из параметров является &self
, то возвращаемое значение получает время жизни переменой &self
. Готово — все времена жизни теперь выведены.
Время жизни 'static
Одно особенное время жизни, которое мы должны обсудить — это 'static
. Оно означает, что данная ссылка может жить всю продолжительность работы программы. Все строковые литералы по умолчанию имеют время жизни 'static
, но мы можем указать его и явно:
#![allow(unused)] fn main() { let s: &'static str = "Я буду здесь всё время."; }
Содержание этой строки сохраняется внутри бинарного файла программы и всегда доступно для использования. Следовательно, время жизни всех строковых литералов — '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, "Сегодня кто-то — именинник!", ); println!("Самая длинная строка: {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!("Внимание! {ann}"); if x.len() > y.len() { x } else { y } }
Это — функция longest
из Листинга 10-21, которая возвращает наибольший из двух строковых срезов. Но теперь у неё есть дополнительный параметр с именем ann
обобщённого типа T
, который может быть представлен любым типом, реализующим трейт Display
, как указано за where
. Этот дополнительный параметр будет печататься с использованием {}
, b необходимо ограничение по трейту Display
. Поскольку времена жизни являются обобщениями, то объявления параметра времени жизни 'a
и параметра обобщённого типа T
помещаются в один список внутри угловых скобок после имени функции.
Подведём итоги
В этой главе мы рассмотрели много всего! Теперь вы знакомы с параметрами обобщённого типа, с трейтами и ограничениями по трейтам, с обобщёнными параметрами времени жизни. Отныне вы способны писать код без избыточностей, который будет работать во множестве различных ситуаций. Параметры обобщённого типа позволяют использовать код для различных типов данных. Трейты и ограничения по трейтам помогают убедиться, что, хотя типы и обобщённые, они будут вести себя, как этого требует ваш код. Вы изучили, как использовать аннотации времени жизни чтобы убедиться, что ваш новый гибкий код не будет генерировать никаких висячих ссылок. И весь этот анализ происходит в момент компиляции и не влияет на производительность программы во время работы!
Верьте или нет, но в рамках этой темы всё ещё есть чему поучиться: в Главе 18 обсуждаются трейт-объекты, которые являются ещё одним способом использования трейтов. Существуют также более сложные сценарии аннотирования времени жизни, которые вам понадобятся только в очень сложных случаях; для этого вам следует прочитать Справочник Rust. Далее вы узнаете, как писать тесты на Rust, чтобы быть уверенным, что ваш код работает так, как задумано.