Trait

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 --> A

Static 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());
}
Aspekimpl Trait (static)dyn Trait (dynamic)
DispatchCompile-timeRuntime via vtable
PerformaNol overheadAda overhead indirection
Ukuran binaryLebih besar (kode per tipe)Lebih kecil
Koleksi heterogenTidak bisaBisa (Vec<Box<dyn Trait>>)
Return berbagai tipeTidak bisaBisa
Object safetyTidak perluTrait 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);
}
TraitMengaktifkanCara implementasi
Display{} format, .to_string()Manual
Debug{:?} format#[derive(Debug)] atau manual
Clone.clone()#[derive(Clone)] atau manual
CopyCopy semantics#[derive(Copy, Clone)]
PartialEq / Eq==, !=#[derive(PartialEq)] atau manual
PartialOrd / Ord<, >, .sort()#[derive(PartialOrd)] atau manual
HashDipakai di HashMap#[derive(Hash)] atau manual
DefaultType::default()#[derive(Default)] atau manual
From / IntoKonversi tipeImplement From, Into gratis
Add, Sub, dll.Overload operatorManual via std::ops
Iteratorfor loop, adaptorManual — 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 tunggaltype Output yang ditentukan implementor, bukan parameter tipe tambahan di setiap pemanggilan.
  • impl Trait untuk static dispatch, dyn Trait untuk dynamic dispatch — static nol overhead tapi tipe harus diketahui compile-time; dynamic fleksibel untuk koleksi heterogen tapi ada biaya vtable.
  • Supertrait mendefinisikan dependensi traittrait A: B berarti 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 Display untuk Vec<String> dari crate luar.
  • Blanket implementation memberi method ke semua tipe yang memenuhi syarat — standard library menggunakannya untuk ToString (semua tipe Display) dan banyak lagi.
  • where clause untuk constraint panjang — lebih mudah dibaca dari inline trait bound ketika ada banyak constraint atau banyak type parameter.
  • Trait standar penting — implement Display untuk output user-friendly, From untuk konversi, Default untuk nilai awal, dan Iterator untuk membuat tipe iterable.
  • Trait dan generik bekerja bersama — gunakan trait bound pada generik untuk membuat kode yang reusable tanpa kehilangan keamanan tipe.

← Sebelumnya: Struct   Berikutnya: Eksepsi →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact