Collections, Iterators, dan Closures di Rust: Panduan Praktis
Di artikel sebelumnya kita sudah belajar concurrency dan async programming. Sekarang saatnya belajar tiga konsep yang akan kamu pakai setiap hari saat menulis Rust: collections, iterators, dan closures.
Ketiga konsep ini saling berkaitan erat. Collections menyimpan data, closures mendefinisikan operasi, dan iterators menghubungkan keduanya dengan cara yang elegan dan efisien.
Collections
Collections adalah tipe data yang menyimpan kumpulan nilai. Berbeda dari array yang ukurannya tetap, collections bisa berubah ukurannya. Ada tiga collections utama di Rust:
- Vec: List yang terurut, bisa bertambah ukurannya
- HashMap: Pasangan key-value, akses cepat berdasarkan key
- String: Teks yang bisa dimodifikasi
Vec (Vector)
Vec adalah collection yang paling sering digunakan. Mirip seperti array dinamis di bahasa lain.
Membuat Vec
fn main() {
// Cara 1: Vec::new()
let mut v1: Vec<i32> = Vec::new();
v1.push(1);
v1.push(2);
v1.push(3);
// Cara 2: vec! macro
let v2 = vec![1, 2, 3, 4, 5];
// Cara 3: dengan kapasitas
let mut v3 = Vec::with_capacity(100);
for i in 0..100 {
v3.push(i); // Tidak perlu realokasi
}
println!("v1: {:?}", v1);
println!("v2: {:?}", v2);
println!("v3 length: {}", v3.len());
}
Vec::with_capacity() lebih efisien jika kamu tahu berapa banyak elemen yang akan disimpan.
Mengakses Elemen
fn main() {
let v = vec![10, 20, 30, 40, 50];
// Cara 1: indexing (panic jika out of bounds)
let third = v[2];
println!("Elemen ke-3: {}", third);
// Cara 2: get() (mengembalikan Option)
match v.get(2) {
Some(value) => println!("Elemen ke-3: {}", value),
None => println!("Elemen tidak ada"),
}
// Akses terakhir
let last = v.last().unwrap();
println!("Terakhir: {}", last);
// Cek apakah kosong
println!("Kosong? {}", v.is_empty());
// Panjang
println!("Panjang: {}", v.len());
}
Gunakan get() jika index mungkin tidak valid. Gunakan [] jika kamu yakin index valid.
Memodifikasi Vec
fn main() {
let mut v = vec![1, 2, 3];
// Tambah di akhir
v.push(4);
v.push(5);
// Hapus dari akhir
let last = v.pop(); // Some(5)
// Sisip di posisi tertentu
v.insert(1, 10); // Sisip 10 di index 1
// Hapus di posisi tertentu
v.remove(1); // Hapus elemen di index 1
// Hapus semua elemen tertentu
v.retain(|&x| x > 2); // Sisakan yang > 2
// Clear semua
// v.clear();
println!("{:?}", v); // [3, 4]
}
Vec dengan Berbagai Tipe (menggunakan Enum)
#[derive(Debug)]
enum CellValue {
Int(i32),
Float(f64),
Text(String),
}
fn main() {
let row = vec![
CellValue::Int(1),
CellValue::Text(String::from("ERLKIM")),
CellValue::Float(3.14),
];
for cell in &row {
match cell {
CellValue::Int(n) => println!("Integer: {}", n),
CellValue::Float(f) => println!("Float: {}", f),
CellValue::Text(s) => println!("Text: {}", s),
}
}
}
HashMap
HashMap menyimpan data dalam pasangan key-value. Akses data sangat cepat berdasarkan key.
Membuat HashMap
use std::collections::HashMap;
fn main() {
// Cara 1: HashMap::new()
let mut scores = HashMap::new();
scores.insert(String::from("ERLKIM"), 95);
scores.insert(String::from("Guest"), 80);
// Cara 2: dari iterator
let names = vec!["Alice", "Bob", "Charlie"];
let ages = vec![25, 30, 35];
let people: HashMap<_, _> = names.into_iter().zip(ages.into_iter()).collect();
println!("Scores: {:?}", scores);
println!("People: {:?}", people);
}
zip() menggabungkan dua iterator menjadi pasangan. collect() mengumpulkan hasilnya ke dalam HashMap.
Mengakses dan Memodifikasi
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("ERLKIM"), 95);
// Akses dengan get
let score = scores.get("ERLKIM");
match score {
Some(s) => println!("Score: {}", s),
None => println!("Tidak ditemukan"),
}
// Update: overwrite
scores.insert(String::from("ERLKIM"), 100);
// Update: hanya jika belum ada
scores.entry(String::from("Guest")).or_insert(80);
// Update: berdasarkan nilai lama
let text = "hello world hello rust hello world";
let mut word_count = HashMap::new();
for word in text.split_whitespace() {
let count = word_count.entry(word).or_insert(0);
*count += 1;
}
println!("Word count: {:?}", word_count);
// Hapus
scores.remove("Guest");
// Cek key
if scores.contains_key("ERLKIM") {
println!("ERLKIM ada di scores");
}
// Iterasi
for (key, value) in &scores {
println!("{}: {}", key, value);
}
}
Method entry().or_insert() sangat powerful. Jika key belum ada, masukkan default value. Jika sudah ada, kembalikan reference ke value yang ada.
Word Count - Contoh Klasik
use std::collections::HashMap;
fn word_count(text: &str) -> HashMap<&str, usize> {
let mut count = HashMap::new();
for word in text.split_whitespace() {
*count.entry(word).or_insert(0) += 1;
}
count
}
fn main() {
let text = "the quick brown fox jumps over the lazy dog the fox";
let count = word_count(text);
let mut sorted: Vec<_> = count.iter().collect();
sorted.sort_by(|a, b| b.1.cmp(a.1));
for (word, freq) in sorted {
println!("{}: {}", word, freq);
}
}
Output:
the: 3
fox: 2
quick: 1
brown: 1
jumps: 1
over: 1
lazy: 1
dog: 1
Closures
Closure adalah anonymous function yang bisa menangkap variabel dari lingkungan sekitarnya. Closures sangat umum digunakan di Rust, terutama bersama iterators.
Syntax Dasar
fn main() {
// Function biasa
fn add(a: i32, b: i32) -> i32 { a + b }
// Closure
let add_closure = |a: i32, b: i32| -> i32 { a + b };
// Closure tanpa tipe (compiler infer)
let add_short = |a, b| a + b;
println!("{}", add(1, 2));
println!("{}", add_closure(1, 2));
println!("{}", add_short(1, 2));
}
Perbedaan utama dari function: closure bisa menangkap variabel dari luar.
Menangkap Variabel
fn main() {
let multiplier = 3;
// Closure menangkap multiplier
let multiply = |x| x * multiplier;
println!("5 x {} = {}", multiplier, multiply(5));
println!("10 x {} = {}", multiplier, multiply(10));
}
Closures menangkap variabel dengan tiga cara:
fn main() {
let name = String::from("ERLKIM");
// 1. Borrow (immutable)
let greet = || println!("Halo, {}!", name);
greet();
greet();
println!("Masih bisa: {}", name); // OK
// 2. Borrow mutable
let mut count = 0;
let mut increment = || {
count += 1;
println!("Count: {}", count);
};
increment(); // Count: 1
increment(); // Count: 2
// 3. Move (ambil ownership)
let data = vec![1, 2, 3];
let process = move || {
println!("Data: {:?}", data);
};
process();
// println!("{:?}", data); // ERROR! data sudah di-move
}
Closure sebagai Parameter
fn apply<F>(f: F, x: i32) -> i32
where
F: Fn(i32) -> i32,
{
f(x)
}
fn main() {
let double = |x| x * 2;
let square = |x| x * x;
println!("Double 5: {}", apply(double, 5));
println!("Square 5: {}", apply(square, 5));
}
Ada tiga trait untuk closures:
- Fn: Membaca variabel yang ditangkap (immutable borrow)
- FnMut: Memodifikasi variabel yang ditangkap (mutable borrow)
- FnOnce: Mengambil ownership, bisa dipanggil sekali
fn call_fn<F: Fn()>(f: F) { f(); }
fn call_fn_mut<F: FnMut()>(mut f: F) { f(); }
fn call_fn_once<F: FnOnce()>(f: F) { f(); }
fn main() {
let name = String::from("ERLKIM");
// Fn: hanya baca
let greet = || println!("Halo, {}!", name);
call_fn(greet);
call_fn(greet);
// FnMut: bisa ubah
let mut count = 0;
let mut inc = || { count += 1; };
call_fn_mut(&mut inc);
call_fn_mut(&mut inc);
// FnOnce: sekali pakai
let consume = move || { println!("Consumed: {}", name); };
call_fn_once(consume);
// call_fn_once(consume); // ERROR! sudah dipanggil
}
Iterators
Iterator adalah trait yang memungkinkan kamu memproses setiap elemen dalam collection secara berurutan. Ini adalah salah satu fitur paling powerful di Rust.
Trait Iterator
// Simplified version
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Setiap tipe yang mengimplementasikan Iterator punya method next() yang mengembalikan Some(item) atau None saat habis.
Membuat Iterator
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// .iter() menghasilkan &T
for n in numbers.iter() {
print!("{} ", n);
}
println!();
// .into_iter() menghasilkan T (mengambil ownership)
for n in numbers.into_iter() {
print!("{} ", n);
}
println!();
// numbers sudah tidak valid
let mut names = vec!["Alice", "Bob", "Charlie"];
// .iter_mut() menghasilkan &mut T
for name in names.iter_mut() {
*name = "Mr. ";
}
println!("{:?}", names);
}
Iterator Adapters
Iterator adapters mengubah iterator menjadi iterator baru. Mereka lazy artinya tidak dieksekusi sampai di-consume.
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// map: transformasi setiap elemen
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
println!("Doubled: {:?}", doubled);
// filter: sisakan yang memenuhi syarat
let evens: Vec<&i32> = numbers.iter().filter(|&&x| x
## Contoh Praktis: Data Processing Pipeline
Berikut contoh nyata yang menggabungkan collections, closures, dan iterators:
```rust
use std::collections::HashMap;
#[derive(Debug)]
struct Student {
name: String,
grade: u32,
score: f64,
}
fn main() {
let students = vec![
Student { name: "Alice".into(), grade: 10, score: 85.5 },
Student { name: "Bob".into(), grade: 11, score: 92.0 },
Student { name: "Charlie".into(), grade: 10, score: 78.3 },
Student { name: "Diana".into(), grade: 11, score: 95.1 },
Student { name: "Eve".into(), grade: 10, score: 88.7 },
Student { name: "Frank".into(), grade: 11, score: 71.2 },
];
// 1. Filter siswa dengan score > 80
let high_scores: Vec<&Student> = students
.iter()
.filter(|s| s.score > 80.0)
.collect();
println!("Score > 80: {}", high_scores.len());
// 2. Rata-rata score per grade
let mut grade_scores: HashMap<u32, Vec<f64>> = HashMap::new();
for student in &students {
grade_scores
.entry(student.grade)
.or_insert_with(Vec::new)
.push(student.score);
}
for (grade, scores) in &grade_scores {
let avg: f64 = scores.iter().sum::<f64>() / scores.len() as f64;
println!("Grade {}: avg {:.1} ({} students)", grade, avg, scores.len());
}
// 3. Top 3 siswa
let mut sorted = students.iter().collect::<Vec<_>>();
sorted.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
let top3: Vec<&Student> = sorted.into_iter().take(3).collect();
println!("\nTop 3:");
for (i, s) in top3.iter().enumerate() {
println!("{}. {} - {:.1}", i + 1, s.name, s.score);
}
// 4. Group names by grade
let names_by_grade: HashMap<u32, Vec<&str>> = students
.iter()
.fold(HashMap::new(), |mut acc, s| {
acc.entry(s.grade)
.or_insert_with(Vec::new)
.push(&s.name);
acc
});
println!("\nNames by grade:");
for (grade, names) in &names_by_grade {
println!("Grade {}: {:?}", grade, names);
}
}
Pipeline di atas melakukan:
- Filter siswa berdasarkan score
- Group dan hitung rata-rata per grade
- Sort dan take top 3
- Fold untuk group names by grade
Semua dilakukan dengan iterator chains yang elegan dan efisien.
Contoh: CLI Text Analyzer
use std::collections::HashMap;
fn analyze_text(text: &str) {
// Word frequency
let mut word_freq: HashMap<&str, usize> = HashMap::new();
let words: Vec<&str> = text.split_whitespace().collect();
for word in &words {
*word_freq.entry(word).or_insert(0) += 1;
}
// Statistics
let total_words = words.len();
let unique_words = word_freq.len();
let avg_length: f64 = words.iter().map(|w| w.len()).sum::<usize>() as f64 / total_words as f64;
let longest = words.iter().max_by_key(|w| w.len()).unwrap();
let shortest = words.iter().min_by_key(|w| w.len()).unwrap();
// Character frequency
let mut char_freq: HashMap<char, usize> = HashMap::new();
for c in text.chars().filter(|c| !c.is_whitespace()) {
*char_freq.entry(c).or_insert(0) += 1;
}
// Top 5 most frequent words
let mut word_vec: Vec<_> = word_freq.iter().collect();
word_vec.sort_by(|a, b| b.1.cmp(a.1));
let top5: Vec<_> = word_vec.into_iter().take(5).collect();
// Top 5 most frequent characters
let mut char_vec: Vec<_> = char_freq.iter().collect();
char_vec.sort_by(|a, b| b.1.cmp(a.1));
let top5_chars: Vec<_> = char_vec.into_iter().take(5).collect();
println!("=== Text Analysis ===");
println!("Total words: {}", total_words);
println!("Unique words: {}", unique_words);
println!("Average word length: {:.1} chars", avg_length);
println!("Longest word: {}", longest);
println!("Shortest word: {}", shortest);
println!("\nTop 5 words: {:?}", top5);
println!("Top 5 chars: {:?}", top5_chars);
}
fn main() {
let text = "Rust is a systems programming language Rust is fast Rust is safe Rust is concurrent";
analyze_text(text);
}
Performance: Iterator vs Loop
Iterators di Rust sama cepatnya dengan loop biasa. Compiler mengoptimasi iterator chains menjadi kode yang sangat efisien melalui proses yang disebut zero-cost abstractions.
fn main() {
let numbers: Vec<i32> = (1..=1000000).collect();
// Iterator (dioptimasi oleh compiler)
let sum1: i32 = numbers.iter().filter(|&&x| x % 2 == 0).sum();
let mut sum2 = 0;
for &n in &numbers {
if n % 2 == 0 {
sum2 += n;
}
}
assert_eq!(sum1, sum2);
println!("Sum: {}", sum1);
println!("Kedua cara menghasilkan performa yang sama!");
}
Dalam benchmark, kedua versi biasanya menghasilkan assembly yang identik.
Tips
1. Prefer Iterator dari pada Loop
rust
rust
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
2. Gunakan turbofish untuk type inference
rust
rust
let v = vec!["1", "2", "3"];
let numbers: Vec<i32> = v.iter().map(|s| s.parse().unwrap()).collect();
3. Hindari collect berulang
rust
rust
let result: Vec<_> = data.iter().filter(|&&x| x > 5).map(|x| x * 2).collect();
Kesimpulan
Collections, iterators, dan closures adalah trio yang sangat kuat di Rust:
Vec, HashMap, String menyimpan dan mengelola data
Closures mendefinisikan operasi secara inline dan fleksibel
Iterators memproses data dengan cara yang ekspresif dan efisien
Dengan menggabungkan ketiganya, kamu bisa menulis kode yang singkat, jelas, dan secepat loop manual. Ini adalah gaya Rust yang sesungguhnya.
Di artikel selanjutnya, kita akan belajar tentang smart pointers, macros, dan advanced pattern di Rust. ~Erlkim
Artikel Terkait

Concurrency dan Async Programming di Rust: Panduan Lengkap
Belajar concurrency dan async programming di Rust. Thread, mutex, channel, dan async/await untuk kode yang cepat dan aman.

Error Handling, Traits, dan Generic di Rust: Panduan Lanjutan
Lanjutan tutorial Rust: cara menangani error dengan Result dan Option, membuat trait, dan menggunakan generic untuk kode yang fleksibel.

Ownership, Borrowing, dan Lifetimes di Rust: Panduan Lengkap
Memahami konsep paling penting di Rust: ownership, borrowing, dan lifetimes. Tanpa garbage collector, tanpa memory leak.
Komentar