Apa aturan auto-dereferencing yang tepat dari Rust?

181

Saya belajar / bereksperimen dengan Rust, dan dalam semua keanggunan yang saya temukan dalam bahasa ini, ada satu kekhasan yang membingungkan saya dan tampaknya benar-benar tidak pada tempatnya.

Karat secara otomatis referensi pointer saat membuat panggilan metode. Saya membuat beberapa tes untuk menentukan perilaku yang tepat:

struct X { val: i32 }
impl std::ops::Deref for X {
    type Target = i32;
    fn deref(&self) -> &i32 { &self.val }
}

trait M { fn m(self); }
impl M for i32   { fn m(self) { println!("i32::m()");  } }
impl M for X     { fn m(self) { println!("X::m()");    } }
impl M for &X    { fn m(self) { println!("&X::m()");   } }
impl M for &&X   { fn m(self) { println!("&&X::m()");  } }
impl M for &&&X  { fn m(self) { println!("&&&X::m()"); } }

trait RefM { fn refm(&self); }
impl RefM for i32  { fn refm(&self) { println!("i32::refm()");  } }
impl RefM for X    { fn refm(&self) { println!("X::refm()");    } }
impl RefM for &X   { fn refm(&self) { println!("&X::refm()");   } }
impl RefM for &&X  { fn refm(&self) { println!("&&X::refm()");  } }
impl RefM for &&&X { fn refm(&self) { println!("&&&X::refm()"); } }


struct Y { val: i32 }
impl std::ops::Deref for Y {
    type Target = i32;
    fn deref(&self) -> &i32 { &self.val }
}

struct Z { val: Y }
impl std::ops::Deref for Z {
    type Target = Y;
    fn deref(&self) -> &Y { &self.val }
}


#[derive(Clone, Copy)]
struct A;

impl M for    A { fn m(self) { println!("A::m()");    } }
impl M for &&&A { fn m(self) { println!("&&&A::m()"); } }

impl RefM for    A { fn refm(&self) { println!("A::refm()");    } }
impl RefM for &&&A { fn refm(&self) { println!("&&&A::refm()"); } }


fn main() {
    // I'll use @ to denote left side of the dot operator
    (*X{val:42}).m();        // i32::m()    , Self == @
    X{val:42}.m();           // X::m()      , Self == @
    (&X{val:42}).m();        // &X::m()     , Self == @
    (&&X{val:42}).m();       // &&X::m()    , Self == @
    (&&&X{val:42}).m();      // &&&X:m()    , Self == @
    (&&&&X{val:42}).m();     // &&&X::m()   , Self == *@
    (&&&&&X{val:42}).m();    // &&&X::m()   , Self == **@
    println!("-------------------------");

    (*X{val:42}).refm();     // i32::refm() , Self == @
    X{val:42}.refm();        // X::refm()   , Self == @
    (&X{val:42}).refm();     // X::refm()   , Self == *@
    (&&X{val:42}).refm();    // &X::refm()  , Self == *@
    (&&&X{val:42}).refm();   // &&X::refm() , Self == *@
    (&&&&X{val:42}).refm();  // &&&X::refm(), Self == *@
    (&&&&&X{val:42}).refm(); // &&&X::refm(), Self == **@
    println!("-------------------------");

    Y{val:42}.refm();        // i32::refm() , Self == *@
    Z{val:Y{val:42}}.refm(); // i32::refm() , Self == **@
    println!("-------------------------");

    A.m();                   // A::m()      , Self == @
    // without the Copy trait, (&A).m() would be a compilation error:
    // cannot move out of borrowed content
    (&A).m();                // A::m()      , Self == *@
    (&&A).m();               // &&&A::m()   , Self == &@
    (&&&A).m();              // &&&A::m()   , Self == @
    A.refm();                // A::refm()   , Self == @
    (&A).refm();             // A::refm()   , Self == *@
    (&&A).refm();            // A::refm()   , Self == **@
    (&&&A).refm();           // &&&A::refm(), Self == @
}

( Taman bermain )

Jadi, sepertinya itu, kurang lebih:

  • Compiler akan memasukkan sebanyak mungkin operator dereference untuk memanggil metode.
  • Kompiler, ketika menyelesaikan metode yang dinyatakan menggunakan &self(call-by-reference):
    • Pertama kali mencoba memanggil dereference tunggal self
    • Kemudian coba panggil jenis persis self
    • Lalu, coba masukkan sebanyak mungkin operator dereferensi untuk pertandingan
  • Metode yang dinyatakan menggunakan self(panggilan-oleh-nilai) untuk tipe Tberperilaku seolah-olah mereka dideklarasikan menggunakan &self(panggilan-oleh-referensi) untuk tipe &Tdan dipanggil pada referensi untuk apa pun yang ada di sisi kiri operator titik.
  • Aturan di atas pertama kali dicoba dengan dereferencing bawaan bawaan, dan jika tidak ada kecocokan, kelebihan dengan Derefsifat tersebut digunakan.

