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.

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 mirip object sederhana: kumpulan field yang diberi nama. Field bisa diubah jika instance-nya mut, dan method didefinisikan dalam blok impl.
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 runimpl Pengguna mendefinisikan method sapa yang meminjam &self. Konstruksi struct memakai sintaks field: nilai. Jalankan dengan cargo run untuk melihat outputnya.
Selain struct bernama, Rust punya tuple struct yang diakses via indeks dan unit struct sebagai marker:
struct Warna(u8, u8, u8);
struct Identitas; // unit structWarna(u8, u8, u8) diakses seperti merah.0 untuk field pertama. Unit struct sering dipakai untuk marker type dan konfigurasi.
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).
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 runmatch 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 mendeklarasikan method yang harus diimplementasikan tipe lain. Method dengan implementasi default bisa di-override jika perlu:
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 runcetak 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 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:
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 runimpl 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.
Untuk beberapa batasan yang kompleks, gunakan where clause agar lebih terbaca:
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 runT: 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.
Inti yang harus dibawa pulang:
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.