Penutupan tipe generik di Rust


Dalam artikel singkat ini, saya akan berbicara tentang pola di Rust yang memungkinkan Anda untuk "menyimpan" untuk nanti menggunakan tipe yang melewati metode generik. Pola ini ditemukan di pustaka sumber pustaka Rust, dan kadang-kadang saya juga menggunakannya dalam proyek saya. Saya tidak dapat menemukan publikasi tentang dia di jaringan, jadi saya memberinya nama saya: "Penutupan tipe umum", dan dalam artikel ini saya ingin memberi tahu Anda apa itu, mengapa dan bagaimana itu dapat digunakan.


Masalah


Di Rust, sistem tipe statis yang dikembangkan dan kemampuan statisnya sudah cukup untuk sekitar 80% kasus. Tetapi kebetulan pengetikan dinamis diperlukan ketika Anda ingin menyimpan objek dari tipe yang berbeda di tempat yang sama. Jenis-objek karakter datang untuk menyelamatkan di sini: mereka menghapus jenis objek yang sebenarnya, menguranginya menjadi antarmuka umum tertentu yang ditentukan oleh jenisnya, dan kemudian Anda dapat beroperasi pada objek-objek ini sebagai objek tipe-jenis-objek yang sama.


Ini bekerja dengan baik di setengah dari kasus yang tersisa. Tapi bagaimana jika kita masih perlu mengembalikan objek yang terhapus saat menggunakannya? Misalnya, jika perilaku objek kita diatur oleh tipe yang tidak dapat digunakan sebagai objek-tipe . Ini adalah situasi umum untuk sifat dengan tipe terkait. Apa yang harus dilakukan dalam kasus ini?


Solusi


Untuk semua tipe 'static (yaitu, tipe yang tidak mengandung tautan non-statis), Rust mengimplementasikan tipe Any , yang memungkinkan konversi dyn Any tipe objek ke referensi ke tipe objek asli:


 let value = "test".to_string(); let value_any = &value as &dyn Any; //       String.  //   -      . if let Some(as_string) = value_any.downcast_ref::<String>() { println!("String: {}", as_string); } else { println!("Unknown type"); } 

Lari


Box juga memiliki metode downcast untuk tujuan ini.


Solusi ini cocok untuk kasus-kasus ketika jenis sumber diketahui di tempat bekerja dengannya. Tetapi bagaimana jika tidak demikian? Bagaimana jika kode panggilan tidak tahu tentang jenis sumber objek di tempat penggunaannya? Maka kita perlu entah bagaimana mengingat jenis aslinya, membawanya ke tempat yang telah ditentukan, dan menyimpannya dengan dyn Any Jenis objek apa pun, sehingga nanti yang terakhir dikonversi ke jenis asli di tempat yang tepat.


Tipe yang digeneralisasi dalam Rust dapat diperlakukan sebagai tipe variabel ke dalam mana satu atau tipe lainnya nilai dapat diteruskan ketika dipanggil. Tetapi di Rust tidak ada cara untuk mengingat jenis ini untuk digunakan lebih lanjut di tempat lain. Namun, ada cara untuk mengingat semua fungsi menggunakan tipe ini, bersama dengan tipe ini. Ini adalah ide dari pola "Penutupan tipe umum": kode yang menggunakan tipe dieksekusi dalam bentuk penutupan, yang disimpan sebagai fungsi normal, karena tidak menggunakan objek lingkungan apa pun, kecuali untuk tipe umum.


Implementasi


Mari kita lihat contoh implementasi. Misalkan kita ingin membuat pohon rekursif yang mewakili hierarki objek grafik, di mana setiap node dapat berupa grafik primitif dengan simpul anak, atau komponen - pohon objek grafis yang terpisah:


 enum Node { Prim(Primitive), Comp(Component), } struct Primitive { shape: Shape, children: Vec<Node>, } struct Component { node: Box<Node>, } enum Shape { Rectangle, Circle, } 

Pengemasan Node dalam struktur Component diperlukan karena struktur Component itu sendiri digunakan dalam Node .


