Lewati ke konten

Generik Lanjutan di Rust: Const Generics, const fn, dan PhantomData

Di halaman Generik, parameter generik selalu berupa tipe: Pair<T> bisa berisi i32 atau &str. Halaman ini menunjukkan tiga alat yang membawa ide yang sama lebih jauh:

  • Const generics (generik konstan) membuat sebuah angka bisa menjadi parameter generik, seperti angka 3 di [i32; 3].
  • const fn membuat compiler bisa menjalankan fungsimu saat ia sedang membangun program, jadi jawabannya sudah siap sebelum program dimulai.
  • PhantomData membuat sebuah tipe bisa membawa label (misalnya “ini ID user” atau “panjang ini dalam kaki”) tanpa menyimpan data tambahan apa pun.

Bayangkan kotak telur. Kotak isi 6 dan kotak isi 12 adalah produk yang berbeda, walaupun keduanya sama-sama “kotak telur”. Ukurannya adalah bagian dari apa mereka. Const generics membuatmu bisa mengatakan hal itu di Rust. Baca halaman ini pelan-pelan saja: setiap bagian bisa berdiri sendiri, dan kamu boleh kembali ke bagian-bagian akhir nanti saat kamu membutuhkannya.

Const generics: angka sebagai parameter generik

Section titled “Const generics: angka sebagai parameter generik”

Di Rust, panjang array adalah bagian dari tipenya. [u32; 3] dan [u32; 4] sama berbedanya dengan i32 dan String. Fungsi yang menerima [u32; 3] tidak akan mau menerima empat item:

error[E0308]: mismatched types
--> src/main.rs:7:26
|
7 | println!("{}", total(basket));
| ----- ^^^^^^ expected an array with a size of 3, found one with a size of 4
| |
| arguments to this function are incorrect

Compiler bilang tipenya tidak cocok: ia mengharapkan array berukuran 3, tapi menerima array berukuran 4. Const generic menyelesaikan masalah ini. Kamu menulis const N: usize di dalam kurung sudut, dan fungsinya bekerja untuk array dengan panjang berapa pun:

fn summary<const N: usize>(prices: [u32; N]) -> String {
let mut sum = 0;
for price in prices {
sum += price;
}
// Di dalam fungsi, N hanyalah angka biasa
format!("{N} barang seharga {sum}")
}
fn main() {
let small_basket = [4, 10, 2];
let big_basket = [3, 8, 1, 12, 6];
println!("{}", summary(small_basket));
println!("{}", summary(big_basket));
}

Output:

3 barang seharga 16
5 barang seharga 30
▶ Coba sendiri

Baca <const N: usize> sebagai “untuk angka N berapa pun yang bertipe usize”. Rust menyimpulkan N dari array yang kamu berikan, sama seperti ia menyimpulkan T pada generics biasa. Dan sama seperti generics biasa, compiler membuat salinan summary yang terpisah untuk setiap panjang yang kamu pakai, jadi tidak ada biaya saat runtime.

Jujur saja: untuk fungsi sekecil ini, slice (prices: &[u32]) juga bisa dan lebih sederhana. Lihat Slice. Const generics benar-benar berguna saat panjangnya harus menjadi bagian dari sebuah tipe: disimpan di dalam struct tanpa alokasi heap, atau dipakai di tipe kembalian. Itu yang kita bahas berikutnya.

Bayangkan denah kursi bioskop: beberapa baris, masing-masing dengan jumlah kursi yang sama. Berikut sebuah matriks yang jumlah barisnya R dan jumlah kolomnya C sama-sama menjadi bagian dari tipenya:

struct Matrix<const R: usize, const C: usize> {
cells: [[i32; C]; R],
}
impl<const R: usize, const C: usize> Matrix<R, C> {
fn new(cells: [[i32; C]; R]) -> Self {
Matrix { cells }
}
fn size(&self) -> String {
format!("{R}x{C}")
}
// Baris jadi kolom: matriks R x C berubah menjadi matriks C x R
fn transpose(&self) -> Matrix<C, R> {
let mut cells = [[0; R]; C];
for row in 0..R {
for col in 0..C {
cells[col][row] = self.cells[row][col];
}
}
Matrix { cells }
}
}
fn main() {
let seats = Matrix::new([[1, 2, 3], [4, 5, 6]]);
let flipped = seats.transpose();
println!("{} {:?}", seats.size(), seats.cells);
println!("{} {:?}", flipped.size(), flipped.cells);
}

Output:

2x3 [[1, 2, 3], [4, 5, 6]]
3x2 [[1, 4], [2, 5], [3, 6]]
▶ Coba sendiri

