Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Filename: src/main.rs
fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-6: Memakai operator dereferensi buat mengikuti sebuah referensi ke nilai i32

Variabel 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.

Filename: src/main.rs
fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-7: Memakai operator dereferensi pada sebuah 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 dengan Box<T> yang asli: versi kita ini tidak bakal menyimpan datanya di heap. Kita memfokuskan contoh ini pada Deref, 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>.

Filename: src/main.rs
struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {}
Listing 15-8: Mendefinisikan sebuah tipe 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.

Filename: src/main.rs
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);
}
Listing 15-9: Mencoba memakai 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>.

Filename: src/main.rs
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);
}
Listing 15-10: Mengimplementasikan 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.

Filename: src/main.rs
fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {}
Listing 15-11: Sebuah fungsi hello yang punya parameter name bertipe &str

Kita 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.

Filename: src/main.rs
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);
}
Listing 15-12: Memanggil hello dengan referensi ke nilai MyBox<String>, yang mana berhasil berkat deref coercion

Di 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>.

Filename: src/main.rs
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)[..]);
}
Listing 15-13: Kode yang harus kita tulis kalau seandainya Rust tidak punya deref coercion

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:

  1. Dari &T ke &U saat T: Deref<Target=U>
  2. Dari &mut T ke &mut U saat T: DerefMut<Target=U>
  3. Dari &mut T ke &U saat T: 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.