Lewati ke konten

Trik Pustaka Standar di Rust: Idiom Kecil untuk Sehari-hari

Setiap dapur punya laci berisi alat-alat kecil: pengupas, pembuka botol, penghancur bawang putih. Sebenarnya semua pekerjaan bisa kamu lakukan dengan satu pisau besar, tapi alat-alat kecil itu membuat pekerjaan lebih cepat dan lebih aman. Pustaka standar (standard library) Rust juga punya laci seperti itu.

Halaman ini mengajakmu berkeliling isi laci itu. Setiap trik dibuat singkat: gambaran dari kehidupan sehari-hari, contoh yang bisa dijalankan, dan satu baris “pakai ini kalau…” supaya kamu tahu kapan memakainya. Kamu tidak perlu menghafal semuanya sekaligus. Baca sekilas dulu, lalu kembali lagi ke sini saat kamu melihat pola di kodemu sendiri yang terasa kaku.

Memindahkan nilai keluar dari &mut: mem::take, replace, swap

Section titled “Memindahkan nilai keluar dari &mut: mem::take, replace, swap”

Bayangkan tempat sampah yang ada kantongnya. Kamu boleh mengambil kantong yang sudah penuh, asalkan langsung memasang kantong baru. Tempat sampah itu tidak pernah dibiarkan tanpa kantong.

Rust punya aturan yang sama. Kalau kamu hanya punya &mut self, kamu tidak bisa begitu saja memindahkan sebuah field keluar, karena itu akan meninggalkan “lubang” di dalam struct. Compiler akan menghentikanmu dengan E0507. Fungsi-fungsi di std::mem menyelesaikan masalah ini dengan selalu meninggalkan sesuatu sebagai pengganti:

  • mem::take(&mut x) mengambil nilainya dan meninggalkan nilai default tipe itu (Vec kosong, String kosong, 0, None).
  • mem::replace(&mut x, new) memasukkan new dan memberimu nilai yang lama.
  • Option::take() mengambil isi sebuah Option dan meninggalkan None.
use std::mem;
struct Inbox {
unread: Vec<String>,
status: String,
draft: Option<String>,
}
impl Inbox {
// Serahkan semua pesan yang belum dibaca dan tinggalkan Vec kosong.
fn read_all(&mut self) -> Vec<String> {
mem::take(&mut self.unread)
}
// Pasang status baru dan dapatkan status yang lama.
fn set_status(&mut self, new: &str) -> String {
mem::replace(&mut self.status, new.to_string())
}
// Ambil draf (kalau ada) dan tinggalkan None.
fn send_draft(&mut self) -> Option<String> {
self.draft.take()
}
}
fn main() {
let mut inbox = Inbox {
unread: vec!["Makan siang?".to_string(), "Tagihan #42".to_string()],
status: "online".to_string(),
draft: Some("Sampai ketemu jam 6".to_string()),
};
let messages = inbox.read_all();
println!("dibaca {:?}, belum dibaca: {:?}", messages, inbox.unread);
let old = inbox.set_status("sedang rapat");
println!("status tadi '{old}', sekarang '{}'", inbox.status);
println!("terkirim: {:?}", inbox.send_draft());
println!("terkirim lagi: {:?}", inbox.send_draft());
}

Output:

dibaca ["Makan siang?", "Tagihan #42"], belum dibaca: []
status tadi 'online', sekarang 'sedang rapat'
terkirim: Some("Sampai ketemu jam 6")
terkirim lagi: None
▶ Coba sendiri

Tidak ada clone() sama sekali: pesan-pesan dan status lama itu dipindahkan keluar, bukan disalin. Itulah sebabnya fungsi-fungsi ini aman sekaligus murah.

Dua “kerabat” melengkapi set ini. mem::swap(&mut a, &mut b) menukar dua nilai, seperti menukar dua piring di atas meja. Option::replace(new) memasukkan nilai baru ke dalam Option dan mengembalikan nilai yang lama.

use std::mem;
fn main() {
// Isi satu daftar selagi daftar yang lain sedang dikerjakan.
let mut filling = vec!["pesanan 1", "pesanan 2"];
let mut processing: Vec<&str> = Vec::new();
mem::swap(&mut filling, &mut processing);
println!("diproses {processing:?}, diisi {filling:?}");
// Option::replace: masukkan nilai baru, dapatkan nilai lama.
let mut now_playing = Some("Lagu A");
let previous = now_playing.replace("Lagu B");
println!("tadi {previous:?}, sekarang {now_playing:?}");
}

Output:

diproses ["pesanan 1", "pesanan 2"], diisi []
tadi Some("Lagu A"), sekarang Some("Lagu B")
▶ Coba sendiri