Apa aturan auto-dereferensi yang tepat? Adakah yang bisa memberikan alasan formal untuk keputusan desain seperti itu?

kFYatek
sumber
Saya telah memposting-silang ini ke subreddit Rust dengan harapan mendapatkan jawaban yang bagus!
Shepmaster
Untuk kesenangan ekstra, coba ulangi percobaan dalam obat generik dan bandingkan hasilnya.
user2665887

Jawaban:

137

Kode semu Anda cukup benar. Untuk contoh ini, misalkan kita memiliki pemanggilan metode di foo.bar()mana foo: T. Saya akan menggunakan sintaksis yang sepenuhnya memenuhi syarat (FQS) untuk menjadi tidak ambigu tentang apa jenis metode yang dipanggil, misalnya A::bar(foo)atau A::bar(&***foo). Saya hanya akan menulis setumpuk huruf kapital acak, masing-masing hanya beberapa jenis / sifat sewenang-wenang, kecualiT selalu jenis variabel asli fooyang dipanggil oleh metode ini.

Inti dari algoritma ini adalah:

  • Untuk setiap "langkah dereferensi" U (yaitu, aturU = T dan kemudian U = *T, ...)
    1. jika ada metode di barmana jenis penerima (jenis selfdalam metode) cocok Upersis, gunakan itu ( "metode nilai" )
    2. jika tidak, tambahkan satu auto-ref (ambil &atau &mutdari penerima), dan, jika beberapa penerima metode cocok &U, gunakan itu ( "metode autorefd" )

Terutama, semuanya menganggap "jenis penerima" metode, bukan yang Selfjenis sifat tersebut, yaitu impl ... for Foo { fn method(&self) {} }berpikir tentang &Foosaat pencocokan metode, danfn method2(&mut self) akan berpikir tentang &mut Foosaat pencocokan.

Ini adalah kesalahan jika ada beberapa metode sifat yang valid di langkah-langkah dalam (yaitu, hanya ada nol atau satu metode sifat yang valid di masing-masing 1. atau 2., tetapi bisa ada satu yang valid untuk masing-masing: satu dari 1 akan diambil terlebih dahulu), dan metode bawaan lebih diutamakan daripada yang sifat. Ini juga kesalahan jika kita sampai pada akhir loop tanpa menemukan apa pun yang cocok. Ini juga merupakan kesalahan untuk memiliki rekursifDeref implementasi , yang membuat loop tak terbatas (mereka akan mencapai "batas rekursi").

Aturan-aturan ini tampaknya melakukan apa yang saya maksudkan di sebagian besar keadaan, walaupun memiliki kemampuan untuk menulis formulir FQS yang tidak ambigu sangat berguna dalam beberapa kasus tepi, dan untuk pesan kesalahan yang masuk akal untuk kode yang dihasilkan makro.

Hanya satu referensi otomatis yang ditambahkan karena

  • jika tidak ada batasan, semuanya menjadi buruk / lambat, karena setiap jenis dapat memiliki jumlah referensi yang diambil secara acak
  • mengambil satu referensi &foomempertahankan koneksi yang kuat ke foo(itu adalah alamat fooitu sendiri), tetapi mengambil lebih banyak mulai kehilangan itu: &&fooadalah alamat dari beberapa variabel sementara di stack yang menyimpan &foo.

Contohnya

Misalkan kita memiliki panggilan foo.refm(), jika foomemiliki tipe:

  • X, kemudian kita mulai dengan U = X, refmmemiliki tipe penerima &..., jadi langkah 1 tidak cocok, mengambil auto-ref memberi kita &X, dan ini tidak cocok (dengan Self = X), jadi panggilannya adalahRefM::refm(&foo)
  • &X, dimulai dengan U = &X, yang cocok &selfdengan langkah pertama (dengan Self = X), dan panggilannyaRefM::refm(foo)
  • &&&&&X, ini tidak cocok dengan salah satu langkah (sifat tidak diterapkan untuk &&&&Xatau &&&&&X), jadi kami dereferensi sekali untuk mendapatkan U = &&&&X, yang cocok dengan 1 (dengan Self = &&&X) dan panggilannya adalahRefM::refm(*foo)
  • Z, tidak cocok dengan langkah mana pun sehingga dereferensi sekali, untuk mendapatkan Y, yang juga tidak cocok, jadi dereferensi lagi, untuk mendapatkan X, yang tidak cocok dengan 1, tetapi cocok dengan setelah autorefing, jadi panggilannya RefM::refm(&**foo).
  • &&A, 1. tidak cocok dan 2. tidak karena sifat tersebut tidak diterapkan untuk &A(untuk 1) atau &&A(untuk 2), sehingga dereferenced ke &A, yang cocok dengan 1., denganSelf = A

