Kita kini bisa membentuk tipe sendiri yang rapi — tetapi coba cetak salah satunya ke layar, dan kompilator menolak. Bandingkan dua tipe berikut: println!("{}", judul) pada String berjalan mulus, sedangkan println!("{}", student) pada struct buatan kita sendiri gagal dikompilasi. Bandingkan pula perbandingan: saldo1 == saldo2 sah pada u64, tetapi titik1 == titik2 pada struct ditolak. Kemampuan mencetak dan membandingkan bukan bawaan gratis untuk tipe bentukan; keduanya adalah implementasi trait yang harus tersedia — dan menuliskannya manual akan sangat membosankan. Rust menyiapkan jalan pintas bernama derive macro: satu baris #[derive(...)] di atas definisi tipe memerintahkan kompilator menuliskan implementasi tersebut untuk kita. Pada chapter ini kita akan menurunkan Debug untuk pencetakan dan PartialEq untuk perbandingan.
Struct yang Menolak Dicetak #
Mari kita mulai dari kegagalannya sendiri. Struct siswa berikut sah, tetapi println! memakainya langsung:
struct Student {
name: String,
score: u32,
}
fn main() {
let student = Student {
name: String::from("Budi Santoso"),
score: 85,
};
println!("Siswa: {}", student);
}Pesan error kompilator:
error[E0277]: `Student` doesn't implement `std::fmt::Display`
--> main.rs:12:27
|
12 | println!("Siswa: {}", student);
| -- ^^^^^^^ `Student` cannot be formatted with the default formatter
| |
| required by this formatting parameter
|
help: the trait `std::fmt::Display` is not implemented for `Student`
--> main.rs:1:1
|
1 | struct Student {
| ^^^^^^^^^^^^^^
= note: in format strings you may be able to use `{:?}` (or `{:#?}` for pretty-print) instead
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.Pesan E0277 menjelaskan inti persoalannya: format {} meminta trait Display — aturan pencetakan untuk tampilan ramah pembaca — dan Student tidak mengimplementasikannya. Pesan tersebut bahkan mengisyaratkan dua kemungkinan jalan keluar: tulis implementasi Display sendiri, atau gunakan format {:?} yang meminta trait Debug. Trait Display memang layak ditulis manual karena hanya pembuat tipe yang tahu bentuk tampilan terbaik bagi pengguna akhir. Untuk Debug, yang kebutuhannya sekadar “tampilkan isinya untuk keperluan pengembang”, menuliskan implementasi manual untuk setiap struct adalah pekerjaan berulang tanpa keputusan desain — dan di sinilah derive masuk.
Menurunkan Debug dengan derive #
Kata derive berarti menurunkan — kita menurunkan implementasi Debug dari definisi struct. Caranya satu baris: anotasi #[derive(Debug)] tepat di atas struct:
#[derive(Debug)]
struct Student {
name: String,
score: u32,
}
fn main() {
let student = Student {
name: String::from("Budi Santoso"),
score: 85,
};
println!("Data lengkap: {:?}", student);
println!("Versi rapi:");
println!("{:#?}", student);
if student.score >= 75 {
println!("{} lulus mata kuliah ini", student.name);
}
}Output yang Dihasilkan:
Data lengkap: Student { name: "Budi Santoso", score: 85 }
Versi rapi:
Student {
name: "Budi Santoso",
score: 85,
}
Budi Santoso lulus mata kuliah iniMari kita bedah kode di atas baris per baris:
- Baris 1 (
#[derive(Debug)]): anotasi ini adalah derive macro — kode yang memerintahkan kompilator menuliskan implementasi traitDebuguntukStudentsecara otomatis saat kompilasi. Hasilnya sama dengan implementasi manual: setiap field turut dicetak. - Baris 13 (
println!("Data lengkap: {:?}", student);): format{:?}kini sah karenaStudentsudah mengimplementasikanDebug. Tampilannya memakai gaya teknis untuk keperluan developer: nama tipe plus isi seluruh field. - Baris 15 (
println!("{:#?}", student);): format{:#?}adalah varian rapi (pretty-print) — setiap field mendapat baris tersendiri dengan indentasi. Cocok untuk struct besar saat inspeksi nilai. - Baris 17-19 (
if student.score >= 75): pembacaan field biasa tetap bekerja seperti sebelumnya; derive tidak mengubah perilaku struct sedikit pun — ia hanya menambahkan kemampuan baru.
Perbandingan Nilai dengan PartialEq #
Kegagalan kedua menyangkut kesetaraan. Operator == pada struct buatan kita juga menuntut implementasi trait — PartialEq — dan tanpanya kompilator menolak:
struct Point {
x: i32,
y: i32,
}
fn main() {
let start = Point { x: 0, y: 0 };
let current = Point { x: 3, y: 0 };
println!("Posisi sama? {}", start == current);
}Pesan error kompilator:
error[E0369]: binary operation `==` cannot be applied to type `Point`
--> main.rs:10:39
|
10 | println!("Posisi sama? {}", start == current);
| ----- ^^ ------- Point
| |
| Point
|
note: an implementation of `PartialEq` might be missing for `Point`
--> main.rs:1:1
|
1 | struct Point {
| ^^^^^^^^^^^^ must implement `PartialEq`
help: consider annotating `Point` with `#[derive(PartialEq)]`
|
1 + #[derive(PartialEq)]
2 | struct Point {
|
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0369`.Pesan E0369 bahkan menuliskan perbaikannya: tambahkan #[derive(PartialEq)]. Dua anotasi derive dapat digabung dalam satu kurung siku dengan pemisah koma. Program drone berikut menurunkan Debug dan PartialEq sekaligus, lalu memakai == dan != untuk memantau posisi:
#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let home = Point { x: 0, y: 0 };
let mut drone = Point { x: 4, y: 7 };
println!("Posisi drone: {:?}", drone);
println!("Drone sudah pulang? {}", drone == home);
drone.x = 0;
drone.y = 0;
println!("Posisi drone: {:?}", drone);
println!("Drone sudah pulang? {}", drone == home);
println!("Posisi berubah? {}", drone != home);
}Output yang Dihasilkan:
Posisi drone: Point { x: 4, y: 7 }
Drone sudah pulang? false
Posisi drone: Point { x: 0, y: 0 }
Drone sudah pulang? true
Posisi berubah? falsePenjelasan baris-baris pentingnya:
- Baris 1 (
#[derive(Debug, PartialEq)]): dua trait diturunkan sekaligus. Aturan perbandingannya sederhana dan aman: dua nilai dianggap sama bila seluruh field-nya sama —dronedanhomesama ketikaxmaupunybernilai identik. - Baris 12 (
drone == home): operator==kini sah padaPoint. Pembandingan(4, 7)melawan(0, 0)menghasilkanfalse. - Baris 14-15 (
drone.x = 0;dandrone.y = 0;): koordinat drone digeser ke titik asal. Perubahan ini yang membuat pembandingan berikutnya menghasilkantrue. - Baris 19 (
drone != home): operator ketaksamaan!=ikut tersedia bersamaPartialEq— tanda perbandingan terbalik dari==, di sinifalsekarena keduanya sudah sama.
Apa yang Sebenarnya Dilakukan derive #
Derive bukan sihir runtime — seluruh pekerjaannya terjadi saat kompilasi. Baris #[derive(PartialEq)] memicu macro prosedural yang membaca definisi struct kita, lalu menuliskan kode impl PartialEq for Point { ... } dengan pembandingan field-demi-field, persis seperti yang akan kita tulis sendiri. Kode hasil derive menjadi bagian program kita; tidak ada mekanisme tersembunyi saat program berjalan. Cara kerja macro itu sendiri — termasuk cara membuat derive custom — adalah materi Course Macros.
Kemampuan hasil derive juga bekerja berlapis. Struct yang memuat struct lain tetap sah dicetak selama tipe di dalamnya ikut menurunkan Debug — pencetakan bergerak rekursif dari tipe terluar ke field terdalam, sehingga Order yang memuat field PaymentMethod akan tampil utuh dengan seluruh isinya. Aturan yang sama berlaku untuk PartialEq: pembandingan dua struct induk membandingkan seluruh field, dan setiap pembandingan field memakai implementasi milik tipe field itu. Sifat berlapis inilah yang membuat satu baris derive di tipe terluar cukup untuk menghidupkan seluruh struktur.
Selain Debug dan PartialEq, daftar trait yang bisa diturunkan dengan derive bawaan antara lain Clone (kemampuan menyalin nilai lewat .clone()), Copy (penyalinan implisit untuk tipe tanpa data heap), Default (nilai bawaan), dan Eq (versi penuh dari PartialEq). Dua yang pertama pada daftar tersebut akan sering kita pakai; sisanya akan muncul di course-course berikutnya.
Ringkasan #
Kemampuan pencetakan dan perbandingan tidak otomatis melekat pada tipe buatan: format {} menuntut Display (E0277) dan operator == menuntut PartialEq (E0369). Derive menghapus beban penulisan manual — #[derive(Debug)] membuka format {:?} dan {:#?}, #[derive(PartialEq)] membuka == dan !=, dan keduanya dapat digabung dalam satu anotasi. Kompilator yang menuliskan implementasinya, field demi field, saat kompilasi berlangsung. Perangkat kita kini lengkap: data terkelompok, perilaku menempel, pilihan termodel, dan ketiadaan terjamin. Tersisa satu keterampilan perancangan: memilih bentuk data yang tepat untuk setiap masalah.