Pakai ini kalau kamu hanya punya akses &mut, tapi perlu memiliki nilai di dalamnya.

Anggap windows(2) sebagai kaca pembesar yang digeser di sepanjang penggaris: ia menunjukkan dua garis sekaligus, lalu bergeser satu langkah. chunks(3) seperti mengemas telur ke kotak isi tiga: kotak terakhir mungkin tidak penuh. chunks_exact(3) hanya memberimu kotak yang penuh dan menyimpan sisa telurnya di .remainder().

Method-method ini bekerja pada slice (potongan data) apa saja, jadi juga bisa dipakai pada array dan vektor.

fn main() {
let temps = [12, 15, 14, 19, 21];
// windows(2): setiap pasangan tetangga, saling tumpang tindih
for pair in temps.windows(2) {
println!("{} -> {}: berubah {}", pair[0], pair[1], pair[1] - pair[0]);
}
// chunks(3): kelompok isi 3, yang terakhir bisa lebih kecil
for batch in emails.chunks(3) {
println!("mengirim ke {batch:?}");
}
// chunks_exact(2): hanya kelompok penuh; sisanya menunggu di remainder()
let seats = [1, 2, 3, 4, 5];
let pairs = seats.chunks_exact(2);
let leftover = pairs.remainder();
for pair in pairs {
println!("pasangan {pair:?}");
}
println!("sisa: {leftover:?}");
}

Output:

12 -> 15: berubah 3
15 -> 14: berubah -1
14 -> 19: berubah 5
19 -> 21: berubah 2
pasangan [1, 2]
pasangan [3, 4]
sisa: [5]
▶ Coba sendiri

Beberapa helper slice lain bisa menjawab pertanyaan umum tanpa perulangan. split_first() memberimu item pertama dan sisanya, seperti guru yang membacakan nama pertama di daftar lalu menyerahkan sisa daftarnya ke asisten. split_last() melakukan hal yang sama dari ujung belakang.

fn main() {
let command = ["git", "commit", "-m", "perbaiki typo"];
// split_first: item pertama, dan slice berisi sisanya
if let Some((program, args)) = command.split_first() {
println!("program: {program}, argumen: {args:?}");
}
// split_last: item terakhir, dan semua yang ada sebelumnya
if let Some((message, _rest)) = command.split_last() {
println!("pesan: {message}");
}
println!("apakah git? {}", command.starts_with(&["git"]));
println!("ada -m? {}", command.contains(&"-m"));
// iter().rev(): berjalan mundur tanpa mengubah slice-nya
let history = ["buka", "edit", "simpan"];
let undo_order: Vec<_> = history.iter().rev().collect();
println!("urutan undo: {undo_order:?}");
}

Output:

program: git, argumen: ["commit", "-m", "perbaiki typo"]
pesan: perbaiki typo
apakah git? true
ada -m? true
urutan undo: ["simpan", "edit", "buka"]
▶ Coba sendiri

split_first dan split_last mengembalikan Option, karena slice kosong tidak punya item pertama. Itulah sebabnya kita memakai if let.

Pakai ini kalau kamu butuh tetangga (windows), kelompok (chunks), atau “yang pertama dan sisanya” (split_first).

Helper iterator yang akan sering kamu pakai

Section titled “Helper iterator yang akan sering kamu pakai”

Halaman Iterator sudah membahas map, filter, sum dan collect. Berikut alat-alat berikutnya di kotak perkakas.

Bayangkan kamu sedang membagikan kartu. Kadang kamu ingin melihat kartu berikutnya sebelum memutuskan untuk mengambilnya. .peekable() memberimu iterator yang bisa melakukan itu: .peek() menunjukkan item berikutnya tanpa mengambilnya, dan .next_if(...) hanya mengambilnya kalau item itu lolos tes. Ini persis yang dibutuhkan parser kecil.

fn main() {
let input = "12+345-6";
let mut chars = input.chars().peekable();
let mut tokens: Vec<String> = Vec::new();
while let Some(c) = chars.next() {
if c.is_ascii_digit() {
let mut number = c.to_string();
// Ambil karakter berikutnya hanya selama itu juga angka.
while let Some(d) = chars.next_if(|next| next.is_ascii_digit()) {
number.push(d);
}
tokens.push(number);
} else {
tokens.push(c.to_string());
}
}
println!("{tokens:?}");
}

Output:

["12", "+", "345", "-", "6"]
▶ Coba sendiri

Tanpa peekable, kamu akan ikut membaca + saat mencari angka berikutnya, dan setelah itu + tersebut sudah hilang.

Pakai ini kalau item berikutnya menentukan apa yang kamu lakukan dengan item sekarang.

Menelusuri daftar: zip, enumerate, step_by, take_while, skip_while, chain

