Memperlakukan Smart Pointers seperti Referensi Biasa dengan Deref
Mengimplementasikan trait Deref memungkinkan kita buat mengkustomisasi
perilaku dari dereference operator (operator dereferensi) * (jangan
tertukar sama operator perkalian atau operator glob). Dengan
mengimplementasikan Deref sedemikian rupa sehingga sebuah smart pointer
bisa diperlakukan seperti referensi biasa, kita bisa menulis kode yang
beroperasi pada referensi dan memakai kode tersebut buat smart pointers
juga.
Mari kita lihat dulu gimana cara kerja operator dereferensi pada referensi
biasa. Kemudian kita bakal mencoba mendefinisikan sebuah tipe kustom yang
berperilaku mirip Box<T>, dan melihat kenapa operator dereferensi tidak
bekerja seperti referensi pada tipe yang baru kita definisikan itu. Kita
bakal mengeksplorasi gimana mengimplementasikan trait Deref membikin
smart pointers mungkin buat bekerja dengan cara yang mirip seperti
referensi. Kemudian kita bakal melihat fitur deref coercion di Rust dan
gimana ia membiarkan kita bekerja entah itu pakai referensi maupun pakai
smart pointers.
Mengikuti Referensi ke Nilainya
Sebuah referensi biasa adalah salah satu jenis pointer, dan salah satu
cara buat membayangkan sebuah pointer adalah sebagai tanda panah yang
menunjuk ke sebuah nilai yang disimpan di tempat lain. Di Listing 15-6,
kita membuat sebuah referensi ke nilai i32 lalu memakai operator
dereferensi buat mengikuti referensi tersebut ke nilainya.
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
i32Variabel x memegang sebuah nilai i32 yaitu 5. Kita menge-set y agar
sama dengan sebuah referensi ke x. Kita bisa menegaskan (assert) kalau
x itu sama dengan 5. Namun, kalau kita mau bikin penegasan soal nilai
di dalam y, kita harus memakai *y buat mengikuti referensi tersebut ke
nilai yang lagi dia tunjuk (karenanya disebut dereference) supaya
compiler bisa membandingkan nilai aslinya. Begitu kita men-dereferensi y,
kita punya akses ke nilai integer yang ditunjuk sama y yang bisa kita
bandingkan dengan 5.
Kalau kita malah mencoba nulis assert_eq!(5, y);, kita bakal dapat error
kompilasi ini:
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
Membandingkan sebuah angka dan sebuah referensi ke angka itu tidak diperbolehkan karena mereka adalah tipe yang berbeda. Kita harus memakai operator dereferensi buat mengikuti referensi tersebut ke nilai yang ditunjuknya.
Memakai Box<T> Layaknya Sebuah Referensi
Kita bisa menulis ulang kode di Listing 15-6 buat memakai sebuah Box<T>
bukannya referensi; operator dereferensi yang dipakai pada Box<T> di
Listing 15-7 berfungsi dengan cara yang sama kayak operator dereferensi
yang dipakai pada referensi di Listing 15-6.
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Box<i32>Perbedaan utama antara Listing 15-7 dan Listing 15-6 adalah di sini kita
menge-set y agar menjadi sebuah instance dari sebuah box yang menunjuk
ke salinan nilai dari x ketimbang sebuah referensi yang menunjuk ke
nilai dari x. Di penegasan terakhir, kita bisa memakai operator
dereferensi buat mengikuti pointer box tersebut dengan cara yang sama
seperti saat y tadinya adalah sebuah referensi. Selanjutnya, kita bakal
mengeksplorasi apa yang spesial dari Box<T> yang memungkinkan kita buat
memakai operator dereferensi dengan mendefinisikan tipe box kita sendiri.
Mendefinisikan Smart Pointer Kita Sendiri
Mari kita bikin sebuah tipe pembungkus (wrapper type) yang mirip sama tipe
Box<T> yang disediakan sama standard library buat merasakan gimana
tipe smart pointer berperilaku secara berbeda dari referensi secara
default. Kemudian kita bakal melihat gimana cara menambahkan kemampuan
buat memakai operator dereferensi.
Catatan: Ada satu perbedaan besar antara tipe
MyBox<T>yang mau kita bikin ini denganBox<T>yang asli: versi kita ini tidak bakal menyimpan datanya di heap. Kita memfokuskan contoh ini padaDeref, jadi di mana datanya sebenarnya disimpan itu kurang penting dibanding perilaku yang mirip pointer-nya.
Tipe Box<T> pada akhirnya didefinisikan sebagai sebuah tuple struct dengan
satu elemen, jadi Listing 15-8 mendefinisikan sebuah tipe MyBox<T> dengan
cara yang sama. Kita juga bakal mendefinisikan sebuah fungsi new agar
cocok sama fungsi new yang didefinisikan pada Box<T>.
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {}
MyBox<T>Kita mendefinisikan sebuah struct bernama MyBox dan mendeklarasikan sebuah
parameter generik T karena kita mau tipe kita bisa memegang nilai dari
tipe apa pun. Tipe MyBox adalah sebuah tuple struct dengan satu elemen
bertipe T. Fungsi MyBox::new menerima satu parameter bertipe T dan
mengembalikan sebuah instance MyBox yang memegang nilai yang dimasukkan.
Mari coba tambahkan fungsi main di Listing 15-7 ke Listing 15-8 lalu
ubah kodenya biar memakai tipe MyBox<T> yang sudah kita definisikan
bukannya Box<T>. Kode di Listing 15-9 tidak bakal bisa di-compile karena
Rust tidak tahu gimana cara men-dereferensi MyBox.
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
MyBox<T> dengan cara yang sama seperti kita memakai referensi dan Box<T>Berikut adalah error kompilasi yang dihasilkan:
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^ can't be dereferenced
For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
Tipe MyBox<T> kita tidak bisa di-dereferensi karena kita belum
mengimplementasikan kemampuan tersebut pada tipe kita. Untuk memungkinkan
proses dereferensi dengan operator *, kita harus mengimplementasikan
trait Deref.
Mengimplementasikan Trait Deref
Seperti yang sudah dibahas di “Mengimplementasikan sebuah Trait pada suatu
Tipe” di Bab 10, untuk mengimplementasikan sebuah trait kita
perlu menyediakan implementasi buat method-method yang diwajibkan oleh
trait tersebut. Trait Deref, yang disediakan oleh standard library,
mewajibkan kita buat mengimplementasikan satu method bernama deref yang
meminjam self lalu mengembalikan sebuah referensi ke data internalnya.
Listing 15-10 mengandung implementasi Deref buat ditambahkan ke definisi
MyBox<T>.
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Deref pada MyBox<T>Sintaks type Target = T; mendefinisikan sebuah associated type buat
dipakai oleh trait Deref. Associated types adalah cara yang sedikit
berbeda dalam mendeklarasikan sebuah parameter generik, tapi kita tidak usah
pusing dulu soal itu buat sekarang; kita bakal membahasnya lebih detail
di Bab 20.
Kita mengisi body dari method deref dengan &self.0 supaya deref
mengembalikan referensi ke nilai yang mau kita akses dengan operator *;
ingat kembali dari “Memakai Tuple Structs tanpa Field Bernama buat Bikin
Tipe yang Beda” di Bab 5 kalau .0 mengakses nilai pertama
di dalam sebuah tuple struct. Fungsi main di Listing 15-9 yang
memanggil * pada nilai MyBox<T> sekarang sudah bisa di-compile, dan
penegasannya sukses!
Tanpa trait Deref, compiler cuma bisa men-dereferensi referensi &.
Method deref memberi compiler kemampuan buat mengambil sebuah nilai dari
tipe apa pun yang mengimplementasikan Deref lalu memanggil method deref
buat mendapatkan sebuah referensi & yang dia tahu gimana cara
men-dereferensinya.
Saat kita memasukkan *y di Listing 15-9, di balik layar Rust sebenarnya
menjalankan kode ini:
*(y.deref())
Rust mengganti operator * dengan pemanggilan ke method deref dan lalu
sebuah dereferensi biasa sehingga kita tidak perlu repot mikirin apakah
kita butuh memanggil method deref atau tidak. Fitur Rust ini membiarkan
kita menulis kode yang fungsinya identik baik saat kita punya referensi
biasa maupun tipe yang mengimplementasikan Deref.
Alasan kenapa method deref mengembalikan sebuah referensi ke sebuah nilai,
dan kenapa dereferensi biasa di luar tanda kurung di *(y.deref()) itu
tetap diperlukan, ada hubungannya sama sistem ownership. Kalau method
deref mengembalikan nilainya secara langsung bukannya referensi ke
nilainya, nilainya bakal di-move keluar dari self. Kita tidak mau
mengambil kepemilikan (ownership) dari nilai internal di dalam MyBox<T>
di kasus ini maupun di kebanyakan kasus di mana kita memakai operator
dereferensi.
Perhatikan bahwa operator * digantikan dengan pemanggilan ke method
deref dan kemudian pemanggilan ke operator * cuma satu kali saja, setiap
kali kita memakai tanda * di kode kita. Karena penggantian operator *
tersebut tidak berulang secara rekursif tanpa henti, kita akhirnya mendapatkan
data bertipe i32, yang cocok dengan angka 5 di assert_eq! di
Listing 15-9.
Deref Coercion Implisit dengan Fungsi dan Method
Deref coercion mengubah sebuah referensi ke sebuah tipe yang
mengimplementasikan trait Deref menjadi sebuah referensi ke tipe lainnya.
Misalnya, deref coercion bisa mengubah &String menjadi &str karena
String mengimplementasikan trait Deref sedemikian rupa sehingga ia
mengembalikan &str. Deref coercion adalah sebuah kemudahan yang
dilakukan Rust pada argumen buat fungsi dan method, dan cuma bekerja pada
tipe yang mengimplementasikan trait Deref. Hal ini terjadi secara
otomatis saat kita meneruskan sebuah referensi ke nilai dari tipe tertentu
sebagai argumen ke sebuah fungsi atau method yang tipenya tidak cocok dengan
tipe parameter di definisi fungsi atau method tersebut. Serangkaian
pemanggilan ke method deref mengubah tipe yang kita berikan menjadi tipe
yang dibutuhkan sama parameternya.
Deref coercion ditambahkan ke Rust supaya para programmer yang menulis
pemanggilan fungsi dan method tidak perlu menambahkan terlalu banyak
referensi dan dereferensi eksplisit dengan & dan *. Fitur deref coercion
juga membiarkan kita menulis lebih banyak kode yang bisa bekerja baik
buat referensi maupun buat smart pointers.
Buat melihat deref coercion beraksi, mari kita gunakan tipe MyBox<T> yang
kita definisikan di Listing 15-8 beserta implementasi Deref yang kita
tambahkan di Listing 15-10. Listing 15-11 menunjukkan definisi dari sebuah
fungsi yang punya parameter berupa string slice.
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {}
hello yang punya parameter name bertipe &strKita bisa memanggil fungsi hello dengan sebuah string slice sebagai
argumennya, misalnya hello("Rust");. Deref coercion memungkinkan kita
buat memanggil hello dengan sebuah referensi ke sebuah nilai bertipe
MyBox<String>, seperti yang ditunjukkan di Listing 15-12.
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
hello dengan referensi ke nilai MyBox<String>, yang mana berhasil berkat deref coercionDi sini kita memanggil fungsi hello dengan argumen &m, yang mana adalah
sebuah referensi ke sebuah nilai MyBox<String>. Karena kita
mengimplementasikan trait Deref pada MyBox<T> di Listing 15-10, Rust
bisa mengubah &MyBox<String> menjadi &String dengan memanggil deref.
Standard library menyediakan sebuah implementasi Deref pada String
yang mengembalikan sebuah string slice, dan ini ada di dokumentasi API
buat Deref. Rust memanggil deref sekali lagi buat mengubah &String
menjadi &str, yang mana cocok sama definisi fungsi hello.
Kalau Rust tidak mengimplementasikan deref coercion, kita harus menulis
kode di Listing 15-13 bukannya kode di Listing 15-12 buat memanggil hello
dengan sebuah nilai bertipe &MyBox<String>.
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
Tanda (*m) men-dereferensi MyBox<String> menjadi sebuah String.
Kemudian tanda & dan [..] mengambil sebuah string slice dari String
tersebut yang setara dengan keseluruhan string-nya agar cocok sama signature
dari hello. Kode tanpa deref coercions ini lebih susah buat dibaca, ditulis,
dan dipahami dengan semua simbol yang terlibat ini. Deref coercion membiarkan
Rust menangani konversi-konversi ini buat kita secara otomatis.
Saat trait Deref didefinisikan buat tipe-tipe yang terlibat, Rust bakal
menganalisis tipe-tipenya dan memakai Deref::deref sebanyak yang
dibutuhkan buat mendapatkan sebuah referensi yang cocok sama tipe
parameternya. Berapa kali Deref::deref perlu disisipkan itu sudah
diselesaikan (resolved) pas compile time, jadi tidak ada hukuman performa
saat runtime karena memanfaatkan deref coercion!
Gimana Deref Coercion Berinteraksi sama Mutabilitas
Sama seperti gimana kita memakai trait Deref buat menimpa operator * pada
referensi immutable, kita juga bisa memakai trait DerefMut buat menimpa
operator * pada referensi mutable.
Rust melakukan deref coercion saat ia menemukan tipe-tipe dan implementasi trait di tiga kasus ini:
- Dari
&Tke&UsaatT: Deref<Target=U> - Dari
&mut Tke&mut UsaatT: DerefMut<Target=U> - Dari
&mut Tke&UsaatT: Deref<Target=U>
Dua kasus pertama itu sama saja kecuali kalau yang kedua mengimplementasikan
mutabilitas. Kasus pertama menyatakan kalau kita punya sebuah &T, dan T
mengimplementasikan Deref ke suatu tipe U, kita bisa mendapatkan sebuah
&U secara transparan. Kasus kedua menyatakan kalau deref coercion yang
sama terjadi buat referensi mutable.
Kasus ketiga itu sedikit lebih tricky: Rust juga bakal me-coerce sebuah referensi mutable menjadi referensi immutable. Tapi kebalikannya itu tidak mungkin: referensi immutable tidak bakal pernah bisa di-coerce menjadi referensi mutable. Karena aturan borrowing, kalau kita punya sebuah referensi mutable, referensi mutable tersebut haruslah menjadi satu-satunya referensi ke data tersebut (kalau tidak, programnya tidak bakal bisa di-compile). Mengonversi satu referensi mutable menjadi satu referensi immutable tidak bakal pernah melanggar aturan borrowing. Mengonversi sebuah referensi immutable menjadi referensi mutable bakal mewajibkan kalau referensi immutable awalnya adalah satu-satunya referensi immutable ke data tersebut, tapi aturan borrowing tidak menjamin hal itu. Oleh karena itu, Rust tidak bisa membuat asumsi kalau mengonversi sebuah referensi immutable menjadi referensi mutable itu mungkin dilakukan.