Chapter sebelumnya menghitung biaya dua fungsi kembar yang hanya berbeda tipe. Obatnya sudah kita intip: jadikan tipe sebagai parameter, seperti nilai menjadi parameter pada fungsi biasa. Pada chapter ini kita menulis fungsi generik pertama dengan notasi <T>, mengamati bagaimana kompilator menebak tipe pengganti T di setiap pemanggilan, lalu menemukan pagar pengamannya: T polos tidak boleh melakukan operasi apa pun selain dipindahkan. Pemahaman dua sisi ini — kekuatan dan batasnya — adalah fondasi seluruh section Generics.
Fungsi Generik Pertama Kita #
Kita mulai dari kebutuhan sederhana: mengambil elemen pertama sebuah koleksi. Logika ini berguna untuk array skor i32 sekaligus array nama String, jadi tipe elemen kita jadikan parameter:
fn first_element<T>(items: &[T]) -> &T {
&items[0]
}
fn main() {
let scores = [72, 85, 90, 68];
let names = [String::from("Budi"), String::from("Siti"), String::from("Andi")];
let first_score = first_element(&scores);
let first_name = first_element(&names);
println!("Skor pertama: {}", first_score);
println!("Nama pertama: {}", first_name);
}Output yang Dihasilkan:
Skor pertama: 72
Nama pertama: BudiSatu definisi first_element melayani dua tipe sekaligus. Mari kita bedah kode di atas baris per baris:
- Baris 1 (
fn first_element<T>(items: &[T]) -> &T {): bagian<T>mendeklarasikan type parameter bernamaT— huruf besar tunggal sesuai kebiasaan komunitas Rust. Deklarasi ini membuatTsah dipakai pada signature: parameteritemsbertipe&[T](slice berisi elemen bertipeT) dan nilai kembali bertipe&T(reference ke satu elemen bertipe sama). - Baris 2 (
&items[0]): fungsi mengembalikan reference menuju elemen pertama — tidak menyalin, tidak mengambil kepemilikan. Karena hanya meminjam,Tboleh berupaStringyang tidakCopy; aturan elision lifetime mengikat umur reference hasil ke umur pinjaman slice, sama seperti fungsi slice pada Section Slice dan Referensi. - Baris 6-7 (dua deklarasi
let):scoresbertipe[i32; 4], sedangkannamesbertipe[String; 3]— dua tipe elemen yang berbeda total. - Baris 9 (
first_element(&scores)): pemanggilan pertama mengikatT = i32. Pemanggilan kedua pada baris 10 mengikatT = String. Dua ikatan berbeda dari satu definisi yang sama. - Baris 12-13 (dua
println!): hasil dicetak dengan formatter{}. Pencetakan sah karena pada titik inifirst_scoresudah pasti bertipei32danfirst_namesudah pasti bertipe&String— dua tipe konkret yang mengimplementasikanDisplay.
Mekanisme: Inferensi Tipe pada Setiap Pemanggilan #
Kompilator tidak pernah menjalankan kode dengan T yang mengambang. Saat membaca first_element(&scores), kompilator memeriksa argumen: &scores bertipe &[i32; 4], lalu deref coercion mengubahnya menjadi &[i32], sehingga T disimpulkan i32. Pemanggilan first_element(&names) menjalankan penalaran yang sama dan menyimpulkan T = String. Proses ini disebut type inference — mekanisme yang sama dengan inferensi let x = 5; yang sudah kita kenal di Chapter Type Inference, hanya saja yang kini disimpulkan adalah type parameter.
Kompilator memeriksa body fungsi generik tepat satu kali dengan T sebagai simbol. Pemeriksaan ini bertuju memastikan body sah untuk T apa pun yang diperbolehkan signature — misalnya body tidak boleh memanggil operasi yang tidak dijamin tersedia, pembatasan yang akan kita rasakan di bagian akhir chapter. Setelah seluruh program lolos pemeriksaan, tahap pembuatan kode menyalin body untuk setiap tipe konkret yang benar-benar dipakai; hasil salinan itulah yang dieksekusi, dan T tidak pernah muncul di runtime.
Saat kita ingin menegaskan pilihan tipe secara eksplisit — atau saat petunjuk inferensi memang tidak tersedia — Rust menyediakan sintaks turbofish ::<Tipe>:
fn first_element<T>(items: &[T]) -> &T {
&items[0]
}
fn main() {
let scores = [72, 85, 90, 68];
let inferred = first_element(&scores);
let explicit = first_element::<i32>(&scores);
println!("Hasil inferensi: {}", inferred);
println!("Hasil eksplisit: {}", explicit);
}Output yang Dihasilkan:
Hasil inferensi: 72
Hasil eksplisit: 72Mari kita bedah bagian penting kode di atas:
- Baris 8 (
let inferred = first_element(&scores);): tanpa anotasi apa pun, inferensi menyimpulkanT = i32dari argumen. - Baris 9 (
first_element::<i32>(&scores)): turbofish::<i32>menyebut tipe penggantiTsecara eksplisit sebelum argumen. Hasil pemanggilan sama persis; penulisan ini berguna saat inferensi kehabisan petunjuk, misalnya pada pemanggilan"30".parse()yang harus tahu tipe hasil konversinya. - Baris 11-12 (dua
println!): kedua nilai identik — pemilihan antara inferensi dan turbofish hanyalah soal kejelasan, bukan perilaku.
Lebih dari Satu Type Parameter #
Fungsi generik tidak terbatas satu placeholder. Ketika dua bagian data boleh bertipe berbeda, kita mendeklarasikan dua type parameter:
fn make_pair<A, B>(first: A, second: B) -> (A, B) {
(first, second)
}
fn main() {
let player = make_pair(String::from("Budi"), 7);
let position = make_pair('x', 4.5);
println!("{} bernomor punggung {}", player.0, player.1);
println!("Titik {}: nilai {}", position.0, position.1);
}Output yang Dihasilkan:
Budi bernomor punggung 7
Titik x: nilai 4.5Mari kita bedah kode di atas baris per baris:
- Baris 1 (
fn make_pair<A, B>(first: A, second: B) -> (A, B)): dua type parameterAdanBdideklarasikan sekaligus dan keduanya saling bebas —AdanBboleh sama boleh beda. Nilai kembali berupa tuple(A, B)yang tipe pasangannya mengikuti input. - Baris 6 (
make_pair(String::from("Budi"), 7)): inferensi mengikatA = StringdanB = i32, menghasilkan tipe(String, i32). - Baris 7 (
make_pair('x', 4.5)): pemanggilan kedua mengikatA = chardanB = f64— tipe pasangan yang sama sekali berbeda dari pemanggilan pertama. - Baris 9-10 (akses
.0dan.1): elemen tuple diakses dengan indeks bernama; tiap elemen sudah bertipe konkret sehingga pencetakan{}berjalan normal.
Penamaan T hanyalah kebiasaan — kompilator menerima nama CamelCase apa pun. Standard library memakai nama pendek yang bermakna: HashMap<K, V> untuk key dan value, Result<T, E> untuk success type dan error type. Nama yang deskriptif membantu pembaca signature, sama seperti nama parameter fungsi yang deskriptif.
T Polos Hanya Bisa Dipindahkan #
Batas pertama yang akan kita tabrak: type parameter tanpa syarat apa pun tidak bisa dioperasikan. Fungsi berikut mencoba mencetak nilai generik dan gagal dikompilasi:
fn announce<T>(value: T) {
println!("Nilai: {}", value);
}
fn main() {
announce(42);
}Pesan error kompilator:
error[E0277]: `T` doesn't implement `std::fmt::Display`
--> main.rs:2:27
|
2 | println!("Nilai: {}", value);
| -- ^^^^^ `T` cannot be formatted with the default formatter
| |
| required by this formatting parameter
|
= note: in format strings you may be able to use `{:?}` (or `{:#?}` for pretty-print) instead
help: consider restricting type parameter `T` with trait `Display`
|
1 | fn announce<T: std::fmt::Display>(value: T) {
| +++++++++++++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.Pesan E0277 ini menarik untuk dibaca pelan-pelan. Meskipun pemanggil mengirim 42 — dan i32 jelas bisa dicetak — kompilator tetap menolak, karena pemeriksaan body dilakukan terhadap semua T yang mungkin, termasuk tipe yang tidak mengimplementasikan Display. Formatter {} hanya sah untuk tipe yang mengimplementasikan trait Display, dan T polos tidak menjanjikan apa-apa. Solusinya sudah tersirat pada saran kompilator: tambahkan syarat T: std::fmt::Display pada deklarasi. Syarat semacam ini disebut trait bound dan menjadi pokok bahasan Chapter Trait Bound.
Ringkasan #
Notasi <T> mengubah tipe menjadi parameter: satu definisi fungsi melayani banyak tipe, dengan inferensi yang mengikat T per pemanggilan dan turbofish ::<Tipe> sebagai penegasan eksplisit. Fungsi boleh mendeklarasikan beberapa type parameter yang saling bebas seperti make_pair<A, B>. Pagar pengamennya tegas — T polos tidak bisa dicetak atau dioperasikan karena kompilator memeriksa body terhadap semua kemungkinan T. Kunci untuk membuka operasi adalah trait bound, dan chapter berikutnya menggunakannya untuk menghidupkan kembali fungsi largest dari Chapter Masalah Duplikasi Kode.