A.36. Traits
Trait jika diartikan dalam Bahasa Indonesia artinya adalah sifat. Chapter ini akan membahas tentang apa itu trait, apa kegunaannya, dan bagaimana cara penerapannya di Rust programming.
A.36.1. Konsep traits
Di Rust kita bisa mendefinisikan trait/sifat, isinya adalah definisi header method yang bisa di-share ke banyak tipe data.
Trait isinya hanya definisi header method (bisa diartikan method tanpa isi). Ketika ada tipe data yang meng-implement suatu trait, maka tipe tersebut wajib untuk menuliskan implementasi method sesuai dengan header method yang ada di trait.
Pada bahasa pemrograman lain, contohnya Java, konsep trait mirip dengan
interface
Ada dua bagian penting dalam trait yang harus diketahui:
- Deklarasi trait
- Implementasi trait ke tipe data
Perihal point pertama, intinya kita bisa menciptakan trait sesuai kebutuhan. Terlepas dari itu, Rust juga menyediakan cukup banyak traits yang diimplement ke banyak tipe data yang ada di Rust standard library. Beberapa di antaranya:
- Trait
std::fmt::Debug, digunakan agar data bisa di-print menggunakan formatted print{:?}. - Trait
std::iter::Iterator, digunakan untuk operasi iterasi data. - Trait
std::ops::Add, diimplementasikan agar data bisa digunakan pada operasi aritmatik penambahan+.
Ok, biar lebih jelas, mari lanjut pembelajaran menggunakan contoh. Kita mulai dengan pembahasan tentang cara implementasi trait. Contoh yang digunakan adalah implementasi salah satu trait milik Rust standard library, yaitu trait std::fmt::Debug.
◉ Jenis traits berdasarkan tempat dideklarasikannya
Berdasarkan tempat dimana traits dibuat, ada 2 jenis traits:
External traits (atau foreign traits).
Yaitu traits yang tempat dideklarasikannya berada di luar crate kode yang ditulis. Misalnya, trait
std::fmt::Debugdanstd::ops::Add, keduanya merupakan external traits yang berada di cratestdatau crate Rust Standard Library.Pada kasus seperti ini, kita biasanya hanya fokus ke cara memakai external traits yang sudah ada.
Local traits.
Adalah traits yang kita ciptakan di crate yang berada di dalam package/project yang sedang kita kerjakan.
Chapter ini fokusnya adalah pembahasan tentang dasar implementasi external traits dan cara kerjanya. Setelah itu, kita juga akan lihat sedikit contoh local trait supaya bedanya lebih terasa.
◉ Aturan penting dalam implementasi trait
Di Rust ada aturan yang perlu diingat:
external traitboleh diimplementasikan kelocal typelocal traitboleh diimplementasikan ke tipe apa punexternal traittidak boleh diimplementasikan keexternal type
Aturan ini biasanya disebut orphan rule atau coherence rule.
Kalau dibahas dengan bahasa sederhana, Rust ingin mencegah dua crate berbeda saling berebut implementasi untuk trait dan type yang sama. Dengan begitu, perilaku program tetap jelas dan tidak ambigu.
Kalau kita tetap ingin memakai external trait pada external type, solusinya adalah memakai wrapper pattern. Caranya, kita bungkus type external tersebut ke type local buatan kita sendiri, lalu implementasikan trait ke wrapper itu.
Contoh sederhananya:
struct Wrapper(Vec<String>);
impl std::fmt::Display for Wrapper {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.0)
}
}
Pada contoh di atas:
Vec<String>adalahexternal typeDisplayadalahexternal traitWrapperadalahlocal type
Karena Wrapper adalah type lokal, kita bebas mengimplementasikan trait ke sana.
Pemakaiannya di main bisa seperti ini:
fn main() {
let v = Wrapper(vec![String::from("a"), String::from("b")]);
println!("{}", v); // output ➜ ["a", "b"]
}
◉ Contoh local trait
Sekarang kita coba kebalikannya: membuat trait sendiri, lalu mengimplementasikannya ke type yang sudah ada.
Misalnya kita punya trait sederhana bernama Message:
trait Message {
fn log(&self);
}
Lalu kita implementasikan ke String:
impl Message for String {
fn log(&self) {
println!("{}", self);
}
}
Karena Message adalah local trait, kita boleh mengimplementasikannya ke type String yang berasal dari standard library.
Pemakaiannya di main akan terlihat seperti ini:
fn main() {
let s = String::from("hello");
s.log(); // output ➜ hello
}
A.36.2. Implementasi trait
Kita pilih trait std::fmt::Debug milik Rust standard library untuk belajar cara implementasi trait pada tipe data.
Kegunaan dari trait ini adalah: jika diimplement ke tipe data tertentu maka data dengan tipe tersebut bisa di-print via macro println atau macro printing lainnya, dengan menggunakan formatted print {:?}.
Trait Debug ini diimplementasikan pada banyak tipe data yang di Rust standard library, baik itu tipe primitif maupun non-primitif. Contohnya bisa dilihat pada kode berikut:
let number = 12;
println!("{:?}", number);
let text = String::from("hello");
println!("{:?}", text);
Dua variabel di atas sukses di-print tanpa error, karena tipe data i32 dan String by default sudah implement trait std::fmt::Debug.
Jika tertarik untuk pengecekan lebih lanjut, silakan lihat di halaman dokumentasi tipe data i32 dan String.
Bagaimana dengan custom type yang kita buat sendiri? Misalnya struct.
fn main() {
let circle_one = Circle{radius: 6};
println!("{:?}", circle_one);
}
struct Circle {
radius: i32,
}

