Topik ini membahas trait, yaitu kontrak perilaku yang mendefinisikan sekumpulan metode yang dapat diimplementasikan oleh tipe-tipe berbeda. Pembahasan mencakup deklarasi trait, implementasinya untuk tipe bentukan sendiri, metode default yang memungkinkan sebagian perilaku dibagikan tanpa penulisan ulang, serta aturan lingkup yang menentukan kapan sebuah metode dapat dipanggil. Setelah membaca topik ini, satu nama metode dapat dipanggil pada banyak tipe dengan perilaku masing-masing, dan error dari implementasi yang tidak lengkap dapat dibaca sampai ke penyebabnya.
Trait sebagai kontrak perilaku#
Trait mendefinisikan perilaku bersama, bukan data. Deklarasinya memuat daftar tanda tangan metode yang harus tersedia pada setiap tipe yang mengimplementasikannya, sedangkan isi setiap metode disediakan oleh tipe yang bersangkutan. Konsepnya menyerupai interface pada banyak bahasa pemrograman lain, tetapi Rust mengizinkan trait memuat isi metode sekaligus, sehingga sebagian perilaku juga dapat dibagikan langsung dari dalam trait.
Deklarasi trait memakai kata kunci trait yang diikuti nama dan daftar metode dalam kurung kurawal. Metode yang ditulis tanpa isi dan ditutup titik koma bersifat wajib, artinya setiap tipe implementor harus menyediakan isi metode tersebut. Contoh berikut mendeklarasikan trait Ringkas dengan satu metode wajib bernama ringkas.
// tidak-dijalankan
trait Ringkas {
fn ringkas(&self) -> String;
}Fragmen di atas tidak dijalankan karena berupa potongan deklarasi, bukan program utuh dengan fn main. Penerima &self pada tanda tangannya berarti metode dipanggil pada instance tipe dengan cara yang sama seperti metode struct pada blok impl. Nilai kembalian String menyatakan hasil ringkasan berupa teks milik sendiri, sehingga hasilnya tidak bergantung pada masa hidup data yang diringkas.
Implementasi trait untuk tipe#
Implementasi ditulis dengan sintaks impl NamaTrait for NamaTipe yang diikuti isi seluruh metode wajibnya. Tanda tangan setiap metode pada blok implementasi harus sama persis dengan yang dideklarasikan trait, mulai dari parameter sampai tipe hasilnya. Program berikut mengimplementasikan Ringkas untuk dua tipe, yaitu Artikel dan Pengumuman, lalu memanggil metode yang sama pada keduanya.
trait Ringkas {
fn ringkas(&self) -> String;
}
struct Artikel {
judul: String,
penulis: String,
isi: String,
}
struct Pengumuman {
penulis: String,
topik: String,
}
impl Ringkas for Artikel {
fn ringkas(&self) -> String {
let mut potongan = self.isi.clone();
potongan.truncate(20);
format!("{} oleh {}: {}", self.judul, self.penulis, potongan)
}
}
impl Ringkas for Pengumuman {
fn ringkas(&self) -> String {
format!("{}: {}", self.penulis, self.topik)
}
}
fn main() {
let artikel = Artikel {
judul: String::from("Rilis Rust Terbaru"),
penulis: String::from("Budi"),
isi: String::from("Rilis kali ini membawa perbaikan pada kompilator."),
};
let pengumuman = Pengumuman {
penulis: String::from("Citra"),
topik: String::from("jadwal piket laboratorium"),
};
println!("1. {}", artikel.ringkas());
println!("2. {}", pengumuman.ringkas());
}1. Rilis Rust Terbaru oleh Budi: Rilis kali ini memba
2. Citra: jadwal piket laboratoriumKedua pemanggilan pada keluaran itu memakai nama metode yang sama, tetapi hasilnya berbeda karena isi metode ditulis pada blok impl masing-masing. Implementasi untuk Artikel membentuk ringkasan dari judul, penulis, dan potongan isi sepanjang dua puluh karakter pertama hasil truncate, sedangkan implementasi untuk Pengumuman cukup menggabungkan penulis dan topiknya. Pemanggil tidak perlu tahu perbedaan tersebut karena kontrak ringkas yang mengembalikan String sudah dipenuhi keduanya.
Satu trait dapat diimplementasikan untuk banyak tipe, termasuk tipe dari pustaka standar apabila aturan implementasi mengizinkannya. Tiap implementasi berdiri sendiri, sehingga menambah tipe baru tidak mengubah trait maupun implementasi yang sudah ada. Sifat inilah yang membuat trait menjadi cara utama Rust berbagi perilaku lintas tipe.
Metode default#
Trait dapat memuat metode yang isinya sudah ditulis langsung pada deklarasinya. Metode semacam itu disebut metode default, dan setiap implementor memakainya apa adanya tanpa perlu menulis ulang. Implementor tetap boleh menimpa (override) metode default tersebut bila perilakunya perlu berbeda, dan isi metode default dapat memanggil metode lain pada trait yang sama meskipun metode yang dipanggil dideklarasikan tanpa isi.
Program berikut menambahkan metode default tampilkan pada trait Ringkas. Isi defaultnya mencetak hasil self.ringkas(), sehingga setiap implementor otomatis memperoleh cara pencetakan tanpa menulis satu baris pun.
trait Ringkas {
fn ringkas(&self) -> String;
fn tampilkan(&self) {
println!("Ringkasan: {}", self.ringkas());
}
}
struct Artikel {
judul: String,
penulis: String,
isi: String,
}
struct Pengumuman {
penulis: String,
topik: String,
}
impl Ringkas for Artikel {
fn ringkas(&self) -> String {
let mut potongan = self.isi.clone();
potongan.truncate(20);
format!("{} oleh {}: {}", self.judul, self.penulis, potongan)
}
}
impl Ringkas for Pengumuman {
fn ringkas(&self) -> String {
format!("{}: {}", self.penulis, self.topik)
}
fn tampilkan(&self) {
println!("PENTING - {}: {}", self.penulis, self.topik);
}
}
fn main() {
let artikel = Artikel {
judul: String::from("Rilis Rust Terbaru"),
penulis: String::from("Budi"),
isi: String::from("Rilis kali ini membawa perbaikan pada kompilator."),
};
let pengumuman = Pengumuman {
penulis: String::from("Citra"),
topik: String::from("jadwal piket laboratorium"),
};
artikel.tampilkan();
pengumuman.tampilkan();
}Ringkasan: Rilis Rust Terbaru oleh Budi: Rilis kali ini memba
PENTING - Citra: jadwal piket laboratoriumBaris pertama keluaran dihasilkan metode default karena Artikel tidak menulis tampilkan. Baris kedua dihasilkan versi yang ditimpa karena Pengumuman mendefinisikan ulang tampilkan dengan format penekanan berbeda. Kombinasi metode wajib dan metode default memungkinkan trait menetapkan inti perilaku yang pasti ada, sekaligus menyediakan perilaku tambahan yang dapat dipakai tanpa biaya penulisan.
Lingkup dan aturan implementasi#
Metode trait hanya dapat dipanggil apabila traitnya berada dalam lingkup pada titik pemanggilan. Pada program satu berkas seperti contoh-contoh di atas, seluruh item berada dalam lingkup yang sama sehingga tidak ada langkah tambahan. Ketika trait didefinisikan pada modul lain atau crate lain, trait harus dibawa ke lingkup dengan use sebelum metodenya dapat dipanggil, meskipun tipe datanya sudah terlihat. Pernyataan use itu sendiri tidak mengimpor implementasi apa pun, melainkan hanya membuat nama trait dan metodenya terlihat.
Rust juga menerapkan aturan koherensi yang sering disebut orphan rule. Implementasi sebuah trait untuk sebuah tipe hanya boleh ditulis pada crate yang mendefinisikan trait itu atau tipe itu. Aturan ini mencegah dua crate berbeda mengimplementasikan trait yang sama untuk tipe yang sama dengan isi berbeda, sehingga pemanggil tidak pernah menghadapi dua implementasi yang saling bertentangan. Akibat praktisnya, implementasi untuk tipe pustaka standar dengan trait buatan sendiri tetap sah, sedangkan trait asing untuk tipe asing ditolak kompilator.
Kesalahan umum#
Kesalahan yang paling sering muncul adalah blok implementasi yang lupa memuat salah satu metode wajib. Kompilator menolaknya dengan error E0046 sekaligus menyebutkan nama metode yang hilang. Contoh berikut memperlihatkan pesan aslinya.
export PATH="$HOME/.cargo/bin:$PATH"
# contoh implementasi yang belum memuat seluruh metode wajib
printf 'trait Berat {\n fn berat(&self) -> f64;\n}\n\nstruct Keping {\n gram: f64,\n}\n\nimpl Berat for Keping {\n}\n\nfn main() {\n let keping = Keping { gram: 2.5 };\n println!("Berat keping: {} gram", keping.gram);\n}\n' > belum-lengkap.rs
rustc --edition=2021 belum-lengkap.rs 2>&1 | sed -n '1,8p'error[E0046]: not all trait items implemented, missing: `berat`
--> belum-lengkap.rs:9:1
|
2 | fn berat(&self) -> f64;
| ----------------------- `berat` from trait
...
9 | impl Berat for Keping {
| ^^^^^^^^^^^^^^^^^^^^^ missing `berat` in implementationPerbaikannya langsung terbaca dari pesan tersebut, yaitu menambahkan fn berat(&self) -> f64 beserta isinya ke dalam blok impl Berat for Keping. Kesalahan kedua yang sering terjadi adalah tanda tangan metode pada implementasi yang berbeda dari deklarasinya, misalnya parameter atau tipe hasil tidak sama, dan kompilator menolaknya dengan error E0053. Kesalahan ketiga adalah memanggil metode trait ketika traitnya belum dibawa ke lingkup dengan use, yang muncul sebagai error E0599 dengan saran menambahkan use untuk trait yang dimaksud. Ketiganya ditolak saat kompilasi, sehingga program yang berhasil dikompilasi dijamin memenuhi seluruh kontrak trait yang dipakainya.
Ringkasan#
- Trait mendefinisikan kontrak perilaku berupa daftar metode, dan setiap tipe implementor menyediakan isinya melalui blok
impl NamaTrait for NamaTipe. - Metode tanpa isi bersifat wajib, sedangkan metode default dapat dipakai apa adanya atau ditimpa oleh implementor.
- Metode trait hanya dapat dipanggil bila trait berada dalam lingkup, dan aturan koherensi membatasi implementasi pada crate pemilik tipe atau traitnya.
- Implementasi yang tidak lengkap ditolak dengan error E0046 yang menyebutkan nama metode yang hilang.
Referensi#
- Traits: Defining Shared Behavior — The Rust Programming Language — Bab resmi tentang definisi trait, implementasi untuk tipe, metode default, dan trait sebagai parameter fungsi.
- Error code E0046 — Rust compiler — Penjelasan resmi untuk error implementasi trait yang tidak memuat seluruh itemnya.