Generics di Rust — Rust

Generics di Rust Generics memungkinkan kita menulis kode yang bekerja untuk banyak tipe sekaligus, tanpa mengorbankan type safety. Kamu sudah pernah melihat…

Generics membuat satu fungsi atau struct bekerja untuk banyak tipe sekaligus: tipenya ditulis sebagai tempat kosong, biasanya T, yang diisi compiler sesuai pemanggilnya. Bayangin kartu SOP di dinding Kedai Martabak Bang Rusdi: "Dari satu tumpukan, ambil yang paling besar." Kartu yang sama dipakai untuk tumpukan nota (nominal paling besar), daftar nama menu (paling akhir menurut abjad), dan rating ulasan (bintang paling tinggi). Kartunya cuma ditulis sekali, tapi di pojoknya ada satu syarat: "barangnya harus bisa dibandingkan." Kartu itu fungsi generic, jenis barangnya T, dan syarat di pojok itu trait bound.

🧪 Tebakan cepat: syarat T: PartialOrd dihapus, jadi fn terbesar<T>(items: &[T]) -> &T, padahal fungsinya cuma dipanggil dengan Vec<u32>, angka yang jelas bisa dibandingkan. Lolos compile? Tebak dulu, jawabannya ada di bagian "Trait bound: syarat untuk T".

Satu fungsi untuk banyak tipe

fn terbesar<T: PartialOrd>(items: &[T]) -> &T {
    let mut hasil = &items[0];
    for item in items {
        if item > hasil {
            hasil = item;
        }
    }
    hasil
}

fn main() {
    let pesanan: Vec<u32> = vec![102000, 70000, 38000, 45000];
    let menu = vec!["martabak manis", "martabak telur", "es teh"];
    let rating: Vec<f64> = vec![4.5, 4.8, 3.9];
    println!("{}", terbesar(&pesanan));
    println!("{}", terbesar(&menu));
    println!("{}", terbesar(&rating));
}
102000
martabak telur
4.8

Ini jejak terbesar(&menu). Teks dibandingkan huruf demi huruf seperti urutan kamus, jadi "martabak telur" lebih besar dari "martabak manis" (t sesudah m), dan "es teh" paling kecil.

item item > hasil? hasil sesudahnya
"martabak manis" false (sama) "martabak manis"
"martabak telur" true "martabak telur"
"es teh" false "martabak telur"

Trait bound: syarat untuk T

Trait adalah nama untuk satu kemampuan yang dimiliki sebuah tipe. PartialOrd artinya "bisa dibandingkan dengan < dan >", Display artinya "bisa dicetak dengan {}". u32, &str, dan f64 punya keduanya. Cara membuat trait sendiri ada di pelajaran traits. Ini kode tebakan tadi, <T> tanpa syarat:

error[E0369]: binary operation `>` cannot be applied to type `&T`
 --> src/main.rs:4:17
  |
4 |         if item > hasil {
  |            ---- ^ ----- &T
  |            |
  |            &T
  |
help: consider restricting type parameter `T` with trait `PartialOrd`
  |
1 | fn terbesar<T: std::cmp::PartialOrd>(items: &[T]) -> &T {
  |              ++++++++++++++++++++++

Ditolak, walaupun pemanggilnya cuma memakai u32. Itu jawaban tebakannya. Rust memeriksa isi fungsi generic sendirian: T boleh tipe apa saja yang memenuhi syaratnya, dan tanpa syarat, T bisa saja tipe yang gak bisa dibandingkan. Saran help: menulis nama lengkapnya, std::cmp::PartialOrd; T: PartialOrd saja sudah cukup.

Syarat itu janji, dan janjinya cuma sebatas yang ditulis. Fungsi ini mencetak hasil terbesar, tapi T-nya cuma dijanjikan bisa dibandingkan:

fn umumkan<T: PartialOrd>(items: &[T]) {
    println!("Paling besar: {}", terbesar(items));
}
error[E0277]: `T` doesn't implement `std::fmt::Display`
  --> src/main.rs:12:34
   |
12 |     println!("Paling besar: {}", terbesar(items));
   |                             --   ^^^^^^^^^^^^^^^ `T` cannot be formatted with the default formatter

Butuh dua kemampuan? Gabung dengan +. Kalau syaratnya panjang, pindahkan ke klausa where sesudah kurung parameter. Dua bentuk ini artinya sama:

use std::fmt::Display;

fn umumkan<T: PartialOrd + Display>(items: &[T]) {
    println!("Paling besar: {}", terbesar(items));
}

fn umumkan_lagi<T>(items: &[T])
where
    T: PartialOrd + Display,
{
    println!("Paling besar: {}", terbesar(items));
}

umumkan(&pesanan) mencetak Paling besar: 102000, dan umumkan(&menu) mencetak Paling besar: martabak telur. Display perlu use std::fmt::Display; di atas file.

Struct generic: Kotak

Struct mengelompokkan beberapa nilai bernama jadi satu tipe baru (dibahas lengkap di pelajaran berikutnya). Struct juga bisa generic:

struct Kotak<T> {
    isi: T,
}

impl<T> Kotak<T> {
    fn lihat(&self) -> &T {
        &self.isi
    }
}

fn main() {
    let bungkus = Kotak { isi: "martabak telur" };
    let harga: Kotak<u32> = Kotak { isi: 38000 };
    println!("{}", bungkus.lihat());
    println!("{}", harga.lihat());
}

Output-nya martabak telur lalu 38000.

Monomorphization: generic tanpa biaya

Saat compile, Rust membuat salinan konkret fungsi generic untuk tiap tipe yang benar-benar dipakai. Dari program terbesar tadi, compiler menulis tiga fungsi: terbesar::<u32>, terbesar::<&str>, dan terbesar::<f64>, masing-masing persis seperti kalau kamu menulisnya sendiri untuk tipe itu. Proses ini namanya monomorphization. Karena semuanya beres saat compile, generic di Rust zero-cost: saat program jalan gak ada pengecekan tipe atau tabel pencarian tambahan, secepat fungsi yang ditulis manual per tipe. Ongkosnya ada di waktu compile dan ukuran binary yang sedikit lebih besar.

⚠️ Jebakan yang sering ketemu

TL;DR: Generics menulis kode sekali untuk banyak tipe: fn terbesar<T: PartialOrd>(items: &[T]) -> &T. T adalah type parameter yang diisi dari argumennya (u32, &str, f64), dan T: PartialOrd adalah trait bound, syarat kemampuan yang wajib dimiliki T. Rust memeriksa isi fungsi generic sendirian, jadi tanpa bound > ditolak (E0369), dan mencetak T tanpa Display ditolak (E0277). Beberapa syarat digabung dengan + (T: Display + Clone), atau ditulis di klausa where. Struct juga bisa generic, struct Kotak<T> { isi: T }, dengan method di impl<T> Kotak<T>. Saat compile, monomorphization membuat salinan konkret per tipe yang dipakai, jadi generic gak menambah biaya saat jalan.

Yang akan kamu pelajari