Fungsi generik menyelesaikan separuh masalah duplikasi; separuh sisanya menunggu di struct. Struct menyimpan data, dan tipe field tertulis permanen pada definisi — Locker untuk kode angka akan mengunci field bertipe i32, Locker untuk catatan mengunci field bertipe String, dan keduanya harus didefinisikan terpisah. Chapter ini memindahkan type parameter dari fungsi ke struct: satu nama struct yang tipe field-nya diisi belakangan, dua parameter sekaligus bila perlu, plus satu kejutan dari sistem inferensi tipe yang menuntut anotasi ketika struct dibuat dalam keadaan kosong.

Satu Struct untuk Semua Isi #

Kita lanjutkan studi kasus loker gudang: satu loker menyimpan kode akses berbentuk angka, loker lain menyimpan catatan berbentuk teks. Alih-alih dua struct, kita deklarasikan Locker dengan type parameter T:

rust
struct Locker<T> {
    content: T,
}

fn main() {
    let code_locker = Locker { content: 4582 };
    let name_locker = Locker { content: String::from("Kunci cadangan gudang") };

    println!("Kode loker: {}", code_locker.content);
    println!("Isi loker: {}", name_locker.content);
}

Output yang Dihasilkan:

text
Kode loker: 4582
Isi loker: Kunci cadangan gudang