Lihat tipe kembalian transpose: Matrix<C, R>. Compiler tahu bahwa membalik matriks 2x3 menghasilkan matriks 3x2. Kalau kamu salah di dalamnya, misalnya membuat matriks dengan bentuk yang keliru, kodenya tidak akan bisa dikompilasi. Seluruh grid tinggal di dalam struct, tanpa Vec dan tanpa heap. Baris impl<const R: usize, const C: usize> bekerja seperti impl<T>: artinya “method-method ini ada untuk semua ukuran”.

Contoh praktis: ring buffer berukuran tetap

Section titled “Contoh praktis: ring buffer berukuran tetap”

Dashcam mobil hanya menyimpan video beberapa menit terakhir. Saat penuh, rekaman terbaru menimpa yang paling lama. Itulah ring buffer (buffer melingkar), dan ia sangat cocok untuk const generics: kapasitasnya tetap, jadi item-itemnya bisa disimpan di dalam array.

Ring buffer ini generik terhadap tipe T dan ukuran N sekaligus, jadi baris impl-nya menjadi impl<T, const N: usize>:

struct RingBuffer<T, const N: usize> {
items: [T; N],
next: usize, // tempat push berikutnya
len: usize, // jumlah item yang tersimpan (tidak pernah lebih dari N)
}
impl<T: Copy + Default, const N: usize> RingBuffer<T, N> {
fn new() -> Self {
RingBuffer { items: [T::default(); N], next: 0, len: 0 }
}
fn push(&mut self, item: T) {
self.items[self.next] = item; // menimpa item paling lama saat penuh
self.next = (self.next + 1) % N;
if self.len < N {
self.len += 1;
}
}
// Item-item yang tersimpan, dari yang paling lama
fn to_vec(&self) -> Vec<T> {
let start = (self.next + N - self.len) % N;
(0..self.len).map(|i| self.items[(start + i) % N]).collect()
}
}
fn main() {
// Simpan hanya 3 bacaan suhu terakhir
let mut recent: RingBuffer<i32, 3> = RingBuffer::new();
for temp in [18, 19, 21, 24, 22] {
recent.push(temp);
println!("setelah {temp}: {:?}", recent.to_vec());
}
}

Output:

setelah 18: [18]
setelah 19: [18, 19]
setelah 21: [18, 19, 21]
setelah 24: [19, 21, 24]
setelah 22: [21, 24, 22]
▶ Coba sendiri

[T::default(); N] mengisi array dengan N salinan nilai default (0 untuk i32). Mengulang nilai seperti itu butuh T: Copy, karena itulah impl-nya meminta Copy + Default. RingBuffer<i32, 3> dan RingBuffer<i32, 60> adalah tipe yang berbeda, jadi kamu tidak akan pernah bisa memberikan buffer 3 slot di tempat yang mengharapkan buffer 60 slot. Kalau kamu lebih butuh versi standard library yang bisa membesar, lihat VecDeque & BinaryHeap.

Nilai default, kurung kurawal, dan batasan saat ini

Section titled “Nilai default, kurung kurawal, dan batasan saat ini”

Parameter const bisa punya nilai default, ditulis const N: usize = 4. Kamu juga bisa memberikan konstanta bernama, atau ekspresi di dalam kurung kurawal:

struct Buffer<const N: usize = 4> {
data: [u8; N],
}
impl<const N: usize> Buffer<N> {
fn new() -> Self {
Buffer { data: [0; N] }
}
}
const PAGE: usize = 8;
fn main() {
let small: Buffer = Buffer::new(); // tanpa angka: default, 4
let page = Buffer::<PAGE>::new(); // konstanta bernama
let double = Buffer::<{ PAGE * 2 }>::new(); // ekspresi butuh { }
println!("{} {} {}", small.data.len(), page.data.len(), double.data.len());
}

Output:

4 8 16
▶ Coba sendiri

Nilai default hanya berlaku saat kamu menuliskan tipenya tanpa angka, seperti let small: Buffer. let small = Buffer::new(); saja akan gagal dengan error[E0284]: type annotations needed for Buffer<_> (“butuh anotasi tipe”), karena Rust tidak menebak nilai default dari sebuah ekspresi.

