Bahasa Pemrograman Rust: Smart Pointers - Trait Deref
Sebuah smart pointer adalah sebuah tipe yang mengimplementasikan trait Deref
dan trait Drop
. Di artikel kali ini, kita akan membahas tentang trait Deref
yang membuat kita dapat memperlakukan sebuah pointer seperti sebuah reference biasa. Lalu apa maksud dari memperlakukan sebuah pointer seperti sebuah reference biasa?
Sebelum itu, mari kita membahas lebih lanjut tentang Dereferencing.
Dereferencing
Dereferencing adalah sebuah cara untuk mengakses nilai dari sebuah lokasi memori yang ditunjuk oleh sebuah pointer. Pada Rust, seperti dalam bahasa seperti C++, kita menggunakan operator *
untuk dereferencing.
Sekarang, mari kita lihat kode berikut
1
2
3
4
5
6
7
fn main() {
let a = 10;
let b = &a;
assert_eq!(10, a);
assert_eq!(10, b);
}
Kita menggunakan macro assert_eq!
pada kode diatas untuk mengecek apakah sebuah nilai setara (equal) dengan nilai yang lainnya. Namun, hal yang akan terjadi saat kita compile kode diatas adalah compile error yakni sebagai berikut:
1
2
3
4
5
6
7
error[E0277]: can't compare `{integer}` with `&{integer}`
--> deref.rs:7:2
|
7 | assert_eq!(10, b);
| ^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
Error diatas menyatakan kalau kita tidak bisa membandingkan sebuah integer
dengan sebuah reference kepada sebuah integer
. Mereka adalah tipe yang berbeda sehingga kita harus menggunakan dereference operator.
Dereference operator menggunakan tanda asterisk (*
)
1
2
3
4
5
6
7
fn main() {
let a = 10;
let b = &a;
assert_eq!(10, a);
assert_eq!(10, *b);
}
Pada Rust, sebuah reference (&
) sebenarnya juga merupakan pointer. Jadi, variabel b
diatas adalah sebuah pointer yang menyimpan alamat memory a
dan menunjuk kepada dimana valuenya, 10
disimpan.
Pada assertion pertama, kita membandingkan 10 dan a
, yang hasilnya adalah benar. Pada assertion kedua, kita membandingkan 10 dan b
yang sudah kita dereference sehingga b
disana merupakan value
yang ia tunjuk, yaitu 10. Kode akan berjalan dengan baik.
Trait Deref
Memperlakukan smart pointer seperti sebuah reference biasa??
Untuk contoh yang lebih lanjut, kita akan mengganti kode diatas dan menggunakan sebuah smart pointer daripada sebuah reference. Kita akan menggunakan Box<T>
.
1
2
3
4
5
6
7
fn main() {
let a = 10;
let b = Box::new(a);
assert_eq!(10, a);
assert_eq!(10, *b);
}
Seperti reference, Box
juga menunjuk kepada nilai yang disimpan di suatu tempat di memori, yang dalam hal ini adalah 10
. Perbedaannya disini adalah b
menunjuk pada sebuah copy dari 10
karena value tipe primitif akan di-copy, bukan di-move ownershipnya.
Box<T>
merupakan sebuah smart pointer yang mengimplementasikan trait Deref
. Inilah yang dimaksud dengan memperlakukan sebuah pointer seperti sebuah reference biasa. Trait Deref
memperbolehkan dereference operator bekerja pada Box<T>
sama seperti ia bekerja pada reference biasa.
Untuk mengerti bagaimana itu bekerja, kita akan mendefinisikan sebuah smart pointer kita sendiri yang akan mengimplementasikan Deref
.
Mendefinisikan smart pointer kita sendiri
Kita akan mendefiniskan sebuah smart pointer yang serupa dengan Box<T>
. Hanya saja, disini kita tidak akan menyimpan nilai pada heap. Disini kita akan berfokus pada dereference operator, bukan dimana lokasi data disimpan.
Sekarang, mari kita langsung definisikan.
1
2
3
4
5
6
7
struct Kotak<T>(T);
impl<T> Kotak<T> {
fn new(x: T) -> Self {
Self(x)
}
}
Sekarang, kita ganti Box<T>
di fungsi main
dengan Kotak<T>
kita.
1
2
3
4
5
6
7
fn main() {
let a = 10;
let b = Kotak::new(a);
assert_eq!(10, a);
assert_eq!(10, *b);
}
Namun pada kode diatas, kita akan mendapatkan error dimana kita tidak bisa melakukan dereference pada tipe Kotak
kita. Sekarang, mari kita implementasikan trait Deref
.
Mengimplementasikan Deref
pada smart pointer kita
Pertama-tama. panggil trait Deref
di baris paling atas kode.
1
use std::ops::Deref;
Lalu kita implementasikan pada Kotak<T>
.
1
2
3
4
5
6
7
impl<T> Deref for Kotak<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
Kalian tidak perlu terlalu mengkhawatirkan type Target = T
untuk sekarang. Itu adalah sebuah associated type yang akan kita bahas di lain waktu. Kalian juga bisa mengganti return type method deref
menjadi hanya &T
.
Trait Deref
mengharuskan kita untuk mengimplementasikan satu method bernama deref
yang menerima &self
, dan mengembalikan sebuah reference kepada inner data atau data didalam struct kita. Ingat, kita disini memakai tuple struct yang menggunakan index 0
, 1
, dan seterusnya untuk mengambil inner data.
Sekarang, kode kita akan terlihat seperti ini
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::ops::Deref;
struct Kotak<T>(T);
impl<T> Kotak<T> {
fn new(x: T) -> Self {
Self(x)
}
}
impl<T> Deref for Kotak<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
fn main() {
let a = 10;
let b = Kotak::new(a);
assert_eq!(10, a);
assert_eq!(10, *b);
}
Dan assertion kedua kita akan berhasil. Kita akan dapat melakukan dereference pada tipe Kotak
kita. Kode akan dapat kita compile.
Tanpa trait Deref
, compiler hanya mengetahui cara dereference reference saja. Trait Deref
membuat compiler Rust untuk memanggil method deref
untuk semua tipe yang mengimplementasikan trait Deref
untuk mendapatkan sebuah reference kepada sebuah nilai (Self::Target
atau &T
kita diatas), yang si compiler tahu bagaimana cara dereferencenya.
Saat kita menggunakan operator dereference kepada sebuah nilai yang telah mengimplementasikan trait Deref
, assertion kedua kita diatas contohnya, sebenarnya Rust memanggil kode seperti berikut:
1
assert_eq!(10, *(b.deref()));
Rust akan memanggil method deref
terlebih dahulu untuk mendapatkan reference kepada nilai kita, yang pada kasus diatas adalah 10
, lalu melakukan dereferencing dengan operator dereference sehingga kira-kira hal yang terjadi adalah berikut:
1
Kotak(10) -> deref() terpanggil -> &10 -> dereference operator digunakan -> 10
Karena Rust melakukan hal itu secara otomatis, kita tidak perlu memikirkan perlu atau tidaknya memanggil method deref
secara eksplisit sehingga kita bisa memperlakukan reference biasa, dan sebuah tipe yang mengimplementasikan trait Deref
dengan sama.
Lalu kenapa method deref
mengembalikan reference kepada suatu nilai bukan nilainya itu sendiri?
Tentunya itu berhubungan dengan ownership pada Rust. Kalau deref
mengembalikan nilainya secara langsung, maka ownership dari nilai tersebut akan di-move keluar dari tipe kita, yang dalam kasus ini, smart pointer kita Kotak<T>
. Dan di banyak kasus saat kita menggunakan operator dereference, kita tidak mau itu terjadi.
Deref Coercion
Deref Coercion adalah sebuah fitur sangat praktis yang Rust akan secara otomatis gunakan pada dan hanya pada tipe yang mengimplementasikan trait Deref
ketika tipe tersebut dijadikan argumen untuk fungsi atau method. Deref Coercion akan mengubah sebuah reference dari satu tipe kepada sebuah reference dari tipe yang berbeda.
Untuk lebih jelasnya, mari kita lanjutkan kode diatas dengan menambahkan sebuah prosedur untuk mencetak &str
.
1
2
3
4
5
6
7
8
9
10
11
fn main() {
let a = 10;
let b = Kotak::new(a);
assert_eq!(10, a);
assert_eq!(10, *b);
}
fn prosedur(a: &str) {
println!("A adalah: {}", a);
}
Sekarang, kita akan membuat sebuah variabel baru yang menggunakan smart pointer Kotak<T>
kita dan sebuah String
untuk nilai didalam Kotak<T>
kita, lalu kita panggil prosedur kita dengan variabel tersebut sebagai argumen.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
let a = 10;
let b = Kotak::new(a);
assert_eq!(10, a);
assert_eq!(10, *b);
let c = Kotak::new(String::from("Hai"));
prosedur(&c);
}
fn prosedur(a: &str) {
println!("A adalah: {}", a);
}
Seperti yang kalian lihat, prosedur prosedur
menerima &str
sebagai argumen. Diatas, kita memberikannya sebuah reference kepada Kotak<T>
yang memiliki String
didalamnya. Namun, kode diatas TIDAK AKAN error! Kode diatas akan berjalan dengan sempurna.
Apa yang terjadi disini?
Inilah yang terjadi:
Saat kita memakai operator reference di argumen prosedur
pada variabel c
, method deref
akan terpanggil dan kita akan mendapatkan sebuah reference kepada String
, nilai yang kita wrap dalam Kotak<T>
.
1
&Kotak<String> -> &String
Lalu, String
juga mengimplementasikan trait Deref
. Bila kita memanggil deref
, String
akan mengembalikan sebuah &str
sehingga hal yang akan terjadi berikutnya adalah:
1
&Kotak<String> -> &String -> &str
Rust melakukannya dengan otomatis. Tanpa Deref Coercion, bila ingin melakukan hal seperti diatas, kita harus menuliskannya seperti ini:
1
prosedur(&(*c)[..]);
Disana kita melakukan dereference pada c
sehingga kita mendapat sebuah String
, kemudian &
dan [..]
(slice yang berisi operator RangeFull
) akan membuat sebuah &str
dari si String
yang setara dengan panjang penuh (full range) si String
. Sangat merepotkan bukan? Kode akan lebih sulit ditulis dan lebih sulit dibaca. Terima kasih Deref Coercion!
Untuk mutable reference, kita harus menggunakan trait DerefMut
.
Rust melakukan Deref Coercion bila ia bertemu tipe dan implementasi trait dalam tiga kasus:
- Dari
&T
ke&U
ketikaT: Deref<Target=U>
- Dari
&mut T
ke&mut U
ketikaT: DerefMut<Target=U>
- Dari
&mut T
ke&U
ketikaT: Deref<Target=U>
Terima Kasih telah membaca, sampai bertemu di artikel selanjutnya! :)