Section titled “Menelusuri daftar: zip, enumerate, step_by, take_while, skip_while, chain”
Method Bayangkan seperti Pakai ini kalau…
zip ritsleting yang menyatukan dua sisi gigi demi gigi kamu punya dua daftar yang saling berpasangan
enumerate memberi nomor pada baris-baris daftar kamu butuh indeks sekaligus item-nya
step_by(n) naik tangga dua (atau n) anak tangga sekaligus kamu ingin setiap item ke-n
take_while makan permen sampai ketemu yang tidak kamu suka kamu ingin awal daftar, sampai “tidak” yang pertama
skip_while melewati iklan sampai acaranya mulai kamu ingin sisanya, mulai dari “tidak” yang pertama
chain menyambung dua gerbong kereta kamu ingin satu daftar disusul daftar lain
fn main() {
let names = ["Ana", "Ben", "Chen"];
let scores = [88, 92, 75];
// zip memasangkan dua daftar; enumerate menambah penghitung mulai dari 0
for (i, (name, score)) in names.iter().zip(scores.iter()).enumerate() {
println!("{}. {name}: {score}", i + 1);
}
// step_by: setiap menit ke-15 dalam satu jam
let quarters: Vec<u32> = (0..60).step_by(15).collect();
println!("tiap seperempat jam: {quarters:?}");
// into_iter() pada array memberi angka-angkanya langsung
let queue = [3, 5, 8, 12, 4];
let small: Vec<i32> = queue.into_iter().take_while(|n| *n < 10).collect();
let rest: Vec<i32> = queue.into_iter().skip_while(|n| *n < 10).collect();
println!("sebelum angka besar pertama: {small:?}, mulai dari situ: {rest:?}");
// chain: satu daftar disusul daftar lainnya
let morning = ["email", "standup"];
let afternoon = ["review", "deploy"];
let day: Vec<_> = morning.iter().chain(afternoon.iter()).collect();
println!("{day:?}");
}

Output:

1. Ana: 88
2. Ben: 92
3. Chen: 75
tiap seperempat jam: [0, 15, 30, 45]
sebelum angka besar pertama: [3, 5, 8], mulai dari situ: [12, 4]
["email", "standup", "review", "deploy"]
▶ Coba sendiri

Perhatikan bahwa take_while berhenti di 12 untuk selamanya. Angka 4 sesudahnya memang kecil, tapi tidak pernah diperiksa. Itulah bedanya dengan filter, yang memeriksa setiap item.

Menjawab pertanyaan: fold, position, find_map, partition, flat_map

Section titled “Menjawab pertanyaan: fold, position, find_map, partition, flat_map”
Method Bayangkan seperti Pakai ini kalau…
sum tombol tambah di kalkulator kamu cuma ingin totalnya
fold bola salju yang menggelinding turun, makin besar di setiap langkah kamu membangun satu hasil dengan caramu sendiri
position “Ben duduk di kursi nomor berapa?” kamu butuh indeks dari kecocokan pertama
find_map mencoba kunci satu per satu sampai pintunya terbuka kamu mencoba sesuatu pada tiap item dan menyimpan keberhasilan pertama
partition memilah cucian ke dua keranjang kamu membagi satu daftar jadi dua berdasarkan pertanyaan ya/tidak
flat_map menuang isi beberapa tas ke satu meja setiap item berubah menjadi beberapa item
fn main() {
let orders = [12, 40, 7, 55, 30];
// sum: cukup jumlahkan semuanya
let total: i32 = orders.iter().sum();
// fold: jumlahkan dengan caramu sendiri (di sini: tiap pesanan kena biaya admin 2)
let with_fees = orders.iter().fold(0, |acc, price| acc + price + 2);
println!("total {total}, dengan biaya {with_fees}");
// position: indeks dari kecocokan pertama
println!("pesanan pertama di atas 50 ada di indeks {:?}", orders.iter().position(|p| *p > 50));
// partition: dua Vec, satu untuk "ya" dan satu untuk "tidak"
let (big, small): (Vec<i32>, Vec<i32>) = orders.into_iter().partition(|p| *p >= 30);
println!("besar {big:?}, kecil {small:?}");
// find_map: coba parse satu per satu, simpan yang pertama berhasil
let inputs = ["abc", "", "42", "7"];
let first_number = inputs.iter().find_map(|s| s.parse::<i32>().ok());
println!("angka pertama: {first_number:?}");
// flat_map: setiap baris menjadi beberapa kata, semuanya dalam satu daftar
let lines = ["beli susu", "telepon ibu sekarang"];
let words: Vec<&str> = lines.iter().flat_map(|line| line.split(' ')).collect();
println!("{words:?}");
}

Output:

