Belajar Rust - Struct, Enum, Trait, dan Generic
Episode 5 of 19

Belajar Rust - Struct, Enum, Trait, dan Generic

Episode ini membangun tipe data di Rust: struct, tuple struct, dan unit struct, enum dengan match sebagai algebraic data types, trait sebagai interface dengan default impl, serta generic constraints yang membuat kode reusable tanpa biaya runtime.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Pendahuluan

Setelah menangani error, saatnya membangun blok data yang mencerminkan domain aplikasi. Episode 5 membedah tiga pilar pemodelan tipe di Rust: struct untuk data, enum untuk pilihan, dan trait untuk perilaku — digabungkan dengan generic agar kode bekerja untuk banyak tipe.

Pemodelan tipe bukan sekadar estetika. Enum yang dirancang baik bisa membuat "state invalid" tidak dapat direpresentasikan, sehingga banyak bug hilang sebelum terjadi. Episode ini memberi kalian kosakata untuk menulis API yang ekspresif dan aman.

Struct: Membungkus Data

Struct Klasik

Struct mirip object sederhana: kumpulan field yang diberi nama. Field bisa diubah jika instance-nya mut, dan method didefinisikan dalam blok impl.

Struct Pengguna
cat > src/main.rs <<'EOF'
struct Pengguna {
    nama: String,
    umur: u32,
    aktif: bool,
}
 
impl Pengguna {
    fn sapa(&self) -> String {
        format!("halo, {}", self.nama)
    }
}
 
fn main() {
    let pengguna = Pengguna {
        nama: String::from("Budi"),
        umur: 28,
        aktif: true,
    };
    println!("{}", pengguna.sapa());
}
EOF
cargo run

impl Pengguna mendefinisikan method sapa yang meminjam &self. Konstruksi struct memakai sintaks field: nilai. Jalankan dengan cargo run untuk melihat outputnya.

Tuple Struct dan Unit Struct

Selain struct bernama, Rust punya tuple struct yang diakses via indeks dan unit struct sebagai marker:

Tuple struct
struct Warna(u8, u8, u8);
struct Identitas; // unit struct

Warna(u8, u8, u8) diakses seperti merah.0 untuk field pertama. Unit struct sering dipakai untuk marker type dan konfigurasi.

Enum: Algebraic Data Types

Pilihan dengan Nilai

Enum merepresentasikan satu pilihan dari beberapa kemungkinan, dan setiap varian bisa membawa data. Inilah yang disebut algebraic data type — kombinasi dari "atau" (enum) dan "dan" (struct).

Enum Status
cat > src/main.rs <<'EOF'
enum Status {
    Baru,
    Diproses { oleh: String },
    Selesai(u32),
}
 
fn deskripsi(status: &Status) -> String {
    match status {
        Status::Baru => String::from("menunggu"),
        Status::Diproses { oleh } => format!("diproses oleh {}", oleh),
        Status::Selesai(jumlah) => format!("selesai {} item", jumlah),
    }
}
 
fn main() {
    let s = Status::Diproses {
        oleh: String::from("Ani"),
    };
    println!("{}", deskripsi(&s));
}
EOF
cargo run

match membongkar enum dengan pola: Diproses { oleh } menangkap field, Selesai(jumlah) menangkap nilai tuple. Karena match ekshaustif, menambahkan varian baru di enum akan memaksa kalian menanganinya di semua tempat — compiler menjaga kode tetap konsisten.

Trait: Interface dengan Implementasi Default

Mendefinisikan Perilaku

Trait mendeklarasikan method yang harus diimplementasikan tipe lain. Method dengan implementasi default bisa di-override jika perlu:

Trait dengan default impl
cat > src/main.rs <<'EOF'
trait Keluar {
    fn keluaran(&self) -> String;
 
    fn cetak(&self) {
        println!("{}", self.keluaran());
    }
}
 
struct Server(String);
 
impl Keluar for Server {
    fn keluaran(&self) -> String {
        format!("[server] {}", self.0)
    }
}
 
fn main() {
    let server = Server(String::from("api"));
    server.cetak();
}
EOF
cargo run

cetak memiliki implementasi default yang memanggil keluaran. Tipe Server cukup mengimplementasikan keluaran, lalu mendapat cetak secara gratis. Pola ini dipakai luas oleh stdlib, misalnya trait Iterator.

Trait sebagai Parameter dan Kembalian

Trait menjadi batasan generic: fn cetak<T: Keluar>(item: &T) menerima tipe apa pun yang mengimplementasi Keluar. Sintaks modern memakai impl Trait untuk parameter dan return type:

impl Trait
cat > src/main.rs <<'EOF'
fn gabungkan(a: impl ToString, b: impl ToString) -> String {
    format!("{}{}", a.to_string(), b.to_string())
}
 
fn main() {
    let hasil = gabungkan(1, "x");
    println!("{}", hasil);
}
EOF
cargo run

impl ToString pada parameter berarti "tipe apa pun yang mengimplementasi ToString". Kode tetap monomorphic: compiler men-generate versi khusus per tipe, jadi tidak ada overhead dispatch dinamis.

Generic Constraints

Batasan dengan Where Clause

Untuk beberapa batasan yang kompleks, gunakan where clause agar lebih terbaca:

Generic dengan batasan
cat > src/main.rs <<'EOF'
fn cetak_dua<T, U>(a: T, b: U)
where
    T: std::fmt::Display,
    U: std::fmt::Display,
{
    println!("{} dan {}", a, b);
}
 
fn main() {
    cetak_dua("rust", 42);
}
EOF
cargo run

T: std::fmt::Display, U: std::fmt::Display menyatakan kedua tipe bisa diformat sebagai teks. Generic menambah fleksibilitas tanpa menambah biaya runtime — inilah esensi zero-cost abstraction yang dibahas di episode 2.

Penutup

Inti yang harus dibawa pulang:

  • Struct membungkus data; tuple struct dan unit struct menangani kasus ringkas.
  • Enum sebagai algebraic data type membuat state invalid tidak bisa direpresentasikan.
  • Trait adalah interface; method default membuat implementasi ringkas.
  • impl Trait untuk parameter dan return type menjaga kode tetap monomorphic.

Di episode 6 selanjutnya kita akan membahas memory safety, borrowing, dan lifetimes — aturan ownership dan borrowing immutable serta mutable, anotasi lifetime dasar dan validitas referensi, serta praktik transfer ownership dengan move, clone, dan copy. Inilah jantung keamanan memori Rust.

Belajar Rust - Struct, Enum, Trait, dan Generic | Belajar Rust