Lewati ke konten

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 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,
}
▶ Coba sendiri

Makro dbg! yang mungkin kamu lihat di kode lain juga memakai Debug.

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 Herbert
Teks itu punya 24 karakter
▶ Coba sendiri

write! 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.

Keduanya membuat duplikat dari sebuah nilai, tapi dengan cara yang berbeda.

  • Clone memberimu method .clone(). Kamu memanggilnya dengan sengaja, dan method ini bisa melakukan kerja sungguhan, misalnya menyalin semua teks di dalam String.
  • Copy membuat 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" }
▶ Coba sendiri

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 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: true
a != c: true
▶ Coba sendiri

Lalu 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: true
1.2.0
1.10.3
2.0.0
▶ Coba sendiri

Karena 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 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: "" }
▶ Coba sendiri

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 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°F
Suhu tubuh adalah 98.6°F
▶ Coba sendiri

Dengan .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 }))
▶ Coba sendiri

Halaman Error kustom mengembangkan ide ini lebih detail.

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 }
▶ Coba sendiri

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.

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.

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 expansion
2 | 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 expansion
2 | 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.

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? true
Task { priority: 1, title: "Bayar kos" }
Task { priority: 2, title: "Balas pesan Sam" }
Task { priority: 3, title: "Siram tanaman" }
▶ Coba sendiri

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.50
Kue: $4.05
Total: $7.55
▶ Coba sendiri
  • Debug ({:?}) untuk developer dan hampir selalu di-derive. Display ({}) untuk pengguna dan ditulis sendiri.
  • Clone menyalin saat diminta dengan .clone(). Copy menyalin otomatis, tapi hanya untuk tipe sederhana.
  • PartialEq/Eq memberimu ==. PartialOrd/Ord memberimu < dan pengurutan, dengan membandingkan field dari atas ke bawah.
  • Default membuat nilai awal; ..Default::default() mengisi field yang tidak kamu tentukan.
  • Implementasikan From dan kamu dapat Into secara gratis. Operator ? memakai From untuk 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

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