Chapter Struct dan Derive di section Struktur Data sudah memperkenalkan anotasi #[derive(...)] sebagai jalan pintas untuk membuka format {:?} dan operator == pada struct. Saat itu kita menerimanya sebagai kemampuan siap pakai; kini, setelah memahami trait sebagai kontrak dan impl Trait for Type sebagai pemenuhan kontrak, kita bisa membuka kap mesinnya. Derive adalah macro yang membaca definisi tipe kita saat kompilasi lalu menuliskan blok impl lengkap di tempat kita — kode hasilnya menjadi bagian program seperti kode tulisan tangan. Pada chapter ini kita membedah apa persisnya yang ditulis derive untuk empat trait yang paling sering dipakai — Debug, Clone, PartialEq, dan Default — beserta aturan berlapisnya ketika struct memuat struct lain.

Apa yang Ditulis derive di Balik Layar #

Cara paling jujur memahami derive adalah menuliskan manual versi yang setara, lalu membandingkan. Program berikut mendefinisikan dua struct identik: ProductManual dengan implementasi tulisan tangan, dan ProductDerived yang menyerahkan semuanya pada satu baris anotasi:

rust
#[derive(Debug)]
struct ProductManual {
    name: String,
    price: u32,
}

impl PartialEq for ProductManual {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name && self.price == other.price
    }
}

impl Default for ProductManual {
    fn default() -> Self {
        ProductManual {
            name: String::from("Tanpa Nama"),
            price: 0,
        }
    }
}

#[derive(Debug, PartialEq, Default)]
struct ProductDerived {
    name: String,
    price: u32,
}

fn main() {
    let kopi_a = ProductManual {
        name: String::from("Kopi Gayo"),
        price: 65000,
    };
    let kopi_b = ProductManual {
        name: String::from("Kopi Gayo"),
        price: 65000,
    };

    println!("Manual sama? {}", kopi_a == kopi_b);
    println!("Manual default: {:?}", ProductManual::default());

    let teh_a = ProductDerived {
        name: String::from("Teh Melati"),
        price: 28000,
    };
    let teh_b = ProductDerived {
        name: String::from("Teh Melati"),
        price: 28000,
    };

    println!("Derived sama? {}", teh_a == teh_b);
    println!("Derived default: {:?}", ProductDerived::default());
}

Output yang Dihasilkan:

text
Manual sama? true
Manual default: ProductManual { name: "Tanpa Nama", price: 0 }
Derived sama? true
Derived default: ProductDerived { name: "", price: 0 }

