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: PartialOrddihapus, jadifn terbesar<T>(items: &[T]) -> &T, padahal fungsinya cuma dipanggil denganVec<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
<T: PartialOrd>tepat setelah nama fungsi adalah daftar type parameter.Tadalah nama tempat kosong untuk sebuah tipe, dan: PartialOrdsyaratnya.items: &[T]adalah slice berisiT, pinjaman ke isi Vec seperti di pelajaran Vec. Hasilnya&T: pinjaman ke salah satu isinya, gak ada yang disalin.hasilmulai dari isi nomor 0, lalu tiapitemyang lebih besar menggantikannya.- Di
main,Tdiisi otomatis dari argumennya:u32untukpesanan,&struntukmenu,f64untukrating. Mau ditulis jelas juga bisa:terbesar::<u32>(&pesanan). - Untuk mencari yang terkecil, logikanya sama, cukup ganti
>jadi<.
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.
Kotak<T>punya satu fieldisibertipeT.bungkusjadiKotak<&str>,hargajadiKotak<u32>: dua tipe berbeda dari satu definisi.impl<T> Kotak<T>menambah fungsi milik Kotak untuk semuaT.<T>ditulis dua kali: sesudahimpluntuk mengumumkan tempat kosongnya, sesudahKotakuntuk memakainya.fn lihat(&self)adalah method: dipanggil dengan titik,bungkus.lihat(), dan&selfartinya meminjam kotaknya sendiri.Vec<T>,Option<T>, danHashMap<K, V>yang sudah kamu pakai juga generic.HashMappunya dua type parameter:Kuntuk kunci,Vuntuk nilai.
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
- Lupa trait bound.
>padaTtanpa syarat ditolak dengan E0369. TambahT: PartialOrd. - Syarat dipisah koma.
<T: Display, Clone>bukan dua syarat: koma memisahkan type parameter, jadiClonemalah jadi type parameter kedua, danitem.clone()ditolak denganno method named `clone` found for type parameter `T`. Tulis<T: Display + Clone>. Orduntuk angka desimal.T: Ordlebih ketat dariPartialOrd.f64gak punyaOrdkarena ada nilaiNaNyang gak bisa diurutkan, jaditerbesar(&rating)ditolak denganthe trait bound `f64: Ord` is not satisfied. Untuk<dan>biasa,PartialOrdcukup.- Daftar tipe di tempat yang salah.
fn<T> terbesar(...)danfn terbesar(items: &[T: PartialOrd])ditolak. Daftar tipe selalu tepat setelah nama fungsi. - Slice kosong.
&items[0]pada Vec kosong panic saat jalan:index out of bounds: the len is 0 but the index is 0.
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.