Const generics di Rust stable memang berguna, tapi sengaja dibatasi. Sebaiknya kamu tahu batas-batasnya sebelum menabraknya:

  • Tipe yang diizinkan: hanya integer (usize, u8, i32, …), bool, dan char. Mencoba const NAME: &'static str menghasilkan “&'static str is forbidden as the type of a const generic parameter” (&'static str dilarang sebagai tipe parameter const generic).
  • Tidak boleh berhitung dengan parameter generik di dalam tipe. [f32; N * 2] di dalam struct atau signature generik adalah error (“generic parameters may not be used in const operations”). N saja tidak masalah, begitu juga N * 2 di dalam badan fungsi, tempat N hanyalah sebuah nilai. Fitur yang akan mengizinkan N * 2 di dalam tipe (generic_const_exprs) masih hanya ada di nightly.
  • Memeriksa nilai. Kamu tidak bisa menulis batasan seperti “N harus lebih dari 0”. Yang paling mendekati adalah blok const inline di dalam fungsi: const { assert!(N > 0, "need at least one slot") };. Ia gagal saat kamu membangun program dengan N yang salah (cargo check mungkin tidak menangkapnya).

Beberapa toko mencetak daftar harganya sekali saja, daripada menghitung setiap harga di kasir. const fn adalah fungsi yang boleh dijalankan compiler saat ia membangun programmu, jadi jawabannya sudah “tercetak” di dalam program sejak awal.

Kamu sudah bertemu item const di halaman Variabel. Nilainya harus diketahui saat kompilasi (compile time), jadi biasanya kamu hanya bisa memakai literal dan hitungan sederhana. Dengan const fn, kamu juga bisa memanggil fungsimu sendiri di sana:

const fn square(n: usize) -> usize {
n * n
}
const BOARD_SIDE: usize = 4;
const CELLS: usize = square(BOARD_SIDE); // dihitung oleh compiler
fn main() {
// Panjang array harus diketahui saat kompilasi, dan CELLS memenuhinya
let board = [0u8; CELLS];
println!("papan {BOARD_SIDE}x{BOARD_SIDE} punya {} sel", board.len());
println!("square(12) saat runtime adalah {}", square(12));
}

Output:

papan 4x4 punya 16 sel
square(12) saat runtime adalah 144
▶ Coba sendiri

const fn tetaplah fungsi biasa. square(12) di main adalah pemanggilan biasa saat runtime. Ia hanya wajib berjalan saat kompilasi kalau dipakai di const context (konteks konstan): item const atau static, panjang array, argumen const generic, atau blok const { ... }.

Contoh klasiknya adalah lookup table (tabel pencarian): hitung sederet nilai sekali saja saat kompilasi, lalu tinggal membacanya saat runtime. Di sini compiler membangun tabel “hari ke berapa dalam setahun setiap bulan dimulai” untuk tahun yang bukan kabisat:

const DAYS_IN_MONTH: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const fn days_before_each_month() -> [u32; 12] {
let mut table = [0; 12];
let mut month = 1;
while month < 12 {
table[month] = table[month - 1] + DAYS_IN_MONTH[month - 1];
month += 1;
}
table
}
// Dibangun sekali oleh compiler. Saat runtime isinya cuma 12 angka.
const DAYS_BEFORE: [u32; 12] = days_before_each_month();
fn day_of_year(month: usize, day: u32) -> u32 {
DAYS_BEFORE[month - 1] + day
}
fn main() {
println!("{:?}", DAYS_BEFORE);
println!("1 Maret adalah hari ke-{}", day_of_year(3, 1));
println!("25 Desember adalah hari ke-{}", day_of_year(12, 25));
}

Output:

[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
1 Maret adalah hari ke-60
25 Desember adalah hari ke-359
▶ Coba sendiri

Perhatikan perulangan while yang dipakai, bukan for. Itu bukan soal selera gaya. Berikut yang bisa kamu pakai di dalam const fn saat ini:

  • let dan let mut, aritmetika, perbandingan, if, match, loop, dan while
  • array, tuple, struct dan enum, serta pengindeksan seperti table[month]
  • pemanggilan const fn lain (banyak fungsi standar yang sudah const, seperti u32::pow atau slice.len())
  • referensi &mut dan hitungan floating-point (stable sejak Rust 1.83 dan 1.82)
  • panic! dan assert!, yang menjadi error kompilasi kalau terpicu saat kompilasi

Dan yang belum bisa dipakai: perulangan for (butuh trait Iterator, dan method trait belum bisa const di stable), pemanggilan fungsi biasa seperti String::from atau println!, dan apa pun yang mengalokasikan memori di heap, seperti push ke Vec.

const atau static? Keduanya nilai global yang tidak pernah berubah, tapi cara penyimpanannya berbeda:

const static
Tempat tinggalnya disalin ke setiap tempat kamu memakainya satu tempat tetap di memori
Alamat bisa berbeda setiap kali selalu sama, hidup selama program berjalan ('static)
Bisa mutable? tidak pernah static mut ada, tapi butuh unsafe
Cocok untuk angka, tabel kecil, pengaturan tabel besar yang ingin disimpan sekali, atau global yang perlu kamu ambil referensinya

Kalau ragu, pakai const. Kalau kamu butuh global yang bisa diubah, pakai static yang berisi atomic atau Mutex, bukan static mut. Lihat Shared State.

PhantomData: label yang tidak bisa tertukar

Section titled “PhantomData: label yang tidak bisa tertukar”

Ini bug yang sering sekali terjadi di sistem nyata. ID user dan ID pesanan sama-sama cuma angka, jadi tidak ada yang mencegahmu memberikan ID user ke cancel_order. Kamu ingin tipenya membawa label: Id<User> versus Id<Order>. Percobaan pertamamu mungkin seperti ini:

struct Id<T> {
value: u32,
}
error[E0392]: type parameter `T` is never used
--> src/main.rs:1:11
|
1 | struct Id<T> {
| ^ unused type parameter
|
= help: consider removing `T`, referring to it in a field, or using a marker such as `PhantomData`

Rust menolak parameter tipe yang tidak dipakai oleh field mana pun (“type parameter T is never used”). Solusinya persis seperti yang disarankan compiler. std::marker::PhantomData<T> adalah field yang menyatakan “struct ini terhubung dengan T”, tapi tidak menyimpan apa-apa. Ukurannya nol byte:

use std::marker::PhantomData;
struct User;
struct Order;
struct Id<T> {
value: u32,
_marker: PhantomData<T>, // nol byte, cuma label
}
impl<T> Id<T> {
fn new(value: u32) -> Self {
Id { value, _marker: PhantomData }
}
}
fn find_user(id: &Id<User>) -> &'static str {
if id.value == 7 { "Amara" } else { "tidak dikenal" }
}
fn cancel_order(id: &Id<Order>) {
println!("membatalkan pesanan #{}", id.value);
}
fn main() {
let user_id: Id<User> = Id::new(7);
let order_id: Id<Order> = Id::new(7);
println!("user #{} adalah {}", user_id.value, find_user(&user_id));
cancel_order(&order_id);
println!("sebuah Id berukuran {} byte", std::mem::size_of::<Id<User>>());
}

Output:

user #7 adalah Amara
membatalkan pesanan #7
sebuah Id berukuran 4 byte
▶ Coba sendiri

Kedua ID berisi angka 7, dan keduanya berukuran 4 byte, persis seperti u32 biasa. Tapi sekarang tambahkan cancel_order(&user_id); sebagai baris terakhir main:

error[E0308]: mismatched types
--> src/main.rs:32:18
|
32 | cancel_order(&user_id);
| ------------ ^^^^^^^^ expected `&Id<Order>`, found `&Id<User>`
| |
| arguments to this function are incorrect

Compiler mengharapkan &Id<Order>, tapi menerima &Id<User>. Kesalahan tertukar ini tertangkap sebelum program sempat berjalan. PhantomData adalah sebuah nilai sekaligus sebuah tipe, karena itulah kamu menulis _marker: PhantomData saat membuat struct-nya. Awalan _ hanyalah kebiasaan penamaan yang umum untuk “field ini tidak pernah kamu baca”.

Pada tahun 1999, NASA kehilangan wahana Mars Climate Orbiter karena satu perangkat lunak menghasilkan angka dalam satuan imperial, sementara perangkat lunak lain mengharapkan satuan metrik. Keduanya “cuma angka”. Dengan PhantomData, satuannya menjadi bagian dari tipe, dan satu Length<U> generik bisa mencakup semua satuan:

use std::marker::PhantomData;
use std::ops::Add;
struct Meters;
struct Feet;
struct Length<U> {
value: f64,
_unit: PhantomData<U>,
}
impl<U> Length<U> {
fn new(value: f64) -> Self {
Length { value, _unit: PhantomData }
}
}
// Satu impl untuk semua satuan, tapi kedua sisi harus bersatuan sama
impl<U> Add for Length<U> {
type Output = Length<U>;
fn add(self, other: Length<U>) -> Length<U> {
Length::new(self.value + other.value)
}
}
impl Length<Feet> {
fn to_meters(self) -> Length<Meters> {
Length::new(self.value * 0.3048)
}
}
fn main() {
let run: Length<Meters> = Length::new(400.0) + Length::new(250.0);
let climb: Length<Feet> = Length::new(1000.0);
let climb_m = climb.to_meters();
println!("tanjakan: {:.1} m", climb_m.value);
let total = run + climb_m;
println!("total: {:.1} m", total.value);
}

Output:

tanjakan: 304.8 m
total: 954.8 m
▶ Coba sendiri

Tulis run + climb (lupa memanggil to_meters()), dan Rust menolaknya:

error[E0308]: mismatched types
--> src/main.rs:39:23
|
39 | let total = run + climb;
| ^^^^^ expected `Length<Meters>`, found `Length<Feet>`

Di halaman Trait Lanjutan, Meters dan Centimeters adalah dua struct terpisah, masing-masing dengan impl Add sendiri. Di sini, satu impl<U> Add for Length<U> melayani semua satuan yang akan pernah kamu jumlahkan.

Typestate: pintu yang tidak bisa dikunci saat terbuka

Section titled “Typestate: pintu yang tidak bisa dikunci saat terbuka”

Trik yang sama bisa melacak state (keadaan) sesuatu. Sebuah pintu bisa terbuka, tertutup, atau terkunci. Kamu bisa mengunci pintu yang tertutup, tapi mengunci pintu yang terbuka tidak masuk akal. Dengan label state di dalam tipe, langkah yang tidak valid memang tidak ada:

use std::marker::PhantomData;
struct Open;
struct Closed;
struct Locked;
struct Door<State> {
room: String,
_state: PhantomData<State>,
}
// Tersedia di semua state
impl<State> Door<State> {
fn room(&self) -> &str {
&self.room
}
// Pintu yang sama, label state baru
fn into_state<Next>(self, action: &str) -> Door<Next> {
println!("{}: {action}", self.room);
Door { room: self.room, _state: PhantomData }
}
}
impl Door<Closed> {
fn new(room: &str) -> Self {
Door { room: room.to_string(), _state: PhantomData }
}
fn open(self) -> Door<Open> {
self.into_state("dibuka")
}
fn lock(self) -> Door<Locked> {
self.into_state("dikunci")
}
}
impl Door<Open> {
fn close(self) -> Door<Closed> {
self.into_state("ditutup")
}
}
impl Door<Locked> {
fn unlock(self) -> Door<Closed> {
self.into_state("kuncinya dibuka")
}
}
fn main() {
let door = Door::new("Ruang server").open().close().lock();
println!("{} dikunci untuk malam ini", door.room());
let door = door.unlock().open();
println!("{} terbuka lagi", door.room());
}

Output:

Ruang server: dibuka
Ruang server: ditutup
Ruang server: dikunci
Ruang server dikunci untuk malam ini
Ruang server: kuncinya dibuka
Ruang server: dibuka
Ruang server terbuka lagi
▶ Coba sendiri

Sekarang coba kunci pintu yang sedang terbuka, Door::new("Ruang server").open().lock():

error[E0599]: no method named `lock` found for struct `Door<Open>` in the current scope
--> src/main.rs:50:49
|
7 | struct Door<State> {
| ------------------ method `lock` not found for this struct
...
50 | let door = Door::new("Ruang server").open().lock();
| ^^^^ method not found in `Door<Open>`
|
= note: the method was found for `Door<Closed>`

Pesannya bilang tidak ada method lock untuk Door<Open>, dan catatannya menambahkan bahwa method itu ada untuk Door<Closed>. Setiap method menerima self secara nilai (by value), jadi pintu yang lama “habis terpakai” dan kamu tidak bisa terus memakai pintu “tertutup” setelah membukanya. Halaman Design Pattern menunjukkan ide typestate yang sama, tapi setiap state juga menyimpan datanya sendiri. PhantomData adalah versi ringannya, untuk saat state-nya hanya berupa label.

Method untuk satu ukuran, dan associated const

Section titled “Method untuk satu ukuran, dan associated const”

Kamu sudah melihat impl Pair<f64> di halaman Generik: method khusus untuk satu tipe konkret saja. Hal yang sama bisa dilakukan dengan const generics. Ada method yang hanya masuk akal untuk ukuran tertentu:

struct Matrix<const R: usize, const C: usize> {
cells: [[i32; C]; R],
}
// Hanya matriks persegi: jumlah baris dan kolom sama-sama N
impl<const N: usize> Matrix<N, N> {
fn trace(&self) -> i32 {
let mut sum = 0;
for i in 0..N {
sum += self.cells[i][i];
}
sum
}
}
// Hanya matriks 2x2
impl Matrix<2, 2> {
fn determinant(&self) -> i32 {
let [[a, b], [c, d]] = self.cells;
a * d - b * c
}
}
fn main() {
let small = Matrix { cells: [[3, 8], [4, 6]] };
let big = Matrix { cells: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] };
println!("kecil: trace {}, determinan {}", small.trace(), small.determinant());
println!("besar: trace {}", big.trace());
}

Output:

kecil: trace 9, determinan -14
besar: trace 15
▶ Coba sendiri

big.determinant() tidak akan bisa dikompilasi, begitu juga trace() pada matriks 2x3: kamu akan mendapat error seperti error[E0599]: no method named trace found for struct Matrix<2, 3>, dengan catatan the method was found for Matrix<N, N> (method-nya ada, tapi hanya untuk matriks persegi). Aturan “hanya matriks persegi yang punya trace” kini tertulis di dalam tipe.

Associated const (konstanta terkait) adalah konstanta milik sebuah tipe atau trait, sama seperti method. Di dalam trait, setiap implementasi memilih nilainya sendiri, dan kode generik bisa membacanya sebagai U::NAME. Yuk beri satuan-satuan tadi sebuah simbol dan ukuran:

use std::fmt;
use std::marker::PhantomData;
trait Unit {
const SYMBOL: &'static str;
const IN_METERS: f64; // satu satuan ini sama dengan berapa meter
}
struct Meters;
struct Feet;
struct Miles;
impl Unit for Meters {
const SYMBOL: &'static str = "m";
const IN_METERS: f64 = 1.0;
}
impl Unit for Feet {
const SYMBOL: &'static str = "ft";
const IN_METERS: f64 = 0.3048;
}
impl Unit for Miles {
const SYMBOL: &'static str = "mi";
const IN_METERS: f64 = 1609.344;
}
struct Length<U> {
value: f64,
_unit: PhantomData<U>,
}
impl<U: Unit> Length<U> {
fn new(value: f64) -> Self {
Length { value, _unit: PhantomData }
}
// Bekerja untuk dua satuan apa pun, memakai konstanta mereka
fn convert<To: Unit>(&self) -> Length<To> {
Length::new(self.value * U::IN_METERS / To::IN_METERS)
}
}
impl<U: Unit> fmt::Display for Length<U> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:.2} {}", self.value, U::SYMBOL)
}
}
fn main() {
let marathon: Length<Miles> = Length::new(26.2);
let everest: Length<Feet> = Length::new(29032.0);
println!("{} = {}", marathon, marathon.convert::<Meters>());
println!("{} = {}", everest, everest.convert::<Meters>());
println!("{} = {}", everest, everest.convert::<Miles>());
}