total 144, dengan biaya 154
pesanan pertama di atas 50 ada di indeks Some(3)
besar [40, 55, 30], kecil [12, 7]
angka pertama: Some(42)
["beli", "susu", "telepon", "ibu", "sekarang"]
▶ Coba sendiri

fold menerima nilai awal (0) dan sebuah closure (fungsi anonim) yang mendapat “hasil sejauh ini” dan item berikutnya. Kalau kamu cuma butuh total biasa, sum menyampaikannya dengan lebih jelas. Pakai fold saat aturannya kamu buat sendiri.

Dua ini adalah method Vec, bukan method iterator, tapi tempatnya tetap di laci yang sama. dedup itu seperti DJ yang tidak pernah memutar lagu yang sama dua kali berturut-turut (lagunya boleh muncul lagi nanti). retain itu penjaga pintu: hanya item yang lolos tes yang boleh tetap di dalam.

fn main() {
// dedup menghapus pengulangan yang letaknya bersebelahan
let mut clicks = vec!["beranda", "beranda", "toko", "toko", "toko", "keranjang", "beranda"];
clicks.dedup();
println!("{clicks:?}");
// retain hanya menyimpan item yang lolos tes
let mut cart = vec![("apel", 3), ("roti", 0), ("susu", 1), ("telur", 0)];
cart.retain(|(_, qty)| *qty > 0);
println!("{cart:?}");
}

Output:

["beranda", "toko", "keranjang", "beranda"]
[("apel", 3), ("susu", 1)]
▶ Coba sendiri

"beranda" yang terakhir tetap ada karena letaknya tidak bersebelahan dengan "beranda" lain. Untuk menghapus semua duplikat, urutkan dulu datanya, seperti yang ditunjukkan di halaman Pengurutan.

Pakai ini kalau kamu ingin mengubah sebuah Vec tanpa membuat yang baru.

Option itu seperti paket yang mungkin sudah datang, mungkin juga belum. Kamu bisa membuka setiap paket dengan match lengkap, tapi biasanya satu helper pendek sudah cukup untuk menyampaikan hal yang sama. Halaman Option membahas unwrap_or dan match; berikut helper-helper berikutnya.

fn main() {
let nickname: Option<String> = None;
let age: Option<u32> = Some(17);
let width: Option<u32> = Some(3);
let height: Option<u32> = Some(4);
// unwrap_or_default: nilainya, atau default tipe itu ("", 0, Vec kosong...)
println!("nama panggilan: '{}'", nickname.unwrap_or_default());
// map_or: nilai cadangan kalau None, kalau tidak, ubah nilainya
println!("{}", age.map_or("umur tidak diketahui".to_string(), |a| format!("{a} tahun")));
// filter: simpan nilainya hanya kalau lolos tes
println!("umur dewasa: {:?}", age.filter(|a| *a >= 18));
// zip: Some hanya kalau KEDUANYA Some
println!("luas: {:?}", width.zip(height).map(|(w, h)| w * h));
// and_then: sambung dengan langkah yang juga bisa menghasilkan None
let good = Some("42").and_then(|s| s.parse::<u32>().ok());
let bad = Some("empat puluh").and_then(|s| s.parse::<u32>().ok());
println!("hasil parse: {good:?} dan {bad:?}");
}

Output:

nama panggilan: ''
17 tahun
umur dewasa: None
luas: Some(12)
hasil parse: Some(42) dan None
▶ Coba sendiri

Di dalam fungsi, ? juga bisa dipakai pada Option: kalau nilainya None, fungsi langsung mengembalikan None. Di fungsi yang mengembalikan Result, ubah dulu Option-nya menjadi Result dengan ok_or(error) atau ok_or_else(|| error). Pakai ok_or_else kalau membuat error-nya butuh usaha, karena closure itu hanya dijalankan saat diperlukan.

struct Employee {
name: String,
manager: Option<String>,
}
// `?` pada Option: kalau None, langsung kembalikan None.
fn manager_initial(e: &Employee) -> Option<char> {
let manager = e.manager.as_ref()?;
manager.chars().next()
}
// ok_or_else mengubah None menjadi error, jadi `?` bisa dipakai di fungsi Result.
fn port_from(setting: Option<&str>) -> Result<u16, String> {
let raw = setting.ok_or_else(|| "PORT belum diatur".to_string())?;
raw.parse::<u16>().map_err(|e| e.to_string())
}
fn main() {
let ana = Employee { name: "Ana".to_string(), manager: Some("Grace".to_string()) };
let grace = Employee { name: "Grace".to_string(), manager: None };
println!("{}: {:?}", ana.name, manager_initial(&ana));
println!("{}: {:?}", grace.name, manager_initial(&grace));
println!("{:?}", port_from(Some("8080")));
println!("{:?}", port_from(None));
println!("{:?}", port_from(Some("delapan puluh")));
// transpose: Option<Result<..>> menjadi Result<Option<..>>
let retries_setting: Option<&str> = Some("3");
let retries: Result<Option<u8>, _> = retries_setting.map(|s| s.parse::<u8>()).transpose();
println!("{retries:?}");
}