Hasilnya error, karena struct Circle yang dibuat tidak implement trait std::fmt::Debug.
Solusi agar tidak error adalah dengan mengimplementasikan trait std::fmt::Debug ke tipe Circle, dengan begitu semua data bertipe Circle akan bisa di-print menggunakan formatted print {:?}.
Selain via implementasi trait, tipe data custom bisa di-print dengan cara menambahkan atribut
#[derive(Debug)]pada definisi tipe data-nya. Namun kita tidak membahas itu pada chapter ini.
Langkah pertama untuk implementasi trait adalah mencari tau terlebih dahulu spesifikasi trait yang ingin diimplementasikan. Trait std::fmt::Debug adalah traits milik Rust standard library, maka harusnya spesifikasi bisa dilihat di dokumentasi Rust.
Pada URL dokumentasi bisa dilihat kalau trait Debug memiliki struktur kurang lebih seperti berikut:
pub trait Debug {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}
Trait Debug mempunyai satu spesifikasi method, bernama fmt yang detail strukturnya bisa dilihat di atas.
Kita akan implement trait Debug ini ke tipe Circle, maka wajib untuk menuliskan implementasi method sesuai dengan yang ada di trait Debug.
Di bawah ini adalah contoh cara implementasi trait.
struct Circle {
radius: i32,
}
impl std::fmt::Debug for Circle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Circle radius: {}", self.radius)
}
}
fn main() {
let circle_one = Circle{radius: 6};
println!("{:?}", circle_one);
}
Ketika program di-run, hasilnya sukses tanpa error. Artinya implementasi trait Debug pada tipe data struct Circle adalah sukses.
Cara implementasi trait ke struct Circle memang step-nya agak panjang, tapi penulis yakin lama-kelamaan pasti terbiasa. Ok, sekarang kita bahas satu per satu kode di atas.
◉ Struct Circle
Block kode definisi struct Circle cukup straightforward, isinya hanya 1 property bernama radius bertipe i32.
◉ Block kode impl X for Y
Notasi penulisan implementasi trait adalah impl X for Y, yang mana X adalah trait yang ingin diimplementasikan dan Y adalah tipe data tujuan implementasi.
Pada contoh di atas, trait Debug diimplementasikan ke custom type struct Circle. Maka statement-nya adalah:
impl std::fmt::Debug for Circle {
// ...
}
◉ Block kode method dalam impl
Block kode impl harus diikuti dengan implementasi method. Pada contoh ini, method fmt milik trait Debug wajib untuk diimplementasikan. Spesifikasi method ini adalah fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> (lebih jelasnya silakan lihat dokumentasi).
Silakan copy method tersebut kemudian paste ke dalam block kode impl yang sudah ditulis, kemudian tambahkan block kurung kurawal.
impl std::fmt::Debug for Circle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// ...
}
}
Kemudian tulis implementasi method fmt dalam block method. Di sini kita memakai macro write untuk menulis data string yang ingin di-print ke variabel f.
Di contoh, format Circle radius: {} digunakan. Dengan ini nantinya saat printing data, yang muncul adalah text Circle radius: {}.
impl std::fmt::Debug for Circle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Circle radius: {}", self.radius)
}
}
Tips untuk pengguna visual studio code dengan rust-analyzer extension ter-install: setelah selesai menulis block kode
impl, cukup jalankanctrl+spaceataucmd+spaceuntuk men-trigger autocomplete suggestion. Kemudian klik opsi method yang ada di situ, maka kode implementasi method langsung muncul dengan sendirinya.
◉ Macro write
Macro ini digunakan untuk menuliskan sebuah data ke object tertentu. Pada contoh kita gunakan untuk menulis string Circle radius: {} ke variabel f yang bertipe std::fmt::Formatter<'_>.
Notasi penulisan macro write:
// notasi penulisan
write!(variabel_tujuan, data_yang_ingin_di_print, arg1, arg2, ...);
// contoh penerapan
write!(f, "Circle radius: {}", self.radius);
◉ Print data menggunakan formatted print {:?}
Step terakhir adalah print variabel circle menggunakan macro println. Hasilnya sukses, tidak error seperti sebelumnya.
◉ Print data menggunakan formatted print {}
Coba tambahkan statement println, tetapi kali ini gunakan formatted print {}, apakah hasilnya juga tidak error?

