Mengapa saya tidak bisa menyimpan nilai dan referensi ke nilai itu di struct yang sama?


222

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?


1
Untuk contoh terakhir, definisi Parentdan Childdapat membantu ...
Matthieu M.

1
@ MatthieuM. Saya memperdebatkan itu, tetapi memutuskan untuk tidak melakukannya berdasarkan pada dua pertanyaan terkait. Tak satu pun dari pertanyaan-pertanyaan itu yang melihat definisi struct atau metode yang dipertanyakan, jadi saya pikir akan lebih baik untuk meniru bahwa dengan begitu orang dapat lebih mudah mencocokkan pertanyaan ini dengan situasi mereka sendiri. Perhatikan bahwa saya lakukan menunjukkan metode tanda tangan dalam jawaban.
Shepmaster

Jawaban:


245

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::newdengan 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::newdengan 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 parentadalah dari 1 sampai 4, inklusif (yang saya akan mewakili sebagai [1,4]). Umur konkrit dari childadalah [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 childitu 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 childdari 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, Childinstance berisi referensi ke Parentyang membuatnya, dan dengan demikian tidak bisa hidup lebih lama dari Parentinstance 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 'statichidup 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 Rcatau Arc.

Informasi lebih lanjut

Setelah pindah parentke struct, mengapa kompiler tidak bisa mendapatkan referensi baru parentdan menugaskannya ke childdalam 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 Optionmembuatnya 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 Pinmenambah tabel adalah cara umum untuk menyatakan bahwa nilai yang diberikan dijamin tidak bergerak.

Lihat juga:


1
Apakah sesuatu seperti ini ( is.gd/wl2IAt ) dianggap idiomatik? Yaitu, untuk mengekspos data melalui metode, bukan data mentah.
Peter Hall

2
@PeterHall yakin, itu hanya berarti Combinedmemiliki Childyang memiliki Parent. Itu mungkin atau mungkin tidak masuk akal tergantung pada jenis aktual yang Anda miliki. Mengembalikan referensi ke data internal Anda sendiri cukup tipikal.
Shepmaster

Apa solusi untuk masalah timbunan?
derekdreery

@derekdreery mungkin Anda dapat memperluas komentar Anda? Mengapa seluruh paragraf berbicara tentang peti owning_ref tidak cukup?
Shepmaster

1
@FynnBecker masih tidak mungkin untuk menyimpan referensi dan nilai referensi itu. Pinsebagian 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.
Shepmaster

4

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 Sessiondan Channelmemperoleh dari sesi itu satu sama lain ke dalam sebuah struct, menyembunyikan detail implementasi dari pengguna. Namun, perhatikan bahwa Channeldefinisi memiliki masa 'sesspakai 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 Sessionluar 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 a Box, yang memberi kami solusi yang memungkinkan berikut:

use ssh2::{Channel, Error, Session};
use std::net::TcpStream;

use owning_ref::OwningHandle;

struct DeviceSSHConnection {
    tcp: TcpStream,
    channel: OwningHandle<Box<Session>, Box<Channel<'static>>>,
}

impl DeviceSSHConnection {
    fn new(targ: &str, c_user: &str, c_pass: &str) -> Self {
        use std::net::TcpStream;
        let mut session = Session::new().unwrap();
        let mut tcp = TcpStream::connect(targ).unwrap();

        session.handshake(&tcp).unwrap();
        session.set_timeout(5000);
        session.userauth_password(c_user, c_pass).unwrap();

        let mut sess = Box::new(session);
        let mut oref = OwningHandle::new_with_fn(
            sess,
            unsafe { |x| Box::new((*x).channel_session().unwrap()) },
        );

        oref.shell().unwrap();
        let ret = DeviceSSHConnection {
            tcp: tcp,
            channel: oref,
        };
        ret
    }
}

Hasil dari kode ini adalah bahwa kita tidak dapat menggunakan Sessionlagi, tetapi disimpan bersama dengan Channelyang akan kita gunakan. Karena OwningHandleobjek referensi Box, yang referensi Channel, 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 tentang OwningHandleketidakamanan .

Satu detail penasaran di sini adalah bahwa Sessionsecara logis memiliki hubungan yang sama dengan TcpStreamyang Channelharus Session, 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:

Sesi ini tidak mengambil kepemilikan dari soket yang disediakan, disarankan untuk memastikan bahwa soket tetap ada selama sesi ini untuk memastikan bahwa komunikasi dilakukan dengan benar.

Sangat disarankan juga bahwa aliran yang disediakan tidak digunakan secara bersamaan di tempat lain selama sesi ini karena dapat mengganggu protokol.

Jadi dengan TcpStreampenggunaannya, sepenuhnya tergantung pada programmer untuk memastikan kebenaran kode. Dengan itu OwningHandle, perhatian ke tempat "sihir berbahaya" terjadi ditarik menggunakan unsafe {}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.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.