Trait adalah daftar kemampuan bernama yang dijanjikan sebuah tipe: method apa saja yang wajib ia punya. Banyak tipe bisa memegang janji yang sama, masing-masing dengan caranya sendiri. Bayangin mesin kasir Kedai Martabak Bang Rusdi. Mesinnya gak peduli yang dibayar itu nota martabak, segelas es teh, atau ongkos bungkus. Syaratnya cuma satu: barangnya bisa menyebut totalnya. Nota menjumlah semua item, es teh cukup menyebut harganya sendiri. Syarat "bisa menyebut total" itu trait Harga, dan tiap jenis barang menepatinya dengan impl Harga for ....
🧪 Tebakan cepat: nota
pesanan(tipePesanan) danes(tipeMinuman) sama-sama punyaHarga. Kasir menaruh keduanya di satu nampan:let nampan = vec![pesanan, es];. Lolos compile? Tebak dulu, jawabannya ada di bagian "dyn Trait: banyak tipe dalam satu Vec".
Mendefinisikan dan mengimplementasikan trait
trait Harga {
fn total(&self) -> u32;
fn label(&self) -> String {
format!("Total: {}", self.total())
}
}
struct Pesanan {
pelanggan: String,
item: Vec<u32>,
}
struct Minuman {
nama: String,
harga: u32,
}
impl Harga for Pesanan {
fn total(&self) -> u32 {
self.item.iter().sum()
}
}
impl Harga for Minuman {
fn total(&self) -> u32 {
self.harga
}
fn label(&self) -> String {
format!("{}: {}", self.nama, self.harga)
}
}
fn main() {
let pesanan = Pesanan {
pelanggan: String::from("Sari"),
item: vec![32000, 32000, 38000],
};
let es = Minuman {
nama: String::from("es teh"),
harga: 5000,
};
println!("Struk {}", pesanan.pelanggan);
println!("{}", pesanan.label());
println!("{}", es.label());
}
Struk Sari
Total: 102000
es teh: 5000
trait Harga { ... }menulis janjinya.fn total(&self) -> u32;diakhiri titik koma, tanpa isi: method wajib, tiap tipe harus menulis isinya sendiri.fn labelpunya isi: default method. Tipe yang gak menulislabelmemakai versi ini. Default method boleh memanggil method wajib, di siniself.total().impl Harga for Pesanan { ... }menepati janji untukPesanan. Pesanan cuma menulistotal, jadilabel-nya default:Total: 102000.Minumanmenulislabelsendiri. Ini override: versinya menggantikan default, jadi tercetakes teh: 5000.- Kalau method wajib lupa ditulis, compiler menolak:
not all trait items implemented, missing: `total`(E0046).
Trait sebagai parameter: impl Trait dan trait bound
Fungsi yang menerima "apa saja yang punya Harga" ditulis dengan impl Harga, atau dengan trait bound seperti di pelajaran generics. Dua bentuk ini artinya sama:
fn cetak_struk(barang: &impl Harga) {
println!("[struk] {}", barang.label());
}
fn cetak_struk_v2<T: Harga>(barang: &T) {
println!("[struk] {}", barang.label());
}
cetak_struk(&pesanan) mencetak [struk] Total: 102000, dan cetak_struk_v2(&es) mencetak [struk] es teh: 5000. Di dalam fungsinya cuma method Harga yang boleh dipakai, dan tipe tanpa Harga ditolak di pemanggilnya: cetak_struk(&5000) memberi the trait bound `{integer}: Harga` is not satisfied. Seperti generic lain, compiler membuat satu salinan per tipe (monomorphization), jadi panggilannya sudah pasti saat compile. Ini static dispatch.
dyn Trait: banyak tipe dalam satu Vec
Ini kode tebakan tadi:
error[E0308]: mismatched types
--> src/main.rs:45:32
|
45 | let nampan = vec![pesanan, es];
| ^^ expected `Pesanan`, found `Minuman`
Gak lolos. Itu jawaban tebakannya. Vec cuma berisi satu tipe, dan isi pertamanya sudah menetapkan tipe itu: Pesanan. Supaya beda tipe bisa satu nampan, simpan masing-masing sebagai Box<dyn Harga>:
let nampan: Vec<Box<dyn Harga>> = vec![Box::new(pesanan), Box::new(es)];
let mut semua = 0;
for barang in &nampan {
println!("{}", barang.label());
semua += barang.total();
}
println!("Semua: {}", semua);
Total: 102000
es teh: 5000
Semua: 107000
dyn Hargaartinya "sebuah tipe apa pun yang punyaHarga", dan tipe aslinya baru diketahui saat program jalan. Ukurannya beda-beda, jadi nilainya ditaruh di heap (memori untuk data yang ukurannya ditentukan saat jalan) lewatBox::new(...), dan Vec cukup menyimpan penunjuknya yang ukurannya sama. Box dibahas lengkap di pelajaran smart pointer.- Tiap
barang.label()mencari method yang benar saat jalan lewat sebuah tabel kecil (vtable). Ini dynamic dispatch: sedikit lebih lambat dari static dispatch, tapi bisa menampung banyak tipe sekaligus. - Pilih generic (
impl Harga,<T: Harga>) kalau tiap panggilan cuma satu tipe. PilihBox<dyn Harga>kalau satu koleksi harus berisi berbagai tipe.
Display dan Debug: mencetak Rp 102.000
{:?} memakai trait Debug, yang bisa di-derive: bentuknya untuk programmer. {} memakai trait Display, yang gak bisa di-derive: bentuknya untuk pembeli, jadi kamu yang menulisnya. Struk perlu Rp 102.000, dengan titik tiap tiga digit:
use std::fmt;
#[derive(Debug)]
struct Rupiah(u32);
impl fmt::Display for Rupiah {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let angka = self.0.to_string();
let mut hasil = String::new();
for (i, c) in angka.chars().enumerate() {
if i > 0 && (angka.len() - i) % 3 == 0 {
hasil.push('.');
}
hasil.push(c);
}
write!(f, "Rp {}", hasil)
}
}
fn main() {
let total = Rupiah(102000);
println!("{:?}", total);
println!("{}", total);
println!("{}", Rupiah(5000));
println!("{}", Rupiah(1250000));
let teks = total.to_string();
println!("{} karakter", teks.len());
}
Rupiah(102000)
Rp 102.000
Rp 5.000
Rp 1.250.000
10 karakter
impl fmt::Display for Rupiahmenepati janjiDisplay: satu method,fmt.fadalah tujuan tulisannya, danwrite!(f, ...)menulis ke sana persis sepertiprintln!.- Titiknya disisipkan sebelum digit yang sisanya (
angka.len() - i) habis dibagi 3. Untuk"102000": digit nomor 3 masih menyisakan 3 digit, jadi titiknya masuk di sana:102.000. - Begitu ada
Display,.to_string()ikut tersedia gratis:"Rp 102.000", 10 karakter. - Tanpa
impl Display,println!("{}", total)ditolak:`Rupiah` doesn't implement `std::fmt::Display`.
Kenapa angkanya dibungkus Rupiah, bukan impl fmt::Display for u32? Karena orphan rule: kamu cuma boleh impl sebuah trait untuk sebuah tipe kalau trait-nya atau tipenya buatanmu sendiri. Display dan u32 dua-duanya milik standard library, jadi ditolak (E0117), sedangkan Rupiah milikmu.
⚠️ Jebakan yang sering ketemu
- Method wajib lupa ditulis.
impl Harga for Minuman { }tanpatotalditolak:missing: `total`(E0046). - Beda tipe dalam satu Vec.
vec![pesanan, es]ditolak denganexpected `Pesanan`, found `Minuman`. PakaiVec<Box<dyn Harga>>. - Mencetak dengan
{}tanpa Display. Tulisimpl fmt::Display, atau cetak dengan{:?}kalau cuma untuk dicek. - impl trait orang lain untuk tipe orang lain.
impl fmt::Display for Vec<u32>ditolak:only traits defined in the current crate can be implemented for types defined outside of the crate. Bungkus dulu tipenya:struct Daftar(Vec<u32>);.
TL;DR: Trait adalah janji kemampuan: trait Harga { fn total(&self) -> u32; fn label(&self) -> String { ... } }. Method tanpa isi wajib ditulis tiap tipe, method berisi adalah default yang boleh di-override. impl Harga for Pesanan { ... } menepatinya. Fungsi yang menerima apa saja yang punya trait ditulis &impl Harga atau <T: Harga> (static dispatch, satu tipe per panggilan). Vec<Box<dyn Harga>> menampung berbagai tipe sekaligus, dengan dynamic dispatch lewat vtable. Debug bisa di-derive untuk {:?}, Display ditulis sendiri untuk {} (Rp 102.000) dan sekalian memberi .to_string(). Orphan rule: trait atau tipenya harus milikmu, makanya u32 dibungkus jadi Rupiah.