Давайте розглянемо просту реалізацію цього :
struct Parent {
count: u32,
}
struct Child<'a> {
parent: &'a Parent,
}
struct Combined<'a> {
parent: Parent,
child: Child<'a>,
}
impl<'a> Combined<'a> {
fn new() -> Self {
let parent = Parent { count: 42 };
let child = Child { parent: &parent };
Combined { parent, child }
}
}
fn main() {}
Це не вдасться з помилкою:
error[E0515]: cannot return value referencing local variable `parent`
--> src/main.rs:19:9
|
17 | let child = Child { parent: &parent };
| ------- `parent` is borrowed here
18 |
19 | Combined { parent, child }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function
error[E0505]: cannot move out of `parent` because it is borrowed
--> src/main.rs:19:20
|
14 | impl<'a> Combined<'a> {
| -- lifetime `'a` defined here
...
17 | let child = Child { parent: &parent };
| ------- borrow of `parent` occurs here
18 |
19 | Combined { parent, child }
| -----------^^^^^^---------
| | |
| | move out of `parent` occurs here
| returning this value requires that `parent` is borrowed for `'a`
Щоб повністю зрозуміти цю помилку, потрібно подумати про те, як значення представлені в пам'яті і що відбувається при переміщенні
цих значень. Давайте анотуватимемо Combined::new
деякі гіпотетичні адреси пам'яті, які показують, де знаходяться значення:
let parent = Parent { count: 42 };
// `parent` lives at address 0x1000 and takes up 4 bytes
// The value of `parent` is 42
let child = Child { parent: &parent };
// `child` lives at address 0x1010 and takes up 4 bytes
// The value of `child` is 0x1000
Combined { parent, child }
// The return value lives at address 0x2000 and takes up 8 bytes
// `parent` is moved to 0x2000
// `child` is ... ?
Що має статися child
? Якщо значення було просто переміщено таким parent
, яким воно було, то воно буде посилатися на пам'ять, що більше не гарантується наявність у ньому дійсного значення. Будь-який інший фрагмент коду може зберігати значення за адресою пам'яті 0x1000. Доступ до цієї пам'яті, припускаючи, що це ціле число, може призвести до збоїв та / або помилок безпеки, і є однією з основних категорій помилок, які Rust запобігає.
Це саме та проблема, яку життя заважає. Тривалість життя - це трохи метаданих, які дозволяють вам і компілятору знати, як довго значення буде чинним у його поточному місці пам'яті . Це важлива відмінність, оскільки це звичайна помилка, яку роблять новачки Руста. Життя іржі немає є періодом часу між тим, коли об’єкт створений, і коли він знищений!
Як аналогію, подумайте про це так: Протягом життя людини вони проживатимуть у багатьох різних місцях, кожен з яких має окрему адресу. Іржаве життя стосується адреси, яку ви отримуєте зараз проживаєте , а не про те, коли ви помрете в майбутньому (хоча вмирання також змінює вашу адресу). Кожен раз, коли ви рухаєтесь, це актуально, тому що ваша адреса більше не дійсна.
Важливо також зазначити, що життя не змінює ваш код; ваш код контролює життя, ваш час життя не контролює код. Прислівна приказка - "життя є описовим, а не розпорядчим".
Давайте анотуватимемо Combined::new
деякі номери рядків, які ми будемо використовувати для виділення строків життя:
{ // 0
let parent = Parent { count: 42 }; // 1
let child = Child { parent: &parent }; // 2
// 3
Combined { parent, child } // 4
} // 5
Термін служби бетону від parent
від 1 до 4 включно (який я представляю як [1,4]
). Конкретний термін експлуатації child
-[2,4]
, а конкретний термін повернення - значення [4,5]
. Можливо мати конкретні терміни життя, які починаються з нуля - це буде представляти час життя параметра чи функції, що існувала поза блоком.
Зауважте, що саме життя child
є [2,4]
, але воно відноситься до значення, яке має час життя [1,4]
. Це добре, доки значення, що посилається, стає недійсним до того, як згадане значення зробить. Проблема виникає, коли ми намагаємось повернутися child
з блоку. Це дозволило б "перетягнути" термін експлуатації за межі його природної тривалості.
Ці нові знання повинні пояснити перші два приклади. Третій вимагає перегляду виконання Parent::child
. Швидше за все, це буде виглядати приблизно так:
impl Parent {
fn child(&self) -> Child { /* ... */ }
}
Для цього використовується елісія протягом усього життя, щоб уникнути написання явних загальних параметрів життя . Він еквівалентний:
impl Parent {
fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}
В обох випадках метод говорить, що Child
повернеться структура, параметризована з конкретним терміном експлуатації
self
. Згаданий інший спосіб, Child
екземпляр містить посилання на те, Parent
що його створило, і тому не може жити довше, ніж цей
Parent
екземпляр.
Це також дозволяє нам визнати, що щось насправді не так з нашою функцією створення:
fn make_combined<'a>() -> Combined<'a> { /* ... */ }
Хоча ви швидше бачите це написане в іншій формі:
impl<'a> Combined<'a> {
fn new() -> Combined<'a> { /* ... */ }
}
В обох випадках не існує жодного параметру часу, що надається через аргумент. Це означає, що все життя, щоCombined
буде налаштовано параметр, нічим не обмежується - він може бути таким, яким хоче той, хто його вимагає. Це безглуздо, тому що абонент може вказати 'static
термін служби, і немає можливості виконати цю умову.
Як це виправити?
Найпростіше і найбільш рекомендоване рішення - не намагатися об'єднати ці елементи в одну структуру. Роблячи це, ваша структура гніздування буде імітувати життя вашого коду. Розташуйте типи, які володіють даними, в структуру разом, а потім надайте методи, що дозволяють отримувати посилання або об’єкти, що містять посилання, якщо це потрібно.
Існує особливий випадок, коли відстеження всього життя надмірне: коли у вас щось розміщено на купі. Це відбувається, коли ви використовуєте
Box<T>
, наприклад, У цьому випадку структура, яка переміщується, містить вказівник на купу. Наведене значення залишиться стабільним, але адреса самого вказівника буде переміщуватися. На практиці це не має значення, оскільки ви завжди слідуєте за вказівником.
Оренда кліть (БІЛЬШЕ НЕ підтримувати чи ПІДТРИМКА) або owning_ref лати способи подання цієї справи, але вони вимагають , щоб базовий адреса ніколи не рухатися . Це виключає мутуючі вектори, що може спричинити перерозподіл та переміщення значень, виділених купу.
Приклади проблем, вирішених з орендою:
В інших випадках ви можете перейти на деякий тип підрахунку довідок, наприклад, використовуючи Rc
або Arc
.
Більше інформації
Після переходу parent
в структуру, чому компілятор не може отримати нове посилання на нього parent
та призначити його child
в структурі?
Хоча теоретично це можна зробити, це може призвести до великої складності та витрат. Кожен раз, коли об’єкт переміщується, компілятору потрібно буде вставляти код, щоб "виправити" посилання. Це означатиме, що копіювання структури - це вже не дуже дешева операція, яка просто переміщує деякі біти. Це навіть може означати, що такий код коштує дорого, залежно від того, наскільки хорошим буде гіпотетичний оптимізатор:
let a = Object::new();
let b = a;
let c = b;
Замість того, щоб змушувати це робити на кожному кроці, програміст отримує вибір коли це станеться, створивши методи, які будуть приймати відповідні посилання лише тоді, коли ви їх викликаєте.
Тип із посиланням на себе
Є один конкретний випадок, коли ви можете створити тип із посиланням на себе. Вам потрібно використовувати щось на кшталт, Option
щоб зробити це в два етапи:
#[derive(Debug)]
struct WhatAboutThis<'a> {
name: String,
nickname: Option<&'a str>,
}
fn main() {
let mut tricky = WhatAboutThis {
name: "Annabelle".to_string(),
nickname: None,
};
tricky.nickname = Some(&tricky.name[..4]);
println!("{:?}", tricky);
}
Це в деякому сенсі працює, але створене значення сильно обмежене - його ніколи не можна переміщувати. Зокрема, це означає, що він не може бути повернутий з функції або переданий за значенням ні в чому. Функція конструктора показує ту ж проблему з життям, що і вище:
fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }
Про що Pin
?
Pin
Стабілізований в іржі 1.33, це містить в документації модуля :
Прекрасним прикладом такого сценарію може бути побудова самореференційних структур, оскільки переміщення об'єкта з покажчиками на себе призведе до їх недійсності, що може спричинити невизначеність поведінки.
Важливо зауважити, що "самореференційний" не обов'язково означає використання посилання . Дійсно, на прикладі самореференційної структури конкретно сказано (моє наголос):
Ми не можемо повідомити про це компілятора за допомогою звичайної посилання, оскільки ця модель не може бути описана звичайними правилами запозичення. Натомість ми використовуємо необроблений покажчик , хоча той, який, як відомо, не є нульовим, оскільки ми знаємо, що він вказує на рядок.
Можливість використовувати необроблений покажчик для такої поведінки існує з Rust 1.0. Дійсно, власник-ref та оренда використовують необроблені покажчики під кришкою.
Єдине, що Pin
додає до таблиці, - це звичайний спосіб констатувати, що задане значення гарантовано не рухається.
Дивитися також:
Parent
таChild
могло б допомогти ...