Topik ini membahas enum, yaitu tipe yang menyatukan sejumlah nilai tetap ke dalam satu nama, dengan penekanan pada kemampuan setiap variantnya untuk membawa data. Pembahasan mencakup hubungan enum dengan konsep tagged union, penulisan metode pada enum, serta tipe Option<T> dari pustaka standar sebagai contoh enum yang paling sering dipakai. Setelah membaca topik ini, tipe pilihan seperti status mesin atau hasil pembacaan sensor dapat dirancang sendiri dengan data yang menempel pada tiap pilihan, dan kelebihan bentuk ini dibanding struct yang memakai banyak field opsional akan terlihat jelas.
Variant sebagai anggota tipe#
Sebuah enum dideklarasikan dengan kata kunci enum, dan setiap anggotanya disebut variant. Nilai bertipe enum pasti berupa salah satu variant, bukan campuran, dan variant dibuat dengan sintaks NamaEnum::NamaVariant. Kemampuan yang membuat enum Rust berbeda dari enum pada banyak bahasa lain adalah setiap variant boleh membawa data. Variant boleh tanpa data, boleh membawa satu nilai seperti Tulis(String), dan boleh membawa beberapa field bernama seperti Pindah { x: i32, y: i32 } dalam satu variant yang sama.
Karena tiap variant dapat membawa data dengan bentuk yang berbeda-beda, enum Rust bekerja seperti tagged union: satu penanda (tag) menyimpan variant mana yang sedang aktif, dan hanya data variant itu yang hadir di memori. Diagram berikut menggambarkan tiga variant dari enum PesanMesin sebagai pasangan tag dan data.
Representasi ini menjelaskan mengapa enum lebih tepat daripada struct dengan banyak field opsional untuk data yang sifatnya pilihan. Sebuah struct CatatanSuhu selalu menyediakan seluruh fieldnya, sedangkan nilai PesanMesin::Mati tidak menyediakan teks maupun posisi apa pun. Kesalahan seperti membaca posisi dari pesan Mati tidak dapat terjadi pada enum karena data itu memang tidak pernah ada pada variant tersebut. Ukuran memori sebuah enum mengikuti variant terbesarnya, dan penanda tag memastikan tidak ada data yang terbaca dengan tafsiran yang salah.
Program berikut mendefinisikan enum PesanMesin dengan empat variant, kemudian melengkapinya dengan metode jalankan yang membedah tiap pesan.
#[derive(Debug)]
enum PesanMesin {
Mati,
Pindah { x: i32, y: i32 },
Tulis(String),
AturKecepatan(u8),
}
impl PesanMesin {
fn jalankan(&self) {
match self {
PesanMesin::Mati => println!("Mesin dihentikan"),
PesanMesin::Pindah { x, y } => println!("Pindah ke posisi ({}, {})", x, y),
PesanMesin::Tulis(teks) => println!("Tulis ke layar: {}", teks),
PesanMesin::AturKecepatan(v) => println!("Kecepatan diatur ke {}", v),
}
}
}
fn main() {
let daftar = [
PesanMesin::Pindah { x: 10, y: -4 },
PesanMesin::Tulis(String::from("catat posisi sekarang")),
PesanMesin::AturKecepatan(60),
PesanMesin::Mati,
];
for pesan in &daftar {
pesan.jalankan();
}
println!("Representasi debug: {:?}", daftar[0]);
}Pindah ke posisi (10, -4)
Tulis ke layar: catat posisi sekarang
Kecepatan diatur ke 60
Mesin dihentikan
Representasi debug: Pindah { x: 10, y: -4 }Metode pada enum ditulis dengan blok impl seperti pada struct. Di dalam jalankan, ekspresi match memeriksa variant dari setiap pesan sekaligus mengambil data yang dibawanya, sehingga teks hanya bisa dibaca pada variant Tulis dan kecepatan hanya pada variant AturKecepatan. Baris terakhir memperlihatkan keluaran {:?} untuk enum dengan derive Debug, yaitu nama variant beserta isinya. Pola pemeriksaan lengkap seperti ini akan dibahas lebih rinci pada topik pencocokan dengan match.
Option sebagai enum dari pustaka standar#
Kemampuan variant membawa data dipakai pustaka standar untuk memecahkan masalah ketidakhadiran nilai. Rust tidak memiliki null, yaitu penanda nilai kosong yang dapat menempel pada tipe apa pun, dan sebagai gantinya disediakan enum Option<T> yang definisinya dapat ditulis sebagai berikut.
// tidak-dijalankan
enum Option<T> {
Some(T),
None,
}Fragmen di atas tidak dijalankan karena Option sudah ada di pustaka standar dan tidak perlu didefinisikan ulang. Variant Some membawa satu nilai bertipe T, sedangkan None tidak membawa apa-apa dan menyatakan ketidakhadiran nilai. Kompiler memperlakukan kedua keadaan itu sebagai satu tipe yang sama, sehingga setiap pemakaian nilai Option<T> harus melalui pencocokan pola atau metode milik Option, bukan langsung dibaca sebagai T. Pola ini menjadi fondasi penanganan ketidakhadiran nilai sekaligus dasar bagi Result<T, E> pada penanganan error yang dapat dipulihkan.
fn cari_laci(daftar: &[&str], nama: &str) -> Option<usize> {
for (indeks, isi) in daftar.iter().enumerate() {
if *isi == nama {
return Some(indeks);
}
}
None
}
fn main() {
let lemari = ["kunci", "pulpen", "notebook"];
match cari_laci(&lemari, "pulpen") {
Some(nomor) => println!("Pulpen ada di laci nomor {}", nomor + 1),
None => println!("Pulpen tidak ditemukan"),
}
match cari_laci(&lemari, "penghapus") {
Some(nomor) => println!("Penghapus ada di laci nomor {}", nomor + 1),
None => println!("Penghapus tidak ditemukan"),
}
}Pulpen ada di laci nomor 2
Penghapus tidak ditemukanFungsi cari_laci mengembalikan Option<usize> karena pencarian memiliki dua hasil yang mungkin, yaitu indeks yang ditemukan atau kegagalan pencarian. Kedua hasil itu tidak dicampur dengan nilai penanda seperti -1 atau 0 yang maknanya harus dihafal, melainkan dibungkus variant yang berdiri sendiri. Pemanggil menangani kedua kemungkinan lewat match, dan kompilator menolak pemakaian nilai indeks tanpa melalui pencocokan itu. Option berada di prelude, yaitu kumpulan nama yang diimpor otomatis ke setiap program, sehingga dapat dipakai tanpa use.
Kesalahan umum#
Kesalahan pertama adalah membandingkan nilai enum dengan == padahal tipenya belum menerapkan PartialEq. Kompilator menolaknya dengan error E0369 yang menyatakan bahwa operasi biner == tidak dapat diterapkan pada tipe tersebut, disertai catatan bahwa implementasi PartialEq mungkin kurang. Perbaikannya adalah menambah #[derive(PartialEq)] pada deklarasi enum apabila seluruh field variantnya dapat dibandingkan, atau mengimplementasikan trait itu secara manual ketika aturan persamaannya khusus. Derive ini hanya melihat isi data dan tidak menambahkan makna lain, sehingga dua nilai dengan variant dan isi yang sama akan dianggap sama.
Kesalahan kedua adalah menganggap data variant dapat dibaca langsung seperti field struct. Penulisan pesan.teks tidak sah karena teks hanya hadir pada variant Tulis, dan bentuk aksesnya memang harus melalui pencocokan pola. Rancangan ini disengaja agar pembacaan data selalu disertai kepastian variant, sehingga bug akibat membaca data variant yang salah terdeteksi saat kompilasi dan bukan saat program berjalan.
Kesalahan ketiga adalah mencetak nilai enum dengan {:?} tanpa #[derive(Debug)]. Kompilator menolaknya dengan error E0277 yang menyatakan bahwa tipe tersebut tidak mengimplementasikan Debug, dan saran perbaikannya adalah menambahkan derive pada deklarasi enum. Representasi debug sangat membantu saat pengembangan karena satu pemanggilan println! langsung memperlihatkan variant beserta seluruh data di dalamnya, tanpa perlu menulis pola match yang lengkap hanya demi melihat isi sebuah nilai.
Ringkasan#
- Enum menyatukan sejumlah variant dalam satu tipe, dan setiap variant boleh tanpa data, membawa satu nilai, atau membawa beberapa field bernama.
- Pada representasi tagged union, satu tag menandai variant aktif dan hanya data variant itu yang hadir di memori.
- Metode enum ditulis pada blok
impldan biasanya membedah variant lewatmatch. Option<T>adalah enum standar dengan variantSome(T)danNoneyang menggantikan null serta menjadi fondasi penanganan ketidakhadiran nilai.
Referensi#
- Defining an Enum — The Rust Programming Language
— Definisi enum, variant pembawa data, dan enum
Option<T>. - Enum — The Rust Reference — Aturan bahasa tentang deklarasi enum dan bentuk data variant.
- Option — Standard library
— Dokumentasi tipe
Option<T>beserta seluruh metodenya.