Output:

Ana: Some('G')
Grace: None
Ok(8080)
Err("PORT belum diatur")
Err("invalid digit found in string")
Ok(Some(3))
▶ Coba sendiri

Pesan invalid digit found in string berasal dari Rust sendiri (pesan error bawaan saat parse gagal), jadi tetap dalam bahasa Inggris. transpose memang jarang dipakai, tapi ia jawaban yang rapi untuk situasi “aku punya pengaturan opsional, dan parse-nya bisa gagal”. Dengannya, kamu bisa memasang ? pada hasilnya dan tetap menyimpan Option-nya.

Pakai ini kalau sebuah match pada Option cuma punya satu cabang yang menarik dan satu cabang yang membosankan.

Bayangkan meja penitipan jaket. Kamu menyebutkan sebuah nama, lalu petugas mencari gantungan itu atau memasang yang baru, kemudian menyerahkannya padamu. Cukup satu kali jalan, bukan dua. Itulah .entry(key). Halaman HashMap menunjukkan entry(key).or_insert(value), si penghitung klasik. Ada dua variasi lagi yang perlu kamu tahu:

  • or_insert_with(f) hanya membuat nilai default saat key-nya belum ada. or_default() melakukan hal yang sama dengan nilai default tipe itu.
  • and_modify(f) mengubah nilainya kalau key-nya sudah ada; sambung dengan or_insert sesudahnya untuk kasus “key baru”.
use std::collections::HashMap;
fn main() {
let sales = [("apel", 3), ("pir", 2), ("apel", 5), ("plum", 1), ("pir", 4)];
// and_modify + or_insert: perbarui kalau ada, sisipkan kalau belum
let mut totals: HashMap<&str, i32> = HashMap::new();
for (fruit, qty) in sales {
totals.entry(fruit).and_modify(|t| *t += qty).or_insert(qty);
}
// or_insert_with: buat Vec kosong hanya saat dibutuhkan
let mut orders: HashMap<&str, Vec<i32>> = HashMap::new();
for (fruit, qty) in sales {
orders.entry(fruit).or_insert_with(Vec::new).push(qty);
}
// Urutan HashMap acak, jadi urutkan key-nya sebelum dicetak
let mut fruits: Vec<&str> = totals.keys().copied().collect();
fruits.sort();
for fruit in fruits {
println!("{fruit}: total {}, pesanan {:?}", totals[fruit], orders[fruit]);
}
}

Output:

apel: total 8, pesanan [3, 5]
pir: total 6, pesanan [2, 4]
plum: total 1, pesanan [1]
▶ Coba sendiri

Pakai ini kalau tanpa itu kamu akan menulis “kalau key-nya ada, ubah; kalau tidak, sisipkan”, yang mencari key yang sama dua kali.

Never type !: kode yang tidak pernah kembali

Section titled “Never type !: kode yang tidak pernah kembali”

Ada jalan yang satu arah: sekali masuk, kamu tidak pernah keluar lagi. Di Rust, ekspresi yang tidak pernah selesai punya never type (tipe “tidak pernah”), ditulis !. panic!, return, break, continue, loop tanpa akhir, dan std::process::exit semuanya bertipe ini.

Karena nilai bertipe ! tidak pernah benar-benar ada, compiler mengizinkannya menggantikan tipe apa pun. Itulah sebabnya sebuah match boleh punya satu cabang yang menghasilkan i32 dan cabang lain yang melakukan return: cabang kedua tidak pernah menghasilkan nilai, jadi tidak bisa merusak kecocokan tipenya.

use std::process;
// `-> !` artinya "fungsi ini tidak pernah kembali"
fn quit(message: &str) -> ! {
println!("{message}");
process::exit(0)
}
fn level_points(level: &str) -> u32 {
let points: u32 = match level {
"mudah" => 10,
"sulit" => 30,
_ => return 0, // `return` bertipe `!`, jadi cocok di tempat yang mengharapkan u32
};
points * 2
}
fn main() {
println!("mudah: {}, sulit: {}, aneh: {}", level_points("mudah"), level_points("sulit"), level_points("aneh"));
let mut total = 0;
for input in ["4", "oops", "6"] {
let n: i32 = match input.parse() {
Ok(n) => n,
Err(_) => continue, // `continue` juga bertipe `!`
};
total += n;
}
println!("total: {total}");
let config_file: Option<&str> = None;
let config = match config_file {
Some(path) => path,
None => quit("file konfigurasi tidak ditemukan, berhenti"),
};
println!("dimuat {config}"); // tidak pernah dicetak
}