Output:

26.20 mi = 42164.81 m
29032.00 ft = 8848.95 m
29032.00 ft = 5.50 mi
▶ Coba sendiri

U::IN_METERS dicari saat kompilasi, jadi tidak ada tabel yang perlu ditelusuri saat runtime. Menambah satuan baru, misalnya Kilometers, cukup dengan satu blok impl Unit kecil, dan convert serta Display langsung bekerja untuknya. impl biasa (inherent impl) juga bisa punya associated const, misalnya impl<T, const N: usize> RingBuffer<T, N> { const CAPACITY: usize = N; }, yang dibaca sebagai RingBuffer::<i32, 3>::CAPACITY.

Kalau kamu ingin satu implementasi trait untuk semua tipe yang memenuhi suatu syarat, itu namanya blanket impl, dibahas di halaman Trait Lanjutan.

Kapan sebaiknya tidak memakai alat-alat ini

Section titled “Kapan sebaiknya tidak memakai alat-alat ini”

Fitur-fitur ini sangat kuat, dan justru karena itu menggoda untuk dipakai di mana-mana. Beberapa saran jujur:

  • Slice atau Vec sering kali sudah cukup. Kalau sebuah fungsi hanya membaca item, &[T] menerima panjang berapa pun dan lebih mudah dibaca. Kalau panjangnya baru diketahui saat runtime (dari file atau input user), kamu memang butuh Vec.
  • Array besar tinggal di stack. RingBuffer<u8, 10_000_000> berarti sepuluh megabyte di dalam struct, dan itu bisa membuat stack meluap (stack overflow). Masukkan ke dalam Box (lihat Smart Pointer) atau pakai Vec.
  • Setiap ukuran atau tipe baru berarti satu salinan kode baru. Memakai generik dengan 20 nilai N yang berbeda akan membangun 20 versi. Itu menambah waktu kompilasi dan ukuran binary. Lihat Performa.
  • Typestate menambah jumlah tipe dan membuat pesan error lebih panjang. Ia sangat bagus untuk API yang akan dipakai banyak orang (builder, koneksi, alur pembayaran). Untuk state yang berubah berdasarkan data saat runtime, seperti status yang dimuat dari database, pakai enum dan match.

