Topik ini membahas trait bound, yaitu batasan yang menyatakan tipe apa saja yang boleh mengisi parameter tipe generik. Pembahasan mencakup sintaks T: Trait pada fungsi generik, penggabungan beberapa batasan dengan tanda +, klausa where untuk tanda tangan yang panjang, serta proses monomorphization yang mengubah kode generik menjadi salinan per tipe konkret. Setelah membaca topik ini, fungsi dapat ditulis sekali untuk banyak tipe tanpa biaya penelusuran saat runtime, dan error akibat batasan yang kurang dapat diperbaiki langsung dari saran kompilator.
Fungsi generik dan kebutuhan batasan#
Fungsi generik menerima parameter tipe yang ditulis dalam kurung siku, misalnya fn nama<T>(item: T). Satu definisi seperti itu dapat dipanggil dengan tipe pengisi yang berbeda-beda, dan kompilator memeriksa isi fungsi hanya dengan asumsi yang berlaku untuk semua tipe sekaligus. Akibatnya, isi fungsi hanya boleh memakai operasi yang tersedia pada semua tipe, karena kompilator tidak mengetahui tipe pengisinya sampai fungsi dipanggil.
Perbandingan nilai memerlukan kemampuan yang tidak dimiliki semua tipe. Fungsi terbesar berikut membandingkan dua nilai bertipe T dengan operator >, padahal T belum dinyatakan memiliki kemampuan pembandingan, sehingga kompilator menolaknya.
export PATH="$HOME/.cargo/bin:$PATH"
# fungsi generik tanpa batasan pembandingan
printf 'fn terbesar<T>(a: T, b: T) -> T {\n if a > b {\n a\n } else {\n b\n }\n}\n\nfn main() {\n println!("Terbesar: {}", terbesar(3, 7));\n}\n' > tanpa-batas.rs
rustc --edition=2021 tanpa-batas.rs 2>&1 | sed -n '1,12p'error[E0369]: binary operation `>` cannot be applied to type `T`
--> tanpa-batas.rs:2:10
|
2 | if a > b {
| - ^ - T
| |
| T
|
help: consider restricting type parameter `T` with trait `PartialOrd`
|
1 | fn terbesar<T: std::cmp::PartialOrd>(a: T, b: T) -> T {
| ++++++++++++++++++++++Pesan tersebut memuat perbaikannya secara langsung. Bagian help menyarankan batasan T: PartialOrd, yaitu trait pustaka standar yang menyediakan operator pembandingan seperti < dan >. Setelah batasan itu ditambahkan, fungsi dapat menerima setiap tipe yang mengimplementasikan PartialOrd, misalnya bilangan bulat, pecahan, maupun teks milik sendiri.
Batasan tipe T: Trait#
Batasan dituliskan setelah nama parameter tipe dengan tanda titik dua. Pada tanda tangan fn tampilkan_semua<T: Ringkas>(daftar: &[T]), bagian T: Ringkas menyatakan bahwa T hanya boleh diisi tipe yang mengimplementasikan trait Ringkas. Isi fungsi kemudian bebas memanggil metode ringkas karena kompilator sudah tahu setiap pengisi memilikinya, dan pemanggilan dengan tipe yang tidak mengimplementasikan trait itu ditolak saat kompilasi.
trait Ringkas {
fn ringkas(&self) -> String;
}
struct Artikel {
judul: String,
penulis: String,
}
struct Pengumuman {
penulis: String,
topik: String,
}
impl Ringkas for Artikel {
fn ringkas(&self) -> String {
format!("{} oleh {}", self.judul, self.penulis)
}
}
impl Ringkas for Pengumuman {
fn ringkas(&self) -> String {
format!("{}: {}", self.penulis, self.topik)
}
}
fn tampilkan_semua<T: Ringkas>(daftar: &[T]) {
for item in daftar {
println!("- {}", item.ringkas());
}
}
fn main() {
let artikel = [
Artikel {
judul: String::from("Rilis Rust Terbaru"),
penulis: String::from("Budi"),
},
Artikel {
judul: String::from("Panduan Ownership"),
penulis: String::from("Dewi"),
},
];
let pengumuman = [
Pengumuman {
penulis: String::from("Citra"),
topik: String::from("jadwal piket"),
},
Pengumuman {
penulis: String::from("Ali"),
topik: String::from("rapat laboratorium"),
},
];
tampilkan_semua(&artikel);
tampilkan_semua(&pengumuman);
}- Rilis Rust Terbaru oleh Budi
- Panduan Ownership oleh Dewi
- Citra: jadwal piket
- Ali: rapat laboratoriumFungsi tampilkan_semua ditulis satu kali tetapi dipanggil dua kali dengan isi array yang bertipe berbeda, yaitu [Artikel] dan [Pengumuman]. Kedua pemanggilan sah karena kedua tipe mengimplementasikan Ringkas. Pemanggilan ketiga dengan array bilangan bulat akan ditolak karena i32 tidak mengimplementasikan trait tersebut, dan penolakan itu terjadi sebelum program berjalan.
Monomorfisasi saat kompilasi#
Kode generik dengan batasan trait dikompilasi melalui proses monomorphization (monomorfisasi), yaitu pembuatan salinan kode untuk setiap tipe konkret yang benar-benar dipakai. Kompilator menelusuri seluruh pemanggilan tampilkan_semua pada program di atas, menemukan dua tipe pengisi, lalu membuat dua salinan fungsi yang masing-masing bekerja langsung pada Artikel dan Pengumuman. Pada hasil akhirnya, tidak ada kode generik yang tersisa di dalam berkas biner.
Karena setiap salinan memanggil metode secara langsung pada tipe konkretnya, pemanggilan metode ini termasuk static dispatch, yakni pengarahan pemanggilan yang sudah selesai saat kompilasi. Tidak ada penelusuran tabel atau pemeriksaan tipe saat runtime, sehingga kecepatan pemanggilannya sama dengan fungsi yang ditulis khusus untuk satu tipe. Konsekuensinya ada pada ukuran berkas biner, karena setiap tipe pengisi baru menambah satu salinan kode.
Beberapa batasan dengan + dan klausa where#
Satu parameter tipe boleh memuat beberapa batasan sekaligus yang dipisah tanda +. Tanda tangan fn terpanjang<T: Display + PartialOrd>(a: &T, b: &T) menyatakan bahwa T harus dapat dicetak dan dapat dibandingkan, karena isi fungsinya membutuhkan keduanya. Ketika daftar batasan atau jumlah parameter tipe bertambah hingga tanda tangan sulit dibaca, daftar tersebut dipindah ke klausa where yang ditulis sebelum kurung kurawal isi fungsi.
Batasan juga dapat dipasang pada blok impl generik. Pada program berikut, hanya Kotak<T> dengan T: Display yang memiliki metode tampilkan, sehingga metode itu muncul dan hilang mengikuti kemampuan tipe pengisinya.
use std::fmt::Display;
struct Kotak<T> {
isi: T,
}
impl<T: Display> Kotak<T> {
fn tampilkan(&self) {
println!("Kotak berisi: {}", self.isi);
}
}
fn terpanjang<T: Display + PartialOrd>(a: &T, b: &T) -> String {
if a > b {
format!("{}", a)
} else {
format!("{}", b)
}
}
fn bingkai<T, U>(judul: &T, isi: &U) -> String
where
T: Display,
U: Display,
{
format!("[{}] {}", judul, isi)
}
fn main() {
let kotak_angka = Kotak { isi: 42 };
let kotak_teks = Kotak {
isi: String::from("catatan harian"),
};
let sensor = String::from("sensor");
let satuan = String::from("suhu");
let judul = String::from("Laporan");
kotak_angka.tampilkan();
kotak_teks.tampilkan();
println!("Terpanjang: {}", terpanjang(&sensor, &satuan));
println!("{}", bingkai(&judul, &36.5));
}Kotak berisi: 42
Kotak berisi: catatan harian
Terpanjang: suhu
[Laporan] 36.5Fungsi terpanjang memakai dua batasan pada satu parameter karena isinya mencetak hasil dengan Display sekaligus membandingkan dengan PartialOrd. Fungsi bingkai memakai dua parameter tipe dengan batasan yang dipindah ke klausa where, sehingga baris pertama tanda tangannya tetap pendek. Hasil Terpanjang: suhu muncul karena pembandingan teks berjalan leksikografis dari karakter pertama, dan huruf u pada suhu lebih besar daripada huruf e pada sensor.
Metode tampilkan tidak tersedia untuk semua pengisi Kotak. Fragmen berikut memakai Vec<i32> yang tidak mengimplementasikan Display, sehingga pemanggilan metodenya ditolak.
// tidak-dijalankan
let kotak_daftar: Kotak<Vec<i32>> = Kotak { isi: vec![1, 2, 3] };
kotak_daftar.tampilkan();Fragmen tersebut tidak dijalankan karena bergantung pada definisi Kotak pada program sebelumnya di halaman ini. Kompilator menolak pemanggilan tampilkan dengan error E0599 yang menyatakan bahwa metode itu ada untuk Kotak<Vec<i32>> tetapi batasan traitnya tidak terpenuhi, karena Vec<i32> tidak mengimplementasikan Display. Struktur data tetap sah dipakai untuk menyimpan tipe tersebut, dan hanya metode berbatasan Display yang tidak dapat dipanggil.
Kesalahan umum#
Kesalahan pertama adalah lupa menuliskan batasan sama sekali, sehingga error E0369 muncul seperti pada bagian pembuka. Perbaikannya selalu berupa penambahan batasan yang disebutkan bagian help, bukan pengubahan isi fungsi, karena isi fungsi memang membutuhkan kemampuan yang diminta batasan itu.
Kesalahan kedua menyangkut batasan Sized yang diberikan secara implisit. Setiap parameter tipe seperti T otomatis disyaratkan memiliki ukuran yang diketahui saat kompilasi, padahal tipe potongan teks str tidak memiliki ukuran tetap. Akibatnya, pemanggilan bingkai("Laporan", "suhu") dengan literal membuat T disimpulkan sebagai str dan ditolak dengan error E0277 yang menyarankan T: Display + ?Sized. Penanda ?Sized melonggarkan syarat ukuran itu agar tipe potongan juga dapat masuk, dan alternatif yang lebih sederhana adalah mengirim teks milik sendiri seperti &String sehingga T disimpulkan sebagai String.
Kesalahan ketiga adalah memasang batasan yang lebih luas daripada kebutuhan, misalnya mewajibkan Clone padahal isi fungsi hanya membaca data. Batasan yang berlebihan menutup pemanggilan dari tipe yang sebenarnya layak, sehingga daftar batasan sebaiknya berisi kemampuan yang benar-benar dipakai isi fungsi.
Ringkasan#
- Batasan
T: Traitmembatasi tipe pengisi parameter generik pada tipe yang mengimplementasikan trait tersebut, dan pelanggarannya ditolak saat kompilasi. - Monomorphization membuat salinan fungsi per tipe konkret sehingga pemanggilan metode berlangsung sebagai static dispatch tanpa biaya runtime.
- Beberapa batasan digabung dengan tanda
+, dan daftar batasan yang panjang dipindah ke klausawhere. - Batasan pada blok
implmembuat ketersediaan metode mengikuti kemampuan tipe pengisi, misalnya hanyaKotak<T>denganT: Displayyang memiliki metodetampilkan.
Referensi#
- Traits: Defining Shared Behavior — The Rust Programming Language — Bab resmi tentang trait bound, beberapa batasan, klausa where, dan monomorphization.
- Generic Data Types — The Rust Programming Language — Bab resmi tentang sintaks fungsi dan struct generik sebagai dasar batasan tipe.
- Error code E0369 — Rust compiler — Penjelasan resmi error operator biner yang tidak dapat diterapkan pada tipe generik tanpa batasan.