Output:

mudah: 20, sulit: 60, aneh: 0
total: 10
file konfigurasi tidak ditemukan, berhenti
▶ Coba sendiri

Satu peringatan: process::exit menghentikan program saat itu juga, tanpa menjalankan kode pembersihan (destructor) untuk nilai-nilai yang masih hidup. Di program sungguhan, kembali dari main biasanya pilihan yang lebih halus.

Ada dua makro yang memakai ! sebagai penanda sementara saat kamu masih membuat sketsa kode. todo!() artinya “nanti aku tulis”. unimplemented!() artinya “ini tidak didukung di sini”. Keduanya akan panic kalau sampai dijalankan, tapi keduanya membuat sisa programmu tetap bisa di-compile:

fn export_pdf(report: &str) -> Vec<u8> {
todo!("ekspor PDF menyusul di v2")
}

Kalau fungsi itu dipanggil, kamu akan mendapat not yet implemented: ekspor PDF menyusul di v2. Dengan unimplemented!("ekspor PDF") pesannya menjadi not implemented: ekspor PDF. Awalan not yet implemented dan not implemented berasal dari Rust, jadi selalu dalam bahasa Inggris.

Pakai ini kalau sebuah fungsi harus menghentikan program, atau sebuah cabang harus keluar lebih awal alih-alih memberi nilai.

Ini alat-alat saku: kecil, tapi akan kamu pakai setiap minggu.

  • matches!(value, pattern) menjawab “apakah ini cocok dengan pattern-nya?” dengan true atau false (selengkapnya di halaman Pattern Matching).
  • std::cmp::min / max memilih yang lebih kecil atau lebih besar dari dua nilai, dan .clamp(low, high) menjaga nilai tetap di dalam rentang, seperti tombol volume yang mentok di 100.
  • .abs_diff(other) memberi jarak antara dua angka. Hasilnya selalu positif dan tidak pernah overflow, bahkan untuk tipe unsigned.
  • dbg!(expr) mencetak nama file, baris, ekspresi dan nilainya, lalu mengembalikan nilai itu, jadi kamu bisa membungkus apa saja dengannya.
  • assert!(condition) akan panic kalau kondisinya false. debug_assert! melakukan hal yang sama, tapi hanya di build debug, jadi tidak ada biayanya di build release.
use std::cmp::{max, min};
enum Order {
Pending,
Shipped,
Delivered,
Cancelled,
}
fn main() {
let orders = [Order::Pending, Order::Delivered, Order::Shipped, Order::Cancelled, Order::Pending];
// matches!: pengecekan pattern ya/tidak, dengan | untuk "atau"
let open = orders.iter().filter(|o| matches!(o, Order::Pending | Order::Shipped)).count();
println!("pesanan yang masih berjalan: {open}");
// min, max dan clamp
println!("min {}, max {}", min(7, 3), max(7, 3));
let volume = 140;
let safe = volume.clamp(0, 100);
println!("volume {volume} dibatasi jadi {safe}");
// abs_diff: seberapa jauh jaraknya, tidak pernah negatif
let (guess, secret): (u32, u32) = (30, 42);
println!("tebakanmu meleset {}", guess.abs_diff(secret));
// dbg! mencetak ke stderr dan mengembalikan nilainya
let doubled = dbg!(safe * 2);
// assert! menghentikan program kalau ini sampai false
assert!(doubled <= 200, "volume ganda terlalu tinggi: {doubled}");
debug_assert!(safe <= 100); // hanya diperiksa di build debug
println!("semua pemeriksaan lolos");
}

Output:

pesanan yang masih berjalan: 3
min 3, max 7
volume 140 dibatasi jadi 100
tebakanmu meleset 12
semua pemeriksaan lolos
▶ Coba sendiri

Baris dari dbg! tidak muncul di output di atas, karena dbg! menulis ke stderr (keluaran untuk error), bukan ke stdout. Di Playground, baris itu muncul di panel “Standard Error”:

[src/main.rs:28:19] safe * 2 = 200

Itu juga alasan dbg! aman ditaburkan di mana-mana saat debugging: ia tidak tercampur dengan output asli programmu. Hanya saja, jangan lupa menghapusnya sebelum kodenya dirilis.

Pengurutan juga punya alat-alat kecilnya sendiri. Halaman Pengurutan membahasnya secara mendalam; berikut versi singkatnya. sort_unstable sedikit lebih cepat daripada sort dan cocok untuk angka biasa. sort_by_key mengurutkan berdasarkan satu field. binary_search menemukan item di data yang sudah terurut dengan sangat cepat, seperti membuka kamus langsung di tengah alih-alih membaca dari halaman pertama.

