Deref and DerefMut
Deref
adalah trait yang memungkinkan Anda untuk menggunakan *
untuk melakukan dereference. Kita tahu bahwa reference tidaklah sama dengan value:
// ⚠️ fn main() { let value = 7; // Ini bertype i32 let reference = &7; // Ini bertype &i32 println!("{}", value == reference); }
Dan Rust bahkan tidak akan memberikan false
karena keduanya tidak bisa dibandingkan.
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src\main.rs:4:26
|
4 | println!("{}", value == reference);
| ^^ no implementation for `{integer} == &{integer}`
Tentu saja, solusinya adalah menggunakan *
. Sehingga programnya akan mencetak true
:
fn main() { let value = 7; let reference = &7; println!("{}", value == *reference); }
Sekarang mari kita bayangkan type struct sederhana yang hanya menampung angka. Ia akan menjadi mirip seperti Box
, dan kita memiliki ide untuk menambahkan beberapa function tambahan untuk struct tersebut. Tapi jika kita hanya memberikannya angka, kita tidak bisa berbuat banyak pada struct tersebut.
Kita tidak bisa menggunakan *
sebagaimana kita bisa melakukannya dengan Box
:
// ⚠️ struct HoldsANumber(u8); fn main() { let my_number = HoldsANumber(20); println!("{}", *my_number + 20); }
Errornya adalah seperti berikut:
error[E0614]: type `HoldsANumber` cannot be dereferenced
--> src\main.rs:24:22
|
24 | println!("{:?}", *my_number + 20);
Tentu saja kita bisa melakukannya dengan cara seperti ini: println!("{:?}", my_number.0 + 20);
. Kita hanya ingin menambahkan isi dari struct yang bertype u8
dengan 20. Alangkah baiknya jika kita bisa langsung menjumlahkannya dengan menggunakan *
. Pesan cannot be dereferenced
memberikan kita petunjuk: kita perlu mengimplementasikan Deref
. Sesuatu yang mengimplementasikan Deref
terkadang disebut sebagai "smart pointer". Smart pointer bisa merujuk pada sebuah item, memiliki informasi tentang item tersebut, dan bisa menggunakan method-method yang tersedia untuk item tersebut. Karena untuk sekarang ini, kita bisa menggunakan my_number.0
, yang mana bertype u8
, namun kita tidak bisa berbuat banyak dengan HoldsANumber
: satu-satunya yang kita miliki sejauh ini hanyalah Debug
.
Fakta menariknya adalah: String
adalah smart pointer dari &str
dan Vec
adalah smart pointer dari array (atau type lainnya). Jadi sebenarnya kita telah menggunakan smart pointer sejak awal.
Mengimplementasikan Deref
tidaklah terlalu sulit dan contohnya di standard library cukup mudah. Ini adalah contoh code dari standard library:
use std::ops::Deref; struct DerefExample<T> { value: T } impl<T> Deref for DerefExample<T> { type Target = T; fn deref(&self) -> &Self::Target { &self.value } } fn main() { let x = DerefExample { value: 'a' }; assert_eq!('a', *x); }
Jadi kita mengikuti contoh tersebut dan sekarang Deref
kita menjadi seperti berikut:
#![allow(unused)] fn main() { // 🚧 impl Deref for HoldsANumber { type Target = u8; // Ingat, ini adalah "associated type": type yang ikut dijalankan bersama. // Anda harus menggunakan type Target yang tepat = (type yang ingin Anda kembalikan) fn deref(&self) -> &Self::Target { // Rust menggunakan .deref() di saat Anda menggunakan *. Kita hanya mendefinisikan Target sebagai u8, Sehingga ia menjadi mudah untuk dipahami &self.0 // Kita memilih &self.0 karena ia adalah struct tuple. Di dalam struct yang bernama, ia akan menjadi seperti "&self.number" } } }
Dan sekarang kita bisa menjalankan ini dengan menggunakan *
:
use std::ops::Deref; #[derive(Debug)] struct HoldsANumber(u8); impl Deref for HoldsANumber { type Target = u8; fn deref(&self) -> &Self::Target { &self.0 } } fn main() { let my_number = HoldsANumber(20); println!("{:?}", *my_number + 20); }
Maka ia akan mencetak 40
dan kita tidak perlu untuk menulis my_number.0
. Ini artinya kita mendapatkan method dari u8
dan kita bisa menuliskan method kita sendiri untuk HoldsANumber
. Kita akan menambahkan method sederhana buatan kita sendiri dan menggunakan method lainnya yang kita dapatkan dari u8
yang bernama .checked_sub()
. Method .checked_sub()
adalah operasi pengurangan yang safe, yang mana kembaliannya adalah Option
. Jika ia berhasil melakukan pengurangan, maka ia akan memberikan hasilnya terbungkus di dalam Some
. Dan apabila ia tidak bisa melakukannya, maka ia akan mengembalikan None
. Ingatlah, u8
tidak bisa negatif sehingga akan lebih safe untuk menggunakan .checked_sub()
sehingga tidak menimbulkan panic.
use std::ops::Deref; struct HoldsANumber(u8); impl HoldsANumber { fn prints_the_number_times_two(&self) { println!("{}", self.0 * 2); } } impl Deref for HoldsANumber { type Target = u8; fn deref(&self) -> &Self::Target { &self.0 } } fn main() { let my_number = HoldsANumber(20); println!("{:?}", my_number.checked_sub(100)); // Method yang ini diambil dari u8 my_number.prints_the_number_times_two(); // Ini adalah method buatan kita sendiri }
Hasilnya adalah:
None
40
Kita juga bisa mengimplementasikan DerefMut
sehingga kita bisa mengubah valuenya melalui *
. Ini terlihat hampir sama. Anda membutuhkan Deref
sebelum Anda bisa mengimplement DerefMut
.
use std::ops::{Deref, DerefMut}; struct HoldsANumber(u8); impl HoldsANumber { fn prints_the_number_times_two(&self) { println!("{}", self.0 * 2); } } impl Deref for HoldsANumber { type Target = u8; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for HoldsANumber { // Anda tidak memerlukan type Target = u8; dibagian ini, karena ia sudah mengetahuinya. Thanks to Deref :D fn deref_mut(&mut self) -> &mut Self::Target { // Semua yang tertulis di sini adalah sama seperti yang tertulis di Deref. Yang berbeda adalah disini diselipkan mut dimana-mana &mut self.0 } } fn main() { let mut my_number = HoldsANumber(20); *my_number = 30; // DerefMut memungkinkan kita untuk melakukan ini println!("{:?}", my_number.checked_sub(100)); my_number.prints_the_number_times_two(); }
Jadi, kita bisa melihat bahwa Deref
memberikan kemampuan lebih kepada type yang kita buat.
Ini juga alasan mengapa standard library mengatakan: Deref should only be implemented for smart pointers to avoid confusion
. Hal itu dikarenakan Anda bisa melakukan suatu hal yang aneh menggunakan Deref
untuk type yang rumit. Mari kita bayangkan contoh yang sangat membingungkan untuk memahami apa yang dimaksud oleh kalimat di standard library tersebut. Kita mulai dengan struct Character
untuk sebuah game. Character
baru memerlukan stats seperti intelligence dan strength. Jadi, inilah karakter pertama kita:
struct Character { name: String, strength: u8, dexterity: u8, health: u8, intelligence: u8, wisdom: u8, charm: u8, hit_points: i8, alignment: Alignment, } impl Character { fn new( name: String, strength: u8, dexterity: u8, health: u8, intelligence: u8, wisdom: u8, charm: u8, hit_points: i8, alignment: Alignment, ) -> Self { Self { name, strength, dexterity, health, intelligence, wisdom, charm, hit_points, alignment, } } } enum Alignment { Good, Neutral, Evil, } fn main() { let billy = Character::new("Billy".to_string(), 9, 8, 7, 10, 19, 19, 5, Alignment::Good); }
Sekarang mari bayangkan bahwa kita ingin untuk menyimpan hit points karakter tersebut di dalam vec yang besar. Mungkin kita akan menempatkan data monster di sana juga, dan menyimpan semuanya bersama-sama menjadi satu. Karena hit_points
adalah i8
, kita implementasikan Deref
sehingga kita bisa melakukan segala macam operasi matematika kepadanya. Tapi lihatlah dan perhatikan baik-baik, betapa anehnya codenya pada function main()
sekarang ini:
use std::ops::Deref; // Semua code yang ada di sini sama, kecuali setelah enum Alignment struct Character { name: String, strength: u8, dexterity: u8, health: u8, intelligence: u8, wisdom: u8, charm: u8, hit_points: i8, alignment: Alignment, } impl Character { fn new( name: String, strength: u8, dexterity: u8, health: u8, intelligence: u8, wisdom: u8, charm: u8, hit_points: i8, alignment: Alignment, ) -> Self { Self { name, strength, dexterity, health, intelligence, wisdom, charm, hit_points, alignment, } } } enum Alignment { Good, Neutral, Evil, } impl Deref for Character { // impl Deref for Character. Sekarang kita bisa melakukan operasi matematis (untuk integer) yang kita inginkan! type Target = i8; fn deref(&self) -> &Self::Target { &self.hit_points } } fn main() { let billy = Character::new("Billy".to_string(), 9, 8, 7, 10, 19, 19, 5, Alignment::Good); // buat dua buah karakter, billy dan brandy let brandy = Character::new("Brandy".to_string(), 9, 8, 7, 10, 19, 19, 5, Alignment::Good); let mut hit_points_vec = vec![]; // masukkan data hit pointnya ke dalam vec ini hit_points_vec.push(*billy); // Push *billy ? hit_points_vec.push(*brandy); // Push *brandy ? println!("{:?}", hit_points_vec); }
Ia akan mencetak [5, 5]
. Code yang kita buat di atas sangatlah aneh untuk dibaca oleh orang lain. Kita bisa membaca Deref
di bagian atas main()
dan mengetahui bahwa *billy
itu adalah i8
, tapi bagaimana jika codenya menjadi sangat banyak? Mungkin code kita panjangnya adalah 2000 baris, dan tiba-tiba kita harus mencari tahu mengapa kita melakukan .push()
kepada *billy
. Character
tentu lebih dari sekedar smart pointer untuk i8
.
Tentu saja, tidaklah ilegal/haram/terlarang (atau apapun itu :D) untuk menuliskan hit_points_vec.push(*billy)
, tapi itu membuat codenya jadi terlihat aneh. Mungkin dengan membuat method sederhana .get_hp()
akan membuatnya jauh lebih baik. Atau juga bisa dengan cara membuat struct lain yang menyimpan karakter-karakter tersebut. Lalu kemudian Anda bisa mengiterasinya dan melakukan push setiap hit_points
yang ada pada karakter tersebut. Deref
memberikan Anda keleluasaan dan kemampuan lebih, tapi akan lebih baik untuk memastikan bahwa code yang kita buat itu logis untuk dipahami oleh kita sendiri dan orang lain.