Misalkan kita memiliki foo.m(), dan itu Atidak Copy, jika foomemiliki tipe:

  • A, Maka U = Apertandingan selflangsung sehingga panggilan tersebut M::m(foo)denganSelf = A
  • &A, maka 1. tidak cocok, dan 2. tidak (tidak &Ajuga &&Amengimplementasikan sifat), sehingga dereferenced ke A, yang cocok, tetapi M::m(*foo)membutuhkan pengambilan Aoleh nilai dan karenanya keluar foo, karenanya kesalahan.
  • &&A, 1. tidak cocok, tetapi autorefing memberi &&&A, yang cocok, jadi panggilannya adalah M::m(&foo)dengan Self = &&&A.

(Jawaban ini didasarkan pada kode , dan cukup dekat dengan README (agak ketinggalan jaman) . Niko Matsakis, penulis utama dari bagian kompiler / bahasa ini, juga melirik jawaban ini.)

huon
sumber
15
Jawaban ini tampaknya lengkap dan terperinci, tetapi saya pikir itu tidak memiliki musim panas yang pendek dan mudah diakses dari aturan. Satu ringkasan seperti itu diberikan dalam komentar ini oleh Shepmaster : "Ini [algoritma deref] akan melakukan deref sebanyak mungkin ( &&String-> &String-> String-> str) dan kemudian referensi pada maks sekali ( str-> &str)".
Lii
(Saya tidak tahu seberapa akurat dan lengkap penjelasan itu bagi saya.)
Lii
1
Dalam kasus apa sajakah dereferencing otomatis terjadi? Apakah hanya digunakan untuk ekspresi penerima untuk panggilan metode? Untuk akses bidang juga? Penugasan sisi kanan? Sisi kiri? Parameter fungsi? Mengembalikan ekspresi nilai?
Lii
1
Catatan: Saat ini, nomicon memiliki catatan TODO untuk mencuri informasi dari jawaban ini dan menuliskannya di static.rust-lang.org/doc/master/nomicon/dot-operator.html
Samb
1
Apakah paksaan (A) dicoba sebelum ini atau (B) dicoba setelah ini atau (C) dicoba di setiap langkah algoritma ini atau (D) sesuatu yang lain?
haslersn
8

Referensi Rust memiliki bab tentang ekspresi pemanggilan metode . Saya menyalin bagian terpenting di bawah ini. Pengingat: kita berbicara tentang ekspresi recv.m(), di mana recvdisebut "ekspresi penerima" di bawah ini.

Langkah pertama adalah membuat daftar jenis calon penerima. Dapatkan ini dengan berulang kali mendereferensi jenis ekspresi penerima, menambahkan setiap jenis yang ditemui ke daftar, kemudian akhirnya mencoba paksaan tanpa ukuran di bagian akhir, dan menambahkan jenis hasil jika itu berhasil. Kemudian, untuk setiap kandidat T, tambahkan &Tdan &mut Tke daftar segera setelah itu T.

Misalnya, jika penerima memiliki tipe Box<[i32;2]>, maka jenis kandidat akan Box<[i32;2]>, &Box<[i32;2]>, &mut Box<[i32;2]>, [i32; 2](oleh dereferencing), &[i32; 2], &mut [i32; 2], [i32](dengan paksaan unsized), &[i32]dan akhirnya&mut [i32] .

Kemudian, untuk setiap jenis kandidat T, cari metode yang terlihat dengan penerima jenis itu di tempat-tempat berikut:

  1. TMetode inheren (metode diterapkan langsung pada T [¹]).
  2. Salah satu metode yang disediakan oleh sifat yang terlihat diimplementasikan oleh T. [...]

( Catatan tentang [¹] : Saya benar-benar berpikir frasa ini salah. Saya telah membuka masalah . Mari kita abaikan saja kalimat itu dalam tanda kurung.)


Mari kita telusuri beberapa contoh dari kode Anda secara detail! Sebagai contoh Anda, kami dapat mengabaikan bagian tentang "paksaan tanpa ukuran" dan "metode yang melekat".

(*X{val:42}).m(): tipe ekspresi penerima adalah i32. Kami melakukan langkah-langkah ini:

  • Membuat daftar jenis calon penerima:
    • i32 tidak dapat disangkal, jadi kita sudah selesai dengan langkah 1. Daftar: [i32]
    • Selanjutnya, kita tambahkan &i32dan &mut i32. Daftar:[i32, &i32, &mut i32]
  • Mencari metode untuk setiap jenis calon penerima:
    • Kami menemukan <i32 as M>::myang memiliki tipe penerima i32. Jadi kita sudah selesai.