Satu definisi struct, dua jenis isi. Mari kita bedah kode di atas baris per baris:

  1. Baris 1 (struct Locker<T> {): bagian <T> setelah nama struct mendeklarasikan type parameter untuk seluruh struct. Nama T kini sah dipakai pada semua field di dalam body definisi.
  2. Baris 2 (content: T,): field content dideklarasikan bertipe T — tipe pastinya ditentukan saat struct dipakai, bukan saat struct didefinisikan.
  3. Baris 6 (let code_locker = Locker { content: 4582 };): literal struct dengan field bertipe i32 menghasilkan instansiasi Locker<i32>. Inferensi bekerja di sini: 4582 bertipe i32, maka T = i32.
  4. Baris 7 (let name_locker = Locker { content: String::from("Kunci cadangan gudang") };): pemakaian kedua mengikat T = String, menghasilkan tipe Locker<String> — instansiasi yang berbeda dari Locker<i32>.
  5. Baris 9-10 (akses field content): tiap akses menghasilkan tipe konkret (i32 dan String), sehingga pencetakan {} berjalan tanpa syarat tambahan.

Setiap Instansiasi Adalah Tipe yang Berbeda #

Locker<i32> dan Locker<String> bukan satu tipe dengan isi berbeda — keduanya adalah dua tipe yang benar-benar terpisah, seberbeda i32 dan f64. Kompilator memperlakukan setiap instansiasi sebagai tipe lengkap dengan namanya sendiri: nilai Locker<i32> tidak bisa dioper ke variabel Locker<String>, tidak bisa dimasukkan ke Vec<Locker<String>>, dan tidak bisa dibandingkan dengan == hanya karena keduanya “sesama Locker”. Kesalahan mencampur keduanya akan ditolak sebagai E0308 mismatched types — perilaku yang persis sama dengan mencampur tipe primitif berbeda.

Konsekuensinya melegakan pembaca kode: ketika kita membaca signature fn open(locker: &Locker<u32>), kita tahu persis isi loker tanpa perlu menebak. Generik pada struct tidak berarti “tipe sembarangan”, melainkan “tipe yang diputuskan sekali di tiap titik pemakaian, lalu terkunci”. Kepastian ini pula yang kelak memungkinkan Rust menolak method yang tidak relevan untuk sebuah instansiasi — pokok bahasan Chapter Method Generik.

Dua Parameter Tipe pada Satu Struct #

Data berpasangan sering memuat dua tipe yang berbeda peran — nama dan nomor punggung pemain, koordinat dan label. Struct generik mendukung beberapa type parameter sekaligus:

rust
struct Pair<A, B> {
    first: A,
    second: B,
}

fn main() {
    let player = Pair { first: String::from("Budi"), second: 7 };
    let position = Pair { first: 4.5, second: -2 };

    println!("{} memakai nomor {}", player.first, player.second);
    println!("Titik ({}, {})", position.first, position.second);
}

Output yang Dihasilkan:

text
Budi memakai nomor 7
Titik (4.5, -2)

Mari kita bedah kode di atas baris per baris:

  1. Baris 1 (struct Pair<A, B> {): dua type parameter dideklarasikan sekaligus. Keduanya saling bebas — A dan B boleh diisi tipe yang sama atau berbeda pada tiap instansiasi.
  2. Baris 7 (let player = Pair { first: String::from("Budi"), second: 7 };): instansiasi Pair<String, i32>. A = String untuk nama, B = i32 untuk nomor punggung.
  3. Baris 8 (let position = Pair { first: 4.5, second: -2 };): instansiasi kedua Pair<f64, i32> — posisi pertama dari tipe A berganti menjadi f64 tanpa mengubah definisi struct.
  4. Baris 10-11 (pencetakan): field first dan second bertipe konkret pada tiap instansiasi, sehingga {} mencetak keduanya tanpa masalah.

Pola <A, B> ini adalah pola HashMap<K, V> di standard library: satu definisi, dua peran tipe yang deskriptif namanya. Type parameter juga boleh diisi tipe generik lain — Pair<Vec<u8>, Option<String>> sah adanya — karena instansiasi hanya mensubstitusi T dengan tipe apa pun, termasuk tipe yang membawa parameter sendiri.

Ketika Inferensi Kehabisan Petunjuk #

Membuat struct generik dalam keadaan kosong memunculkan masalah baru yang khas. Perhatikan struct rak buku berikut:

rust
struct Shelf<T> {
    items: Vec<T>,
}

fn main() {
    let shelf = Shelf { items: Vec::new() };
    let jumlah = shelf.items.len();
    println!("Jumlah buku: {}", jumlah);
}

Field items bertipe Vec<T> — vector di dalam struct generik. Program ini gagal dikompilasi:

text
error[E0282]: type annotations needed for `Shelf<_>`
 --> main.rs:6:9
  |
6 |     let shelf = Shelf { items: Vec::new() };
  |         ^^^^^                  ---------- type must be known at this point
  |
help: consider giving `shelf` an explicit type, where the type for type parameter `T` is specified
  |
6 |     let shelf: Shelf<T> = Shelf { items: Vec::new() };
  |              ++++++++++

error: aborting due to 1 previous error

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

Pesan E0282 menjelaskan intinya: Vec::new() menghasilkan vector kosong yang tidak membawa petunjuk tipe elemen apa pun, sehingga T pada Shelf<T> tidak bisa disimpulkan. Nilai kosong memang jujur — belum ada satu elemen pun yang bisa dibaca tipenya. Perbaikannya adalah menebalkan informasi lewat anotasi tipe pada binding:

rust
struct Shelf<T> {
    items: Vec<T>,
}

fn main() {
    let mut shelf: Shelf<String> = Shelf { items: Vec::new() };
    shelf.items.push(String::from("Laskar Pelangi"));
    shelf.items.push(String::from("Bumi Manusia"));

    println!("Buku pertama: {}", shelf.items[0]);
    println!("Jumlah buku: {}", shelf.items.len());
}

Output yang Dihasilkan:

text
Buku pertama: Laskar Pelangi
Jumlah buku: 2

Mari kita bedah perubahan pentingnya:

  1. Baris 6 (let mut shelf: Shelf<String> = Shelf { items: Vec::new() };): anotasi Shelf<String> menegaskan pilihan T sejak awal. Setelah anotasi ini, items pasti bertipe Vec<String> meskipun vector masih kosong.
  2. Baris 6 (Shelf { items: Vec::new() }): pembuatan vector kosong kini sah karena Vec::new() sudah tahu harus menjadi Vec<String>.
  3. Baris 7-8 (dua push): mut pada binding wajib karena isi vector berubah; setiap push menyimpan String ke dalam Vec<String> yang tipenya sudah terkunci.
  4. Baris 10-11 (pencetakan): akses shelf.items[0] menghasilkan String dan shelf.items.len() menghasilkan usize — dua operasi yang tipenya jelas berkat anotasi.

Ringkasan #

Type parameter <T> pada definisi struct membuat tipe field ditentukan per pemakaian: satu Locker<T> menjadi Locker<i32> dan Locker<String> — dua tipe terpisah yang tidak bisa dicampur. Beberapa parameter seperti Pair<A, B> mengakomodasi field dengan peran berbeda, mengikuti pola HashMap<K, V> milik standard library. Instansiasi berisi koleksi kosong tidak memberi petunjuk inferensi sehingga kompilator menuntut anotasi tipe eksplisit (E0282) — anotasi pada binding menutup celah tersebut. Struct generik masih sunyi tanpa perilaku; Chapter Method Generik berikutnya mengajari struct tersebut bekerja lewat blok impl yang parameternya harus dideklarasikan ulang.