Mengapa masa hidup eksplisit diperlukan di Rust?

199

Saya membaca bab seumur hidup dari buku Rust, dan saya menemukan contoh ini untuk seumur hidup bernama / eksplisit:

struct Foo<'a> {
    x: &'a i32,
}

fn main() {
    let x;                    // -+ x goes into scope
                              //  |
    {                         //  |
        let y = &5;           // ---+ y goes into scope
        let f = Foo { x: y }; // ---+ f goes into scope
        x = &f.x;             //  | | error here
    }                         // ---+ f and y go out of scope
                              //  |
    println!("{}", x);        //  |
}                             // -+ x goes out of scope

Cukup jelas bagi saya bahwa kesalahan yang dicegah oleh kompiler adalah penggunaan-setelah-bebas dari referensi yang ditugaskan untuk x: setelah lingkup bagian dalam dilakukan, fdan karena itu &f.xmenjadi tidak valid, dan seharusnya tidak ditugaskan x.

Masalah saya adalah bahwa masalahnya dapat dengan mudah dianalisis tanpa menggunakan masa hidup eksplisit 'a , misalnya dengan menyimpulkan penugasan ilegal dari referensi ke ruang lingkup yang lebih luas ( x = &f.x;).

Dalam kasus-kasus mana saja masa hidup eksplisit sebenarnya diperlukan untuk mencegah kesalahan penggunaan-setelah-bebas (atau kelas lainnya?)?

corazza
sumber
1
Ini
dibagikan
2
Untuk pembaca yang akan datang dari pertanyaan ini, harap catat
tautannya

Jawaban:

205

Jawaban yang lain semua memiliki poin yang menonjol ( contoh konkret fjh di mana masa pakai eksplisit diperlukan ), tetapi kehilangan satu hal utama: mengapa masa hidup eksplisit diperlukan ketika kompiler akan memberi tahu Anda bahwa Anda salah paham ?

Ini sebenarnya pertanyaan yang sama dengan "mengapa jenis eksplisit diperlukan ketika kompiler dapat menyimpulkannya". Contoh hipotetis:

fn foo() -> _ {  
    ""
}

Tentu saja, kompiler dapat melihat bahwa saya mengembalikan &'static str, jadi mengapa programmer harus mengetiknya?

Alasan utamanya adalah bahwa sementara kompiler dapat melihat apa yang kode Anda lakukan, ia tidak tahu apa maksud Anda.

Fungsi adalah batas alami untuk firewall efek dari perubahan kode. Jika kita membiarkan masa hidup sepenuhnya diperiksa dari kode, maka perubahan yang tampak tidak bersalah dapat memengaruhi masa hidup, yang kemudian dapat menyebabkan kesalahan dalam fungsi yang jauh. Ini bukan contoh hipotetis. Seperti yang saya pahami, Haskell memiliki masalah ini ketika Anda mengandalkan inferensi tipe untuk fungsi tingkat atas. Rust menggigit masalah tertentu sejak awal.

Ada juga manfaat efisiensi bagi kompiler - hanya tanda tangan fungsi yang perlu diuraikan untuk memverifikasi jenis dan masa pakai. Lebih penting lagi, ini memiliki manfaat efisiensi bagi programmer. Jika kami tidak memiliki masa hidup yang eksplisit, apa fungsi fungsi ini:

fn foo(a: &u8, b: &u8) -> &u8

Tidak mungkin untuk mengetahui tanpa memeriksa sumbernya, yang akan bertentangan dengan sejumlah besar praktik pengkodean terbaik.

dengan menyimpulkan penugasan ilegal dari referensi ke ruang lingkup yang lebih luas

Lingkup adalah kehidupan, pada dasarnya. Sedikit lebih jelas, seumur hidup 'aadalah parameter seumur hidup generik yang dapat dikhususkan dengan ruang lingkup tertentu pada waktu kompilasi, berdasarkan situs panggilan.

apakah masa hidup eksplisit sebenarnya diperlukan untuk mencegah kesalahan [...]?

Tidak semuanya. Masa hidup diperlukan untuk mencegah kesalahan, tetapi masa hidup yang eksplisit diperlukan untuk melindungi apa yang dimiliki sedikit programmer kewarasan.

