Проверка ссылок с помощью времени жизни
Время жизни — это ещё один вид обобщений, которые мы уже использовали. Вместо того чтобы гарантировать, что тип обладает нужным поведением, время жизни гарантирует, что ссылки остаются валидными столько, сколько это необходимо.
Одна деталь, которую мы не обсуждали в разделе «Ссылки и заимствование» главы 4, заключается в том, что каждая ссылка в Rust имеет время жизни, то есть область видимости, в течение которой эта ссылка валидна. Чаще всего время жизни неявное и выводится, подобно тому как чаще всего выводятся типы. Мы обязаны аннотировать типы только когда возможны несколько типов. Аналогично, мы должны аннотировать время жизни, когда время жизни ссылок может быть связано несколькими разными способами. Rust требует от нас аннотировать эти связи с помощью параметров обобщённого времени жизни, чтобы гарантировать, что фактические ссылки, используемые во время выполнения, безусловно будут валидными.
Аннотирование времени жизни — это концепция, которой нет в большинстве других языков программирования, поэтому это покажется непривычным. Хотя мы не будем полностью покрывать время жизни в этой главе, мы обсудим распространённые случаи, когда вы можете столкнуться с синтаксисом времени жизни, чтобы вы могли привыкнуть к этой концепции.
Предотвращение висячих ссылок с помощью времени жизни
Основная цель времени жизни — предотвращать висячие ссылки, которые заставляют программу ссылаться на данные, отличные от тех, на которые она должна ссылаться. Рассмотрим небезопасную программу в Листинге 10-16, у которой есть внешняя и внутренняя область видимости.
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
Примечание: Примеры в Листингах 10-16, 10-17 и 10-23 объявляют переменные без начального значения, поэтому имя переменной существует во внешней области видимости. На первый взгляд это может показаться противоречащим отсутствию в Rust пустых значений. Однако, если мы попытаемся использовать переменную до того, как присвоим ей значение, мы получим ошибку компиляции, которая показывает, что Rust действительно не разрешает пустые значения.
Внешняя область видимости объявляет переменную 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 и x, названные 'a и 'b соответственноЗдесь мы аннотировали время жизни r как 'a, а время жизни x как 'b. Как видите, внутренний блок 'b намного меньше, чем внешний блок времени жизни 'a. Во время компиляции Rust сравнивает размер двух времён жизни и видит, что r имеет время жизни 'a, но ссылается на память со временем жизни 'b. Программа отклоняется, потому что 'b короче, чем 'a: объект ссылки живёт не так долго, как ссылка.
Листинг 10-18 исправляет код так, чтобы в нём не было висячей ссылки, и он компилируется без ошибок.
fn main() { let x = 5; // ----------+-- 'b // | let r = &x; // --+-- 'a | // | | println!("r: {r}"); // | | // --+ | } // ----------+
Здесь x имеет время жизни 'b, которое в данном случае больше, чем 'a. Это означает, что r может ссылаться на x, потому что Rust знает, что ссылка в r всегда будет валидной, пока x валидна.
Теперь, когда вы знаете, что такое время жизни ссылок и как Rust анализирует время жизни, чтобы гарантировать, что ссылки всегда будут валидными, давайте изучим обобщённое время жизни параметров и возвращаемых значений в контексте функций.
Обобщённое время жизни в функциях
Мы напишем функцию, которая возвращает более длинную из двух строковых срезов. Эта функция будет принимать два строковых среза и возвращать один строковый срез. После того как мы реализуем функцию longest, код в Листинге 10-19 должен вывести The longest string is abcd.
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
main, которая вызывает функцию longest, чтобы найти более длинную из двух строковых срезовОбратите внимание, что мы хотим, чтобы функция принимала строковые срезы, которые являются ссылками, а не строки, потому что мы не хотим, чтобы функция 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!("The longest string is {result}");
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
longest, которая возвращает более длинную из двух строковых срезов, но пока не компилируетсяВместо этого мы получаем следующую ошибку, которая говорит о времени жизни:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
9 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
Текст справки показывает, что возвращаемый тип нуждается в параметре обобщённого времени жизни, потому что Rust не может определить, на какую из ссылок (x или y) ссылается возвращаемое значение. На самом деле, мы тоже не знаем, потому что блок if в теле этой функции возвращает ссылку на x, а блок else возвращает ссылку на y!
Когда мы определяем эту функцию, мы не знаем конкретные значения, которые будут переданы в эту функцию, поэтому мы не знаем, выполнится ли случай if или случай else. Мы также не знаем конкретные времена жизни ссылок, которые будут переданы, поэтому мы не можем посмотреть на области видимости, как мы делали в Листингах 10-17 и 10-18, чтобы определить, будет ли ссылка, которую мы возвращаем, всегда валидной. Проверка заимствований тоже не может определить это, потому что она не знает, как времена жизни x и y связаны со временем жизни возвращаемого значения. Чтобы исправить эту ошибку, мы добавим параметры обобщённого времени жизни, которые определяют связь между ссылками, чтобы проверка заимствований могла выполнить свой анализ.
Синтаксис аннотаций времени жизни
Аннотации времени жизни не изменяют, как долго живут какие-либо из ссылок. Вместо этого они описывают взаимосвязи времён жизни нескольких ссылок друг с другом, не влияя на сами времена жизни. Подобно тому как функции могут принимать любой тип, когда сигнатура указывает параметр обобщённого типа, функции могут принимать ссылки с любым временем жизни, указав параметр обобщённого времени жизни.
Аннотации времени жизни имеют немного необычный синтаксис: имена параметров времени жизни должны начинаться с апострофа (') и обычно все строчные и очень короткие, подобно обобщённым типам. Большинство людей используют имя 'a для первой аннотации времени жизни. Мы размещаем аннотации параметров времени жизни после & ссылки, используя пробел для отделения аннотации от типа ссылки.
Вот несколько примеров: ссылка на i32 без параметра времени жизни, ссылка на i32, которая имеет параметр времени жизни с именем 'a, и изменяемая ссылка на i32, которая также имеет время жизни 'a.
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
Одна аннотация времени жизни сама по себе не имеет большого смысла, потому что аннотации предназначены для того, чтобы сообщить Rust, как обобщённые параметры времени жизни нескольких ссылок связаны друг с другом. Давайте рассмотрим, как аннотации времени жизни связаны друг с другом в контексте функции longest.
Аннотации времени жизни в сигнатурах функций
Чтобы использовать аннотации времени жизни в сигнатурах функций, нам нужно объявить обобщённые параметры времени жизни внутри угловых скобок между именем функции и списком параметров, как мы это делали с обобщёнными параметрами типа.
Мы хотим, чтобы сигнатура выражала следующее ограничение: возвращаемая ссылка будет валидной так долго, как валидны оба параметра. Это связь между временами жизни параметров и возвращаемым значением. Мы назовём время жизни 'a и затем добавим его к каждой ссылке, как показано в Листинге 10-21.
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 } }
longest, указывающее, что все ссылки в сигнатуре должны иметь одинаковое время жизни 'aЭтот код должен скомпилироваться и дать желаемый результат, когда мы используем его с функцией main в Листинге 10-19.
Теперь сигнатура функции сообщает Rust, что для некоторого времени жизни 'a функция принимает два параметра, оба из которых являются строковыми срезами, которые живут как минимум столько же, сколько время жизни 'a. Сигнатура функции также сообщает Rust, что строковый срез, возвращаемый из функции, будет жить как минимум столько же, сколько время жизни 'a. На практике это означает, что время жизни ссылки, возвращаемой функцией longest, равно меньшему из времён жизни значений, на которые ссылаются аргументы функции. Эти связи — именно то, что мы хотим, чтобы Rust использовал при анализе этого кода.
Помните, когда мы указываем параметры времени жизни в этой сигнатуре функции, мы не изменяем время жизни каких-либо передаваемых или возвращаемых значений. Вместо этого мы указываем, что проверка заимствований должна отклонять любые значения, которые не соответствуют этим ограничениям. Обратите внимание, что функция longest не должна знать точно, как долго будут жить x и y, только что некоторая область видимости может быть подставлена вместо 'a, которая удовлетворит этой сигнатуре.
При аннотировании времени жизни в функциях аннотации помещаются в сигнатуру функции, а не в тело функции. Аннотации времени жизни становятся частью контракта функции, подобно типам в сигнатуре. Наличие в сигнатурах функций контракта времени жизни означает, что анализ, который выполняет компилятор Rust, может быть проще. Если есть проблема с тем, как функция аннотирована или как она вызывается, ошибки компилятора могут указать на часть нашего кода и ограничения более точно. Если бы, вместо этого, компилятор Rust делал больше выводов о том, какие связи времён жизни мы предполагали, компилятор мог бы быть способен указать только на использование нашего кода на многих шагах от причины проблемы.
Когда мы передаём конкретные ссылки в longest, конкретное время жизни, подставляемое вместо 'a, — это та часть области видимости x, которая перекрывается с областью видимости y. Другими словами, обобщённое время жизни 'a получит конкретное время жизни, равное меньшему из времён жизни x и y. Поскольку мы аннотировали возвращаемую ссылку тем же параметром времени жизни 'a, возвращаемая ссылка также будет валидной в течение меньшего из времён жизни x и y.
Давайте посмотрим, как аннотации времени жизни ограничивают функцию longest, передавая ссылки с разными конкретными временами жизни. Листинг 10-22 — простой пример.
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 } }
longest со ссылками на значения String, которые имеют разные конкретные времена жизниВ этом примере string1 валидна до конца внешней области видимости, string2 валидна до конца внутренней области видимости, а result ссылается на нечто, что валидно до конца внутренней области видимости. Запустите этот код, и вы увидите, что проверка заимствований одобряет; он скомпилируется и выведет The longest string is long string is long.
Далее попробуем пример, который показывает, что время жизни ссылки в result должно быть меньшим временем жизни из двух аргументов. Мы переместим объявление переменной result за пределы внутренней области видимости, но оставим присваивание значения переменной result внутри области видимости с string2. Затем мы переместим println!, который использует result, за пределы внутренней области видимости, после того как внутренняя область видимости закончится. Код в Листинге 10-23 не скомпилируется.
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 }
}
result после того, как string2 вышла из области видимостиКогда мы пытаемся скомпилировать этот код, мы получаем эту ошибку:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
--> src/main.rs:6:44
|
5 | let string2 = String::from("xyz");
| ------- binding `string2` declared here
6 | result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^ borrowed value does not live long enough
7 | }
| - `string2` dropped here while still borrowed
8 | println!("The longest string is {result}");
| -------- borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
Ошибка показывает, что для того чтобы result была валидной для оператора println!, string2 должна быть валидной до конца внешней области видимости. Rust знает это, потому что мы аннотировали времена жизни параметров функции и возвращаемых значений, используя один и тот же параметр времени жизни 'a.
Как люди, мы можем посмотреть на этот код и увидеть, что string1 длиннее, чем string2, и поэтому result будет содержать ссылку на string1. Поскольку string1 не вышла из области видимости, ссылка на string1 всё ещё будет валидной для оператора println!. Однако компилятор не может видеть, что ссылка валидна в этом случае. Мы сказали Rust, что время жизни ссылки, возвращаемой функцией longest, равно меньшему из времён жизни ссылок, передаваемых в функцию. Поэтому проверка заимствований запрещает код в Листинге 10-23 как потенциально имеющий невалидную ссылку.
Попробуйте разработать больше экспериментов, которые меняют значения и времена жизни передаваемых в функцию longest ссылок и то, как используется возвращаемая ссылка. Сформулируйте гипотезы о том, пройдут ли ваши эксперименты проверку заимствований, прежде чем компилировать; затем проверьте, правы ли вы!
Мышление в терминах времени жизни
То, как вам нужно указывать параметры времени жизни, зависит от того, что делает ваша функция. Например, если бы мы изменили реализацию функции longest так, чтобы она всегда возвращала первый параметр, а не самый длинный строковый срез, нам не нужно было бы указывать время жизни на параметре y. Следующий код скомпилируется:
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, которая не скомпилируется:
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, которая хранит строковый срез.
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 валидна.
Упрощение времени жизни (Lifetime Elision)
Вы узнали, что каждая ссылка имеет время жизни и что вам нужно указывать параметры времени жизни для функций или структур, которые используют ссылки. Однако у нас была функция в Листинге 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 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 ссылок, называются правилами упрощения времени жизни (lifetime elision rules). Это не правила для программистов, которым нужно следовать; это набор определённых случаев, которые компилятор будет рассматривать, и если ваш код соответствует этим случаям, вам не нужно писать время жизни явно.
Правила упрощения не обеспечивают полный вывод. Если после применения Rust правил всё ещё есть неоднозначность относительно того, какое время жизни у ссылок, компилятор не будет угадывать, каким должно быть время жизни оставшихся ссылок. Вместо угадывания компилятор даст вам ошибку, которую вы можете исправить, добавив аннотации времени жизни.
Время жизни параметров функции или метода называется входным временем жизни (input lifetimes), а время жизни возвращаемых значений — выходным временем жизни (output lifetimes).
Компилятор использует три правила, чтобы определить время жизни ссылок, когда нет явных аннотаций. Первое правило применяется к входным временам жизни, а второе и третье — к выходным. Если компилятор дойдёт до конца трёх правил и всё ещё есть ссылки, для которых он не может определить время жизни, компилятор остановится с ошибкой. Эти правила применяются к определениям fn, а также к блокам impl.
Первое правило заключается в том, что компилятор назначает разный параметр времени жизни каждому времени жизни в каждом входном типе. Ссылки вроде &'_ i32 нуждаются в параметре времени жизни, и структуры вроде ImportantExcerpt<'_> нуждаются в параметре времени жизни. Например:
- Функция
fn foo(x: &i32)получит один параметр времени жизни и станетfn foo<'a>(x: &'a i32). - Функция
fn foo(x: &i32, y: &i32)получит два параметра времени жизни и станетfn foo<'a, 'b>(x: &'a i32, y: &'b i32). - Функция
fn foo(x: &ImportantExcerpt)получит два параметра времени жизни и станетfn foo<'a, 'b>(x: &'a ImportantExcerpt<'b>).
Второе правило: если есть ровно один параметр входного времени жизни, это время жизни назначается всем параметрам выходного времени жизни: 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: компилятор прошёл через правила упрощения времени жизни, но всё ещё не смог определить все времена жизни ссылок в сигнатуре.
Поскольку третье правило действительно применяется только в сигнатурах методов, давайте next посмотрим на время жизни в этом контексте, чтобы увидеть, почему третье правило означает, что нам редко нужно аннотировать времена жизни в сигнатурах методов.
Аннотации времени жизни в определениях методов
Когда мы реализуем методы для структуры со временем жизни, мы используем тот же синтаксис, что и для параметров обобщённого типа, как показано в Листинге 10-11. То, где мы объявляем и используем параметры времени жизни, зависит от того, связаны ли они с полями структуры или с параметрами и возвращаемыми значениями методов.
Имена времени жизни для полей структуры всегда должны быть объявлены после ключевого слова impl и затем использованы после имени структуры, потому что эти времена жизни являются частью типа структуры.
В сигнатурах методов внутри блока impl ссылки могут быть привязаны ко времени жизни ссылок в полях структуры или они могут быть независимы. Кроме того, правила упрощения времени жизни часто делают так, что аннотации времени жизни не требуются в сигнатурах методов. Давайте посмотрим на несколько примеров, используя структуру с именем ImportantExcerpt, которую мы определили в Листинге 10-24.
Сначала мы используем метод с именем level, единственный параметр которого — это ссылка на self, а возвращаемое значение — i32, который не является ссылкой ни на что:
struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } } impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention please: {announcement}"); self.part } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap(); let i = ImportantExcerpt { part: first_sentence, }; }
Объявление параметра времени жизни после impl и его использование после имени типа требуются, но мы не обязаны аннотировать время жизни ссылки на self из-за первого правила упрощения.
Вот пример, где применяется третье правило упрощения времени жизни:
struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } } impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention please: {announcement}"); self.part } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap(); let i = ImportantExcerpt { part: first_sentence, }; }
Есть два входных времени жизни, так что Rust применяет первое правило упрощения времени жизни и даёт и &self, и announcement их собственные времена жизни. Затем, потому что один из параметров — &self, возвращаемый тип получает время жизни &self, и все времена жизни учтены.
Статическое время жизни
Одно особое время жизни, которое нам нужно обсудить, — это 'static, которое обозначает, что затронутая ссылка может жить в течение всей продолжительности программы. Все строковые литералы имеют время жизни 'static, которое мы можем аннотировать следующим образом:
#![allow(unused)] fn main() { let s: &'static str = "I have a static lifetime."; }
Текст этой строки хранится непосредственно в бинарном файле программы, который всегда доступен. Поэтому время жизни всех строковых литералов — 'static.
Вы можете видеть предложения в сообщениях об ошибках использовать время жизни 'static. Но прежде чем указывать 'static в качестве времени жизни для ссылки, подумайте, живёт ли на самом деле ссылка, которую у вас есть, в течение всего времени жизни вашей программы или нет, и хотите ли вы этого. Большинство времени сообщение об ошибке, предлагающее время жизни 'static, возникает из-за попытки создать висячую ссылку или несоответствие доступных времён жизни. В таких случаях решение — исправить эти проблемы, а не указывать время жизни 'static.
Обобщённые параметры типа, ограничения типажа и время жизни вместе
Давайте кратко посмотрим на синтаксис указания обобщённых параметров типа, ограничений типажа и времён жизни всех вместе в одной функции!
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest_with_an_announcement( string1.as_str(), string2, "Today is someone's birthday!", ); println!("The longest string is {result}"); } use std::fmt::Display; fn longest_with_an_announcement<'a, T>( x: &'a str, y: &'a str, ann: T, ) -> &'a str where T: Display, { println!("Announcement! {ann}"); if x.len() > y.len() { x } else { y } }
Это функция longest из Листинга 10-21, которая возвращает более длинную из двух строковых срезов. Но теперь у неё есть дополнительный параметр с именем ann обобщённого типа T, который может быть заполнен любым типом, который реализует типаж Display, как указано в предложении where. Этот дополнительный параметр будет выведен с использованием {}, поэтому ограничение типажа Display необходимо. Поскольку время жизни — это тип обобщения, объявления параметра времени жизни 'a и параметра обобщённого типа T идут в одном списке внутри угловых скобок после имени функции.
Резюме
Мы покрыли многое в этой главе! Теперь, когда вы знаете об обобщённых параметрах типа, типажах и ограничениях типажа, и обобщённых параметрах времени жизни, вы готовы писать код без повторений, который работает во многих разных ситуациях. Обобщённые параметры типа позволяют применять код к разным типам. Типажи и ограничения типажа гарантируют, что даже несмотря на то, что типы обобщены, они будут иметь поведение, необходимое коду. Вы узнали, как использовать аннотации времени жизни, чтобы гарантировать, что этот гибкий код не будет иметь висячих ссылок. И весь этот анализ происходит во время компиляции, что не влияет на производительность во время выполнения!
Считайте ли вы или нет, есть ещё многое, что можно узнать на темах, которые мы обсудили в этой главе: Глава 18 обсуждает объекты типажа, которые — ещё один способ использовать типажи. Есть также более сложные сценарии с аннотациями времени жизни, которые вам понадобятся только в очень продвинутых сценариях; для них вы должны прочитать Справку Rust. Но next вы узнаете, как писать тесты в Rust, чтобы убедиться, что ваш код работает так, как должен.