Lewati ke konten

Generik di Rust

Sebenarnya kamu sudah memakai generics (generik) tanpa sadar: Vec<i32> dan Option<T> sama-sama memakainya. Di halaman ini kamu akan belajar menulis fungsi dan struct generik buatanmu sendiri.

Misalnya kamu butuh fungsi yang mengembalikan angka terbesar dari sebuah daftar, untuk i32 dan juga f64. Tanpa generics, kamu harus menulisnya dua kali:

fn largest_i32(numbers: &[i32]) -> i32 {
let mut largest = numbers[0];
for &n in numbers {
if n > largest {
largest = n;
}
}
largest
}
fn largest_f64(numbers: &[f64]) -> f64 {
let mut largest = numbers[0];
for &n in numbers {
if n > largest {
largest = n;
}
}
largest
}

Logikanya sama persis, hanya disalin-tempel untuk tiap tipe. Dengan generics, cukup ditulis sekali.

fn largest<T: PartialOrd + Copy>(numbers: &[T]) -> T {
let mut largest = numbers[0];
for &n in numbers {
if n > largest {
largest = n;
}
}
largest
}
fn main() {
let integers = [10, 25, 3, 47, 8];
let floats = [1.5, 2.8, 0.3];
println!("{}", largest(&integers));
println!("{}", largest(&floats));
}

Output:

47
2.8
▶ Coba sendiri

<T: PartialOrd + Copy> bisa dibaca “untuk tipe T apa saja, asalkan bisa dibandingkan (PartialOrd) dan disalin (Copy).” T hanyalah penanda tempat: Rust membuat versi fungsi ini untuk tipe konkret apa pun yang benar-benar kamu pakai saat memanggilnya (i32, f64, dan seterusnya).

Struct juga bisa generik:

struct Pair<T> {
first: T,
second: T,
}
fn main() {
let numbers = Pair { first: 5, second: 10 };
let words = Pair { first: "halo", second: "dunia" };
println!("{} {}", numbers.first, numbers.second);
println!("{} {}", words.first, words.second);
}

Output:

5 10
halo dunia
▶ Coba sendiri

Satu definisi Pair<T> bisa dipakai untuk angka, teks, atau tipe lain apa pun. numbers bertipe Pair<i32> dan words bertipe Pair<&str>, tapi kamu hanya menulis struct-nya sekali.

Tidak. Rust membuat versi khusus dari fungsi atau struct untuk setiap tipe konkret yang benar-benar kamu pakai (proses ini disebut monomorphization). Saat program berjalan, sudah tidak ada lagi kode generik, yang tersisa hanya fungsi biasa dengan tipe yang jelas, sama cepatnya seperti kalau kamu menulis masing-masing secara manual.

Kamu bisa menambahkan method ke struct generik dengan blok impl, sama seperti di halaman Struct. Bedanya cuma satu: impl-nya juga butuh <T> sendiri:

struct Pair<T> {
first: T,
second: T,
}
impl<T> Pair<T> {
fn new(first: T, second: T) -> Self {
Pair { first, second }
}
fn swap(self) -> Pair<T> {
Pair { first: self.second, second: self.first }
}
}
impl Pair<f64> {
fn average(&self) -> f64 {
(self.first + self.second) / 2.0
}
}
fn main() {
let words = Pair::new("kiri", "kanan").swap();
println!("{} {}", words.first, words.second);
let scores = Pair::new(7.0, 9.0);
println!("{}", scores.average());
}

Output:

kanan kiri
8
▶ Coba sendiri

Ada dua jenis blok impl di sini:

  • impl<T> Pair<T> berarti “method-method ini ada untuk Pair dengan tipe apa saja”. Baca <T> yang pertama sebagai “untuk tipe T apa pun”. Tanpa itu, Rust akan mengira T adalah nama tipe sungguhan dan protes karena tidak bisa menemukannya.
  • impl Pair<f64> tidak punya <T>. Method di dalamnya hanya ada untuk Pair<f64>. Itulah sebabnya average boleh memakai + dan / 2.0. Coba panggil words.average(), dan compiler akan memberi tahu bahwa method itu tidak ada untuk Pair<&str>.

Struct juga bisa punya lebih dari satu parameter tipe, misalnya struct Pair<T, U> { first: T, second: U }, kalau kedua field-nya boleh berbeda tipe. Dan kalau trait bound-nya mulai panjang, kamu bisa memindahkannya ke klausa where supaya signature fungsi tetap mudah dibaca. Itu dibahas di halaman Trait lanjutan.

Tulis fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T yang mengembalikan item terkecil. Uji dengan [8, 3, 5] dan dengan karakter ['r', 'u', 's', 't']. Petunjuk: ini sama dengan fungsi largest di halaman ini, hanya satu perbandingannya dibalik.

Lihat solusi
fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T {
let mut smallest = items[0];
for &item in items {
if item < smallest {
smallest = item;
}
}
smallest
}
fn main() {
println!("{}", smallest(&[8, 3, 5]));
println!("{}", smallest(&['r', 'u', 's', 't']));
}

Output:

3
r
▶ Coba sendiri

2. Bagian yang lebih besar dari sebuah Pair

Section titled “2. Bagian yang lebih besar dari sebuah Pair”

Tambahkan method larger(&self) -> &T ke Pair<T> yang mengembalikan referensi ke field mana pun yang lebih besar. Uji dengan Pair berisi angka dan Pair berisi kata. Petunjuk: membandingkan butuh PartialOrd, jadi taruh bound-nya langsung di impl: impl<T: PartialOrd> Pair<T>.

Lihat solusi
struct Pair<T> {
first: T,
second: T,
}
impl<T: PartialOrd> Pair<T> {
fn larger(&self) -> &T {
if self.first > self.second {
&self.first
} else {
&self.second
}
}
}
fn main() {
let numbers = Pair { first: 3, second: 9 };
let words = Pair { first: "pir", second: "apel" };
println!("{}", numbers.larger());
println!("{}", words.larger());
}

Output:

9
pir
▶ Coba sendiri

String dibandingkan menurut urutan abjad, jadi "pir" dianggap lebih besar daripada "apel".

  • Generics memungkinkan kamu menulis satu fungsi atau struct yang bekerja untuk banyak tipe, tanpa menduplikasi kode.
  • <T> adalah tipe penanda tempat; trait bound (T: PartialOrd) membatasi tipe apa saja yang boleh dipakai sebagai T.
  • impl<T> Pair<T> menambahkan method untuk semua T; impl Pair<f64> menambahkan method hanya untuk satu tipe konkret.
  • Kode generik dikompilasi menjadi versi khusus untuk tiap tipe, jadi tidak ada biaya performa saat program berjalan.

Kuis singkat

1. Masalah apa yang diselesaikan generics?

2. Pada fn largest<T: PartialOrd + Copy>(...), apa fungsi PartialOrd + Copy?

3. Apakah memakai generics menambah biaya saat runtime dibanding kode yang ditulis manual?

4. Apa yang bisa kamu lakukan dengan satu definisi struct Pair<T>?

Skor: 0 / 4