struct Player {
name: &'static str,
score: u32,
}
fn main() {
// sort_unstable: sedikit lebih cepat; item yang sama bisa bertukar tempat
let mut ids = vec![42, 7, 19, 3, 25];
ids.sort_unstable();
println!("{ids:?}");
// binary_search: Ok(indeks) kalau ketemu, Err(indeks) tempat ia seharusnya berada
println!("cari 19: {:?}", ids.binary_search(&19));
println!("cari 20: {:?}", ids.binary_search(&20));
// sort_by_key: urutkan berdasarkan satu field
let mut players = vec![
Player { name: "Ana", score: 120 },
Player { name: "Ben", score: 95 },
Player { name: "Chen", score: 150 },
];
players.sort_by_key(|p| p.score);
let names: Vec<&str> = players.iter().map(|p| p.name).collect();
println!("terendah ke tertinggi: {names:?}");
}

Output:

[3, 7, 19, 25, 42]
cari 19: Ok(2)
cari 20: Err(3)
terendah ke tertinggi: ["Ben", "Ana", "Chen"]
▶ Coba sendiri

Err(3) bukan kegagalan yang perlu dikhawatirkan. Artinya: 20 tidak ada di daftar, dan indeks 3 adalah tempat kamu bisa menyisipkannya supaya daftarnya tetap terurut.

Pakai ini kalau kamu sadar sedang menulis if atau perulangan kecil yang sebenarnya sudah dikerjakan oleh salah satu alat ini.

Memakai mem::take pada tipe yang tidak punya Default. mem::take harus meninggalkan sesuatu, jadi ia perlu tahu seperti apa nilai “kosong” untuk tipe itu:

use std::mem;
struct Connection {
host: String,
}
struct Pool {
active: Connection,
}
impl Pool {
fn reset(&mut self) -> Connection {
mem::take(&mut self.active) // ❌ seperti apa Connection yang "kosong"?
}
}
error[E0277]: the trait bound `Connection: Default` is not satisfied
--> src/main.rs:13:19
|
13 | mem::take(&mut self.active) // ❌ seperti apa Connection yang "kosong"?
| --------- ^^^^^^^^^^^^^^^^ the trait `Default` is not implemented for `Connection`
| |
| required by a bound introduced by this call
|
help: consider annotating `Connection` with `#[derive(Default)]`

Compiler bilang trait Default belum diimplementasikan untuk Connection. Perbaiki dengan memilih sendiri penggantinya lewat mem::replace(&mut self.active, Connection { host: "db2".to_string() }), atau dengan menyimpan field-nya sebagai Option<Connection> lalu memakai self.active.take(). Tambahkan #[derive(Default)] hanya kalau nilai “kosong” memang masuk akal untuk tipe itu.

Memakai ? pada Option di fungsi yang mengembalikan Result. Keduanya tidak otomatis bisa dicampur:

