Перевірка посилань за допомогою часів життя (Lifetimes)
Часи життя — це ще один вид узагальненого типу, який ми вже використовували. Замість того, щоб гарантувати, що тип має потрібну нам поведінку, часи життя гарантують, що посилання є дійсними так довго, як нам це потрібно.
Одна деталь, яку ми не обговорювали в розділі “References and Borrowing” у Розділі 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 дійсно не дозволяє нульові значення.
Зовнішня область видимості оголошує змінну з ім’ям 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 “does not live long enough.”
Причина в тому, що x буде поза областю видимості, коли внутрішня область
видимості закінчиться на рядку 7. Але r все ще є дійсним для зовнішньої
області видимості; оскільки її область видимості більша, ми кажемо, що вона
“lives longer.” Якби Rust дозволив цьому коду працювати, r посилався б на
пам’ять, яка була деалокована, коли x вийшла з області видимості, і все, що
ми намагалися б зробити з r, не працювало б правильно. Отже, як Rust
визначає, що цей код є недійсним? Він використовує перевірник запозичень.
Перевірник запозичень
Компілятор Rust має borrow checker, який порівнює області видимості, щоб визначити, чи всі запозичення є дійсними. Лістинг 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.
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
Зверніть увагу, що ми хочемо, щоб функція приймала рядкові зрізи, які є
посиланнями, а не рядки, тому що ми не хочемо, щоб функція longest
отримувала власність над своїми параметрами. Зверніться до розділу “String Slices as
Parameters” в Розділі 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 }
}
Натомість ми отримаємо таку помилку, яка говорить про часи життя:
$ 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. Ми також не знаємо конкретних часів життя посилань, які
будуть передані, тому ми не можемо подивитися на області видимості так, як це
робили в Listings 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 }
}
Цей код має скомпілюватися і дати результат, який ми хочемо, коли ми
використаємо його з функцією main у Listing 10-19.
Тепер сигнатура функції каже Rust, що для деякого часу життя 'a функція
приймає два параметри, обидва з яких є рядковими зрізами, що живуть щонайменше
так довго, як час життя 'a. Сигнатура функції також каже Rust, що рядковий
зріз, який повертається з функції, буде жити щонайменше так довго, як час
життя 'a. На практиці це означає, що час життя посилання, яке повертається
функцією longest, є таким самим, як менший із часів життя значень, на які
посилаються аргументи функції. Саме такі зв’язки ми хочемо, щоб Rust
використовував під час аналізу цього коду.
Пам’ятайте, коли ми вказуємо параметри часу життя в цій сигнатурі функції, ми
не змінюємо час життя жодних переданих або повернених значень. Натомість ми
вказуємо, що перевірник запозичень має відхиляти будь-які значення, які не
дотримуються цих обмежень. Зверніть увагу, що функції longest не потрібно
точно знати, як довго житимуть x і y, лише те, що для 'a може бути
підставлена деяка область видимості, яка задовольнить цю сигнатуру.
Під час анотування часів життя у функціях анотації розміщуються в сигнатурі функції, а не в тілі функції. Анотації часу життя стають частиною контракту функції, так само як типи в сигнатурі. Те, що сигнатури функцій містять контракт часу життя, означає, що аналіз, який виконує компілятор Rust, може бути простішим. Якщо є проблема з тим, як функція анотована або як вона викликається, помилки компілятора можуть точніше вказати на частину нашого коду та на обмеження. Якщо ж компілятор Rust робив би більше висновків про те, якими ми маємо на увазі зв’язки між часами життя, компілятор, можливо, міг би вказати лише на використання нашого коду за багато кроків від причини проблеми.
Коли ми передаємо конкретні посилання до longest, конкретний час життя, який
підставляється замість 'a, є частиною області видимості x, що
перетинається з областю видимості y. Іншими словами, узагальнений час життя
'a отримає конкретний час життя, який дорівнює меншому з часів життя x і
y. Оскільки ми анотували посилання, що повертається, тим самим параметром
часу життя 'a, посилання, що повертається, також буде дійсним протягом
меншого з часів життя x і y.
Давайте подивимося, як анотації часу життя обмежують функцію longest, коли
ми передаємо посилання з різними конкретними часами життя. Лістинг 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 }
}
У цьому прикладі 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 }
}
Коли ми намагаємося скомпілювати цей код, ми отримуємо таку помилку:
$ 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, є таким самим, як менший із часів життя
посилань, що передаються. Тому перевірник запозичень не дозволяє код у
Listing 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 є дійсним.
Скорочення часу життя
Ви дізналися, що кожне посилання має час життя і що вам потрібно вказувати параметри часу життя для функцій або структур, які використовують посилання. Однак у нас була функція в Лістингу 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);
}
Причина, чому ця функція компілюється без анотацій часу життя, є історичною: у ранніх версіях Rust (до 1.0) цей код не скомпілювався б, тому що кожне посилання потребувало явного часу життя. На той час сигнатура функції була б написана так:
fn first_word<'a>(s: &'a str) -> &'a str {
Після написання великої кількості коду Rust команда Rust виявила, що програмісти Rust у певних ситуаціях знову й знову вводили ті самі анотації часу життя. Ці ситуації були передбачуваними та підкорялися кільком детермінованим шаблонам. Розробники вбудували ці шаблони в код компілятора, щоб перевірник запозичень міг виводити часи життя в цих ситуаціях і не потребував явних анотацій.
Ця частина історії Rust є важливою, тому що цілком можливо, що з’являться нові детерміновані шаблони й будуть додані до компілятора. У майбутньому може знадобитися ще менше анотацій часу життя.
Шаблони, вбудовані в аналіз посилань Rust, називаються правилами скорочення часу життя (lifetime elision rules). Це не правила, яких мають дотримуватися програмісти; це набір окремих випадків, які компілятор розглядатиме, і якщо ваш код підпадає під ці випадки, вам не потрібно явно писати часи життя.
Правила скорочення не забезпечують повного виведення. Якщо після того, як Rust застосує правила, усе ще залишається неоднозначність щодо того, які часи життя мають посилання, компілятор не вгадуватиме, яким має бути час життя решти посилань. Замість здогадування компілятор видасть помилку, яку ви можете усунути, додавши анотації часу життя.
Часи життя для параметрів функції або методу називаються вхідними часами життя (input lifetimes), а часи життя для значень, що повертаються, — вихідними часами життя (output lifetimes).
Компілятор використовує три правила, щоб визначити часи життя посилань, коли
немає явних анотацій. Перше правило застосовується до вхідних часів життя, а
друге і третє — до вихідних часів життя. Якщо компілятор доходить до кінця
трьох правил, а все ще є посилання, для яких він не може визначити часи
життя, компілятор зупиняється з помилкою. Ці правила застосовуються до
визначень fn, а також до блоків impl.
Перше правило полягає в тому, що компілятор призначає параметр часу життя
кожному параметру, який є посиланням. Іншими словами, функція з одним
параметром отримує один параметр часу життя: fn foo<'a>(x: &'a i32); функція
з двома параметрами отримує два окремі параметри часу життя:
fn foo<'a, 'b>(x: &'a i32, y: &'b i32); і так далі.
Друге правило полягає в тому, що якщо існує рівно один вхідний параметр часу
життя, то цей час життя призначається всім вихідним параметрам часу життя:
fn foo<'a>(x: &'a i32) -> &'a i32.
Третє правило полягає в тому, що якщо є кілька вхідних параметрів часу життя,
але один із них — це &self або &mut self, тому що це метод, то час життя
self призначається всім вихідним параметрам часу життя. Це третє правило
робить методи набагато приємнішими для читання та написання, тому що потрібно
менше символів.
Давайте уявімо, що ми — компілятор. Ми застосуємо ці правила, щоб визначити
часи життя посилань у сигнатурі функції first_word у Listing 10-25.
Сигнатура починається без жодних часів життя, пов’язаних із посиланнями:
fn first_word(s: &str) -> &str {
Потім компілятор застосовує перше правило, яке визначає, що кожен параметр
отримує свій власний час життя. Ми назвемо його 'a, як зазвичай, тож тепер
сигнатура виглядає так:
fn first_word<'a>(s: &'a str) -> &str {
Друге правило застосовується, тому що є рівно один вхідний час життя. Друге правило визначає, що час життя одного вхідного параметра призначається вихідному часу життя, тож тепер сигнатура така:
fn first_word<'a>(s: &'a str) -> &'a str {
Тепер усі посилання в цій сигнатурі функції мають часи життя, і компілятор може продовжити свій аналіз без потреби в тому, щоб програміст анотував часи життя в цій сигнатурі функції.
Розгляньмо ще один приклад, цього разу використовуючи функцію longest,
яка не мала параметрів часу життя, коли ми почали працювати з нею в Listing
10-20:
fn longest(x: &str, y: &str) -> &str {
Застосуймо перше правило: кожен параметр отримує свій власний час життя. Цього разу в нас два параметри замість одного, тож маємо два часи життя:
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
Ви бачите, що друге правило не застосовується, тому що вхідних часів життя
більше ніж один. Третє правило також не застосовується, тому що longest є
функцією, а не методом, тож жоден із параметрів не є self. Після виконання
всіх трьох правил ми все ще не з’ясували, який час життя має тип, що
повертається. Саме тому ми отримали помилку, намагаючись скомпілювати код у
Listing 10-20: компілятор пройшовся правилами скорочення часу життя, але все
ще не зміг визначити всі часи життя посилань в сигнатурі.
Оскільки третє правило справді застосовується лише в сигнатурах методів, далі ми розглянемо часи життя в цьому контексті, щоб побачити, чому третє правило означає, що нам не потрібно дуже часто анотувати часи життя в сигнатурах методів.
У визначеннях методів
Коли ми реалізуємо методи для структури з часами життя, ми використовуємо той самий синтаксис, що й для узагальнених параметрів типу, як показано в Listing 10-11. Те, де ми оголошуємо та використовуємо параметри часу життя, залежить від того, чи пов’язані вони з полями структури, чи з параметрами методу та значеннями, що повертаються.
Імена часів життя для полів структури завжди потрібно оголошувати після
ключового слова impl, а потім використовувати після імені структури, тому
що ці часи життя є частиною типу структури.
У сигнатурах методів усередині блоку impl посилання можуть бути пов’язані з
часом життя посилань у полях структури або можуть бути незалежними. Крім того,
правила скорочення часу життя часто роблять так, що анотації часу життя не є
потрібними в сигнатурах методів. Давайте розглянемо кілька прикладів,
використовуючи структуру з назвою ImportantExcerpt, яку ми визначили в
Listing 10-24.
Спочатку ми використаємо метод з назвою level, єдиним параметром якого є
посилання на self, а значенням, що повертається, є i32, який не є
посиланням на щось:
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 з Listing 10-21, яка повертає довше з двох рядкових
зрізів. Але тепер у неї є додатковий параметр з назвою ann узагальненого
типу T, який може бути підставлений будь-яким типом, що реалізує трейт
Display, як визначено в фразі where. Цей додатковий параметр буде
виводитися за допомогою {}, саме тому обмеження трейтів Display є
необхідним. Оскільки часи життя є видом узагальненого типу, оголошення
параметра часу життя 'a та узагальненого параметра типу T розміщуються в
одному списку всередині кутових дужок після імені функції.
Підсумок
У цьому розділі ми охопили багато! Тепер, коли ви знаєте про узагальнені параметри типу, трейти та обмеження трейтів, а також про узагальнені параметри часу життя, ви готові писати код без повторення, який працює в багатьох різних ситуаціях. Узагальнені параметри типу дозволяють застосовувати код до різних типів. Трейти та обмеження трейтів гарантують, що хоча типи й є узагальненими, вони матимуть поведінку, яка потрібна коду. Ви навчилися використовувати анотації часу життя, щоб гарантувати, що цей гнучкий код не матиме висячих посилань. І все це відбувається під час компіляції, що не впливає на продуктивність під час виконання!
Вірте чи ні, але ще багато чого потрібно вивчити на теми, які ми обговорювали в цьому розділі: Розділ 18 розглядає об’єкти трейтів, які є ще одним способом використання трейтів. Також існують складніші сценарії, що стосуються анотацій часу життя, які вам знадобляться лише в дуже просунутих випадках; для них вам слід прочитати Rust Reference. Але далі ви навчитеся писати тести в Rust, щоб переконатися, що ваш код працює так, як повинен.