Traits di Rust — Rust

Traits di Rust Trait adalah cara mendefinisikan perilaku bersama yang bisa diimplementasikan oleh berbagai tipe — mirip interface di Java atau protocol di…

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 (tipe Pesanan) dan es (tipe Minuman) sama-sama punya Harga. 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 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

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

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

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.

Yang akan kamu pelajari