Aturan yang bagus: mulai dari yang sederhana, dan pakai alat-alat ini saat muncul bug nyata atau biaya nyata yang bisa mereka cegah.

Berhitung dengan parameter const di dalam tipe. Rasanya wajar, tapi Rust stable belum mengizinkannya:

struct Pairs<const N: usize> {
values: [f32; N * 2],
}
error: generic parameters may not be used in const operations
--> src/main.rs:2:19
|
2 | values: [f32; N * 2],
| ^ cannot perform const operation using `N`
|
= help: const parameters may only be used as standalone arguments here, i.e. `N`

Artinya: parameter generik tidak boleh dipakai dalam operasi const, dan di posisi ini parameter const hanya boleh ditulis sendirian, yaitu N. Solusinya: pakai [[f32; 2]; N] (array berisi pasangan), tambahkan parameter kedua, atau pakai Vec kalau ukurannya tidak perlu ada di dalam tipe.

Lupa kurung kurawal di sekitar ekspresi. Buffer::<2 + 2>::new() menghasilkan error: expressions must be enclosed in braces to be used as const generic arguments (ekspresi harus diapit kurung kurawal), dan baris help-nya menunjukkan perbaikannya: Buffer::<{ 2 + 2 }>::new(). Angka biasa atau nama const tidak butuh kurung kurawal.

