Trait yang Sering Dipakai di Rust
Di halaman Trait kamu sudah menulis trait sendiri. Tapi standard library Rust sudah menyediakan sekumpulan trait yang dipakai hampir di setiap program Rust. Trait-trait ini menjawab pertanyaan sederhana tentang sebuah tipe: Bisa dicetak? Bisa disalin? Bisa dibandingkan? Bisa dibuat versi kosongnya?
Bayangkan trait seperti lencana di seragam. Setiap lencana bilang “tipe ini tahu cara melakukan X”. Banyak lencana ini bisa kamu dapat gratis dengan #[derive(...)]. Sisanya kamu tulis sendiri. Halaman ini membahas trait yang paling sering kamu temui, dan diakhiri dengan tabel yang bisa kamu buka lagi kapan saja.
Debug: mencetak untuk developer
Section titled “Debug: mencetak untuk developer”Debug memungkinkan kamu mencetak nilai dengan {:?}. Trait ini ditujukan untuk kamu, sang developer, yaitu untuk memeriksa nilai saat kamu membangun dan memperbaiki kode. Kamu hampir tidak pernah menulisnya sendiri; cukup pakai derive. Gunakan {:#?} untuk versi “rapi” dengan satu field per baris, yang lebih mudah dibaca untuk struct besar:
#[derive(Debug)]struct Book { title: String, pages: u32,}
fn main() { let book = Book { title: String::from("Dune"), pages: 412 }; println!("{:?}", book); println!("{:#?}", book);}Output:
Book { title: "Dune", pages: 412 }Book { title: "Dune", pages: 412,}Makro dbg! yang mungkin kamu lihat di kode lain juga memakai Debug.
Display: mencetak untuk pengguna
Section titled “Display: mencetak untuk pengguna”Display adalah trait yang dipakai oleh {}. Trait ini untuk teks yang dilihat pengguna, yaitu pesan yang dibaca orang di layar. Rust tidak bisa menebak tampilannya harus seperti apa, jadi tidak ada derive untuknya. Kamu menulisnya sendiri dengan mengimplementasikan fmt::Display:
use std::fmt;
struct Book { title: String, author: String,}
impl fmt::Display for Book { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} karya {}", self.title, self.author) }}
fn main() { let book = Book { title: String::from("Dune"), author: String::from("Frank Herbert"), };
println!("{}", book);
let text = book.to_string(); println!("Teks itu punya {} karakter", text.len());}Output:
Dune karya Frank HerbertTeks itu punya 24 karakterwrite! bekerja seperti println!, tapi menulis ke formatter f, bukan ke layar. Bonusnya: begitu sebuah tipe punya Display, kamu langsung dapat .to_string() secara gratis. Kamu akan bertemu Display lagi di halaman Error kustom, tempat trait ini memberi pesan yang ramah untuk error.
Clone dan Copy
Section titled “Clone dan Copy”Keduanya membuat duplikat dari sebuah nilai, tapi dengan cara yang berbeda.
Clonememberimu method.clone(). Kamu memanggilnya dengan sengaja, dan method ini bisa melakukan kerja sungguhan, misalnya menyalin semua teks di dalamString.Copymembuat duplikat secara otomatis setiap kali nilainya di-assign atau dikirim ke fungsi. Trait ini hanya boleh untuk tipe kecil dan sederhana yang bisa disalin bit demi bit: angka,bool,char, dan struct yang isinya hanya tipe-tipe itu.
#[derive(Debug, Clone, Copy)]struct Point { x: i32, y: i32,}
#[derive(Debug, Clone)]struct User { name: String,}
fn main() { let p1 = Point { x: 1, y: 2 }; let p2 = p1; // Copy: p1 masih bisa dipakai println!("{:?} dan {:?}", p1, p2);
let u1 = User { name: String::from("Ana") }; let u2 = u1.clone(); // Clone: kita sengaja meminta salinan println!("{:?} dan {:?}", u1, u2);}Output:
Point { x: 1, y: 2 } dan Point { x: 1, y: 2 }User { name: "Ana" } dan User { name: "Ana" }Tanpa Copy, let p2 = p1; akan memindahkan (move) p1, dan kamu tidak bisa memakainya lagi setelah itu (lihat Ownership). Perhatikan bahwa Copy selalu butuh Clone juga, itulah sebabnya keduanya di-derive bersamaan.
PartialEq dan Eq: membandingkan dengan ==
Section titled “PartialEq dan Eq: membandingkan dengan ==”PartialEq memungkinkan kamu memakai == dan !=. Kalau di-derive, dua nilai dianggap sama jika semua field-nya sama:
#[derive(Debug, PartialEq)]struct Point { x: i32, y: i32,}
fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 1, y: 2 }; let c = Point { x: 5, y: 0 };
println!("a == b: {}", a == b); println!("a != c: {}", a != c);}Output:
a == b: truea != c: trueLalu apa itu Eq? Trait ini tidak menambah method baru. Ia adalah janji: “setiap nilai selalu sama dengan dirinya sendiri”. Kedengarannya jelas, tapi bilangan float melanggarnya: di Rust, f64::NAN == f64::NAN hasilnya false. Jadi f64 punya PartialEq tapi tidak punya Eq. Kamu butuh Eq (ditambah Hash) supaya tipemu bisa dipakai sebagai key HashMap atau isi HashSet. Kalau struct-mu tidak punya field float, derive keduanya.
PartialOrd dan Ord: membandingkan dengan < dan >
Section titled “PartialOrd dan Ord: membandingkan dengan < dan >”PartialOrd memungkinkan kamu memakai <, >, <=, dan >=. Ord menambahkan urutan yang lengkap, yang dibutuhkan oleh .sort(), .max(), dan .min(). Kalau di-derive, Rust membandingkan field dari atas ke bawah: field pertama dulu, dan baru melihat field berikutnya kalau field sebelumnya sama.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]struct Version { major: u32, minor: u32, patch: u32,}
fn main() { let old = Version { major: 1, minor: 2, patch: 0 }; let new = Version { major: 1, minor: 10, patch: 3 }; println!("old < new: {}", old < new);
let mut versions = vec![ Version { major: 2, minor: 0, patch: 0 }, new, old, ]; versions.sort();
for v in &versions { println!("{}.{}.{}", v.major, v.minor, v.patch); }}Output:
old < new: true1.2.01.10.32.0.0Karena field dibandingkan secara berurutan, urutan penulisan field itu penting. Di sini major yang paling menentukan, dan memang begitulah seharusnya untuk nomor versi. Untuk enum, varian yang ditulis lebih dulu dianggap “lebih kecil”. Sama seperti Eq, float hanya mendapat PartialOrd, bukan Ord. Halaman Mengurutkan dan mencari menunjukkan cara tetap mengurutkan float.
Default: nilai awal yang masuk akal
Section titled “Default: nilai awal yang masuk akal”Default memberi tipemu fungsi ::default() yang membuat nilai “awal”. Kalau di-derive, setiap field mendapat nilai default-nya sendiri: 0 untuk angka, false untuk bool, String kosong, Vec kosong. Sintaks ..Default::default() memungkinkan kamu mengisi beberapa field saja dan sisanya diisi otomatis:
#[derive(Debug, Default)]struct Settings { volume: u32, muted: bool, username: String,}
fn main() { let basic = Settings::default(); println!("{:?}", basic);
let custom = Settings { volume: 80, ..Default::default() }; println!("{:?}", custom);}Output:
Settings { volume: 0, muted: false, username: "" }Settings { volume: 80, muted: false, username: "" }Kalau kamu ingin nilai awal yang berbeda (misalnya volume 50), jangan pakai derive. Tulis sendiri impl Default for Settings, dengan fn default() -> Self yang mengembalikan nilai yang kamu mau.
From dan Into: konversi antartipe
Section titled “From dan Into: konversi antartipe”From menjelaskan cara membuat satu tipe dari tipe lain. Kamu sebenarnya sudah memakainya: String::from("hai"). Kalau kamu mengimplementasikan From, Rust memberimu Into secara gratis, untuk arah sebaliknya:
struct Celsius(f64);struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit { fn from(c: Celsius) -> Self { Fahrenheit(c.0 * 9.0 / 5.0 + 32.0) }}
fn main() { let boiling = Fahrenheit::from(Celsius(100.0)); println!("Air mendidih pada {}°F", boiling.0);
let body: Fahrenheit = Celsius(37.0).into(); println!("Suhu tubuh adalah {:.1}°F", body.0);}Output:
Air mendidih pada 212°FSuhu tubuh adalah 98.6°FDengan .into(), Rust perlu tahu tipe tujuannya, itulah sebabnya kita menulis let body: Fahrenheit.
Bagaimana ? memakai From. Di sinilah From benar-benar bersinar. Saat operator ? menemui error, ia memanggil From::from untuk mengubah error itu menjadi tipe error yang dikembalikan fungsimu. Jadi kalau kamu mengimplementasikan From untuk tipe error buatanmu sendiri, ? akan mengonversi error-nya untukmu:
use std::num::ParseIntError;
#[derive(Debug)]enum AppError { BadNumber(ParseIntError),}
impl From<ParseIntError> for AppError { fn from(e: ParseIntError) -> Self { AppError::BadNumber(e) }}
fn parse_age(text: &str) -> Result<u32, AppError> { let age: u32 = text.parse()?; // ParseIntError berubah menjadi AppError di sini Ok(age)}
fn main() { println!("{:?}", parse_age("42")); println!("{:?}", parse_age("abc"));}Output:
Ok(42)Err(BadNumber(ParseIntError { kind: InvalidDigit }))Halaman Error kustom mengembangkan ide ini lebih detail.
Operator overloading dengan Add
Section titled “Operator overloading dengan Add”Di Rust, a + b sebenarnya adalah pemanggilan method add dari trait std::ops::Add. Implementasikan trait itu untuk tipemu, dan + langsung bisa dipakai. type Output menyatakan tipe apa yang dihasilkan oleh penjumlahan:
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]struct Point { x: i32, y: i32,}
impl Add for Point { type Output = Point;
fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y } }}
fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 3, y: 4 }; println!("{:?}", a + b);}Output:
Point { x: 4, y: 6 }Ide yang sama berlaku untuk Sub (-), Mul (*), Neg (- unary), dan AddAssign (+=). Pakai ini kalau maknanya jelas, misalnya menjumlahkan dua titik atau dua jumlah uang. Kalau pembaca harus menebak-nebak arti + untuk tipemu, lebih baik pakai method biasa dengan nama yang jelas.
Trait mana yang sebaiknya di-derive?
Section titled “Trait mana yang sebaiknya di-derive?”Berikut panduan singkat untuk struct pada umumnya:
| Trait | Derive? | Yang kamu dapat |
|---|---|---|
Debug |
Hampir selalu | Mencetak dengan {:?} dan {:#?} |
Clone |
Biasanya | .clone() |
Copy |
Hanya untuk tipe kecil tanpa field String, Vec, atau Box |
Salinan otomatis, bukan move |
PartialEq |
Biasanya | == dan != |
Eq |
Kalau tidak ada field float | Dibutuhkan untuk key HashMap (bersama Hash) |
Hash |
Kalau dipakai sebagai key HashMap atau isi HashSet |
Hashing |
PartialOrd, Ord |
Kalau nilainya punya urutan alami | <, >, .sort(), .max() |
Default |
Kalau nilai “kosong” masuk akal | ::default() dan ..Default::default() |
Display |
Tidak bisa di-derive, tulis sendiri | Mencetak dengan {} dan .to_string() |
Baris awal yang sangat umum adalah #[derive(Debug, Clone, PartialEq)]. Tambahkan yang lain saat kamu membutuhkannya; compiler akan memberi tahu kalau ada yang kurang.
Kesalahan umum
Section titled “Kesalahan umum”Memakai derive Copy pada struct yang berisi String.
#[derive(Clone, Copy)]struct User { name: String, age: u32,}error[E0204]: the trait `Copy` cannot be implemented for this type --> src/main.rs:2:8 |1 | #[derive(Clone, Copy)] | ---- in this derive macro expansion2 | struct User { | ^^^^3 | name: String, | ------------ this field does not implement `Copy`Compiler bilang Copy tidak bisa diimplementasikan untuk tipe ini, karena field name tidak mengimplementasikan Copy. String memiliki memori di heap, jadi tidak bisa disalin bit demi bit. Hapus Copy dan pakai .clone() saat kamu butuh duplikat.
Memakai derive Eq pada struct yang berisi float.
#[derive(Debug, PartialEq, Eq)]struct Product { name: String, price: f64 }error[E0277]: the trait bound `f64: Eq` is not satisfied --> src/main.rs:2:32 |1 | #[derive(Debug, PartialEq, Eq)] | -- in this derive macro expansion2 | struct Product { name: String, price: f64 } | ^^^^^^^^^^ the trait `Eq` is not implemented for `f64`Artinya f64 tidak mengimplementasikan Eq. Pertahankan PartialEq dan buang Eq. Atau simpan harganya dalam satuan sen utuh di u64, yang juga lebih aman untuk uang.
Latihan
Section titled “Latihan”1. Mengurutkan daftar tugas
Section titled “1. Mengurutkan daftar tugas”Buat struct Task dengan priority: u8 dan title: String. Derive trait yang dibutuhkan supaya kamu bisa mengurutkan Vec<Task>, mencetak setiap tugas dengan {:?}, dan membandingkan dua tugas dengan ==. Angka yang lebih kecil berarti lebih mendesak, jadi harus muncul lebih dulu. Petunjuk: .sort() butuh Ord, dan Ord juga butuh Eq, PartialEq, dan PartialOrd. Taruh priority di urutan pertama supaya field itu yang dibandingkan lebih dulu.
Lihat solusi
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]struct Task { priority: u8, title: String,}
fn main() { let mut tasks = vec![ Task { priority: 3, title: String::from("Siram tanaman") }, Task { priority: 1, title: String::from("Bayar kos") }, Task { priority: 2, title: String::from("Balas pesan Sam") }, ];
let copy = tasks[1].clone(); println!("Tugas yang sama? {}", copy == tasks[1]);
tasks.sort(); for task in &tasks { println!("{:?}", task); }}Output:
Tugas yang sama? trueTask { priority: 1, title: "Bayar kos" }Task { priority: 2, title: "Balas pesan Sam" }Task { priority: 3, title: "Siram tanaman" }2. Uang yang bisa dicetak dan dijumlahkan
Section titled “2. Uang yang bisa dicetak dan dijumlahkan”Buat struct Money yang menyimpan cents: u64 (jumlah sen utuh). Implementasikan From<u64> supaya 350.into() bisa dipakai, Display supaya tercetak seperti $3.50, dan Add supaya dua jumlah uang bisa dijumlahkan dengan +. Petunjuk: dolarnya adalah cents / 100 dan sisanya cents % 100. Cetak sisanya dengan {:02} supaya 5 sen tampil sebagai 05. Derive Copy supaya kamu masih bisa mencetak jumlah uangnya setelah dijumlahkan.
Lihat solusi
use std::fmt;use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]struct Money { cents: u64,}
impl From<u64> for Money { fn from(cents: u64) -> Self { Money { cents } }}
impl fmt::Display for Money { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "${}.{:02}", self.cents / 100, self.cents % 100) }}
impl Add for Money { type Output = Money;
fn add(self, other: Money) -> Money { Money { cents: self.cents + other.cents } }}
fn main() { let coffee: Money = 350.into(); let cake = Money::from(405); let total = coffee + cake;
println!("Kopi: {coffee}"); println!("Kue: {cake}"); println!("Total: {total}");}Output:
Kopi: $3.50Kue: $4.05Total: $7.55Ringkasan
Section titled “Ringkasan”Debug({:?}) untuk developer dan hampir selalu di-derive.Display({}) untuk pengguna dan ditulis sendiri.Clonemenyalin saat diminta dengan.clone().Copymenyalin otomatis, tapi hanya untuk tipe sederhana.PartialEq/Eqmemberimu==.PartialOrd/Ordmemberimu<dan pengurutan, dengan membandingkan field dari atas ke bawah.Defaultmembuat nilai awal;..Default::default()mengisi field yang tidak kamu tentukan.- Implementasikan
Fromdan kamu dapatIntosecara gratis. Operator?memakaiFromuntuk mengonversi error. - Implementasikan
std::ops::Add(dan teman-temannya) supaya+bisa dipakai pada tipe buatanmu sendiri.
Mau belajar lebih jauh? Trait lanjutan menjelaskan associated type seperti type Output, parameter tipe default, dan cara mengimplementasikan trait untuk tipe yang bukan buatanmu.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Trait apa yang dipakai oleh {} di println!?
2. Kenapa struct yang punya field String tidak bisa men-derive Copy?
3. Kalau kamu men-derive PartialOrd, bagaimana dua struct dibandingkan?
4. Kamu mengimplementasikan From<ParseIntError> for AppError. Apa yang jadi bisa dilakukan ??
5. Trait mana yang kamu implementasikan supaya a + b bisa dipakai untuk tipe buatanmu sendiri?
Skor: 0 / 5
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- Derivable Traits — The Rust Book (Appendix C) (bahasa Inggris): semua trait yang bisa kamu
#[derive], apa yang dilakukan versi derive-nya, dan kapan kamu membutuhkannya. std::fmt— formatting and Display (bahasa Inggris): semua opsi format, seperti lebar, padding, dan{:#?}, serta peranDisplaydanDebug.FromandInto— Rust by Example (bahasa Inggris): contoh singkat yang bisa dijalankan tentang konversi antartipe buatanmu sendiri.std::ops— operator traits (bahasa Inggris): daftar lengkap operator yang bisa di-overload, dariAddsampaiIndex.std::cmp— comparison traits (bahasa Inggris):PartialEq,Eq,PartialOrd, danOrdsecara detail, beserta aturan yang dijanjikan masing-masing.