Hasilnya error, karena trait std::fmt::Debug hanya berguna untuk formatted print {:?}. Kalau ingin Circle bisa di-print dengan {}, maka trait std::fmt::Display juga harus diimplementasikan.
Ubah kode dengan menambahkan implementasi trait Display. Hasilnya kurang lebih seperti ini:
struct Circle {
radius: i32,
}
impl std::fmt::Debug for Circle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Circle radius: {}", self.radius)
}
}
impl std::fmt::Display for Circle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Circle radius: {}", self.radius)
}
}
- Link dokumentasi trait
Debughttps://doc.rust-lang.org/std/fmt/trait.Debug.html- Link dokumentasi trait
Displayhttps://doc.rust-lang.org/std/fmt/trait.Display.html
Lebih detail tentang trait bound, wrapper pattern, dan aturan implementasi trait untuk type lokal maupun external ada di chapter Traits ➜ Advanced.
A.36.3. Default implementation pada trait method
Trait di Rust memungkinkan kita menyediakan default implementation pada method. Artinya, method tersebut sudah memiliki body langsung di dalam definisi trait, sehingga tipe data yang meng-implement trait tidak wajib menuliskan implementasi method tersebut. Tipe data tersebut bisa memakai implementasi default atau menggantinya dengan implementasi sendiri.
◉ Contoh trait dengan default method
trait Speak {
fn greet(&self) {
println!("Hello from default implementation!");
}
fn introduce(&self); // tanpa default, wajib diimplementasikan
}
Pada trait Speak di atas:
- Method
greet()memiliki default implementation. Tipe data yang meng-implement trait ini tidak wajib menuliskan implementasigreet(), karena bisa langsung menggunakan implementasi default yang sudah tersedia. - Method
introduce()tidak memiliki default implementation, sehingga wajib diimplementasikan oleh tipe data yang meng-implement trait.
◉ Implementasi trait dengan default method
struct Person {
name: String,
}
impl Speak for Person {
// greet() tidak diimplementasikan → pakai default
fn introduce(&self) {
println!("My name is {}", self.name);
}
}
fn main() {
let p = Person { name: String::from("Alice") };
p.greet(); // output ➜ Hello from default implementation!
p.introduce(); // output ➜ My name is Alice
}
Pada contoh di atas, Person hanya mengimplementasikan introduce(). Method greet() tidak ditulis di impl, sehingga otomatis menggunakan default implementation dari trait.
◉ Override default implementation
Jika kita ingin perilaku greet() yang berbeda untuk Person, kita bisa override default implementation-nya:
impl Speak for Person {
fn greet(&self) {
println!("Hi, I'm {}!", self.name);
}
fn introduce(&self) {
println!("My name is {}", self.name);
}
}
fn main() {
let p = Person { name: String::from("Alice") };
p.greet(); // output ➜ Hi, I'm Alice!
p.introduce(); // output ➜ My name is Alice
}
Perhatikan bahwa setelah di-override, method greet() sekarang menggunakan implementasi custom, bukan default lagi.
◉ Kapan menggunakan default implementation?
Default implementation berguna ketika:
- Sebagian besar tipe data memiliki perilaku yang sama untuk method tertentu.
- Kita ingin memberikan "fallback" behavior yang bisa di-override jika diperlukan.
- Kita menambahkan method baru ke trait yang sudah ada tanpa mem-break existing implementations (ini disebut non-breaking change).
Catatan chapter 📑
◉ Source code praktik
github.com/novalagung/dasarpemrogramanrust-example/../traits◉ Work in progress
- Pembahasan tentang trait bounds untuk implementasi method kondisional
- Pembahasan tentang trait overloading