Memanggil sesuatu yang bukan const di dalam const context. Sebuah const tidak bisa memanggil fungsi biasa, dan const fn tidak bisa memakai perulangan for:

const GREETING: String = String::from("hello");
error[E0015]: cannot call non-const associated function `<String as From<&str>>::from` in constants
--> src/main.rs:1:26
|
1 | const GREETING: String = String::from("hello");
| ^^^^^^^^^^^^^^^^^^^^^
|
= note: calls in constants are limited to constant functions, tuple structs and tuple variants

Compiler bilang String::from bukan fungsi const, dan di dalam konstanta kamu hanya boleh memanggil fungsi const, tuple struct, dan tuple variant. Untuk teks, pakai const GREETING: &str = "hello";. Di dalam const fn, ganti for dengan perulangan while dan sebuah indeks, seperti pada lookup table di atas.

#[derive(Clone, Copy)] pada struct dengan PhantomData. Kamu mungkin mengira Id<User> itu Copy, karena isinya cuma u32. Tapi derive menambahkan syarat T: Copy, dan User bukan Copy, jadi ID-nya dipindahkan (move), bukan disalin:

error[E0382]: use of moved value: `id`
--> src/main.rs:16:10
|
14 | let id: Id<User> = Id { value: 1, _marker: PhantomData };
| -- move occurs because `id` has type `Id<User>`, which does not implement the `Copy` trait
15 | show(id);
| -- value moved here
16 | show(id);
| ^^ value used here after move
|
note: if all bounds were met, you could clone the value
--> src/main.rs:4:1
|
3 | #[derive(Clone, Copy)]
| ----- derived `Clone` adds implicit bounds on type parameters

