Kapan waktu yang tepat untuk menggunakan tipe terkait versus tipe generik?

109

Dalam pertanyaan ini , masalah muncul yang dapat diselesaikan dengan mengubah upaya menggunakan parameter tipe generik menjadi tipe terkait. Itu memunculkan pertanyaan "Mengapa jenis terkait lebih sesuai di sini?", Yang membuat saya ingin tahu lebih banyak.

The RFC yang memperkenalkan jenis terkait mengatakan:

RFC ini menjelaskan pencocokan sifat dengan:

  • Memperlakukan semua parameter tipe sifat sebagai tipe masukan , dan
  • Menyediakan tipe terkait, yaitu tipe keluaran .

RFC menggunakan struktur grafik sebagai contoh yang memotivasi, dan ini juga digunakan dalam dokumentasi , tetapi saya akan mengakui bahwa saya tidak sepenuhnya menghargai manfaat dari versi tipe terkait di atas versi tipe-parameterisasi. Hal utama adalah bahwa distancemetode tersebut tidak perlu memperhatikan Edgetipenya. Ini bagus, tetapi tampaknya agak dangkal alasan untuk memiliki tipe terkait sama sekali.

Saya telah menemukan jenis terkait cukup intuitif untuk digunakan dalam praktik, tetapi saya menemukan diri saya kesulitan ketika memutuskan di mana dan kapan saya harus menggunakannya di API saya sendiri.

Saat menulis kode, kapan saya harus memilih tipe terkait di atas parameter tipe generik, dan kapan saya harus melakukan yang sebaliknya?

Shepmaster
sumber

Jawaban:

76

Ini sekarang disinggung dalam edisi kedua The Rust Programming Language . Namun, mari selami sedikit sebagai tambahan.

Mari kita mulai dengan contoh yang lebih sederhana.

Kapan tepat menggunakan metode sifat?

Ada beberapa cara untuk memberikan penjilidan terlambat :

trait MyTrait {
    fn hello_word(&self) -> String;
}

Atau:

struct MyTrait<T> {
    t: T,
    hello_world: fn(&T) -> String,
}

impl<T> MyTrait<T> {
    fn new(t: T, hello_world: fn(&T) -> String) -> MyTrait<T>;

    fn hello_world(&self) -> String {
        (self.hello_world)(self.t)
    }
}

Dengan mengabaikan strategi implementasi / kinerja apa pun, kedua kutipan di atas memungkinkan pengguna untuk menentukan secara dinamis bagaimana hello_worldseharusnya berperilaku.

Satu perbedaan (secara semantik) adalah bahwa traitimplementasi menjamin bahwa untuk tipe tertentu yang Tmengimplementasikan trait, hello_worldakan selalu memiliki perilaku yang sama sedangkan structimplementasi memungkinkan memiliki perilaku yang berbeda pada basis per contoh.

Apakah menggunakan metode itu tepat atau tidak tergantung pada kasus penggunaan!

Kapan waktu yang tepat untuk menggunakan tipe terkait?

Mirip dengan traitmetode di atas, tipe terkait adalah bentuk pengikatan akhir (meskipun terjadi saat kompilasi), yang memungkinkan pengguna traituntuk menentukan jenis yang akan diganti untuk instance tertentu. Ini bukan satu-satunya cara (jadi pertanyaannya):

trait MyTrait {
    type Return;
    fn hello_world(&self) -> Self::Return;
}

Atau:

trait MyTrait<Return> {
    fn hello_world(&Self) -> Return;
}

Setara dengan metode pengikatan akhir di atas:

  • yang pertama memaksakan bahwa untuk yang diberikan Selfada satu yang Returnterkait
  • yang kedua, sebaliknya, memungkinkan menerapkan MyTraituntuk Selfuntuk beberapaReturn

Bentuk mana yang lebih tepat bergantung pada apakah masuk akal untuk menegakkan kesatuan atau tidak. Sebagai contoh:

  • Deref menggunakan tipe yang diasosiasikan karena tanpa unicity compiler akan menjadi gila selama inferensi
  • Add menggunakan tipe terkait karena pembuatnya berpikir bahwa dengan dua argumen akan ada tipe pengembalian logis

Seperti yang Anda lihat, meskipun Derefmerupakan kasus penggunaan yang jelas (kendala teknis), kasusnya Addkurang jelas: mungkin masuk akal untuk i32 + i32menghasilkan salah satu i32atau Complex<i32>bergantung pada konteksnya? Meskipun demikian, penulis menjalankan penilaian mereka dan memutuskan bahwa kelebihan jenis pengembalian untuk penambahan tidak diperlukan.

Pendapat pribadi saya adalah bahwa tidak ada jawaban yang benar. Namun, di luar argumen unicity, saya akan menyebutkan bahwa tipe terkait membuat penggunaan sifat lebih mudah karena mereka mengurangi jumlah parameter yang harus ditentukan, jadi jika manfaat dari fleksibilitas menggunakan parameter sifat biasa tidak jelas, saya menyarankan untuk memulai dengan jenis yang terkait.

Matthieu M.
sumber
4
Izinkan saya mencoba menyederhanakan sedikit: trait/struct MyTrait/MyStructmemungkinkan tepat satu impl MyTrait foratau impl MyStruct. trait MyTrait<Return>memungkinkan beberapa implkarena bersifat umum. Returnbisa tipe apapun. Struct generik sama.
Paul-Sebastian Manole
2
Saya menemukan jawaban Anda jauh lebih mudah untuk dipahami daripada yang ada di "Bahasa Pemrograman Rust"
drojf
"Yang pertama memaksakan bahwa untuk Diri tertentu ada satu Pengembalian yang terkait". Ini benar dalam arti langsung, tetapi seseorang tentu saja dapat mengatasi batasan ini dengan membuat subclass dengan sifat umum. Mungkin kesatuan hanya bisa menjadi saran, dan tidak dipaksakan
joel
37

Jenis terkait adalah mekanisme pengelompokan , sehingga harus digunakan jika memungkinkan untuk mengelompokkan jenis bersama.

Ciri yang Graphdiperkenalkan dalam dokumentasi adalah contohnya. Anda ingin a Graphmenjadi generik, tetapi setelah Anda memiliki jenis tertentu Graph, Anda tidak ingin jenis Nodeatau Edgebervariasi lagi. Seorang tertentu Graphtidak akan ingin memvariasikan jenis-jenis itu dalam satu implementasi, dan pada kenyataannya, ingin mereka selalu sama. Mereka dikelompokkan bersama, atau bahkan bisa dikatakan terkait .

Steve Klabnik
sumber
5
Aku butuh waktu untuk mengerti. Bagi saya ini lebih terlihat seperti mendefinisikan beberapa tipe sekaligus: Edge dan Node tidak masuk akal dari grafik.
tafia