Trait Lanjutan di Rust
Di halaman Trait, kamu sudah belajar cara mendefinisikan dan mengimplementasikan trait. Di halaman Generik dan Trait yang sering dipakai, kamu memakai trait seperti Display, From, dan Add. Itu sudah cukup untuk sebagian besar kode sehari-hari. Tapi saat kamu membaca standard library atau crate populer, kamu akan melihat fitur trait yang awalnya terlihat agak aneh: type Item, Add<Rhs = Self>, trait Pretty: Display, impl<T: Display> ... for T.
Halaman ini menjelaskan fitur-fitur itu satu per satu. Tidak ada yang ajaib. Masing-masing menyelesaikan satu masalah kecil yang nyata, dan begitu kamu paham masalahnya, sintaksnya akan masuk akal. Pelan-pelan saja. Tidak apa-apa membaca satu bagian, mencoba kodenya, lalu kembali lagi nanti.
Associated type: satu jawaban per tipe
Section titled “Associated type: satu jawaban per tipe”Kamu sebenarnya sudah pernah bertemu associated type: Iterator punya type Item. Associated type (tipe terkait) adalah tipe pengganti (placeholder) di dalam trait. Setiap implementasi mengisinya sekali.
Berikut trait Container yang kecil. Sebuah playlist berisi String, dan daftar skor berisi u32, jadi masing-masing memilih Item-nya sendiri:
use std::fmt::Debug;
trait Container { type Item;
fn get(&self, index: usize) -> Option<&Self::Item>; fn size(&self) -> usize;
fn first(&self) -> Option<&Self::Item> { self.get(0) }}
struct Playlist { songs: Vec<String>,}
struct Scores { values: Vec<u32>,}
impl Container for Playlist { type Item = String;
fn get(&self, index: usize) -> Option<&String> { self.songs.get(index) } fn size(&self) -> usize { self.songs.len() }}
impl Container for Scores { type Item = u32;
fn get(&self, index: usize) -> Option<&u32> { self.values.get(index) } fn size(&self) -> usize { self.values.len() }}
// Bisa dipakai untuk Container apa saja yang item-nya bisa dicetak dengan {:?}fn describe<C: Container>(c: &C)where C::Item: Debug,{ println!("{} item, yang pertama {:?}", c.size(), c.first());}
fn main() { let playlist = Playlist { songs: vec![String::from("Intro"), String::from("Outro")] }; let scores = Scores { values: vec![90, 72, 85] };
describe(&playlist); describe(&scores);}Output:
2 item, yang pertama Some("Intro")3 item, yang pertama Some(90)Di dalam trait, Self::Item artinya “tipe apa pun yang dipilih oleh implementasi ini”. C::Item di describe artinya sama, tapi untuk tipe C. (Baris where hanyalah tempat yang lebih rapi untuk menulis bound. Penjelasannya ada di akhir halaman ini.)
Kenapa tidak pakai parameter generik saja? Kamu bisa saja menulis trait Container<T>. Tapi dengan begitu satu tipe bisa mengimplementasikannya berkali-kali, Container<String> dan Container<u32>, dan setiap pemanggil harus menyebutkan yang mana yang dimaksud. Untuk playlist, itu tidak masuk akal: isinya lagu, titik.
Itulah alasan Iterator memakai type Item. Iterator dari Vec<i32> menghasilkan i32 dan tidak ada yang lain, jadi for x in v.iter() tidak pernah butuh anotasi tipe. Patokan sederhananya:
- Satu jawaban yang wajar per tipe → associated type (
Iterator::Item,Add::Output). - Banyak jawaban yang sah per tipe → parameter generik (
From<T>: sebuahStringbisa dibuat dari&str,char,Box<str>, …).
Default type parameter dan operator
Section titled “Default type parameter dan operator”Di halaman Trait yang sering dipakai, kamu membuat + bisa dipakai untuk Point dengan mengimplementasikan Add. Begini definisi Add yang sebenarnya di standard library:
trait Add<Rhs = Self> { type Output; fn add(self, rhs: Rhs) -> Self::Output;}Di sini ada dua fitur sekaligus. Output adalah associated type (hasil dari +). Dan Rhs = Self adalah default type parameter (parameter tipe bawaan): “sisi kanan bertipe sama dengan sisi kiri, kecuali kamu bilang lain”. Saat kamu menulis impl Add for Point, kamu mendapat nilai bawaannya. Saat kamu ingin menjumlahkan tipe yang berbeda, kamu menulis impl Add<OtherType> for Point.
use std::ops::Add;
#[derive(Debug, Clone, Copy)]struct Meters(f64);
#[derive(Debug, Clone, Copy)]struct Centimeters(f64);
// Memakai bawaan Rhs = Self, jadi ini Meters + Metersimpl Add for Meters { type Output = Meters; fn add(self, other: Meters) -> Meters { Meters(self.0 + other.0) }}
// Mengganti bawaannya: Meters + Centimetersimpl Add<Centimeters> for Meters { type Output = Meters; fn add(self, other: Centimeters) -> Meters { Meters(self.0 + other.0 / 100.0) }}
fn main() { let a = Meters(2.0); let b = Meters(3.5); let c = Centimeters(50.0);
println!("{:?}", a + b); println!("{:?}", a + c);}Output:
Meters(5.5)Meters(2.5)Perhatikan bahwa Add memakai kedua alat itu: parameter generik untuk sisi kanan (banyak jawaban yang sah, kamu bisa menjumlahkan Meters atau Centimeters) dan associated type untuk hasilnya (satu jawaban untuk setiap pasangan). Default type parameter paling berguna saat kamu merancang trait: kasus yang umum tetap singkat, tanpa menghalangi kasus yang jarang.
Supertrait: “kamu juga wajib mengimplementasikan ini”
Section titled “Supertrait: “kamu juga wajib mengimplementasikan ini””Kadang trait-mu hanya masuk akal kalau tipenya sudah bisa melakukan hal lain. Supertrait berarti “untuk mengimplementasikan trait-ku, kamu juga wajib mengimplementasikan trait yang itu”. Kamu menulisnya dengan titik dua, seperti bound:
use std::fmt;
// Apa pun yang Pretty juga wajib Displaytrait Pretty: fmt::Display { fn pretty(&self) -> String { format!("*** {} ***", self) }}
struct Temperature { celsius: f64,}
impl fmt::Display for Temperature { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:.1}°C", self.celsius) }}
impl Pretty for Temperature {}
fn main() { let t = Temperature { celsius: 21.456 }; println!("{}", t); println!("{}", t.pretty());}Output:
21.5°C*** 21.5°C ***Method bawaan pretty bisa memakai {} pada self karena supertrait-nya menjamin Display tersedia. Tanpa : fmt::Display, Rust akan menolak format! itu, karena ia tidak bisa tahu apakah tipenya bisa dicetak. impl Pretty for Temperature {} memang kosong: isinya hanya “ya, pakai method bawaannya”.
Contoh nyata dari standard library: Copy: Clone (setiap tipe Copy juga wajib Clone) dan Eq: PartialEq.
Blanket implementation: satu impl untuk banyak tipe
Section titled “Blanket implementation: satu impl untuk banyak tipe”Blanket implementation (implementasi menyeluruh) mengimplementasikan trait untuk setiap tipe yang memenuhi suatu syarat, sekaligus. Sintaksnya impl<T: SomeBound> MyTrait for T:
use std::fmt::Display;
trait Shout { fn shout(&self) -> String;}
// Setiap tipe yang mengimplementasikan Display otomatis dapat Shoutimpl<T: Display> Shout for T { fn shout(&self) -> String { format!("{}!", self.to_string().to_uppercase()) }}
fn main() { println!("{}", "halo".shout()); println!("{}", 42.shout()); println!("{}", 'a'.shout()); println!("{}", String::from("rust itu seru").shout());}Output:
HALO!42!A!RUST ITU SERU!Kita tidak pernah menulis impl Shout for i32 atau impl Shout for char. Blanket impl itu sudah mencakup semuanya, termasuk tipe Display yang belum ada sekarang.
Tanpa sadar, kamu sudah sering memakai blanket impl. Standard library punya (kurang lebih) impl<T: Display> ToString for T. Itulah sebabnya setiap tipe yang punya Display juga punya method .to_string(). Trik yang sama memberimu Into secara gratis saat kamu mengimplementasikan From.
Ada satu batasan: begitu sebuah blanket impl ada, kamu tidak bisa menambah impl kedua yang lebih spesifik untuk tipe yang sudah dicakupnya (misalnya impl Shout for i32 khusus). Rust akan punya dua jawaban, jadi ia menolak dengan error “conflicting implementations” (implementasi yang bertabrakan).
Orphan rule dan solusi newtype
Section titled “Orphan rule dan solusi newtype”Ini aturan yang pernah mengejutkan semua orang. Coba beri Vec<String> sebuah Display yang rapi:
use std::fmt;
impl fmt::Display for Vec<String> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.join(", ")) }}error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate --> src/main.rs:3:1 |3 | impl fmt::Display for Vec<String> { | ^^^^^^^^^^^^^^^^^^^^^^----------- | | | `Vec` is not defined in the current crate | = note: define and implement a trait or new type insteadCompiler bilang: hanya trait yang didefinisikan di crate ini yang boleh diimplementasikan untuk tipe dari luar crate, dan Vec tidak didefinisikan di crate ini. Inilah orphan rule (aturan yatim): kamu hanya boleh mengimplementasikan trait untuk sebuah tipe kalau trait-nya atau tipenya didefinisikan di crate-mu sendiri. Display milik standard library, Vec juga, jadi tidak ada satu pun yang milikmu.
Kenapa aturan ini ada? Bayangkan dua crate yang kamu pakai sama-sama menulis impl Display for Vec<String> versi masing-masing. Rust harus pakai yang mana? Tidak ada jawaban yang bagus, jadi Rust membuat situasi itu mustahil terjadi.
Solusinya adalah newtype pattern (pola newtype): bungkus tipe asing itu di dalam struct kecil buatanmu sendiri. Sekarang tipe itu milikmu, dan kamu bisa mengimplementasikan apa saja untuknya:
use std::fmt;
// Pembungkus lokal untuk tipe dari luarstruct Shopping(Vec<String>);
impl fmt::Display for Shopping { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) }}
fn main() { let list = Shopping(vec![String::from("susu"), String::from("telur"), String::from("roti")]); println!("Yang perlu dibeli: {}", list); println!("Totalnya {} barang", list.0.len());}Output:
Yang perlu dibeli: [susu, telur, roti]Totalnya 3 barangself.0 mengakses Vec di dalamnya. Newtype tidak punya biaya saat program berjalan: di memori, Shopping persis sama dengan Vec<String>. Kekurangannya, method milik Vec tidak langsung tersedia di Shopping, jadi kamu menulis list.0.len() (atau menambahkan method yang kamu perlukan). Newtype berguna untuk lebih dari sekadar orphan rule; lihat Design pattern.
Fully qualified syntax: saat nama bertabrakan
Section titled “Fully qualified syntax: saat nama bertabrakan”Dua trait bisa punya method dengan nama yang sama, dan tipenya sendiri juga bisa punya method dengan nama itu. Jadi, robot.greet() memanggil yang mana?
trait English { fn greet(&self) -> String; fn language() -> String;}
trait Spanish { fn greet(&self) -> String; fn language() -> String;}
struct Robot;
impl Robot { fn greet(&self) -> String { String::from("BEEP BOOP") }}
impl English for Robot { fn greet(&self) -> String { String::from("Hello!") } fn language() -> String { String::from("Inggris") }}
impl Spanish for Robot { fn greet(&self) -> String { String::from("¡Hola!") } fn language() -> String { String::from("Spanyol") }}
fn main() { let r = Robot;
println!("{}", r.greet()); // method milik tipenya sendiri yang menang println!("{}", English::greet(&r)); // pilih trait berdasarkan namanya println!("{}", Spanish::greet(&r));
// Tidak ada `self` di sini, jadi Rust butuh tipe DAN trait-nya: println!("{}", <Robot as English>::language()); println!("{}", <Robot as Spanish>::language());}Output:
BEEP BOOPHello!¡Hola!InggrisSpanyolAda tiga tingkat, dari yang paling singkat sampai yang paling eksplisit:
r.greet(): method milik tipenya sendiri (inherent method) dipilih lebih dulu.English::greet(&r): “versiEnglish”. Rust mencari tahu tipenya dari&r.<Robot as English>::language(): fully qualified syntax (sintaks lengkap). Kamu membutuhkannya untuk fungsi tanpaself, karena tidak ada nilai yang bisa dilihat Rust.English::language()saja akan gagal denganerror[E0790]: cannot call associated function on trait without specifying the corresponding impl type, artinya Rust tidak bisa memanggil fungsi trait itu tanpa tahu impl untuk tipe yang mana.
Kamu tidak akan sering membutuhkan ini. Kalau suatu saat butuh, pesan error compiler biasanya sudah menyarankan perbaikan yang tepat.
impl Trait di argumen dan nilai kembalian, plus where
Section titled “impl Trait di argumen dan nilai kembalian, plus where”Kamu bisa menulis impl Trait di dua tempat, dan artinya sedikit berbeda di masing-masing tempat:
- Posisi argumen,
fn f(x: impl Trait): pemanggil yang memilih tipenya. Ini cara singkat untuk menulis fungsi generikfn f<T: Trait>(x: T). - Posisi kembalian,
fn f() -> impl Trait: fungsinya yang memilih satu tipe konkret dan menyembunyikannya. Pemanggil hanya tahu “ini sesuatu yang mengimplementasikanTrait”.
// Pemanggil yang memilih: iterator u32 apa saja bolehfn print_all(items: impl Iterator<Item = u32>) { let all: Vec<u32> = items.collect(); println!("{:?}", all);}
// Fungsi yang memilih: tipe aslinya Filter<...> yang panjang, tidak perlu kita tulisfn evens_up_to(limit: u32) -> impl Iterator<Item = u32> { (0..=limit).filter(|n| n % 2 == 0)}
fn main() { print_all(vec![3, 1, 4].into_iter()); print_all(evens_up_to(10));}Output:
[3, 1, 4][0, 2, 4, 6, 8, 10]impl Trait di posisi kembalian sangat praktis untuk closure dan iterator. Tipe asli dari hasil evens_up_to mengandung tipe closure-nya, dan closure tidak punya nama yang bisa kamu ketik. Dengan impl Iterator, kamu tidak perlu menuliskannya.
Saat sebuah fungsi punya beberapa parameter generik dengan beberapa bound untuk masing-masing, isi kurung sudutnya jadi sesak. Klausa where memindahkan bound-bound itu ke setelah signature, sehingga bentuk fungsinya lebih mudah dibaca:
use std::fmt::{Debug, Display};
// Sesak: fn report<T: Display, U: Debug + PartialOrd>(label: T, a: U, b: U) -> String// Lebih rapi:fn report<T, U>(label: T, a: U, b: U) -> Stringwhere T: Display, U: Debug + PartialOrd,{ let bigger = if a > b { a } else { b }; format!("{}: yang lebih besar {:?}", label, bigger)}
fn main() { println!("{}", report("skor", 7, 12)); println!("{}", report(String::from("nama"), "ann", "bob"));}Output:
skor: yang lebih besar 12nama: yang lebih besar "bob"Bagi compiler, kedua bentuk itu artinya persis sama. where juga tempat yang wajar untuk bound pada associated type, seperti C::Item: Debug di contoh pertama halaman ini.
Kesalahan umum
Section titled “Kesalahan umum”Mengimplementasikan trait tapi lupa supertrait-nya. Kalau kamu menghapus blok impl fmt::Display for Temperature dari contoh supertrait, kamu akan mendapat:
error[E0277]: `Temperature` doesn't implement `std::fmt::Display` --> src/main.rs:13:17 |13 | impl Pretty for Temperature {} | ^^^^^^^^^^^ unsatisfied trait bound |note: required by a bound in `Pretty` --> src/main.rs:3:15 | 3 | trait Pretty: fmt::Display { | ^^^^^^^^^^^^ required by this bound in `Pretty`Compiler bilang Temperature tidak mengimplementasikan Display, padahal Pretty mewajibkannya. Solusinya, implementasikan juga supertrait-nya (di sini, Display). Bagian note menunjuk langsung ke : fmt::Display yang mewajibkannya.
Mengembalikan dua tipe berbeda dari -> impl Trait. impl Trait menyembunyikan satu tipe. Ia tidak bisa “kadang ini, kadang itu”:
fn numbers(evens: bool) -> impl Iterator<Item = u32> { if evens { (0..10).filter(|n| n % 2 == 0) } else { vec![1, 3, 5].into_iter() }}error[E0308]: `if` and `else` have incompatible types --> src/main.rs:5:9 |2 | / if evens {3 | | (0..10).filter(|n| n % 2 == 0) | | ------------------------------ | | | | | | | the expected closure | | expected because of this4 | | } else {5 | | vec![1, 3, 5].into_iter() | | ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Filter<Range<{integer}>, {closure@...}>`, found `IntoIter<{integer}>`6 | | } | |_____- `if` and `else` have incompatible types | = note: expected struct `Filter<std::ops::Range<{integer}>, {closure@src/main.rs:3:24: 3:27}>` found struct `std::vec::IntoIter<{integer}>`help: you could change the return type to be a boxed trait object |1 - fn numbers(evens: bool) -> impl Iterator<Item = u32> {1 + fn numbers(evens: bool) -> Box<dyn Iterator<Item = u32>> { |help: if you change the return type to expect trait objects, box the returned expressions |3 ~ Box::new((0..10).filter(|n| n % 2 == 0))4 | } else {5 ~ Box::new(vec![1, 3, 5].into_iter()) |Compiler bilang cabang if dan else menghasilkan tipe yang berbeda, lalu menyarankan untuk mengubah tipe kembaliannya menjadi trait object di dalam Box. Kalau tipenya memang berubah saat program berjalan, kembalikan Box<dyn Trait>, seperti saran compiler. Lihat Trait object.
Ringkasan
Section titled “Ringkasan”- Associated type (
type Item) memberi satu jawaban per implementasi. Pakai parameter generik kalau satu tipe bisa mengimplementasikan trait itu dengan banyak cara. - Default type parameter seperti
Add<Rhs = Self>membuat kasus yang umum tetap singkat;impl Add<Other> for Tmenggantinya. - Supertrait (
trait Pretty: Display) mewajibkan trait lain, dan membuat method bawaan bisa memakainya. - Blanket impl (
impl<T: Display> MyTrait for T) mencakup setiap tipe yang cocok sekaligus. Begitulah cara kerja.to_string(). - Orphan rule melarangmu mengimplementasikan trait asing untuk tipe asing. Bungkus tipenya dengan newtype untuk mengatasinya.
- Pakai fully qualified syntax
<Type as Trait>::function()saat nama bertabrakan;impl Traitdi posisi kembalian menyembunyikan satu tipe konkret;wheremembuat bound yang panjang lebih mudah dibaca.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Kenapa Iterator memakai type Item, bukan Iterator<T>?
2. Apa arti Rhs = Self dalam trait Add<Rhs = Self>?
3. Kamu ingin menulis impl Display for Vec<String>, tapi Rust menolak. Apa solusi yang biasa dipakai?
4. Baris mana yang memanggil versi English dari fungsi tanpa parameter self?
5. Pada fn evens() -> impl Iterator<Item = u32>, siapa yang menentukan tipe iterator konkretnya?
Skor: 0 / 5
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- Advanced Traits — The Rust Book (bahasa Inggris): associated type, default type parameter, supertrait, dan newtype pattern, dengan contoh tambahan.
- Traits — The Rust Reference (bahasa Inggris): aturan persis untuk item trait, supertrait, dan associated item.
- Orphan rules — The Rust Reference (bahasa Inggris): definisi persis kapan sebuah impl diizinkan.
impl Trait— The Rust Reference (bahasa Inggris): perbedaanimpl Traitdi posisi argumen dan di posisi kembalian.- Supertraits — Rust by Example (bahasa Inggris): contoh singkat yang bisa dijalankan, berisi trait yang dibangun di atas trait lain.