Sekarang anggaplah bahwa pohon kita hanyalah representasi dari beberapa model yang dengannya ia harus dikaitkan. Selain itu, setiap komponen akan memiliki model sendiri:


 struct Primitive<Model> { shape: Shape, children: Vec<Node<Model>>, } struct Component<Model> { node: Box<Node<Model>>, model: Model, //   Model } 

Kita bisa menulis:


 enum Node<Model> { Prim(Primitive<Model>), Comp(Component<Model>), } 

Tetapi kode ini tidak akan berfungsi sesuai kebutuhan. Karena komponen harus memiliki model sendiri, dan bukan model elemen induk, yang berisi komponen. Artinya, kita perlu:


 enum Node<Model> { Prim(Primitive<Model>), Comp(Component), } struct Primitive<Model> { shape: Shape, children: Vec<Node<Model>>, _model: PhantomData<Model>, //   Model } struct Component { node: Box<dyn Any>, model: Box<dyn Any>, } impl Component { fn new<Model: 'static>(node: Node<Model>, model: Model) -> Self { Self { node: Box::new(node), model: Box::new(model), } } } 

Lari


Kami telah memindahkan indikasi tipe model tertentu ke metode new , dan dalam komponen itu sendiri kami menyimpan model dan subtree yang sudah dengan tipe yang terhapus.


Sekarang tambahkan metode use_model , yang akan menggunakan model, tetapi tidak akan diparameterisasi berdasarkan tipenya:


 struct Component { node: Box<dyn Any>, model: Box<dyn Any>, use_model_closure: fn(&Component), } impl Component { fn new<Model: 'static>(node: Node<Model>, model: Model) -> Self { let use_model_closure = |comp: &Component| { comp.model.downcast_ref::<Model>().unwrap(); }; Self { node: Box::new(node), model: Box::new(model), use_model_closure, } } fn use_model(&self) { (self.use_model_closure)(self); } } 

Lari


Perhatikan bahwa dalam komponen kami menyimpan pointer ke fungsi yang dibuat dalam metode new menggunakan sintaks untuk mendefinisikan penutupan. Tetapi semua yang harus ditangkap dari luar adalah tipe Model , oleh karena itu kita dipaksa untuk mengirimkan tautan ke komponen itu sendiri ke dalam fungsi ini melalui argumen.


Tampaknya alih-alih penutupan, kita dapat menggunakan fungsi internal, tetapi kode seperti itu tidak dikompilasi. Karena fungsi internal di Rust tidak dapat menangkap jenis umum dari yang eksternal karena fakta bahwa itu berbeda dari fungsi tingkat atas yang biasa hanya dalam visibilitas.

Metode use_model dapat digunakan dalam konteks di mana jenis Model tidak diketahui. Misalnya, dalam traversal pohon rekursif yang terdiri dari banyak komponen yang berbeda dengan model yang berbeda.


Alternatif


Jika mungkin untuk mentransfer antarmuka komponen ke jenis yang memungkinkan pembuatan objek-jenis, maka lebih baik melakukannya, dan alih-alih menggunakan komponen itu sendiri untuk beroperasi pada objek-jenisnya:


 enum Node<Model> { Prim(Primitive<Model>), Comp(Box<dyn ComponentApi>), } struct Component<Model> { node: Node<Model>, model: Model, } impl<Model> Component<Model> { fn new(node: Node<Model>, model: Model) -> Self { Self { node, model, } } } trait ComponentApi { fn use_model(&self); } impl<Model> ComponentApi for Component<Model> { fn use_model(&self) { &self.model; } } 

Lari


Kesimpulan


Ternyata penutupan di Rust tidak hanya dapat menangkap objek lingkungan, tetapi juga tipe. Namun, mereka dapat diartikan sebagai fungsi biasa. Properti ini menjadi berguna ketika Anda harus bekerja secara seragam dengan berbagai jenis tanpa kehilangan informasi tentang mereka, jika jenis karakter tidak berlaku.


Saya harap artikel ini membantu Anda menggunakan Rust. Bagikan pemikiran Anda dalam komentar.

Source: https://habr.com/ru/post/id454310/


All Articles