Trait #
Trait adalah mekanisme abstraksi utama di Rust — setara dengan interface di Java atau C#, tapi dengan beberapa perbedaan penting. Trait di Rust bisa ditambahkan ke tipe yang sudah ada (termasuk tipe primitif), bisa punya method dengan implementasi default, dan bisa membawa associated type yang ditentukan oleh implementor. Yang lebih penting: trait di Rust mendukung dua model pengiriman — static dispatch via generik yang nol overhead, dan dynamic dispatch via dyn Trait yang lebih fleksibel tapi punya biaya indirection. Memilih yang tepat antara keduanya adalah keputusan desain yang penting. Artikel ini membahas semua dimensi trait dari dasar hingga pola lanjutan yang sering muncul di kode Rust produksi.
Definisi dan Implementasi Dasar #
Trait mendefinisikan kontrak perilaku — kumpulan method signature yang harus diimplementasikan oleh tipe manapun yang mengaku mengikuti kontrak tersebut:
// Definisi trait — hanya signature, tanpa body (kecuali default method)
trait Ringkasan {
fn ringkas(&self) -> String;
fn penulis(&self) -> String;
}
struct ArtikelBerita {
judul: String,
penulis: String,
isi: String,
}
struct TwitSosialMedia {
akun: String,
konten: String,
reply: u32,
}
// Implementasi trait untuk ArtikelBerita
impl Ringkasan for ArtikelBerita {
fn ringkas(&self) -> String {
format!("{}, oleh {}", self.judul, self.penulis)
}
fn penulis(&self) -> String {
self.penulis.clone()
}
}
// Implementasi trait yang sama untuk TwitSosialMedia
impl Ringkasan for TwitSosialMedia {
fn ringkas(&self) -> String {
format!("{}: {}", self.akun, self.konten)
}
fn penulis(&self) -> String {
self.akun.clone()
}
}
fn main() {
let artikel = ArtikelBerita {
judul: String::from("Rust 2024 Edition Dirilis"),
penulis: String::from("Tim Rust"),
isi: String::from("..."),
};
let twit = TwitSosialMedia {
akun: String::from("@rustlang"),
konten: String::from("Exciting news!"),
reply: 42,
};
println!("{}", artikel.ringkas());
println!("{}", twit.ringkas());
}
Default Method #
Trait bisa menyediakan implementasi default untuk method-nya. Implementor bebas menggunakannya apa adanya atau meng-override dengan implementasi spesifik:
trait Ringkasan {
fn ringkas(&self) -> String;
// Default method — menggunakan method lain dalam trait
fn pratinjau(&self) -> String {
format!("Baca selengkapnya: {}...", &self.ringkas()[..50.min(self.ringkas().len())])
}
// Default method dengan implementasi mandiri
fn label(&self) -> String {
String::from("[Konten]")
}
}
struct ArtikelBerita {
judul: String,
penulis: String,
}
impl Ringkasan for ArtikelBerita {
fn ringkas(&self) -> String {
format!("{} - {}", self.judul, self.penulis)
}
// Override label — tidak menggunakan default
fn label(&self) -> String {
String::from("[Artikel]")
}
// pratinjau tidak di-override — menggunakan implementasi default
}
struct PodcastEpisode {
judul: String,
durasi_menit: u32,
}
impl Ringkasan for PodcastEpisode {
fn ringkas(&self) -> String {
format!("{} ({} menit)", self.judul, self.durasi_menit)
}
// Gunakan semua default method
}
fn main() {
let artikel = ArtikelBerita {
judul: String::from("Pembaruan Rust"),
penulis: String::from("Tim Inti"),
};
let podcast = PodcastEpisode {
judul: String::from("Episode Rust ke-42"),
durasi_menit: 45,
};
println!("{}", artikel.label()); // [Artikel] — overridden
println!("{}", podcast.label()); // [Konten] — default
println!("{}", artikel.pratinjau()); // default method pakai ringkas()
}
Associated Type #
Associated type adalah cara trait mendefinisikan tipe yang akan ditentukan oleh setiap implementor. Berbeda dari generik, associated type hanya punya satu nilai per implementasi — lebih bersih untuk digunakan sebagai constraint:
// Dengan associated type — lebih bersih
trait Konversi {
type Output; // tipe yang ditentukan implementor
fn konversi(&self) -> Self::Output;
}
struct Celsius(f64);
struct Fahrenheit(f64);
impl Konversi for Celsius {
type Output = Fahrenheit;
fn konversi(&self) -> Fahrenheit {
Fahrenheit(self.0 * 9.0 / 5.0 + 32.0)
}
}
impl Konversi for Fahrenheit {
type Output = Celsius;
fn konversi(&self) -> Celsius {
Celsius((self.0 - 32.0) * 5.0 / 9.0)
}
}
// Associated type di trait Iterator (contoh dari standard library)
// trait Iterator {
// type Item;
// fn next(&mut self) -> Option<Self::Item>;
// }
fn main() {
let titik_beku = Celsius(0.0);
let dalam_f = titik_beku.konversi();
println!("0°C = {}°F", dalam_f.0); // 32°F
let tubuh = Fahrenheit(98.6);
let dalam_c = tubuh.konversi();
println!("98.6°F = {:.1}°C", dalam_c.0); // 37.0°C
}
Trait Bound — Generik dengan Constraint #
Trait bound memungkinkan fungsi generik menerima tipe apapun selama ia mengimplementasikan trait tertentu. Compiler menghasilkan kode spesifik per tipe (monomorphization) — tidak ada overhead runtime.
Sintaks Inline dan where Clause
#
use std::fmt::{Debug, Display};
// Sintaks inline — mudah untuk satu atau dua constraint
fn cetak_info<T: Display + Debug>(nilai: &T) {
println!("Display: {}", nilai);
println!("Debug: {:?}", nilai);
}
// Where clause — lebih mudah dibaca untuk constraint yang panjang
fn proses_dan_cetak<T, U>(t: &T, u: &U) -> String
where
T: Display + Clone,
U: Debug + PartialOrd,
{
format!("T={}, U={:?}", t, u)
}
// impl Trait sebagai parameter — singkatan untuk satu trait bound
fn notifikasi(item: &impl Display) {
println!("Notifikasi: {}", item);
}
// Setara dengan:
fn notifikasi_generik<T: Display>(item: &T) {
println!("Notifikasi: {}", item);
}
fn main() {
cetak_info(&42);
cetak_info(&"halo");
let hasil = proses_dan_cetak(&"teks", &vec![1, 2, 3]);
println!("{}", hasil);
notifikasi(&"pesan penting");
notifikasi(&3.14);
}
Multiple Trait Bounds #
use std::fmt::{Display, Debug};
// Fungsi yang butuh tipe bisa dibandingkan, ditampilkan, dan di-debug
fn terbesar_dan_cetak<T>(daftar: &[T]) -> &T
where
T: PartialOrd + Display + Debug,
{
assert!(!daftar.is_empty(), "Daftar tidak boleh kosong");
let mut terbesar = &daftar[0];
for item in daftar {
if item > terbesar {
terbesar = item;
}
}
println!("Semua nilai: {:?}", daftar);
println!("Terbesar: {}", terbesar);
terbesar
}
fn main() {
let angka = vec![34, 50, 25, 100, 65];
terbesar_dan_cetak(&angka);
let huruf = vec!['y', 'm', 'a', 'q'];
terbesar_dan_cetak(&huruf);
}
impl Trait vs dyn Trait
#
Ini adalah salah satu keputusan desain paling penting saat bekerja dengan trait. Keduanya memungkinkan kode bekerja dengan berbagai tipe, tapi dengan mekanisme yang berbeda:
flowchart TD
Q{Tipe konkret diketahui\nsaat kompilasi?}
Q -- Ya --> A["impl Trait / Generik\nStatic dispatch\nZero overhead\nKode di-inline per tipe"]
Q -- Tidak --> B["dyn Trait\nDynamic dispatch\nAda overhead vtable\nTipe bisa berbeda-beda\ndi runtime"]
A --> C{Perlu return\nberbagai tipe berbeda\ndari satu fungsi?}
C -- Ya --> B
C -- Tidak --> AStatic Dispatch dengan impl Trait
#
trait Gambar {
fn gambar(&self) -> String;
fn luas(&self) -> f64;
}
struct Lingkaran { radius: f64 }
struct Persegi { sisi: f64 }
impl Gambar for Lingkaran {
fn gambar(&self) -> String { format!("○ r={}", self.radius) }
fn luas(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
}
impl Gambar for Persegi {
fn gambar(&self) -> String { format!("□ s={}", self.sisi) }
fn luas(&self) -> f64 { self.sisi * self.sisi }
}
// impl Trait sebagai parameter — static dispatch
// Compiler membuat versi berbeda untuk Lingkaran dan Persegi
fn cetak_gambar(bentuk: &impl Gambar) {
println!("{} — Luas: {:.2}", bentuk.gambar(), bentuk.luas());
}
// impl Trait sebagai return — tipe konkret tersembunyi, tapi tetap satu tipe
fn buat_bentuk_default() -> impl Gambar {
Lingkaran { radius: 1.0 }
// Hanya bisa return satu tipe konkret — tidak bisa pilih antara
// Lingkaran dan Persegi berdasarkan kondisi runtime
}
fn main() {
let l = Lingkaran { radius: 5.0 };
let p = Persegi { sisi: 4.0 };
cetak_gambar(&l); // static dispatch
cetak_gambar(&p); // static dispatch — versi berbeda di-generate compiler
}
Dynamic Dispatch dengan dyn Trait
#
// dyn Trait — diperlukan ketika tipe berbeda di runtime
fn cetak_semua(bentuk_list: &[Box<dyn Gambar>]) {
for bentuk in bentuk_list {
println!("{} — Luas: {:.2}", bentuk.gambar(), bentuk.luas());
}
}
fn buat_bentuk(nama: &str) -> Box<dyn Gambar> {
// Bisa return tipe berbeda berdasarkan kondisi runtime
match nama {
"lingkaran" => Box::new(Lingkaran { radius: 3.0 }),
"persegi" => Box::new(Persegi { sisi: 4.0 }),
_ => Box::new(Lingkaran { radius: 1.0 }),
}
}
fn main() {
// Koleksi heterogen — tipe berbeda dalam satu Vec
let bentuk_list: Vec<Box<dyn Gambar>> = vec![
Box::new(Lingkaran { radius: 5.0 }),
Box::new(Persegi { sisi: 3.0 }),
Box::new(Lingkaran { radius: 2.0 }),
];
cetak_semua(&bentuk_list);
// Pilih tipe berdasarkan input runtime
let input = "lingkaran";
let bentuk = buat_bentuk(input);
println!("Dibuat: {}", bentuk.gambar());
}
| Aspek | impl Trait (static) | dyn Trait (dynamic) |
|---|---|---|
| Dispatch | Compile-time | Runtime via vtable |
| Performa | Nol overhead | Ada overhead indirection |
| Ukuran binary | Lebih besar (kode per tipe) | Lebih kecil |
| Koleksi heterogen | Tidak bisa | Bisa (Vec<Box<dyn Trait>>) |
| Return berbagai tipe | Tidak bisa | Bisa |
| Object safety | Tidak perlu | Trait harus object safe |
Supertrait #
Supertrait mendefinisikan dependensi antar trait — tipe yang mengimplementasikan trait A harus juga mengimplementasikan trait B:
use std::fmt;
// Display adalah supertrait dari Cetak
// Siapapun yang impl Cetak, harus juga impl Display
trait Cetak: fmt::Display {
fn cetak(&self) {
println!(">>> {} <<<", self); // bisa pakai Display karena dijamin ada
}
}
#[derive(Debug)]
struct Produk {
nama: String,
harga: f64,
}
// Implementasikan supertrait dulu
impl fmt::Display for Produk {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} (Rp{:.0})", self.nama, self.harga)
}
}
// Baru bisa implementasikan trait yang punya supertrait tersebut
impl Cetak for Produk {} // pakai implementasi default cetak()
fn main() {
let p = Produk {
nama: String::from("Laptop"),
harga: 15_000_000.0,
};
println!("{}", p); // via Display
p.cetak(); // via Cetak (menggunakan Display secara internal)
}
Orphan Rule — Batasan Implementasi #
Rust menegakkan orphan rule: kamu hanya bisa mengimplementasikan trait untuk tipe jika trait-nya atau tipe-nya (atau keduanya) didefinisikan di crate-mu sendiri. Ini mencegah konflik implementasi:
// BENAR: trait kustom untuk tipe kustom
trait Ringkas { fn ringkas(&self) -> String; }
struct Artikel { judul: String }
impl Ringkas for Artikel { /* ... */ fn ringkas(&self) -> String { self.judul.clone() } }
// BENAR: trait kustom untuk tipe bawaan
impl Ringkas for Vec<String> {
fn ringkas(&self) -> String {
format!("{} item", self.len())
}
}
// BENAR: trait bawaan untuk tipe kustom
use std::fmt;
impl fmt::Display for Artikel {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Artikel: {}", self.judul)
}
}
// ANTI-PATTERN: trait bawaan untuk tipe bawaan — tidak bisa!
// impl fmt::Display for Vec<String> { ... }
// error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate
Blanket Implementation #
Blanket implementation mengimplementasikan trait untuk semua tipe yang memenuhi syarat tertentu — sama seperti yang dilakukan standard library Rust secara ekstensif:
use std::fmt;
trait CetakDebug {
fn cetak_debug(&self);
}
// Blanket implementation: semua tipe yang impl Debug otomatis punya cetak_debug()
impl<T: fmt::Debug> CetakDebug for T {
fn cetak_debug(&self) {
println!("[DEBUG] {:?}", self);
}
}
#[derive(Debug)]
struct Titik { x: f64, y: f64 }
fn main() {
// Semua tipe ini mendapat cetak_debug() secara gratis
42i32.cetak_debug();
"halo".cetak_debug();
vec![1, 2, 3].cetak_debug();
Titik { x: 1.0, y: 2.0 }.cetak_debug();
}
Standard library menggunakan blanket implementation untuk ToString — semua tipe yang mengimplementasikan Display otomatis mendapat method .to_string():
// Di standard library — ini yang membuat to_string() ada di mana-mana:
// impl<T: fmt::Display> ToString for T {
// fn to_string(&self) -> String {
// format!("{}", self)
// }
// }
fn main() {
let s1 = 42.to_string(); // i32 → String
let s2 = 3.14.to_string(); // f64 → String
let s3 = true.to_string(); // bool → String
let s4 = 'z'.to_string(); // char → String
println!("{} {} {} {}", s1, s2, s3, s4);
}
Trait Standar yang Wajib Diketahui #
Rust standard library punya banyak trait bawaan. Mengetahui yang utama membantu menulis kode yang lebih idiomatis:
use std::fmt;
use std::ops::Add;
// Display — format yang dibaca manusia
#[derive(Debug)]
struct Vektor2D { x: f64, y: f64 }
impl fmt::Display for Vektor2D {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
// Add — operator +
impl Add for Vektor2D {
type Output = Vektor2D;
fn add(self, lain: Vektor2D) -> Vektor2D {
Vektor2D { x: self.x + lain.x, y: self.y + lain.y }
}
}
// From/Into — konversi antar tipe
struct Meter(f64);
struct Sentimeter(f64);
impl From<Meter> for Sentimeter {
fn from(m: Meter) -> Self {
Sentimeter(m.0 * 100.0)
}
}
// Default — nilai awal
#[derive(Debug)]
struct Konfigurasi {
debug: bool,
level_log: u8,
nama_app: String,
}
impl Default for Konfigurasi {
fn default() -> Self {
Konfigurasi {
debug: false,
level_log: 2,
nama_app: String::from("App"),
}
}
}
fn main() {
let v1 = Vektor2D { x: 1.0, y: 2.0 };
let v2 = Vektor2D { x: 3.0, y: 4.0 };
println!("{} + {} = {}", v1, v2, v1 + v2); // Display + Add
let meter = Meter(1.75);
let cm: Sentimeter = meter.into(); // From otomatis mengaktifkan Into
println!("{} cm", cm.0); // 175
let config = Konfigurasi::default();
println!("{:?}", config);
// Partial override default
let config_debug = Konfigurasi {
debug: true,
..Konfigurasi::default()
};
println!("{:?}", config_debug);
}
| Trait | Mengaktifkan | Cara implementasi |
|---|---|---|
Display | {} format, .to_string() | Manual |
Debug | {:?} format | #[derive(Debug)] atau manual |
Clone | .clone() | #[derive(Clone)] atau manual |
Copy | Copy semantics | #[derive(Copy, Clone)] |
PartialEq / Eq | ==, != | #[derive(PartialEq)] atau manual |
PartialOrd / Ord | <, >, .sort() | #[derive(PartialOrd)] atau manual |
Hash | Dipakai di HashMap | #[derive(Hash)] atau manual |
Default | Type::default() | #[derive(Default)] atau manual |
From / Into | Konversi tipe | Implement From, Into gratis |
Add, Sub, dll. | Overload operator | Manual via std::ops |
Iterator | for loop, adaptor | Manual — implement next() |
Ringkasan #
- Trait = kontrak perilaku — mendefinisikan method signature yang wajib diimplementasikan. Bisa punya method default yang tidak harus di-override.
- Associated type lebih bersih dari generik untuk output tunggal —
type Outputyang ditentukan implementor, bukan parameter tipe tambahan di setiap pemanggilan.impl Traituntuk static dispatch,dyn Traituntuk dynamic dispatch — static nol overhead tapi tipe harus diketahui compile-time; dynamic fleksibel untuk koleksi heterogen tapi ada biaya vtable.- Supertrait mendefinisikan dependensi trait —
trait A: Bberarti implementor A harus juga implement B. Method dari B tersedia di dalam implementasi A.- Orphan rule mencegah konflik — kamu hanya bisa impl trait jika trait-nya atau tipe-nya milikmu. Tidak bisa impl
DisplayuntukVec<String>dari crate luar.- Blanket implementation memberi method ke semua tipe yang memenuhi syarat — standard library menggunakannya untuk
ToString(semua tipeDisplay) dan banyak lagi.whereclause untuk constraint panjang — lebih mudah dibaca dari inline trait bound ketika ada banyak constraint atau banyak type parameter.- Trait standar penting — implement
Displayuntuk output user-friendly,Fromuntuk konversi,Defaultuntuk nilai awal, danIteratoruntuk membuat tipe iterable.- Trait dan generik bekerja bersama — gunakan trait bound pada generik untuk membuat kode yang reusable tanpa kehilangan keamanan tipe.