Setelah cukup lama bekerja dengan ownership, koleksi, dan closure, kita tiba di abstraksi yang menyatukan semuanya: iterator. Hampir semua pengolahan data di Rust — dari memformat daftar menu, menjumlahkan harga pesanan, hingga membangun pipeline transformasi — berpijak pada satu kontrak kecil bernama trait Iterator. Kontrak ini membuat Vec, HashMap, String, range, dan tipe buatan sendiri dapat dijelajahi dengan pola yang sama, tanpa kita menulis perulangan indeks manual di setiap tempat. Pada chapter ini kita bedah kontrak tersebut sampai ke method next dan nilai Option yang dikembalikannya, karena dua hal inilah yang menjadi jantung setiap iterasi di Rust.

Masalah Pengolahan Rangkaian Data #

Mari kita mulai dari masalah nyatanya. Toko kopi kita menyimpan daftar harga dalam sebuah Vec:

rust
fn main() {
    let prices = vec![15000, 17500, 20000];

    let mut index = 0;
    while index < prices.len() {
        println!("Harga: Rp{}", prices[index]);
        index = index + 1;
    }
}

Output yang Dihasilkan:

text
Harga: Rp15000
Harga: Rp17500
Harga: Rp20000

Program di atas berjalan, tetapi mari kita perhatikan tiga beban yang dibawanya:

  1. Baris 4 (let mut index = 0;): kita mengelola satu variabel status tambahan bernama index hanya untuk mengetahui posisi baca saat ini. Status tambahan berarti satu sumber kekeliruan lagi — mulai dari 0 yang salah hingga lupa menaikkan angkanya.
  2. Baris 5 (while index < prices.len()): kondisi berhenti kita rumuskan sendiri setiap kali. Perumusan yang salah, misalnya memakai <=, akan menghasilkan panic index out of bounds.
  3. Baris 6 (prices[index]): pengaksesan dengan indeks hanya cocok untuk koleksi yang mendukung indeks. HashMap dan banyak struktur lain tidak bisa diakses dengan cara ini, sehingga pola perulangan harus ditulis ulang untuk setiap jenis koleksi.

Beban-beban ini menunjukkan bahwa kita belum punya cara baku untuk mengatakan “berikan elemen satu per satu sampai habis”. Rust menjawab kebutuhan tersebut dengan sebuah trait.

Trait Iterator sebagai Kontrak Iterasi #

Pustaka standar mendefinisikan trait Iterator dengan bentuk inti yang mengejutkan kecilnya (versi aslinya membawa puluhan method bawaan, tetapi bagian yang wajib diimplementasi hanya satu):

rust
trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