Sejauh ini sangat mudah. Sekarang mari kita memilih contoh yang lebih sulit: (&&A).m(). Jenis ekspresi penerima adalah &&A. Kami melakukan langkah-langkah ini:

  • Membuat daftar jenis calon penerima:
    • &&Adapat ditinjau ulang &A, jadi kami menambahkannya ke daftar. &Adapat direferensikan lagi, jadi kami juga menambahkan Ake daftar. Atidak bisa dereferensi, jadi kami berhenti. Daftar:[&&A, &A, A]
    • Selanjutnya, untuk setiap jenis Tdalam daftar, kami tambahkan &Tdan &mut Tsegera setelahnya T. Daftar:[&&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A]
  • Mencari metode untuk setiap jenis calon penerima:
    • Tidak ada metode dengan jenis penerima &&A, jadi kami pergi ke jenis berikutnya dalam daftar.
    • Kami menemukan metode <&&&A as M>::myang memang memiliki tipe penerima &&&A. Jadi kita selesai.

Berikut adalah daftar calon penerima untuk semua contoh Anda. Jenis yang dilampirkan ⟪x⟫adalah yang "menang", yaitu jenis pertama yang dapat ditemukan metode pemasangan. Juga ingat bahwa tipe pertama dalam daftar selalu tipe ekspresi penerima. Terakhir, saya memformat daftar dalam baris tiga, tapi itu hanya memformat: daftar ini adalah daftar datar.

  • (*X{val:42}).m()<i32 as M>::m
    [i32, &i32, &mut i32]
  • X{val:42}.m()<X as M>::m
    [⟪X⟫, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&X{val:42}).m()<&X as M>::m
    [&X⟫, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&X{val:42}).m()<&&X as M>::m
    [&&X⟫, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&X{val:42}).m()<&&&X as M>::m
    [&&&X⟫, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&&X{val:42}).m()<&&&X as M>::m
    [&&&&X, &&&&&X, &mut &&&&X,&&&X⟫, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&&&X{val:42}).m()<&&&X as M>::m
    [&&&&&X, &&&&&&X, &mut &&&&&X, 
     &&&&X, &&&&&X, &mut &&&&X,&&&X⟫, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]


  • (*X{val:42}).refm()<i32 as RefM>::refm
    [i32,&i32, &mut i32]
  • X{val:42}.refm()<X as RefM>::refm
    [X,&X⟫, &mut X, 
     i32, &i32, &mut i32]
  • (&X{val:42}).refm()<X as RefM>::refm
    [&X⟫, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&X{val:42}).refm()<&X as RefM>::refm
    [&&X⟫, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&X{val:42}).refm()<&&X as RefM>::refm
    [&&&X⟫, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&&X{val:42}).refm()<&&&X as RefM>::refm
    [&&&&X⟫, &&&&&X, &mut &&&&X, 
     &&&X, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&&&X{val:42}).refm()<&&&X as RefM>::refm
    [&&&&&X, &&&&&&X, &mut &&&&&X,&&&&X⟫, &&&&&X, &mut &&&&X, 
     &&&X, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]


  • Y{val:42}.refm()<i32 as RefM>::refm
    [Y, &Y, &mut Y,
     i32,&i32, &mut i32]
  • Z{val:Y{val:42}}.refm()<i32 as RefM>::refm
    [Z, &Z, &mut Z,
     Y, &Y, &mut Y,
     i32,&i32, &mut i32]


  • A.m()<A as M>::m
    [⟪A⟫, &A, &mut A]
  • (&A).m()<A as M>::m
    [&A, &&A, &mut &A,
     ⟪A⟫, &A, &mut A]
  • (&&A).m()<&&&A as M>::m
    [&&A,&&&A⟫, &mut &&A,
     &A, &&A, &mut &A,
     A, &A, &mut A]
  • (&&&A).m()<&&&A as M>::m
    [&&&A⟫, &&&&A, &mut &&&A,
     &&A, &&&A, &mut &&A,
     &A, &&A, &mut &A,
     A, &A, &mut A]
  • A.refm()<A as RefM>::refm
    [A,&A⟫, &mut A]
  • (&A).refm()<A as RefM>::refm
    [&A⟫, &&A, &mut &A,
     A, &A, &mut A]
  • (&&A).refm()<A as RefM>::refm
    [&&A, &&&A, &mut &&A,&A⟫, &&A, &mut &A,
     A, &A, &mut A]
  • (&&&A).refm()<&&&A as RefM>::refm
    [&&&A,&&&&A⟫, &mut &&&A,
     &&A, &&&A, &mut &&A,
     &A, &&A, &mut &A,
     A, &A, &mut A]
Lukas Kalbertodt
sumber