Saya memiliki nilai dan saya ingin menyimpan nilai itu dan referensi ke sesuatu di dalam nilai itu dalam tipe saya sendiri:
struct Thing {
count: u32,
}
struct Combined<'a>(Thing, &'a u32);
fn make_combined<'a>() -> Combined<'a> {
let thing = Thing { count: 42 };
Combined(thing, &thing.count)
}
Terkadang, saya memiliki nilai dan saya ingin menyimpan nilai itu dan referensi ke nilai itu dalam struktur yang sama:
struct Combined<'a>(Thing, &'a Thing);
fn make_combined<'a>() -> Combined<'a> {
let thing = Thing::new();
Combined(thing, &thing)
}
Terkadang, saya bahkan tidak mengambil referensi nilai dan saya mendapatkan kesalahan yang sama:
struct Combined<'a>(Parent, Child<'a>);
fn make_combined<'a>() -> Combined<'a> {
let parent = Parent::new();
let child = parent.child();
Combined(parent, child)
}
Dalam setiap kasus ini, saya mendapatkan kesalahan bahwa salah satu nilai "tidak hidup cukup lama". Apa artinya kesalahan ini?
lifetime
borrow-checker
rust
Shepmaster
sumber
sumber
Parent
danChild
dapat membantu ...Jawaban:
Mari kita lihat implementasi sederhana ini :
Ini akan gagal dengan kesalahan:
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:Apa yang harus terjadi
child
? Jika nilai itu hanya dipindahkan sepertiparent
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:The seumur hidup konkret dari
parent
adalah dari 1 sampai 4, inklusif (yang saya akan mewakili sebagai[1,4]
). Umur konkrit darichild
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 kembalichild
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:Ini menggunakan elision seumur hidup untuk menghindari penulisan parameter seumur hidup generik yang eksplisit . Itu sama dengan:
Dalam kedua kasus tersebut, metode ini mengatakan bahwa a
Child
struktur akan dikembalikan yang telah diparameterisasi dengan umur betonself
. Dengan kata lain,Child
instance berisi referensi keParent
yang membuatnya, dan dengan demikian tidak bisa hidup lebih lama dariParent
instance itu.Ini juga memungkinkan kami mengenali bahwa ada sesuatu yang salah dengan fungsi pembuatan kami:
Meskipun Anda lebih cenderung melihat ini ditulis dalam bentuk yang berbeda:
Dalam kedua kasus, tidak ada parameter seumur hidup yang disediakan melalui argumen. Ini berarti seumur hidup itu
Combined
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
atauArc
.Informasi lebih lanjut
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:
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: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:
Bagaimana dengan
Pin
?Pin
, distabilkan di Rust 1.33, memiliki ini dalam dokumentasi modul :Penting untuk dicatat bahwa "referensi-diri" tidak berarti menggunakan referensi . Memang, contoh dari struktur referensi-diri secara khusus mengatakan (penekanan milikku):
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:
sumber
Combined
memilikiChild
yang memilikiParent
. Itu mungkin atau mungkin tidak masuk akal tergantung pada jenis aktual yang Anda miliki. Mengembalikan referensi ke data internal Anda sendiri cukup tipikal.Pin
sebagian besar merupakan cara untuk mengetahui keamanan struct yang berisi pointer referensial diri . Kemampuan untuk menggunakan pointer mentah untuk tujuan yang sama telah ada sejak Rust 1.0.Masalah yang sedikit berbeda yang menyebabkan pesan kompiler yang sangat mirip adalah ketergantungan seumur hidup objek, daripada menyimpan referensi eksplisit. Contohnya adalah perpustakaan ssh2 . Ketika mengembangkan sesuatu yang lebih besar daripada proyek uji, tergoda untuk mencoba untuk menempatkan
Session
danChannel
memperoleh dari sesi itu satu sama lain ke dalam sebuah struct, menyembunyikan detail implementasi dari pengguna. Namun, perhatikan bahwaChannel
definisi memiliki masa'sess
pakai dalam anotasi jenisnya, sementaraSession
tidak.Ini menyebabkan kesalahan kompiler serupa yang berhubungan dengan masa hidup.
Salah satu cara untuk menyelesaikannya dengan cara yang sangat sederhana adalah dengan mendeklarasikan bagian
Session
luar pada pemanggil, dan kemudian untuk membubuhi keterangan referensi dalam struct dengan seumur hidup, mirip dengan jawaban di posting Forum Pengguna Karat ini. berbicara tentang masalah yang sama saat merangkum SFTP . Ini tidak akan terlihat elegan dan mungkin tidak selalu berlaku - karena sekarang Anda memiliki dua entitas yang harus dihadapi, bukan yang Anda inginkan!Ternyata peti sewa atau peti owning_ref dari jawaban lain juga merupakan solusi untuk masalah ini. Mari kita mempertimbangkan owning_ref, yang memiliki objek khusus untuk tujuan yang tepat ini:
OwningHandle
. Untuk menghindari objek yang mendasarinya bergerak, kami mengalokasikannya di heap menggunakan aBox
, yang memberi kami solusi yang memungkinkan berikut:Hasil dari kode ini adalah bahwa kita tidak dapat menggunakan
Session
lagi, tetapi disimpan bersama denganChannel
yang akan kita gunakan. KarenaOwningHandle
objek referensiBox
, yang referensiChannel
, ketika menyimpannya dalam sebuah struct, kita beri nama demikian. CATATAN: Ini hanya pemahaman saya. Saya curiga ini mungkin tidak benar, karena tampaknya cukup dekat dengan diskusi tentangOwningHandle
ketidakamanan .Satu detail penasaran di sini adalah bahwa
Session
secara logis memiliki hubungan yang sama denganTcpStream
yangChannel
harusSession
, namun kepemilikannya tidak diambil dan tidak ada anotasi tipe di sekitar melakukannya. Sebagai gantinya, tergantung pada pengguna untuk mengurus ini, seperti dokumentasi metode jabat tangan mengatakan:Jadi dengan
TcpStream
penggunaannya, sepenuhnya tergantung pada programmer untuk memastikan kebenaran kode. Dengan ituOwningHandle
, perhatian ke tempat "sihir berbahaya" terjadi ditarik menggunakanunsafe {}
blok.Diskusi lebih lanjut dan lebih tinggi tentang masalah ini ada di utas Forum Pengguna Karat ini - yang mencakup contoh berbeda dan solusinya menggunakan kotak sewa, yang tidak mengandung blok yang tidak aman.
sumber