Catatan di bagian bawah menjelaskan penyebabnya: Clone hasil derive menambahkan syarat tersembunyi pada parameter tipe. Solusi paling sederhana adalah ikut men-derive Clone, Copy pada tipe penandanya (#[derive(Clone, Copy)] struct User;). Atau tulis impl-nya sendiri, yang tidak butuh syarat apa pun pada T:

impl<T> Clone for Id<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for Id<T> {}

Tulis fn pad_scores<const N: usize>(scores: &[u32]) -> [u32; N] yang menyalin skor-skor ke dalam array berisi tepat N item. Isi tempat yang kosong dengan 0, dan buang skor yang berlebih. Panggil sebagai pad_scores::<5>(&[90, 85]) dan sebagai let top3: [u32; 3] = pad_scores(&[70, 95, 88, 60]);. Petunjuk: mulai dari [0; N], lalu lakukan perulangan pada 0..N dan pakai scores.get(i).

Lihat solusi
fn pad_scores<const N: usize>(scores: &[u32]) -> [u32; N] {
let mut out = [0; N];
for i in 0..N {
if let Some(&score) = scores.get(i) {
out[i] = score;
}
}
out
}
fn main() {
let five = pad_scores::<5>(&[90, 85]);
let top3: [u32; 3] = pad_scores(&[70, 95, 88, 60]);
println!("{:?}", five);
println!("{:?}", top3);
}

Output:

[90, 85, 0, 0, 0]
[70, 95, 88]
▶ Coba sendiri

Di sini N tidak bisa didapat dari argumen (slice tidak menyimpan panjangnya di dalam tipe), jadi ia berasal dari turbofish ::<5> atau dari tipe yang kamu minta, [u32; 3].

Tulis dua const fn, minutes(n: u64) -> u64 dan hours(n: u64) -> u64, yang mengembalikan jumlah detik. Pakai keduanya untuk mendefinisikan const SESSION_TIMEOUT: u64 = hours(1) + minutes(30); dan sebuah array const RETRY_DELAYS: [u64; 3] berisi 1, 5, dan 15 menit. Cetak keduanya. Petunjuk: hours bisa memanggil minutes.

Lihat solusi
const fn minutes(n: u64) -> u64 {
n * 60
}
const fn hours(n: u64) -> u64 {
minutes(n * 60)
}
const SESSION_TIMEOUT: u64 = hours(1) + minutes(30);
const RETRY_DELAYS: [u64; 3] = [minutes(1), minutes(5), minutes(15)];
fn main() {
println!("batas waktu sesi: {SESSION_TIMEOUT} detik");
println!("jeda percobaan ulang: {:?}", RETRY_DELAYS);
}

Output:

batas waktu sesi: 5400 detik
jeda percobaan ulang: [60, 300, 900]
▶ Coba sendiri

Compiler menghitung 5400 dan tabel jeda itu saat membangun program, dan hours(1) + minutes(30) jauh lebih mudah dibaca daripada angka 5400 begitu saja. Kalau kamu menghapus const dari minutes, item-item const itu tidak bisa dikompilasi lagi dan muncul error E0015.

Buat struct Euro; dan struct Dollar;, serta Money<C> dengan field cents: i64 dan sebuah field PhantomData<C>. Beri ia new(cents) dan plus(self, other: Money<C>) -> Money<C>. Jumlahkan harga makan siang dan kopi dalam euro, lalu cetak totalnya. Petunjuk: karena other punya C yang sama, Money<Euro> dan Money<Dollar> tidak bisa dijumlahkan.

Lihat solusi
use std::marker::PhantomData;
struct Euro;
struct Dollar;
struct Money<C> {
cents: i64,
_currency: PhantomData<C>,
}
impl<C> Money<C> {
fn new(cents: i64) -> Self {
Money { cents, _currency: PhantomData }
}
fn plus(self, other: Money<C>) -> Money<C> {
Money::new(self.cents + other.cents)
}
}
fn main() {
let lunch: Money<Euro> = Money::new(1250);
let coffee: Money<Euro> = Money::new(320);
let ticket: Money<Dollar> = Money::new(4999);
let total = lunch.plus(coffee);
println!("total: {}.{:02} EUR", total.cents / 100, total.cents % 100);
println!("tiket: {}.{:02} USD", ticket.cents / 100, ticket.cents % 100);
}

Output:

total: 15.70 EUR
tiket: 49.99 USD
▶ Coba sendiri

Coba lunch.plus(ticket): hasilnya gagal dengan expected Money<Euro>, found Money<Dollar>. Menyimpan sen sebagai integer, bukan f64, juga menghindari kesalahan pembulatan saat mengurus uang.

  • Const generics (const N: usize) menjadikan sebuah angka bagian dari tipe. Array dengan panjang berbeda adalah tipe yang berbeda, dan impl<T, const N: usize> menuliskan method untuk semua ukuran.
  • Di Rust stable, parameter const bisa berupa integer, bool, atau char. Ekspresi sebagai argumen butuh kurung kurawal ({ PAGE * 2 }), dan berhitung dengan N generik di dalam tipe belum diizinkan.
  • const fn bisa berjalan saat kompilasi di const context (const, static, panjang array). Pakai while, bukan for, dan hanya panggil const fn lain.
  • PhantomData<T> adalah field berukuran nol yang mengikat sebuah struct ke tipe yang tidak disimpannya. Pakai untuk ID bertipe, satuan ukur, dan typestate.
  • impl Matrix<2, 2> dan impl<const N: usize> Matrix<N, N> memberi method hanya untuk ukuran tertentu. Associated const seperti U::IN_METERS memberi setiap tipe nilai compile-time miliknya sendiri.
  • Alat-alat ini menambah waktu kompilasi dan mengurangi keterbacaan. Pakai saat mereka mencegah bug yang nyata.

Kuis singkat

1. Kenapa kamu tidak bisa memberikan [u32; 4] ke fungsi yang menerima [u32; 3]?

2. Apa yang ditambahkan PhantomData<T> ke sebuah struct saat runtime?

3. Mana yang TIDAK bisa kamu pakai di dalam const fn di Rust stable?

4. Apa arti impl<const N: usize> Matrix<N, N>?

Skor: 0 / 4