Shepmaster
sumber
18
@ jco Bayangkan Anda memiliki beberapa fungsi tingkat atas f x = x + 1tanpa tanda tangan jenis yang Anda gunakan dalam modul lain. Jika nanti Anda mengubah definisi menjadi f x = sqrt $ x + 1, tipenya berubah dari Num a => a -> amenjadi Floating a => a -> a, yang akan menyebabkan kesalahan ketik di semua situs panggilan tempat fdipanggil dengan Intargumen misalnya . Memiliki tipe tanda tangan memastikan bahwa kesalahan terjadi secara lokal.
fjh
11
"Lingkup adalah masa hidup, pada dasarnya. Sedikit lebih jelas, seumur hidup 'a adalah parameter seumur hidup generik yang dapat dikhususkan dengan ruang lingkup spesifik pada waktu panggilan." Wow itu adalah titik pencahayaan yang sangat bagus. Saya suka kalau itu dimasukkan dalam buku ini secara eksplisit.
corazza
2
@ fjh Terima kasih. Hanya untuk melihat apakah saya grok - intinya adalah bahwa jika jenisnya secara eksplisit dinyatakan sebelum menambahkan sqrt $, hanya kesalahan lokal akan terjadi setelah perubahan, dan tidak banyak kesalahan di tempat lain (yang jauh lebih baik jika kita tidak ingin mengubah tipe yang sebenarnya)?
corazza
5
@ jco Tepat. Tidak menentukan jenis berarti bahwa Anda dapat secara tidak sengaja mengubah antarmuka suatu fungsi. Itulah salah satu alasan mengapa sangat dianjurkan untuk membubuhi keterangan semua item tingkat atas di Haskell.
fjh
5
Juga jika suatu fungsi menerima dua referensi dan mengembalikan referensi maka kadang-kadang mungkin mengembalikan referensi pertama dan kadang-kadang yang kedua. Dalam hal ini tidak mungkin untuk menyimpulkan seumur hidup untuk referensi yang dikembalikan. Kehidupan eksplisit membantu untuk menghindari / mengklarifikasi situasi seperti itu.
MichaelMoser
93

Mari kita lihat contoh berikut.

fn foo<'a, 'b>(x: &'a u32, y: &'b u32) -> &'a u32 {
    x
}

fn main() {
    let x = 12;
    let z: &u32 = {
        let y = 42;
        foo(&x, &y)
    };
}

Di sini, masa hidup eksplisit itu penting. Ini mengkompilasi karena hasil foomemiliki seumur hidup yang sama dengan argumen pertama ( 'a), sehingga mungkin hidup lebih lama dari argumen kedua. Ini diungkapkan oleh nama seumur hidup dalam tanda tangan foo. Jika Anda mengalihkan argumen dalam panggilan ke fookompiler akan mengeluh bahwa ytidak hidup cukup lama:

error[E0597]: `y` does not live long enough
  --> src/main.rs:10:5
   |
9  |         foo(&y, &x)
   |              - borrow occurs here
10 |     };
   |     ^ `y` dropped here while still borrowed
11 | }
   | - borrowed value needs to live until here
fjh
sumber
16

Anotasi seumur hidup dalam struktur berikut:

struct Foo<'a> {
    x: &'a i32,
}

menetapkan bahwa Fooinstance tidak boleh hidup lebih lama dari referensi yang dikandungnya ( xbidang).

Contoh yang Anda temukan dalam buku Karat tidak menggambarkan ini karena fdany variabel keluar dari ruang lingkup pada saat yang sama.

Contoh yang lebih baik adalah ini:

fn main() {
    let f : Foo;
    {
        let n = 5;  // variable that is invalid outside this block
        let y = &n;
        f = Foo { x: y };
    };
    println!("{}", f.x);
}

Sekarang, fbenar-benar hidup lebih lama dari variabel yang ditunjukkan oleh f.x.

pengguna3151599
sumber
9

Perhatikan bahwa tidak ada masa hidup eksplisit dalam potongan kode itu, kecuali definisi struktur. Compiler dapat dengan sempurna menyimpulkan masa hidupmain() .

Dalam definisi tipe, bagaimanapun, masa hidup eksplisit tidak dapat dihindari. Misalnya, ada ambiguitas di sini:

struct RefPair(&u32, &u32);

Haruskah ini berbeda seumur hidup atau haruskah mereka sama? Itu penting dari perspektif penggunaan, struct RefPair<'a, 'b>(&'a u32, &'b u32)sangat berbeda daristruct RefPair<'a>(&'a u32, &'a u32) .