Mari kita bedah anatomi kontrak ini:

  1. Baris 1 (trait Iterator {): trait bernama Iterator dideklarasikan — tipe apa pun yang mengimplementasikan trait ini resmi menjadi “sesuatu yang bisa diiterasi”.
  2. Baris 2 (type Item;): associated type bernama Item menyatakan tipe elemen yang dihasilkan. Iterator atas daftar harga bertugas menghasilkan i32; iterator atas daftar nama bertugas menghasilkan String. Satu Item per iterator, tanpa perlu menulis anotasi di setiap pemanggilan.
  3. Baris 4 (fn next(&mut self) -> Option<Self::Item>;): satu-satunya method yang wajib. Tanda tangan menyimpan tiga janji penting sekaligus — kita urai pada bagian berikutnya.

Kita sudah berkenalan dengan associated type Output pada chapter Operator Overload, jadi pola type Item; bukan hal baru. Perbedaannya hanya peran: Output menyatakan hasil operasi, sedangkan Item menyatakan jenis elemen yang keluar dari proses iterasi.

Membaca Tanda Tangan next #

Method next adalah jantung iterasi, dan tanda tandannya perlu kita baca perlahan:

  • fn next(&mut self): parameter self diambil sebagai &mut self karena memanggil next mengubah kondisi internal iterator — posisi baca harus maju satu langkah setiap pemanggilan. Inilah alasan variabel iterator harus kita deklarasikan dengan mut.
  • Tipe kembalian Option<Self::Item>: iterator tidak pernah mengembalikan elemen polos. Setiap pemanggilan menghasilkan Some(item) ketika masih ada elemen berikutnya, atau None sebagai sinyal resmi bahwa rangkaian sudah habis.

Pilihan Option bukan kebetulan. Kita telah melihat di section Pattern Matching bahwa Option adalah cara Rust menyatakan “ada nilai atau tidak ada” tanpa nilai jadi seperti -1 atau null. Iterasi memang persoalan “ada elemen berikutnya atau tidak” — sehingga Some dan None menjadi kosakata yang tepat, dan match kita langsung dapat dipakai untuk mengolahnya.

Memanggil next Secara Manual #

Biasanya kita tidak pernah memanggil next sendiri, tetapi melakukannya sekali secara sadar adalah cara tercepat memahami apa yang sebenarnya terjadi. Perhatikan program berikut:

rust
fn main() {
    let prices = vec![15000, 17500, 20000];

    let mut price_iterator = prices.iter();

    println!("Panggilan ke-1: {:?}", price_iterator.next());
    println!("Panggilan ke-2: {:?}", price_iterator.next());
    println!("Panggilan ke-3: {:?}", price_iterator.next());
    println!("Panggilan ke-4: {:?}", price_iterator.next());
    println!("Panggilan ke-5: {:?}", price_iterator.next());
}

Output yang Dihasilkan:

text
Panggilan ke-1: Some(15000)
Panggilan ke-2: Some(17500)
Panggilan ke-3: Some(20000)
Panggilan ke-4: None
Panggilan ke-5: None

Mari kita bedah baris demi baris:

  1. Baris 2 (let prices = vec![15000, 17500, 20000];): prices menjadi pemilik Vec berisi tiga harga. Kepemilikan tetap di sini — iterator yang kita buat nanti hanya meminjam isinya.
  2. Baris 4 (let mut price_iterator = prices.iter();): pemanggilan prices.iter() membuat sebuah iterator baru yang siap membaca isi prices dari awal. Kata kunci mut wajib karena setiap pemanggilan next mengambil &mut self untuk memajukan posisi baca.
  3. Baris 6-8 (price_iterator.next()): tiga pemanggilan pertama mengembalikan Some(15000), Some(17500), dan Some(20000) — satu elemen per pemanggilan, berurutan, tanpa lompatan.
  4. Baris 9 (Panggilan ke-4): setelah elemen terakhir diserahkan, pemanggilan ke-4 mengembalikan None. Iterator tidak panic, tidak gagal, dan tidak mengulang dari awal — None adalah jawaban damai bahwa pekerjaan sudah selesai.
  5. Baris 10 (Panggilan ke-5): pemanggilan berikutnya tetap mengembalikan None. Iterator yang sudah habis tidak “bangkit kembali”; posisi baca tidak bergerak mundur.

Perhatikan satu detail penting dari output di atas: next pada iterator hasil iter() menghasilkan nilai bertipe reference &i32, bukan i32. Makro println! dengan {:?} mencetaknya sebagai Some(15000) karena reference mencetak nilai yang ditunjuknya. Hubungan antara iterator dan pinjaman data ini akan kita bedah tuntas pada chapter berikutnya.

loop for sebagai Driver next #

Memanggil next manual jelas tidak praktis untuk pekerjaan harian. Rust menyediakan for sebagai driver yang memanggil next berulang kali atas nama kita:

rust
fn main() {
    let menu = vec!["Kopi Gayo", "Kopi Toraja", "Kopi Flores"];

    for name in menu.iter() {
        println!("Menu hari ini: {}", name);
    }
}

Output yang Dihasilkan:

text
Menu hari ini: Kopi Gayo
Menu hari ini: Kopi Toraja
Menu hari ini: Kopi Flores

Uraian baris-per-barisnya:

  1. Baris 2 (let menu = vec![...]): menu menyimpan tiga nama menu bertipe &str — sekali lagi, kepemilikan koleksi tetap berada di main.
  2. Baris 4 (for name in menu.iter()): di balik sintaks yang rapi ini, for melakukan tiga hal secara berurutan — memanggil menu.iter() untuk membuat iterator, lalu memanggil next pada iterator tersebut di setiap putaran, lalu mengikat nilai di dalam Some ke variabel name. Ketika next mengembalikan None, for menghentikan dirinya sendiri.
  3. Baris 5 (println!("Menu hari ini: {}", name);): badan perulangan berjalan tepat tiga kali — sekali untuk setiap Some — tanpa kita mengelola indeks atau kondisi berhenti apa pun.

Bandingkan dengan versi while di awal chapter: variabel index, kondisi index < prices.len(), dan prices[index] semuanya lenyap. Kondisi berhenti kini menjadi tanggung jawab iterator lewat None, sehingga kesalahan off-by-one kehilangan tempat hidupnya.

Ringkasan #

Trait Iterator adalah kontrak iterasi Rust: satu associated type Item menyatakan jenis elemen, dan satu method next(&mut self) -> Option<Self::Item> menyerahkan elemen berikutnya atau None saat rangkaian habis. Iterator yang sudah habis tetap mengembalikan None pada pemanggilan berikutnya, dan perulangan for hanyalah driver yang memanggil next sampai menerima None. Dengan kontrak setipis ini, semua koleksi — dan nanti juga tipe buatan kita sendiri — berbagi satu pola penjelajahan yang sama. Pertanyaan berikutnya sudah menunggu: for name in menu.iter() meminjam data, tetapi bentuk for name in menu memindahkannya — dan di antara keduanya ada satu bentuk lagi yang bisa mengubah data di tempat. Tiga wajah iterasi tersebut kita bahas pada chapter iter dan into_iter.