Перевірка коректності посилань за допомогою часів існування

Часи існування (lifetimes) є ще одним узагальненим типом даних, який ми вже використовували. Замість того щоб гарантувати, що тип має бажану поведінку, часи існування гарантують що посилання є валідним доти, доки воно може бути нам потрібним.

В частині “Посилання і позичання” четвертого розділу ми не згадали про те, що кожне посилання в Rust має свій час існування, який обмежує час протягом якого посилання є дійсним. В більшості випадків, часи існування є неявними (implicit) та виведеними (inferred), так само як і типи. Ми зобовʼязані додавати анотації лише у випадках коли можливий більше ніж один тип. Відповідно, ми мусимо додавати анотації до часів існування лише якщо останні можуть бути використані у кілька різних способів. Rust зобовʼязує нас анотувати звʼязки використовуючи узагальнені параметри часу існування, щоб впевнитися що посилання використані протягом часу виконання програми будуть коректними.

Додавання анотацій для часів існування не є поширеним в інших мовах програмування, тож може бути здаватися дещо складним для сприйняття. Попри те що в цьому розділі ми не розглядатимемо всі деталі часів існування, ми обговоримо їх найбільш загальні способи використання, щоб в подальшому у вас не виникало проблем зі сприйняттям цієї концепції.

Запобігання висячим посиланням з використанням часів існування

Головною метою використання часів існування є запобігання висячим посиланням, які зберігають в памʼяті дані, котрі більше не будуть використані програмою. Розглянемо приклад з блоку коду 10-16, який має внутрішню і зовнішню область видимості.

