Topik ini membahas trait standar yang paling sering dipakai pada tipe bentukan, yaitu Debug, Clone, PartialEq, dan Default, beserta mekanisme derive yang menghasilkan implementasinya secara otomatis. Pembahasan mencakup perilaku tiap trait, cara mencampur implementasi otomatis dan manual, serta batasan derive ketika field pada struct belum tentu memenuhi syaratnya. Setelah membaca topik ini, tipe bentukan dapat dilengkapi pencetakan, penggandaan, perbandingan, dan nilai awal dengan satu baris anotasi, dan keputusan kapan implementasi manual diperlukan dapat diambil berdasarkan semantik yang diinginkan.
Empat trait yang paling sering di-derive#
Trait standar pada pustaka std terhubung langsung dengan sintaks bahasa, sehingga kehadirannya terasa pada penulisan sehari-hari. Trait Debug menyediakan pencetakan lewat format {:?} untuk keluaran ringkas dan {:#?} untuk keluaran rapi berbaris-baris. Trait Clone menyediakan metode clone() untuk penggandaan dalam yang menghasilkan nilai baru mandiri. Trait PartialEq mengaktifkan operator == dan !=, sedangkan trait Default menyediakan nilai awal melalui Default::default().
Keempatnya dapat diimplementasikan sekaligus dengan satu anotasi #[derive(...)] di atas definisi tipe. Derive adalah permintaan kepada kompilator untuk menulis implementasi sederhana atas nama penulis kodenya, dan hasilnya sama dengan implementasi yang ditulis tangan untuk kasus yang sederhana. Program berikut melengkapi struct Sensor dengan keempat trait tersebut lalu memakainya satu per satu.
#[derive(Debug, Clone, PartialEq, Default)]
struct Sensor {
nama: String,
suhu: f64,
aktif: bool,
}
fn main() {
let sensor_a = Sensor {
nama: String::from("sensor-A"),
suhu: 36.5,
aktif: true,
};
let sensor_b = sensor_a.clone();
let sensor_diam = Sensor {
nama: String::from("sensor-B"),
..Default::default()
};
println!("Debug satu baris : {:?}", sensor_a);
println!("Nilai sama : {}", sensor_a == sensor_b);
println!("Suhu sensor B : {}", sensor_diam.suhu);
println!("Debug rapi :");
println!("{:#?}", sensor_diam);
}Debug satu baris : Sensor { nama: "sensor-A", suhu: 36.5, aktif: true }
Nilai sama : true
Suhu sensor B : 0
Debug rapi :
Sensor {
nama: "sensor-B",
suhu: 0.0,
aktif: false,
}Baris sensor_b = sensor_a.clone() menggandakan seluruh isi termasuk String di dalam heap, sehingga kedua nilai kemudian saling mandiri dan pembandingan sensor_a == sensor_b bernilai true. Nilai sensor_diam dibuat dengan sintaks pembaruan struct yang mengambil sisa fieldnya dari Default::default(), yaitu string kosong untuk nama, 0.0 untuk suhu, dan false untuk aktif. Pencetakan 0 pada baris ketiga adalah bentuk paling ringkas dari 0.0 karena format {} pada f64 membuang nol di belakang, sedangkan format {:?} pada blok terakhir menampilkan nilainya sebagai 0.0.
Perbedaan {:?} dan {:#?} terlihat pada dua blok keluaran terakhir. Format {:?} memuat seluruh field dalam satu baris dan cocok untuk pemeriksaan cepat, sedangkan {:#?} merenggangkan setiap field ke barisnya sendiri sehingga lebih terbaca untuk struct dengan banyak field. Keduanya bergantung pada trait yang sama, sehingga tidak ada anotasi tambahan untuk berganti format.
Implementasi manual untuk semantik khusus#
Implementasi hasil derive selalu memakai semantik standar, yaitu pencetakan memuat seluruh field, kesamaan berarti seluruh field sama, dan nilai awal berupa nol atau kosong. Ketika tipe membutuhkan makna yang berbeda, trait yang bersangkutan diimplementasikan secara manual. Pola umumnya adalah mencampur keduanya, misalnya Debug dan Default dibuat manual sementara kemampuan lain tetap di-derive.
Program berikut mengimplementasikan Debug dengan format ringkas buatan sendiri, Default dengan suhu ruang 25 derajat, serta PartialEq yang menganggap dua suhu sama apabila selisihnya kurang dari 0,05 derajat.
use std::fmt;
struct Suhu {
celsius: f64,
}
impl fmt::Debug for Suhu {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Suhu({} C)", self.celsius)
}
}
impl Default for Suhu {
fn default() -> Self {
Suhu { celsius: 25.0 }
}
}
impl PartialEq for Suhu {
fn eq(&self, lain: &Self) -> bool {
(self.celsius - lain.celsius).abs() < 0.05
}
}
fn main() {
let a = Suhu { celsius: 36.51 };
let b = Suhu { celsius: 36.52 };
let awal = Suhu::default();
println!("a == b : {}", a == b);
println!("a : {:?}", a);
println!("b : {:?}", b);
println!("default : {:?}", awal);
}a == b : true
a : Suhu(36.51 C)
b : Suhu(36.52 C)
default : Suhu(25 C)Keluaran a == b : true menunjukkan semantik toleransi yang tidak tersedia pada derive, karena 36,51 dan 36,52 berbeda secara eksak tetapi dianggap sama oleh aturan selisih kurang dari 0,05. Pencetakan memakai format Suhu(36.51 C) alih-alih daftar field, dan nilai awal berupa 25 derajat alih-alih 0. Perlu diperhatikan bahwa pembandingan pecahan dengan toleransi merupakan pilihan domain, bukan kewajiban bahasa, dan pembandingan eksak untuk f64 tetap tersedia melalui PartialEq bawaan dari derive.
Implementasi manual PartialEq hanya menuntut satu metode bernama eq. Operator != secara otomatis menjadi kebalikan hasil eq, sehingga metode ne tidak perlu ditulis kecuali perilakunya memang harus berbeda. Trait Debug manual ditulis melalui satu metode fmt yang menerima pemformat dan mengembalikan fmt::Result, dan format {:?} maupun {:#?} sama-sama lewat metode ini dengan opsi pemformatan yang berbeda.
Kesalahan umum#
Kesalahan yang paling sering muncul adalah memakai format {:?} pada tipe yang belum mengimplementasikan Debug. Kompilator menolaknya dengan error E0277 yang sekaligus menyarankan anotasi derive. Contoh berikut memperlihatkan pesan aslinya.
export PATH="$HOME/.cargo/bin:$PATH"
# struct tanpa Debug dipakai dengan format {:?}
printf 'struct Sensor {\n nama: String,\n suhu: f64,\n}\n\nfn main() {\n let sensor = Sensor {\n nama: String::from("sensor-A"),\n suhu: 36.5,\n };\n\n println!("Sensor: {:?}", sensor);\n println!("{} {}", sensor.nama, sensor.suhu);\n}\n' > tanpa-debug.rs
rustc --edition=2021 tanpa-debug.rs 2>&1 | sed -n '1,14p'error[E0277]: `Sensor` doesn't implement `Debug`
--> tanpa-debug.rs:12:30
|
12 | println!("Sensor: {:?}", sensor);
| ---- ^^^^^^ `Sensor` cannot be formatted using `{:?}` because it doesn't implement `Debug`
| |
| required by this formatting parameter
|
= help: the trait `Debug` is not implemented for `Sensor`
help: consider annotating `Sensor` with `#[derive(Debug)]`
|
1 + #[derive(Debug)]
2 | struct Sensor {
|Bagian help pada pesan tersebut langsung menyarankan #[derive(Debug)] sebagai perbaikan. Kesalahan kedua adalah menganggap derive PartialEq selalu sesuai untuk semua tipe. Dua nilai f64 yang berbeda sedikit tetap dilaporkan tidak sama oleh implementasi derive, dan nilai seperti NaN menghasilkan pembandingan yang selalu salah terhadap dirinya sendiri, sehingga domain dengan toleransi atau nilai khusus memerlukan implementasi manual. Kesalahan ketiga adalah memakai Default::default() tanpa memeriksa makna nilainya, misalnya mengandalkan suhu 0.0 sebagai keadaan awal padahal domainnya menghendaki nilai lain, dan keadaan awal yang keliru baru terasa setelah data tertukar di dalam program.
Ringkasan#
- Trait
Debug,Clone,PartialEq, danDefaultmengaktifkan format{:?}, metodeclone(), operator==, dan nilai awalDefault::default(). - Anotasi
#[derive(...)]menghasilkan implementasi standar atas nama penulis kode, dan hanya berhasil bila seluruh field memenuhi syarat traitnya. - Implementasi manual dipakai ketika semantik berbeda diperlukan, misalnya pencetakan ringkas, nilai awal khas domain, atau kesamaan dengan toleransi.
- Format
{:?}pada tipe tanpaDebugditolak dengan error E0277 yang menyarankan anotasi derive.
Referensi#
- Appendix C: Derivable Traits — The Rust Programming Language — Daftar resmi trait yang dapat di-derive beserta perilaku bawaannya.
- Trait std::fmt::Debug — Rust std — Dokumentasi resmi trait Debug dan kedua format pencetakannya.
- Trait std::default::Default — Rust std — Dokumentasi resmi trait Default beserta aturan implementasinya.