Kedua struct menghasilkan perilaku pembandingan yang sama persis, dan justru pada nilai bawaan keduanya berbeda. Mari kita bedah kode di atas untuk memahami sumbernya:

  1. Baris 7-11 (impl PartialEq for ProductManual { ... }): method eq wajib milik kontrak PartialEq — inilah yang dituntut operator ==. Versi tulisan tangan kita membandingkan kedua field satu per satu dengan &&. Derive akan menuliskan teks yang setara dengan ini untuk ProductDerived.
  2. Baris 13-20 (impl Default for ProductManual { ... }): Default menjanjikan nilai bawaan lewat method default(). Versi manual kita bebas memilih nilai mana pun — di sini "Tanpa Nama" dan 0.
  3. Baris 22 (#[derive(Debug, PartialEq, Default)]): satu anotasi, tiga trait sekaligus. Baris ini memerintahkan derive macro menuliskan tiga blok impl untuk ProductDerived — termasuk impl PartialEq yang isinya persis pola baris 7-11.
  4. Baris 51 (println!("Derived default: {:?}", ProductDerived::default());): hasil Default buatan derive adalah name: "" — berbeda dari versi manual yang memilih "Tanpa Nama".

Perbedaan pada hasil default() inilah pelajaran pentingnya: derive tidak membaca niat kita; derive mengikuti resep seragam — setiap field diisi dengan Default milik tipe field itu. Default untuk String adalah string kosong, untuk u32 adalah 0. Kapan pun nilai bawaan harus bermakna bisnis (misalnya harga 0 berarti “gratis” atau nama "Tanpa Nama"), implementasi Default tulisan tangan tetap pilihan yang tepat; derive menang ketika nilai kosong memang cukup.

Satu catatan lanjutan tentang PartialEq: versi derive hanya menuntut setiap field bisa dibandingkan dengan ==, sehingga struct dengan field f64 tetap sah diturunkan. Pustaka standar juga menyediakan Eq, versi penuh kesetaraan yang menuntut relasi utuh secara matematis dan tidak diberikan untuk f64 karena NaN tidak sama dengan dirinya sendiri. Untuk struct data biasa dengan field bilangan bulat dan teks, kebiasaan menurunkan PartialEq dan Eq sekaligus aman dan membuat tipe kita siap dipakai sebagai key peta seperti HashMap — mekanisme hashing di baliknya sudah kita sentuh di section Koleksi.

Clone dan Penyalinan Mendalam #

Clone menjanjikan method .clone() yang menyalin nilai secara utuh dan mandiri — perilaku yang sudah kita pakai di section Ownership. Derive untuk Clone menyalin setiap field dengan memanggil .clone() milik tipe field, sehingga penyalinan menjadi mendalam: String disalin isinya, bukan hanya penunjuknya. Program berikut memakai keempat trait sekaligus dalam satu anotasi:

rust
#[derive(Debug, Clone, PartialEq, Default)]
struct Product {
    name: String,
    price: u32,
    stock: u32,
}

fn main() {
    let promo = Product {
        name: String::from("Kopi Gayo 250g"),
        price: 59000,
        ..Default::default()
    };

    let for_sale = promo.clone();
    let for_gift = promo.clone();

    println!("Untuk dijual: {:?} (harga Rp{})", for_sale, for_sale.price);
    println!("Untuk hadiah: {:?}", for_gift);
    println!("Sumber salinan masih hidup: {}", promo.name);
    println!("Kedua salinan identik? {}", for_gift == for_sale);
}

Output yang Dihasilkan:

text
Untuk dijual: Product { name: "Kopi Gayo 250g", price: 59000, stock: 0 } (harga Rp59000)
Untuk hadiah: Product { name: "Kopi Gayo 250g", price: 59000, stock: 0 }
Sumber salinan masih hidup: Kopi Gayo 250g
Kedua salinan identik? true

Mari kita bedah baris-baris pentingnya:

  1. Baris 1 (#[derive(Debug, Clone, PartialEq, Default)]): empat trait diturunkan dalam satu kurung siku. Hasilnya: format {:?} aktif, .clone() aktif, == aktif, dan nilai bawaan tersedia.
  2. Baris 12 (..Default::default()): struct update syntax dipadukan dengan Default — dua field ditulis eksplisit (name, price), field sisanya (stock) diambil dari nilai bawaan Default::default(), yaitu 0. Pola ini menggantikan penulisan stock: 0 secara manual.
  3. Baris 15-16 (let for_sale = promo.clone(); dan let for_gift = promo.clone();): dua salinan independen dibuat dari promo. Tanpa .clone(), penugasan let for_sale = promo; akan memindahkan kepemilikan promo — dan baris 20 yang masih memakai promo.name akan ditolak dengan E0382, skenario yang sudah kita bahas tuntas di section Ownership.
  4. Baris 21 (println!("Kedua salinan identik? {}", for_gift == for_sale);): dua salinan hasil clone() dibandingkan dengan == yang aktif berkat PartialEq. Hasilnya true karena seluruh field bernilai sama.

Syarat Berlapis pada Field #

Derive bekerja dengan cara membaca field, sehingga derive mewariskan syarat ke setiap tipe field: field harus ikut mengimplementasikan trait yang diminta. String dan u32 sudah memenuhi semuanya, tetapi tipe buatan kita yang bersarang harus menurunkan trait yang sama sendiri:

rust
struct Calibration {
    offset: f64,
}

#[derive(Clone)]
struct MeasuringDevice {
    calibration: Calibration,
}

fn main() {
    let device = MeasuringDevice {
        calibration: Calibration { offset: 0.5 },
    };
    let backup = device.clone();
    println!("Offset cadangan: {}", backup.calibration.offset);
}

Pesan error kompilator:

text
error[E0277]: the trait bound `Calibration: Clone` is not satisfied
 --> main.rs:7:5
  |
5 | #[derive(Clone)]
  |          ----- in this derive macro expansion
6 | struct MeasuringDevice {
7 |     calibration: Calibration,
  |     ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `Calibration`
  |
help: consider annotating `Calibration` with `#[derive(Clone)]`
  |
1 + #[derive(Clone)]
2 | struct Calibration {
  |

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0277`.

Pesan E0277 menunjuk akar persoalan dengan sangat tepat: MeasuringDevice meminta Clone, tetapi field calibration bertipe Calibration yang belum mengimplementasikan Clone — bukan MeasuringDevice-nya yang bermasalah. Bantuan kompilator bahkan menuliskan perbaikannya: tambahkan #[derive(Clone)] pada Calibration. Aturan ini menjalar ke seluruh kedalaman struct: struct luar, struct di dalam field-nya, hingga tipe terdalam harus sama-sama menurunkan trait yang dibutuhkan. Kebalikannya juga menenangkan: begitu semua lapisan menurunkan trait yang sama, penyalinan dan pembandingan bekerja rekursif — Product yang memuat Product lain akan tersalin dan terbandingkan hingga lapisan terdalam tanpa kode tambahan.

Ringkasan #

Derive macro menuliskan blok impl untuk kita: PartialEq hasil derive membandingkan field-demi-field, Clone hasil derive menyalin field-demi-field hingga mendalam, Debug mencetak nama tipe beserta seluruh field, dan Default mengisi setiap field dengan nilai bawaan milik tipe field tersebut — kecuali kita menulis Default sendiri ketika nilai bawaan perlu bermakna. Satu anotasi dapat memuat banyak trait sekaligus, dan ..Default::default() memperpendek pembuatan nilai parsial. Seluruh kemudahan itu tunduk pada satu syarat berlapis: setiap tipe field harus ikut menurunkan trait yang sama, atau E0277 menolak saat kompilasi dengan perbaikan yang bahkan sudah dituliskan. Dengan derive, sebagian besar kebutuhan trait sehari-hari selesai dalam satu baris — tetapi ada aturan lain yang mengatur siapa boleh menuliskan impl untuk tipe siapa. Aturan itu bernama orphan rule.