Mari kita lihat implementasi sederhana ini :
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() {}
Ini akan gagal dengan kesalahan:
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`
Untuk benar-benar memahami kesalahan ini, Anda harus berpikir tentang bagaimana nilai-nilai diwakili dalam memori dan apa yang terjadi ketika Anda memindahkan
nilai-nilai itu. Mari kita beri catatan Combined::new
dengan beberapa alamat memori hipotetis yang menunjukkan di mana nilai berada:
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 ... ?
Apa yang harus terjadi child
? Jika nilai itu hanya dipindahkan seperti parent
itu, maka itu akan merujuk ke memori yang tidak lagi dijamin memiliki nilai yang valid di dalamnya. Setiap potongan kode lainnya diizinkan untuk menyimpan nilai di alamat memori 0x1000. Mengakses memori tersebut dengan asumsi bilangan bulat dapat menyebabkan kerusakan dan / atau bug keamanan, dan merupakan salah satu kategori utama kesalahan yang mencegah Karat.
Inilah masalah yang sebenarnya bisa dicegah seumur hidup . Seumur hidup adalah sedikit metadata yang memungkinkan Anda dan kompiler mengetahui berapa lama nilai akan valid di lokasi memori saat ini . Itu perbedaan penting, karena itu adalah kesalahan umum yang dilakukan Rust pendatang baru. Masa hidup karat bukan periode waktu antara saat suatu objek dibuat dan saat objek itu dihancurkan!
Sebagai analogi, pikirkan seperti ini: Selama hidup seseorang, mereka akan tinggal di banyak lokasi berbeda, masing-masing dengan alamat yang berbeda. Seumur hidup Rust berkaitan dengan alamat yang saat ini Anda tinggali , bukan tentang kapan pun Anda akan mati di masa depan (meskipun sekarat juga mengubah alamat Anda). Setiap kali Anda memindahkannya relevan karena alamat Anda tidak lagi valid.
Penting juga untuk dicatat bahwa masa hidup tidak mengubah kode Anda; kode Anda mengontrol masa hidup, masa hidup Anda tidak mengontrol kode. Pepatah bernas adalah "masa hidup adalah deskriptif, bukan preskriptif".
Mari kita beri catatan Combined::new
dengan beberapa nomor baris yang akan kita gunakan untuk menyoroti masa hidup:
{ // 0
let parent = Parent { count: 42 }; // 1
let child = Child { parent: &parent }; // 2
// 3
Combined { parent, child } // 4
} // 5
The seumur hidup konkret dari parent
adalah dari 1 sampai 4, inklusif (yang saya akan mewakili sebagai [1,4]
). Umur konkrit dari child
adalah [2,4]
, dan umur konkret dari nilai kembali adalah [4,5]
. Dimungkinkan untuk memiliki masa hidup konkret yang dimulai dari nol - yang akan mewakili masa pakai parameter ke fungsi atau sesuatu yang ada di luar blok.
Perhatikan bahwa masa pakai child
itu sendiri adalah [2,4]
, tetapi mengacu pada nilai dengan masa pakai [1,4]
. Ini bagus selama nilai referensi menjadi tidak valid sebelum nilai yang dimaksud tidak. Masalah terjadi ketika kami mencoba kembali child
dari blok. Ini akan "memperpanjang" masa hidup melampaui panjang alami.
Pengetahuan baru ini harus menjelaskan dua contoh pertama. Yang ketiga membutuhkan melihat implementasi Parent::child
. Kemungkinannya, akan terlihat seperti ini:
impl Parent {
fn child(&self) -> Child { /* ... */ }
}
Ini menggunakan elision seumur hidup untuk menghindari penulisan parameter seumur hidup generik yang eksplisit . Itu sama dengan:
impl Parent {
fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}
Dalam kedua kasus tersebut, metode ini mengatakan bahwa a Child
struktur akan dikembalikan yang telah diparameterisasi dengan umur beton
self
. Dengan kata lain, Child
instance berisi referensi ke Parent
yang membuatnya, dan dengan demikian tidak bisa hidup lebih lama dari
Parent
instance itu.
Ini juga memungkinkan kami mengenali bahwa ada sesuatu yang salah dengan fungsi pembuatan kami:
fn make_combined<'a>() -> Combined<'a> { /* ... */ }
Meskipun Anda lebih cenderung melihat ini ditulis dalam bentuk yang berbeda:
impl<'a> Combined<'a> {
fn new() -> Combined<'a> { /* ... */ }
}
Dalam kedua kasus, tidak ada parameter seumur hidup yang disediakan melalui argumen. Ini berarti seumur hidup ituCombined
akan diparameterisasi tidak dibatasi oleh apa pun - itu bisa berupa apa pun yang diinginkan pemanggil. Ini tidak masuk akal, karena penelepon dapat menentukan masa 'static
hidup dan tidak ada cara untuk memenuhi kondisi itu.
Bagaimana saya memperbaikinya?
Solusi termudah dan paling direkomendasikan adalah untuk tidak mencoba menyatukan item-item ini dalam struktur yang sama. Dengan melakukan ini, struktur bersarang Anda akan meniru masa hidup kode Anda. Tempatkan tipe yang memiliki data ke dalam struktur bersama dan kemudian berikan metode yang memungkinkan Anda untuk mendapatkan referensi atau objek yang berisi referensi sesuai kebutuhan.
Ada kasus khusus di mana pelacakan seumur hidup terlalu bersemangat: ketika Anda memiliki sesuatu yang ditempatkan di tumpukan. Ini terjadi ketika Anda menggunakan a
Box<T>
, misalnya. Dalam hal ini, struktur yang dipindahkan berisi pointer ke heap. Nilai menunjuk-di akan tetap stabil, tetapi alamat penunjuk itu sendiri akan bergerak. Dalam praktiknya, ini tidak masalah, karena Anda selalu mengikuti pointer.
The sewa peti (TIDAK LAGI YANG DIKELOLA ATAU DIDUKUNG) atau peti owning_ref cara mewakili hal ini, tetapi mereka mengharuskan alamat dasar tidak pernah bergerak . Ini mengesampingkan vektor yang bermutasi, yang dapat menyebabkan realokasi dan perpindahan nilai-nilai yang dialokasikan tumpukan.
Contoh masalah yang diselesaikan dengan Sewa:
Dalam kasus lain, Anda mungkin ingin pindah ke beberapa jenis penghitungan referensi, seperti dengan menggunakan Rc
atau Arc
.
Informasi lebih lanjut
Setelah pindah parent
ke struct, mengapa kompiler tidak bisa mendapatkan referensi baru parent
dan menugaskannya ke child
dalam struct?
Meskipun secara teori dimungkinkan untuk melakukan ini, melakukan hal itu akan memperkenalkan sejumlah besar kompleksitas dan overhead. Setiap kali objek dipindahkan, kompiler harus memasukkan kode untuk "memperbaiki" referensi. Ini berarti bahwa menyalin struct tidak lagi menjadi operasi yang sangat murah yang hanya memindahkan beberapa bit. Bahkan bisa berarti kode seperti ini mahal, tergantung pada seberapa baik pengoptimal hipotetis akan:
let a = Object::new();
let b = a;
let c = b;
Alih-alih memaksa ini terjadi untuk setiap gerakan, programmer dapat memilih kapan ini akan terjadi dengan menciptakan metode yang akan mengambil referensi yang sesuai hanya ketika Anda memanggil mereka.
Tipe dengan referensi untuk dirinya sendiri
Ada satu kasus khusus di mana Anda dapat membuat tipe dengan referensi ke dirinya sendiri. Anda perlu menggunakan sesuatu seperti Option
membuatnya dalam dua langkah:
#[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);
}
Ini memang berhasil, dalam beberapa hal, tetapi nilai yang diciptakan sangat terbatas - tidak pernah dapat dipindahkan. Khususnya, ini berarti tidak dapat dikembalikan dari fungsi atau diteruskan dengan nilai apa pun. Fungsi konstruktor menunjukkan masalah yang sama dengan masa hidup seperti di atas:
fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }
Bagaimana dengan Pin
?
Pin
, distabilkan di Rust 1.33, memiliki ini dalam dokumentasi modul :
Contoh utama dari skenario seperti itu adalah membangun struktur referensi-diri, karena memindahkan objek dengan pointer ke dirinya sendiri akan membatalkannya, yang dapat menyebabkan perilaku yang tidak ditentukan.
Penting untuk dicatat bahwa "referensi-diri" tidak berarti menggunakan referensi . Memang, contoh dari struktur referensi-diri secara khusus mengatakan (penekanan milikku):
Kami tidak dapat memberi tahu kompilator tentang hal itu dengan referensi normal, karena pola ini tidak dapat dijelaskan dengan aturan pinjaman yang biasa. Sebagai gantinya kita menggunakan pointer mentah , meskipun yang diketahui bukan nol, karena kita tahu itu menunjuk ke string.
Kemampuan untuk menggunakan pointer mentah untuk perilaku ini telah ada sejak Rust 1.0. Memang, pemilik-ref dan rental menggunakan pointer mentah di bawah tenda.
Satu-satunya hal yang Pin
menambah tabel adalah cara umum untuk menyatakan bahwa nilai yang diberikan dijamin tidak bergerak.
Lihat juga:
Parent
danChild
dapat membantu ...