fn port_from(setting: Option<&str>) -> Result<u16, String> {
let raw = setting?; // ❌ None tidak punya pesan error untuk dikembalikan
raw.parse::<u16>().map_err(|e| e.to_string())
}
error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result`
--> src/main.rs:2:22
|
1 | fn port_from(setting: Option<&str>) -> Result<u16, String> {
| ---------------------------------------------------------- this function returns a `Result`
2 | let raw = setting?; // ❌ None tidak punya pesan error untuk dikembalikan
| ^ use `.ok_or(...)?` to provide an error compatible with `Result<u16, String>`

Compiler bahkan sudah memberitahumu cara memperbaikinya: setting.ok_or("PORT belum diatur".to_string())?, atau ok_or_else seperti di contoh sebelumnya. Lihat E0277 untuk bentuk lain dari error ini.

Mengira dedup atau binary_search bekerja pada data yang belum terurut. dedup hanya menghapus pengulangan yang bersebelahan, dan binary_search pada data yang belum terurut memberi jawaban yang tidak berarti. Keduanya bukan error compiler, jadi gampang terlewat. Urutkan dulu datanya.

Lupa sisa dari chunks_exact. Perulangan for pada chunks_exact(3) diam-diam melewatkan 1 atau 2 item terakhir. Kalau item itu penting, tangani .remainder(), atau pakai chunks saja.

Kamu punya harga [100, 104, 98, 110, 107]. Buat sebuah Vec berisi perubahan dari tiap hari ke hari berikutnya, cetak, lalu cetak kenaikan terbesarnya. Petunjuk: windows(2) memberi setiap pasangan sebagai slice kecil w, jadi perubahannya adalah w[1] - w[0]. .iter().max() mencari yang terbesar.

Lihat solusi
fn main() {
let prices = [100, 104, 98, 110, 107];
let changes: Vec<i32> = prices.windows(2).map(|w| w[1] - w[0]).collect();
println!("perubahan: {changes:?}");
let biggest_rise = changes.iter().max();
println!("kenaikan terbesar: {biggest_rise:?}");
}

Output:

perubahan: [4, -6, 12, -3]
kenaikan terbesar: Some(12)
▶ Coba sendiri

Lima harga menghasilkan empat perubahan, karena windows(2) membuat satu pasangan di setiap langkah. max() mengembalikan Option, karena daftar kosong tidak punya nilai terbesar.

Tulis struct Printer dengan field queue: Vec<String>. Beri method add(&mut self, file: &str) dan method flush(&mut self) -> Vec<String> yang menyerahkan semua file di antrean dan membiarkan antreannya kosong. Tambahkan dua file, panggil flush, lalu cetak apa saja yang dikeluarkan dan berapa file yang tersisa. Petunjuk: flush hanya punya &mut self, jadi pakai std::mem::take.

Lihat solusi
use std::mem;
struct Printer {
queue: Vec<String>,
}
impl Printer {
fn add(&mut self, file: &str) {
self.queue.push(file.to_string());
}
fn flush(&mut self) -> Vec<String> {
mem::take(&mut self.queue)
}
}
fn main() {
let mut printer = Printer { queue: Vec::new() };
printer.add("laporan.pdf");
printer.add("foto.png");
let jobs = printer.flush();
println!("mencetak {} tugas: {jobs:?}", jobs.len());
println!("sisa di antrean: {}", printer.queue.len());
}

Output:

mencetak 2 tugas: ["laporan.pdf", "foto.png"]
sisa di antrean: 0
▶ Coba sendiri

Menulis self.queue begitu saja di flush akan gagal dengan E0507. mem::take memindahkan Vec-nya keluar dan meninggalkan Vec kosong sebagai gantinya, tanpa menyalin apa pun.

Tulis fungsi first_port(candidates: &[&str]) -> Option<u16> yang mengembalikan string pertama yang bisa di-parse sebagai u16. Panggil fungsi itu dengan ["", "abc", "70000", "8080", "9090"] dan dengan ["none", "-1"], lalu cetak port-nya, atau 3000 kalau tidak ada. Petunjuk: find_map dengan s.parse::<u16>().ok(), lalu unwrap_or(3000).

Lihat solusi
fn first_port(candidates: &[&str]) -> Option<u16> {
candidates.iter().find_map(|s| s.parse::<u16>().ok())
}
fn main() {
let from_config = ["", "abc", "70000", "8080", "9090"];
let all_bad = ["none", "-1"];
println!("memakai port {}", first_port(&from_config).unwrap_or(3000));
println!("memakai port {}", first_port(&all_bad).unwrap_or(3000));
}

Output:

memakai port 8080
memakai port 3000
▶ Coba sendiri

"70000" memang kelihatan seperti angka, tapi tidak muat di u16 (nilai terbesarnya 65535), jadi parse-nya gagal dan find_map lanjut ke item berikutnya. Ia berhenti di "8080" dan bahkan tidak pernah melihat "9090".

  • mem::take, mem::replace dan Option::take memindahkan nilai keluar dari &mut dengan meninggalkan sesuatu di tempatnya; mem::swap menukar dua nilai.
  • Pada slice, windows memberi tetangga yang saling tumpang tindih, chunks memberi kelompok, dan split_first / split_last memberi “satu item dan sisanya”.
  • peekable membuatmu bisa mengintip ke depan; zip, enumerate, chain, fold, find_map dan partition menggantikan banyak perulangan yang ditulis manual.
  • Helper Option seperti unwrap_or_default, map_or, filter dan ok_or menyelamatkanmu dari menulis match kecil-kecil, dan ? juga bisa dipakai pada Option.
  • entry() dengan or_insert_with atau and_modify memperbarui HashMap hanya dengan satu kali pencarian.
  • Never type ! dimiliki kode yang tidak pernah selesai (panic!, return, continue, process::exit), jadi ia cocok di mana saja sebuah nilai diharapkan.

Kuis singkat

1. Apa yang ditinggalkan mem::take(&mut self.items) kalau items adalah sebuah Vec?

2. Apa yang kamu dapat dari [1, 2, 3, 4].windows(2)?

3. Kenapa let x: i32 = match s { "a" => 1, _ => return }; bisa di-compile?

4. Fungsimu mengembalikan Result, tapi yang kamu punya sebuah Option. Apa yang kamu tambahkan sebelum ?

Skor: 0 / 4