Sekarang, untuk kasus-kasus sederhana, seperti yang Anda berikan, kompiler secara teoritis dapat menghilangkan masa hidup seperti di tempat lain, tetapi kasus-kasus seperti itu sangat terbatas dan tidak bernilai kompleksitas tambahan dalam kompiler, dan perolehan kejelasan ini akan berada di paling tidak dipertanyakan.

Vladimir Matveev
sumber
2
Bisakah Anda menjelaskan mengapa mereka sangat berbeda?
AB
@ AB Yang kedua mensyaratkan bahwa kedua referensi memiliki masa pakai yang sama. Ini berarti refpair.1 tidak bisa hidup lebih lama dari refpair.2 dan sebaliknya - sehingga kedua ref perlu menunjukkan sesuatu dengan pemilik yang sama. Namun yang pertama hanya mensyaratkan bahwa RefPair hidup lebih lama dari kedua bagiannya.
llogiq
2
@ AB, ini mengkompilasi karena kedua masa hidup disatukan - karena masa hidup lokal lebih kecil dari itu 'static, 'staticdapat digunakan di mana-mana di mana masa hidup lokal dapat digunakan, karena itu dalam contoh Anda pakan memiliki parameter masa pakai yang disimpulkan sebagai masa hidup lokal y.
Vladimir Matveev
5
@ AB RefPair<'a>(&'a u32, &'a u32)berarti bahwa 'aakan menjadi persimpangan dari kedua input seumur hidup, yaitu dalam hal ini seumur hidup y.
fjh
1
@ llogiq "mengharuskan RefPair hidup lebih lama dari kedua bagiannya"? Saya pikir itu kebalikannya ... a & u32 masih bisa masuk akal tanpa RefPair, sementara RefPair dengan referralnya mati akan aneh.
qed
6

Kasing dari buku ini sangat sederhana dengan desain. Topik kehidupan dianggap kompleks.

Kompiler tidak dapat dengan mudah menyimpulkan masa pakai dalam suatu fungsi dengan banyak argumen.

Juga, peti opsional saya sendiri memiliki OptionBooltipe dengan as_slicemetode yang tanda tangannya sebenarnya adalah:

fn as_slice(&self) -> &'static [bool] { ... }

Sama sekali tidak ada cara kompiler bisa menemukan yang keluar.

llogiq
sumber
IINM, menyimpulkan umur tipe kembalinya fungsi dua argumen akan setara dengan masalah penghentian - TKI, tidak dapat ditentukan dalam jumlah waktu yang terbatas.
dstromberg
4

Saya telah menemukan penjelasan hebat lainnya di sini: http://doc.rust-lang.org/0.12.0/guide-lifetimes.html#returning-references .

Secara umum, hanya mungkin untuk mengembalikan referensi jika diturunkan dari parameter ke prosedur. Dalam hal ini, hasil pointer akan selalu memiliki masa hidup yang sama dengan salah satu parameter; bernama lifeetimes menunjukkan parameter yang mana.

corazza
sumber
4

Jika suatu fungsi menerima dua referensi sebagai argumen dan mengembalikan referensi, maka implementasi fungsi kadang-kadang mengembalikan referensi pertama dan kadang-kadang yang kedua. Tidak mungkin memprediksi referensi mana yang akan dikembalikan untuk panggilan yang diberikan. Dalam hal ini, tidak mungkin untuk menyimpulkan seumur hidup untuk referensi yang dikembalikan, karena setiap referensi argumen dapat merujuk ke variabel yang berbeda yang mengikat dengan masa hidup yang berbeda. Kehidupan eksplisit membantu untuk menghindari atau mengklarifikasi situasi seperti itu.

Demikian juga, jika struktur memegang dua referensi (sebagai dua bidang anggota) maka fungsi anggota struktur kadang-kadang dapat mengembalikan referensi pertama dan kadang-kadang yang kedua. Lagi-lagi masa hidup yang eksplisit mencegah ambiguitas semacam itu.

Dalam beberapa situasi sederhana, ada elision seumur hidup di mana kompiler dapat menyimpulkan masa hidup.

MichaelMoser
sumber
1

Alasan mengapa contoh Anda tidak berhasil adalah karena Rust hanya memiliki inferensi seumur hidup dan jenis lokal. Apa yang Anda sarankan menuntut inferensi global. Setiap kali Anda memiliki referensi yang masa hidupnya tidak dapat dihilangkan, itu harus dijelaskan.

Klas. S
sumber
1