fn main() {
    let r;

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

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

Блок коду 10-16: Спроба використати посилання, значення якого лежить за межами області видимості

Примітка: Приклади у Блоках коду 10-16, 10-17 та 10-23 проголошують змінні без надання їм початкового значення, тож, назва змінної існує в зовнішньої області видимості. На перший погляд, може видатися, що це суперечить тому, що Rust не має нульових значень. Однак, якщо ми спробуємо використати змінну перед наданням їй значення, ми отримаємо помилку часу компіляції, що показує, що 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
  |
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` due to previous error

Змінна x не "існує достатньо довго". Причина в тому, що x вийде з області видимості коли внутрішня область видимості скінчиться у рядку 7. Але змінна r все ще валідна у зовнішній області видимості; оскільки її область видимості більша, ми кажемо, що вона "існує довше". Якби Rust дозволив цьому коду працювати, r посилався би на пам'ять, що була звільнена, коли x вийшов з області видимості, і все, що ми намагатимемося робити з r, не працюватиме належним чином. То як Rust визначає, що код є некоректним? Вона використовує borrow checker.

Borrow Checker

Компілятор Rust має borrow checker, який порівнює області видимості і визначає, чи всі позичання валідні. Блок коду 10-17 показує такий самий код, як у Блоці коду 10-16, але з анотаціями, які показують часи існування змінних.

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

Блок коду 10-17: анотації часів існування 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); //   |       |
                          // --+       |
}                         // ----------+

Блок коду 10-18: посилання є валідним, бо дані мають довший час існування, ніж посилання

Тут x має час існування 'b, що у цьому випадку більше, ніж 'a. Це означає, що r може посилатись на x, тому що Rust знає, що посилання в r завжди буде дійсним, поки x є дійсним.

Тепер, коли ви знаєте, де знаходяться часи існування посилань і як Rust аналізує часи існування, щоб гарантувати, що посилання завжди будуть валідними, розгляньмо узагальнені часи існування параметрів та значень, що повертаються, в контексті функцій.

Узагальнені часи існування у функціях

Ми напишемо функцію, яка повертає довший з двох стрічкових слайсів. Ця функція прийматиме два стрічкові слайси і повертатиме один слайс. Після реалізації функції longest, код у Блоці коду 10-19 має вивести The longest string is abcd.

Filename: src/main.rs

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

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

Блок коду 10-19: функція main, що викликає функцію longest щоб знайти довший з двох стрічкових слайсів

Зверніть увагу, що нам потрібно, щоб функція приймала рядки фрагментів, які є посиланнями, а не стрічки, тому що ми не хочемо, щоб функція longest брала володіння над своїми параметрами. Зверніться до підрозділу "Стрічкові слайси як параметри" Розділу 4 для детальнішого обговорення, чому ми хочемо використати саме такі параметри в Блоці коду 10-19.

Якщо ми спробуємо реалізувати функцію longest як показано у Блоці коду 10-20, вона не скомпілюється.

Файл: src/lib.rs

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

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

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

Блок коду 10-20: реалізація функції 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` due to previous error

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

Коли ми визначаємо цю функцію, ми не знаємо конкретних значень, які будуть передані в цю функцію, тому ми не знаємо, спрацює випадок if чи else. Ми також не знаємо конкретного часу існування посилань, які будуть передані, тож ми не можемо подивитися на область видимості, як ми робили у Блоках коду 10-17 та 10-18, щоб визначити, чи посилання, яке ми повертаємо, буде завжди валідним. Borrow checker також не може визначити цього, оскільки він не знає як час існування x або y стосується часу існування значення, що повертається. Щоб виправити цю помилку, ми додамо узагальнені параметри часу існування, які визначають зв'язок між посиланнями, щоб borrow checker міг його проаналізувати.

Синтаксис анотацій часу існування

Анотації часу існування не змінюють як довго існує посилання. Натомість вони описують взаємозв'язок часів існування багатьох посилань між собою, не впливаючи на ці часи існування. Так само як функції можуть приймати будь-який тип, коли сигнатура визначає параметр узагальненого типу, функції можуть приймати посилання з будь-яким часом існування, якщо заданий узагальнений параметр часу існування.

Анотації часу існування мають дещо незвичний синтаксис: імена параметрів часу існування мають починатися на апостроф (') і зазвичай в нижньому регістрі та дуже короткі, як узагальнені типи. Більшість людей використовують ім'я 'a для першої анотації часу існування. Ми розміщуємо анотації часу існування параметрів після & посилання, використовуючи пробіл для відокремлення анотації від типу посилання.

Ось кілька прикладів: посилання на i32 без параметру часу існування, посилання на на i32, що має параметр часу існування 'a, і мутабельне посилання на i32, що також має час існування 'a.

&i32        // посилання
&'a i32     // посилання з явним часом існування
&'a mut i32 // мутабельне посилання з явним часом існування

Сам по собі одна анотація часу існування не має великого значення. оскільки анотації мають повідомляти Rust, як узагальнені параметри часу існування багатьох посилань співвідносяться один з одним. Дослідімо, як анотації часу існування співвідносяться одна з одною в контексті функції longest.

Анотації часу існування у сигнатурах функцій

Щоб використовувати анотації часу існування в сигнатурах функцій, ми маємо проголосити узагальнений параметр часу існування в кутових дужках між назвою функції і списком параметрів, так, як це робиться з узагальненими параметрами типу.

Ми хочемо, щоб сигнатура виражала наступне обмеження: повернуте посилання буде валідним стільки, скільки обидва параметри будуть валідними. Це відношення між часом існування параметрів та значення, що повертається. Ми назвемо час існування 'a, а тоді додамо його до кожного посилання, як показано в Блоці коду 10-21.

Файл: src/main.rs

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

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

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

Блок коду 10-21: Визначення функції longest із зазначенням, що всі посилання у сигнатурі повинні мати однаковий час існування 'a

Цей код має компілюватися і створювати бажаний результат, коли ми використовуємо його у функції main у Блоці коду 10-19.

Сигнатура функції тепер повідомляє Rust, що для якогось часу існування 'a функція приймає два параметри, обидва з яких - стрічкові слайси, які існують принаймні так довго, як час існування 'a. Сигнатура функції також каже Rust, що стрічковий слайс, повернутий з функції, існуватиме щонайменше так довго, як час існування 'a. На практиці це означає, що час існування посилання, повернутого функцією longest, дорівнює коротшому з часів існування значень, на які посилаються аргументи функції. Ці відносини - це те, що ми хочемо, щоб Rust використовувала під час аналізу цього коду.

Пам'ятайте, що коли ми зазначаємо час існування параметрів на сигнатурі цієї функції, ми не змінюємо час існування жодного значення, які передаються або повертаються. Радше ми уточнюємо, що borrow checker повинен відкидати будь-які значення, які не відповідають цим обмеженням. Зверніть увагу, що функція longest не повинна точно знати, як довго x і y будуть існувати, лише те, що деяка область видимості може бути замінена на 'a що задовольняє цій сигнатурі.

При анотації часів існування у функції анотації зазначаються у сигнатурі функції, а не в її тілі. Анотації часу існування стають частиною контракту функції, так само, як типи у сигнатурі. Те, що сигнатури функцій містять контракт на час існування, означає, що аналіз, який робить компілятор Rust, буде простішим. Якщо є проблема з тим, як функція анотована або як її викликають, помилки компілятора можуть точніше вказувати на частину нашого коду та обмеження. Якщо натомість компілятор Rust робив більше припущень про те, якими ми хотіли б бачити відносини між часами існування, компілятор міг би вказувати лише на використання нашого коду за багато кроків від причини проблеми.

Коли ми передаємо конкретні посилання до longest, конкретний час існування, що підставляється в 'a, є частиною області видимості x, що перетинається з областю видимості y. Іншими словами, узагальнений час існування 'a отримає конкретний час існування, що є рівним меншому з часів існування x та y. Оскільки ми анотували посилання, що повертається, тим самим параметром часу існування 'a, посилання, що повертається, також буде валідним під час тривалості меншого з часів інсування x та y.

Подивімося, як анотації часу існування обмежують функцію longest, передаючи посилання, що мають різне конкретні часи існування. Блок коду 10-22 надає прямолінійний приклад.

Файл: src/main.rs

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

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

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

Блок коду 10-22: використання функції "longest" з посиланнями на значення "String", які мають різний конкретний час існування

У цьому прикладі string1 буде валідним до кінця зовнішньої області видимості, string2 до кінця внутрішньої області видимості, а result посилається на щось, що є валідним до кінця внутрішньої області видимості. Запустіть цей код, і ви побачите, що borrow checker його приймає; код скомпілюється і виведе The longest string is long string is long.

Далі розгляньмо приклад, який показує, що час існування посилання в result має бути меншим з тривалостей існування двох аргументів. Ми перемістимо оголошення змінної result за межі внутрішньої області видимості, але залишимо присвоєння значення змінній result усередині області видимості зі string2. Потім ми перемістимо println!, який використовує result, за межі внутрішньої області видимості, її після закінчення. Код в Блоці коду 10-23 не скомпілюється.

Файл: src/main.rs

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

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

Блок коду 10-23: спроба використати result після виходу string2 з області видимості

Якщо ми спробуємо скомпілювати цей код, то отримаємо таку помилку:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {}", result);
  |                                          ------ borrow later used here

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

Помилка показує, що для того, що result був валідним для інструкції println!, string2 має бути валідним до кінця зовнішньої області видимості. Rust знає це, бо ми анотували час існування параметрів функції та значення, що повертається, за допомогою того самого параметра часу існування 'a.

Як люди, ми можемо подивитися на цей код і побачити, що string1 довша за string2 і тому result міститиме посилання на string1. Оскільки string1 ще не вийшла з області видимості, посилання на string1 все ще буде валідним для інструкції println!. Однак, компілятор не може побачити, що посилання в цьому випадку валідне. Ми повідомили Rust, що час існування посилання, що повертається функцією longest, такий самий, як менший з часів існування переданих їй посилань. Тому borrow checker забороняє код у Блоці коду 10-23 як такий, що потенційно містить неправильне посилання.

Спробуйте провести більше експериментів, які змінюють значення і часи існування посилань, переданих у функцію longest, а також використання посилання, що повертається. Робіть припущення про те, чи пройдуть ваші експерименти borrow checker до компіляції; потім перевірте, щоб побачити, чи маєте ви рацію!

Мислення в термінах часів існування

Спосіб, яким треба позначати параметри часу існування, залежить від того, що саме робить ваша функція. Наприклад, якби ми змінили реалізацію функції longest, щоб та завжди повертала перший параметр замість найдовшої стрічки, то нам не потрібно було б вказувати час існування для параметра y. Цей код буде скомпілюється:

Файл: src/main.rs

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

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

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

Ми зазначили параметр часу існування 'a для параметра x та типу, що повертається, але не для параметра y, оскільки час існування у у не має стосунку до часу існування x або значення, що повертається.

При поверненні посилання з функції, параметр часу існування для типу, що повертається, має збігатися з параметром часу існування для одного з параметрів. Якщо повернуте посилання не посилається на один з параметрів, воно повинне посилатись на значення, створене всередині цієї функції. Однак це буде висяче посилання, тому що значення вийде з області видимості в кінці функції. Розглянемо спробу реалізації функції longest, яка не компілюється:

Файл: src/main.rs

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

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

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

Тут, хоча ми й зазначили параметр часу існування 'a для типу, що повертається, ця реалізація не буде скомпільованою, оскільки час існування значення, що повертається, взагалі не пов'язаний з часом існування параметрів. Ось яке повідомлення про помилку ми отримаємо:

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

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

Проблема в тому, що result виходить з області видимості й очищується в кінці функції longest. Ми також намагаємося повернути з функції посилання на result. Неможливо вказати параметри часу існування, які змінять висяче посилання, і Rust не дозволяє нам створити висяче посилання. У цьому випадку найкращим виправленням було б повернути тип, що володіє даними, замість посилання, щоб функція, яка викликала, була б відповідальною за очищення значення.

Зрештою, синтаксис часу існування стосується зв'язування часів існування різних параметрів і значень, що повертаються з функцій. Коли вони пов'язуються, Rust має достатньо інформації, щоб дозволити безпечні операції з пам'яттю і забороняти операції, які створювали б висячі вказівники або іншим чином порушували безпеку пам’яті.

Анотації часів існування у визначеннях структур

Досі всі визначені нами структури містили типи, що володіють даними. Ми можемо визначити структури, що міститимуть посилання, але в цьому разі ми маємо додати анотацію часу існування до кожного посилання у визначенні структури. Блок коду 10-24 демонструє структуру, що зветься ImportantExcerpt, що містить стрічковий слайс.

Файл: src/main.rs

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

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

Блок коду 10-24: структура, що містить посилання, яке потребує анотації часу існування

Ця структура має єдине поле part, що містить стрічковий слайс, що є посиланням. Як і для узагальнених типів даних, ми проголошуємо назву узагальненого параметру часу існування в кутових дужках після назви структури, щоб ми могли використати цей параметр часу існування у визначенні структури. Ця анотація означає, що екземпляр ImportantExcerpt не може існувати довше, ніж посилання, яке він містить у своєму полі part.

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

Елізія часу існування

Ви дізналися, що кожне посилання має час існування і ви маєте зазначити параметри часу існування для функцій та структур, які використовують посилання. Однак у Розділі 4 у нас була функція в Блоці коду 4-9, показана знову у Блоці коду 10-25, яка скомпілювалася без анотацій часу існування.

Файл: src/lib.rs

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");

    // first_word works on slices of `String`s
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word works on slices of string literals
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

Блок коду 10-25: функція, визначена в Блоці коду 4-9, яка компілюється без анотацій часу існування, хоча параметр і тип, що повертається є посиланнями

Причина, чому ця функція компілюється без анотацій часу існування, є історичною: у ранніх версіях (до 1.0) Rust цей код не скомпілювався б тому, що кожне посилання потребувало явного часу існування. На той момент сигнатура функції була б записана так:

fn first_word<'a>(s: &'a str) -> &'a str {

Після написання великої кількості коду на Rust, команда Rust виявила, що програмісти Rust вводять ті самі анотації часу існування знову і знову в конкретних випадках. Ці ситуації були передбачуваними та відповідали декільком визначеним шаблонів. Розробники запрограмували ці шаблони у код компілятора, щоб borrow checker міг вивести часи існування в цих ситуаціях і не потребував явних анотацій.

Цей шматок історії 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 or &mut self, бо це метод, час існування self призначається усім вихідним параметрам часу існування. Це третє правило набагато полегшує читання і писання методів, бо потрібно менше символів.

Зробімо вигляд, що ми компілятор. Ми застосуємо ці правила, щоб визначити часи існування посилань у сигнатурі функції first_word у Блоці коду 10-25. Сигнатура починається без жодних часів існування, пов'язаних із посиланнями:

fn first_word(s: &str) -> &str {

Потім компілятор застосовує перше правило, яке визначає, що кожен параметр отримує свій власний час існування. Ми назвемо його 'a, як зазвичай, тому тепер сигнатура є такою:

fn first_word<'a>(s: &'a str) -> &str {

Друге правило застосовується, бо тут є рівно один вхідний час існування. Друге правило каже, що час існування єдиного вхідного параметра призначається вихідному часу існування, тож сигнатура тепер така:

fn first_word<'a>(s: &'a str) -> &'a str {

Тепер всі посилання в сигнатурі цієї функції мають часи існування, і компілятор може продовжити свій аналіз, не змушуючи програміста анотувати часи існування в сигнатурі цієї функції.

Розгляньмо інший приклад, цього разу з функцією longest, яка не мала параметрів часу існування, коли ми почали працювати з нею у Блоці коду 10-20:

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

Застосуємо перше правило: кожен параметр отримує свій час існування. Цього разу ми маємо два параметри замість одного, тому ми маємо два часи існування:

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

Як бачите, друге правило не застосовується, тому що є більш ніж один вхідний час існування. Третє правило також не застосовується, тому що longest є функцією, а не методом, тож жоден з параметрів не є self. Виконавши всі три правила, ми все ще не з'ясували час існування типу, що повертається. Саме тому ми отримали помилку при спробі скомпілювати код у Блоці коду 10-20: компілятор пропрацював правила елізії часів існування, але все ж не зміг з'ясувати всі часи існування посилань у сигнатурі.

Оскільки третє правило застосовується тільки в сигнатурі методів, далі ми подивимося на часи існування в цьому контексті, щоб зрозуміти, чому третє правило означає, що нам не дуже часто доводиться анотувати часи існування у сигнатурах методів.

Анотації часів існування у визначеннях методів

Коли ми реалізовуємо методи для структур з часами існування, то використовуємо той самий синтаксис, що і параметри узагальненого типу, показані у Блоці коду 10-11. Де нам проголошувати і використовувати параметри часу існування, залежить від того, чи стосуються вони полів структури, чи параметрів методу і значення, що повертається.

Назви часу існування полів структур завжди треба проголошувати після ключового слова impl, а потім використовувати після назви структури, тому що ці часи існування є частиною типу структури.

У сигнатурах методів всередині блоку impl посилання мають бути або прив'язані до часу існування посилань у полях структури, або ж мають бути незалежними. Крім того, правила елізії часів існування часто роблять анотації часів існування непотрібними у сигнатурах методів. Погляньмо на деякі приклади, використовуючи структуру з назвою ImportantExcerpt, яку ми визначили у Блоці коду 10-24.

Спершу, ми використовуємо метод з назвою level, єдиним параметром якого є посилання на self, а значення, що повертається є i32, що не є посиланням:

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

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

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

Декларація параметра часу існування після impl і його використання після назви типу є необхідними, але ми не маємо анотувати час існування посилання на self через перше правило елізії.

Ось приклад, де застосовується третє правило елізії часу існування:

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

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

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

Тут є два вхідних часи існування, тож Rust застосовує перше правило елізії часів існування і дає обом &self та announcement власні часи існування. Тоді, оскільки один з параметрів є &self, тип, що повертається. отримує час існування &self, і всі часи існування було призначено.

Статичний час існування

Ми маємо обговорити один особливий час існування - 'static, що позначає, що посилання може існувати весь час роботи програми. Всі стрічкові літерали мають час існування 'static, що ми можемо анотувати таким чином:

#![allow(unused)]
fn main() {
let s: &'static str = "I have a static lifetime.";
}

Текст цієї стрічки зберігається безпосередньо в двійковому файлі програми, який є завжди доступним. Таким чином, час життя усіх стрічкових літералів є 'static.

Ви можете побачити пропозиції використовувати час існування 'static у повідомленнях про помилки. Але перш ніж вказати часом існування посилання 'static, подумайте, чи дійсно ваше посилання існує впродовж усієї роботи вашої програми чи ні, і чи хочете, щоб воно таким було. У більшості випадків повідомлення про помилку, що пропонує час існування 'static є результатом спроби створити висяче посилання чи невідповідність наявних часів існування. У таких випадках рішенням є виправити ці проблеми, а не визначити час існування 'static.

Параметри узагальненого типу, обмеження трейтів і часи існування разом

Подивімося коротко на синтаксис визначення параметрів узагальненого типу, обмежень трейтів і часів існування разом в одній функції!

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

    let result = longest_with_an_announcement(
        string1.as_str(),
        string2,
        "Today is someone's birthday!",
    );
    println!("The longest string is {}", result);
}

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Це функція longest зі Блоку коду 10-21, яка повертає довший із двох стрічкових слайсів. Але тепер вона має додатковий параметр з назвою ann узагальненого типу T, який може бути заповнений будь-яким типом, який реалізує трейт Display, як зазначено в клаузі where. Цей додатковий параметр буде виведено за допомогою {}, то й робить необхідним обмеження трейту Display. Оскільки часи існування є узагальненням, проголошення параметра часу існування 'a і параметру узагальненого типу T розміщуються в одному списку в кутових дужках після назви функції.

Підсумок

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

Вірите ви чи ні, є ще багато чого, що можна дізнатися про обговорені в цьому розділі теми: Розділ 17 обговорює трейтові об'єкти, які є іншим способом використання трейтів. Також є складніші сценарії, що використовують анотації часів існування, що знадобляться вам лише у дуже просунутих сценаріях; для них ви маєте прочитати Rust Reference. Та зараз ви навчитеся писати тести на Rust, щоб переконатися, що ваш код працює так, як треба. ch04-02-references-and-borrowing.html#references-and-borrowing ch04-03-slices.html#string-slices-as-parameters