Mengapa Rust memiliki tipe yang terkait, dan apa perbedaan antara mereka dan tipe argumen alias generik, karena mereka sangat mirip? Apakah itu tidak cukup hanya yang terakhir, seperti dalam semua bahasa normal? Bagi mereka yang baru mulai mempelajari Rust, dan terutama bagi orang-orang yang berasal dari bahasa lain ("Ini generik!" - ahli jav, akan mengatakan bertahun-tahun), pertanyaan seperti itu muncul secara teratur. Mari kita perbaiki.
TL; DR Yang pertama mengontrol kode yang dipanggil, yang terakhir pemanggil.
Generik vs tipe terkait
Jadi, kami sudah memiliki argumen jenis, atau obat generik favorit semua orang. Itu terlihat seperti ini:
trait Foo<T> { fn bar(self, x: T); }
Di sini T
adalah tipe argumen. Tampaknya ini harus cukup untuk semua orang (seperti memori 640 kilobyte). Namun di Rust, ada juga tipe terkait, seperti ini:
trait Foo { type Bar;
Sekilas, telurnya sama, tetapi dari sudut yang berbeda. Mengapa Anda perlu memperkenalkan entitas lain ke dalam bahasa? (Omong-omong, yang tidak ada dalam versi awal bahasa.)
Jenis argumen adalah argumen , yang berarti bahwa mereka diteruskan ke sifat di tempat panggilan, dan kontrol atas jenis yang akan digunakan, bukan T
milik pemanggil. Bahkan jika kita tidak secara eksplisit menentukan T
di lokasi panggilan, kompiler akan melakukan ini untuk kita menggunakan inferensi tipe. Artinya, secara implisit, bagaimanapun, jenis ini akan disimpulkan pada penelepon dan disahkan sebagai argumen. (Tentu saja, semua ini terjadi selama kompilasi, bukan di runtime.)
Pertimbangkan sebuah contoh. Pustaka standar memiliki AsRef
AsRef, yang memungkinkan satu jenis berpura-pura menjadi jenis lain untuk sementara waktu, mengubah tautan ke dirinya sendiri menjadi tautan ke sesuatu yang lain. Sederhana, sifat ini terlihat seperti ini (pada kenyataannya, ini sedikit lebih rumit, saya sengaja menghapus semua yang tidak perlu, hanya menyisakan minimum yang diperlukan untuk memahami):
trait AsRef<T> { fn as_ref(&self) -> &T; }
Di sini tipe T
dilewatkan oleh pemanggil sebagai argumen, bahkan jika itu terjadi secara implisit (jika kompiler menyimpulkan jenis ini untuk Anda). Dengan kata lain, penelepon yang memutuskan tipe T
baru mana yang akan berpura-pura menjadi tipe kita yang mengimplementasikan sifat ini:
let foo = Foo::new(); let bar: &Bar = foo.as_ref();
Di sini, kompiler, menggunakan pengetahuan bar: &Bar
, akan menggunakan AsRef<Bar>
untuk memanggil metode as_ref()
, karena itu adalah jenis Bar
yang diperlukan oleh pemanggil. Tak perlu dikatakan bahwa tipe Foo
harus menerapkan sifat AsRef AsRef<Bar>
, dan di samping itu, ia dapat menerapkan AsRef<T>
lainnya, di antaranya penelepon memilih yang diinginkan.
Dalam hal tipe terkait, semuanya justru sebaliknya. Jenis terkait sepenuhnya dikendalikan oleh mereka yang menerapkan sifat ini, dan bukan oleh penelepon.
Contoh umum adalah iterator. Misalkan kita memiliki koleksi, dan kita ingin mendapatkan iterator darinya. Apa jenis nilai yang harus dikembalikan oleh iterator? Persis yang terkandung dalam koleksi ini! Tidak tergantung pada penelepon untuk memutuskan apa yang akan dikembalikan oleh iterator, dan iterator sendiri lebih tahu apa sebenarnya yang ia tahu bagaimana mengembalikannya. Berikut adalah kode singkatan dari perpustakaan standar:
trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; }
Perhatikan bahwa iterator tidak memiliki parameter tipe yang memungkinkan pemanggil untuk memilih apa yang harus dikembalikan oleh iterator. Sebagai gantinya, tipe nilai yang dikembalikan dari metode next()
ditentukan oleh iterator itu sendiri menggunakan tipe yang terkait, tetapi tidak terjebak dengan paku, mis. setiap implementasi iterator dapat memilih tipenya.
Berhenti Jadi apa Semua sama, tidak jelas mengapa ini lebih baik daripada obat generik. Bayangkan sejenak bahwa kita menggunakan generik biasa, bukan tipe terkait. Ciri iterator kemudian akan terlihat seperti ini:
trait GenericIterator<T> { fn next(&mut self) -> Option<T>; }
Tetapi sekarang, pertama, tipe T
perlu diindikasikan berulang kali di setiap tempat di mana iterator disebutkan, dan kedua, sekarang telah menjadi mungkin untuk menerapkan sifat ini beberapa kali dengan tipe yang berbeda, yang bagi iterator terlihat agak aneh. Berikut ini sebuah contoh:
struct MyIterator; impl GenericIterator<i32> for MyIterator { fn next(&mut self) -> Option<i32> { unimplemented!() } } impl GenericIterator<String> for MyIterator { fn next(&mut self) -> Option<String> { unimplemented!() } } fn test() { let mut iter = MyIterator; let lolwhat: Option<_> = iter.next();
Lihat tangkapannya? Kami tidak bisa menerima dan memanggil iter.next()
tanpa squat - kami perlu memberi tahu kompiler, secara eksplisit atau implisit, jenis apa yang akan dikembalikan. Dan itu terlihat canggung: mengapa kita, di sisi panggilan, tahu (dan memberi tahu kompiler!) Jenis iterator akan kembali, sementara iterator ini harus tahu lebih baik apa jenisnya kembali ?! Dan semua karena kami dapat mengimplementasikan GenericIterator
GenericIterator dua kali dengan parameter yang berbeda untuk MyIterator
sama, yang dari sudut pandang semantik iterator juga terlihat konyol: mengapa iterator yang sama dapat mengembalikan nilai dari jenis yang berbeda?
Jika kita kembali ke varian dengan tipe terkait, maka semua masalah ini dapat dihindari:
struct MyIter; impl Iterator for MyIter { type Item = String; fn next(&mut self) -> Option<Self::Item> { unimplemented!() } } fn test() { let mut iter = MyIter; let value = iter.next(); }
Di sini, pertama, kompiler akan menampilkan value: Option<String>
dengan benar value: Option<String>
tipe value: Option<String>
tanpa kata-kata yang tidak perlu, dan kedua, itu tidak akan bekerja untuk menerapkan MyIter
Iterator
untuk MyIter
untuk kedua kalinya dengan jenis pengembalian yang berbeda, dan dengan demikian merusak segalanya.
Untuk memperbaiki. Koleksi dapat menerapkan sifat tersebut untuk dapat mengubah dirinya menjadi iterator:
trait IntoIterator { type Item; type IntoIter: Iterator<Item=Self::Item>; fn into_iter(self) -> Self::IntoIter; }
Dan lagi, ini dia koleksi yang menentukan iterator mana yang akan dipilih, yaitu: sebuah iterator yang tipe pengembaliannya cocok dengan jenis elemen dalam koleksi itu sendiri, dan tidak ada yang lain.
Lebih banyak di jari
Jika contoh-contoh di atas masih tidak dapat dipahami, maka di sini ada penjelasan yang bahkan kurang ilmiah tetapi lebih dapat dipahami. Ketik argumen dapat dianggap sebagai informasi "masukan" yang kami berikan agar sifat tersebut berfungsi. Jenis terkait dapat dianggap sebagai informasi "keluaran" yang disediakan oleh sifat tersebut sehingga kami dapat menggunakan hasil pekerjaannya.
Perpustakaan standar memiliki kemampuan untuk membebani operator matematika untuk jenisnya (penambahan, pengurangan, perkalian, pembagian, dan sejenisnya). Untuk melakukan ini, Anda perlu menerapkan salah satu sifat yang sesuai dari perpustakaan standar. Di sini, misalnya, bagaimana sifat ini terlihat untuk operasi penambahan (sekali lagi, disederhanakan):
trait Add<RHS> { type Output; fn add(self, rhs: RHS) -> Self::Output; }
Di sini kita memiliki argumen "input" RHS
- ini adalah tipe yang akan kita terapkan operasi penambahan dengan tipe kita. Dan ada argumen "output" Add::Output
- ini adalah jenis yang akan dihasilkan dari penambahan. Dalam kasus umum, ini bisa berbeda dari jenis istilah, yang, pada gilirannya, juga bisa dari jenis yang berbeda (menambah enak dengan warna biru dan menjadi lunak - tapi apa, saya melakukan ini sepanjang waktu). Yang pertama ditentukan menggunakan argumen tipe, yang kedua ditentukan menggunakan tipe yang terkait.
Anda dapat mengimplementasikan sejumlah tambahan apa pun dengan berbagai jenis argumen kedua, tetapi setiap kali hanya akan ada satu jenis hasil, dan itu ditentukan oleh penerapan penambahan ini.
Mari kita coba terapkan sifat ini:
use std::ops::Add; struct Foo(&'static str); #[derive(PartialEq, Debug)] struct Bar(&'static str, i32); impl Add<i32> for Foo { type Output = Bar; fn add(self, rhs: i32) -> Bar { Bar(self.0, rhs) } } fn test() { let x = Foo("test"); let y = x + 42;
Dalam contoh ini, tipe variabel y
ditentukan oleh algoritma penjumlahan, bukan kode panggilan. Akan sangat aneh jika ada kemungkinan untuk menulis sesuatu seperti let y: Baz = x + 42
, yaitu, memaksa operasi penjumlahan untuk mengembalikan hasil dari beberapa tipe yang asing. Dari hal-hal seperti itulah tipe terkait Add::Output
menjamin kami.
Total
Kami menggunakan obat generik di mana kami tidak keberatan memiliki beberapa implementasi sifat untuk jenis yang sama, dan di mana dapat diterima untuk menentukan implementasi spesifik pada sisi panggilan. Kami menggunakan tipe terkait di mana kami ingin memiliki satu implementasi "kanonik", yang mengontrol jenisnya sendiri. Gabungkan dan campur dalam proporsi yang tepat, seperti pada contoh terakhir.
Apakah koin itu gagal? Bunuh aku dengan komentar.