Sebagai pendatang baru di Rust, pemahaman saya adalah bahwa kehidupan eksplisit melayani dua tujuan.

  1. Menempatkan anotasi seumur hidup eksplisit pada fungsi membatasi jenis kode yang mungkin muncul di dalam fungsi itu. Masa hidup yang eksplisit memungkinkan kompiler memastikan bahwa program Anda melakukan apa yang Anda inginkan.

  2. Jika Anda (kompiler) ingin memeriksa apakah sepotong kode valid, Anda (kompiler) tidak akan harus melihat ke dalam setiap fungsi yang dipanggil secara iteratif. Cukuplah untuk melihat penjelasan fungsi yang secara langsung dipanggil oleh potongan kode tersebut. Ini membuat program Anda lebih mudah untuk dipikirkan untuk Anda (kompiler), dan membuat waktu kompilasi dapat dikelola.

Pada poin 1., Pertimbangkan program berikut yang ditulis dalam Python:

import pandas as pd
import numpy as np

def second_row(ar):
    return ar[0]

def work(second):
    df = pd.DataFrame(data=second)
    df.loc[0, 0] = 1

def main():
    # .. load data ..
    ar = np.array([[0, 0], [0, 0]])

    # .. do some work on second row ..
    second = second_row(ar)
    work(second)

    # .. much later ..
    print(repr(ar))

if __name__=="__main__":
    main()

yang akan dicetak

array([[1, 0],
       [0, 0]])

Perilaku seperti ini selalu mengejutkan saya. Apa yang terjadi adalah dfberbagi memori ar, jadi ketika beberapa konten dfberubahwork , perubahan itu juga menginfeksi ar. Namun, dalam beberapa kasus ini mungkin persis seperti yang Anda inginkan, karena alasan efisiensi memori (tanpa salinan). Masalah sebenarnya dalam kode ini adalah fungsinyasecond_row mengembalikan baris pertama, bukan yang kedua; semoga berhasil debugging itu.

Pertimbangkan program serupa yang ditulis dalam Rust:

#[derive(Debug)]
struct Array<'a, 'b>(&'a mut [i32], &'b mut [i32]);

impl<'a, 'b> Array<'a, 'b> {
    fn second_row(&mut self) -> &mut &'b mut [i32] {
        &mut self.0
    }
}

fn work(second: &mut [i32]) {
    second[0] = 1;
}

fn main() {
    // .. load data ..
    let ar1 = &mut [0, 0][..];
    let ar2 = &mut [0, 0][..];
    let mut ar = Array(ar1, ar2);

    // .. do some work on second row ..
    {
        let second = ar.second_row();
        work(second);
    }

    // .. much later ..
    println!("{:?}", ar);
}

Kompilasi ini, Anda dapatkan

error[E0308]: mismatched types
 --> src/main.rs:6:13
  |
6 |             &mut self.0
  |             ^^^^^^^^^^^ lifetime mismatch
  |
  = note: expected type `&mut &'b mut [i32]`
             found type `&mut &'a mut [i32]`
note: the lifetime 'b as defined on the impl at 4:5...
 --> src/main.rs:4:5
  |
4 |     impl<'a, 'b> Array<'a, 'b> {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...does not necessarily outlive the lifetime 'a as defined on the impl at 4:5
 --> src/main.rs:4:5
  |
4 |     impl<'a, 'b> Array<'a, 'b> {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^

Bahkan Anda mendapatkan dua kesalahan, ada juga satu dengan peran 'adan 'bdipertukarkan. Melihat anotasi second_row, kami menemukan bahwa output harus &mut &'b mut [i32], yaitu, output seharusnya menjadi referensi ke referensi dengan masa pakai 'b(masa baris kedua Array). Namun, karena kita mengembalikan baris pertama (yang memiliki masa pakai 'a), kompiler mengeluh tentang ketidakcocokan seumur hidup. Di tempat yang tepat. Di waktu yang tepat. Debugging sangat mudah.

Jonas Dahlbæk
sumber
0

Saya menganggap anotasi seumur hidup sebagai kontrak tentang referensi yang diberikan hanya berlaku dalam lingkup penerima saja sementara itu masih berlaku dalam lingkup sumber. Mendeklarasikan lebih banyak referensi dalam jenis kehidupan yang sama menggabungkan lingkup, yang berarti bahwa semua sumber referensi harus memenuhi kontrak ini. Anotasi semacam itu memungkinkan penyusun untuk memeriksa pemenuhan kontrak.

Jorge Gonzalez
sumber