Iterasi Collections

IntoIterator, drain, retensi elemen, dan pola iterasi lazim.

Daftar isi

Topik ini membahas pola iterasi yang berlaku seragam pada koleksi Rust, mulai dari Vec dan array hingga HashMap dan BTreeMap. Pembahasan mencakup hubungan perulangan for dengan trait IntoIterator, tiga rasa iterasi yang dibedakan menurut kepemilikannya, pembangunan koleksi baru dengan collect, pengeluaran elemen melalui drain, penyaringan di tempat dengan retain, serta penolakan kompilator atas perubahan koleksi di tengah perulangan. Setelah membaca topik ini, perulangan atas koleksi mana pun dapat ditulis dengan kesadaran penuh atas pinjaman dan perpindahan kepemilikan yang terjadi di belakangnya.

Perulangan for dan trait IntoIterator#

Perulangan for pada Rust bukan fitur yang berdiri sendiri, melainkan bentuk ringkas dari pemanggilan iterator. Tulisan for n in &angka diubah kompilator menjadi pemanggilan into_iter() pada &angka, kemudian perulangan memanggil next() berulang kali sampai menerima None. Trait IntoIterator adalah kontrak yang membuat mekanisme tersebut berlaku: setiap tipe yang mengimplementasikannya dapat dijadikan sumber perulangan, dan seluruh koleksi pustaka standar mengimplementasikannya. Perbedaan antar bentuk penulisan terletak pada jenis elemen yang dihasilkan: &koleksi meminjam isinya, sedangkan koleksi tanpa tanda & menyerahkan kepemilikan elemennya. Program berikut mengiterasi tiga sumber dengan dua bentuk tersebut.

rust dasar-iterasi.rs
use std::collections::BTreeMap;

fn main() {
    let angka = vec![10, 20, 30];
    let mut total = 0;
    for n in &angka {
        total += n;
    }
    println!("total pinjaman : {total}");

    let kode = [7, 14, 21];
    for k in kode {
        println!("kode           : {k}");
    }

    let bobot = BTreeMap::from([(1, "rendah"), (2, "sedang"), (3, "tinggi")]);
    for (tingkat, label) in &bobot {
        println!("tingkat {tingkat} => {label}");
    }
}
text Keluaran
total pinjaman : 60
kode           : 7
kode           : 14
kode           : 21
tingkat 1 => rendah
tingkat 2 => sedang
tingkat 3 => tinggi

Perulangan pertama meminjam angka melalui &angka, sehingga setiap n adalah referensi ke elemen dan vektor tetap utuh setelah perulangan selesai. Perulangan kedua memakai array kode tanpa tanda &, dan karena elemen i32 mengimplementasikan Copy, elemen disalin satu per satu ke k tanpa memindahkan kepemilikan array. Perulangan ketiga meminjam peta bobot, dan setiap langkah menghasilkan pasangan referensi kunci dan referensi nilai yang langsung dibongkar oleh pola (tingkat, label). Urutan pasangan menaik mengikuti kunci karena peta terurut, sehingga keluarannya sama pada setiap jalankan. Pola yang sama berlaku pada HashMap dengan satu catatan: urutan iterasinya tidak ditentukan.

Tiga rasa iterasi: iter, iter_mut, into_iter#

Setiap koleksi menyediakan tiga metode yang menghasilkan iterator dengan jenis kepemilikan berbeda. Metode iter() menghasilkan referensi tetap &T sehingga koleksi tetap dapat dipakai setelahnya, iter_mut() menghasilkan referensi mutable &mut T untuk mengubah elemen di tempat, dan into_iter() memindahkan kepemilikan elemen sehingga koleksi habis terpakai setelah perulangan. Tipe iterator yang dihasilkan berbeda-beda, tetapi perulangan for dan seluruh adapter memperlakukan ketiganya sama karena semuanya mengimplementasikan trait Iterator.

MetodeJenis elemenDampak pada koleksi
iter()&Tdipinjam untuk dibaca, tetap utuh
iter_mut()&mut Tdipinjam untuk diubah, elemen dapat dimutasi
into_iter()Tkepemilikan elemen berpindah, koleksi habis
rust tiga-rasa.rs
fn main() {
    let mut angka = vec![1, 2, 3];

    let total: i32 = angka.iter().sum();
    println!("total iter       : {total}");

    for n in angka.iter_mut() {
        *n *= 10;
    }
    println!("setelah iter_mut : {:?}", angka);

    for n in angka.into_iter() {
        println!("elemen milik sendiri : {n}");
    }
}
text Keluaran
total iter       : 6
setelah iter_mut : [10, 20, 30]
elemen milik sendiri : 10
elemen milik sendiri : 20
elemen milik sendiri : 30

Metode iter() memberikan referensi elemen kepada sum(), yang menjumlahkan ketiga elemen tanpa memindahkan apa pun. Metode iter_mut() kemudian mengalikan setiap elemen dengan sepuluh melalui dereferensi *n *= 10, dan hasilnya terlihat pada pencetakan kedua. Pemakaian into_iter() pada bagian akhir memindahkan ketiga elemen ke dalam perulangan, sehingga variabel angka tidak lagi memiliki apa pun setelah blok perulangan berakhir. Pencetakan angka pada titik itu akan ditolak dengan error E0382 karena kepemilikan elemennya sudah berpindah.

collect: membangun koleksi baru#

Metode collect() membangun koleksi baru dengan menjalankan iterator sampai habis. Iterator bersifat malas: elemen tidak diproses sampai collect, sum, atau perulangan for memintanya bekerja. Banyak koleksi mengimplementasikan trait FromIterator, sehingga satu sumber yang sama dapat dikumpulkan menjadi Vec, BTreeMap, HashMap, atau String. Karena kandidat tujuannya banyak, bentuk tujuan pengumpulan umumnya dinyatakan melalui anotasi tipe pada variabel penerimanya.

rust kumpul.rs
use std::collections::BTreeMap;

fn main() {
    let detik: Vec<u32> = (0..5).collect();
    println!("detik : {:?}", detik);

    let pasangan = vec![("senin", 24), ("rabu", 27), ("jumat", 25)];
    let peta: BTreeMap<&str, i32> = pasangan.into_iter().collect();
    for (hari, suhu) in &peta {
        println!("{hari} : {suhu}");
    }
}
text Keluaran
detik : [0, 1, 2, 3, 4]
jumat : 25
rabu : 27
senin : 24

Rentang 0..5 adalah iterator, dan anotasi Vec<u32> pada variabel detik menetapkan tujuan pengumpulannya. Vektor pasangan kemudian dikonsumsi melalui into_iter(), dan collect dengan anotasi BTreeMap<&str, i32> menyusun ulang pasangan-pasangan tersebut menjadi peta terurut. Keluaran menampilkan hari dalam urutan jumat, rabu, senin meskipun vektor aslinya berurutan senin, rabu, jumat, karena peta terurut menata pasangan menurut kuncinya sejak penyisipan. Perpindahan kepemilikan dari pasangan ke peta berlangsung tanpa penyalinan manual: setiap pasangan berpindah utuh dari satu koleksi ke koleksi lainnya. Pola yang sama bekerja untuk HashMap sebagai tujuan, dan satu-satunya yang perlu disesuaikan adalah anotasi tujuannya, dengan catatan urutan hasilnya tidak lagi terjamin.

drain: iterasi yang memindahkan elemen keluar#

Metode drain(rentang) mengeluarkan seluruh elemen pada rentang tertentu sekaligus menghasilkan iterator yang membawa kepemilikan elemen tersebut keluar dari koleksi. Dilihat dari sisi iterasi, drain adalah iterator yang sekaligus mengosongkan sebagian koleksi: elemen yang dikunjungi tidak hilang, melainkan berpindah ke pemilik baru, dan elemen di luar rentang tetap tinggal di koleksi aslinya. Iterator hasil drain dapat dikumpulkan dengan collect, dialirkan ke koleksi lain dengan extend, atau dikunjungi langsung oleh for.

rust pindah-drain.rs
fn main() {
    let mut tugas = vec!["catat suhu", "kalibrasi sensor", "kirim laporan", "arsipkan data"];
    let mut selesai: Vec<&str> = Vec::new();

    selesai.extend(tugas.drain(0..2));
    println!("selesai : {:?}", selesai);
    println!("tugas   : {:?}", tugas);
}
text Keluaran
selesai : ["catat suhu", "kalibrasi sensor"]
tugas   : ["kirim laporan", "arsipkan data"]

Rentang 0..2 mengeluarkan dua tugas pertama, dan extend memindahkan keduanya ke vektor selesai satu per satu tanpa penyalinan manual. Vektor tugas menyisakan dua tugas terakhir dalam urutan yang sama, karena elemen setelah rentang bergeser maju mengisi tempat yang dikosongkan. Metode ini menerima rentang penuh .. apabila seluruh isi ingin dipindahkan sekaligus.

retain: penyaringan di tempat#

Metode retain(predikat) menjalankan iterasi internal: koleksi menelusuri elemennya sendiri, memanggil predikat untuk setiap elemen, lalu membuang elemen yang membuat predikatnya mengembalikan false. Elemen yang lolos tetap berada pada koleksi dengan urutan relatif yang dijaga. Berbeda dari perulangan for yang badannya ditulis pemanggil, seluruh penelusuran berlangsung di dalam metode, sehingga tidak ada pinjaman yang tertahan oleh badan perulangan di sisi pemanggil.

rust retensi.rs
fn main() {
    let mut catatan = vec![24, 27, 31, 22, 29, 35, 26];
    catatan.retain(|&n| (22..=31).contains(&n));
    println!("pembacaan wajar : {:?}", catatan);

    let mut kata = vec!["sensor", "udara", "suhu", "lembap"];
    kata.retain(|k| k.len() > 4);
    println!("kata panjang    : {:?}", kata);
}
text Keluaran
pembacaan wajar : [24, 27, 31, 22, 29, 26]
kata panjang    : ["sensor", "udara", "lembap"]

Predikat pertama menerima referensi elemen lewat pola |&n|, memeriksa keanggotaan pada rentang 22..=31, dan pembacaan 35 keluar karena berada di luar rentang wajar. Predikat kedua menerima referensi berlapis pada vektor kata, tetapi metode len() tetap dapat dipanggil karena dereferensi berlapis berlangsung otomatis. Kata suhu yang hanya terdiri atas empat huruf tersaring keluar, sedangkan tiga kata sisanya bertahan dengan urutan aslinya. Retensi semacam ini menggantikan perulangan penghapusan manual yang rawan salah hitung indeks. Metode yang sama juga tersedia pada HashMap dan BTreeMap, dengan predikat yang menerima referensi kunci dan nilai sekaligus sehingga penyaringan dapat mempertimbangkan kedua sisi pasangan.

Perubahan selama iterasi ditolak kompilator#

Perulangan yang meminjam koleksi memegang pinjamannya sampai blok perulangan selesai, sehingga upaya mengubah koleksi dari dalam badan perulangan ditolak. Program berikut mencoba menambahkan tugas baru ke vektor yang sedang diiterasi.

bash
export PATH="$HOME/.cargo/bin:$PATH"
# program yang menambahkan elemen ke vektor yang sedang dipinjam perulangan
printf 'fn main() {\n    let mut tugas = vec!["catat", "kalibrasi", "kirim"];\n    for t in &tugas {\n        if t.len() > 6 {\n            tugas.push("arsipkan");\n        }\n        println!("{t}");\n    }\n}\n' > salah-ubah.rs
# kompilasi ditolak karena pinjaman perulangan masih berlaku saat push dipanggil
rustc --edition=2021 salah-ubah.rs 2>&1 | sed -n '1,15p'
text Keluaran
error[E0502]: cannot borrow `tugas` as mutable because it is also borrowed as immutable
 --> salah-ubah.rs:5:13
  |
3 |       for t in &tugas {
  |       -        ------
  |       |        |
  |       |        immutable borrow occurs here
  |  _____|        immutable borrow later used here
  | |
4 | |         if t.len() > 6 {
5 | |             tugas.push("arsipkan");
  | |             ^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
... |
8 | |     }
  | |_____- this for loop borrows `tugas` immutably, preventing mutation within its body

Pesan error memetakan dua pinjaman yang bertabrakan: pinjaman tetap oleh perulangan for t in &tugas dan pinjaman mutable oleh push di dalam badannya. Penolakan ini mencegah kelas bug yang berbahaya, yaitu iterator yang kehilangan pijakan karena koleksinya berubah bentuk di tengah penelusuran. Dua jalan keluar tersedia: kumpulkan perubahan ke koleksi sementara selama iterasi berlangsung, lalu terapkan setelah perulangan selesai, atau iterasi dengan indeks apabila urutan perubahan benar-benar bergantung pada posisi.

Kebutuhan penghapusan elemen tertutup oleh retain, yang penelusurannya berlangsung di dalam metode sehingga konflik pinjaman tidak terjadi. Batasan yang sama berlaku pada VecDeque, HashMap, dan BTreeMap, karena ketiganya diiterasi melalui pinjaman dengan mekanisme yang identik. Penolakan pada masa kompilasi menggantikan masalah iterator yang terinvalidasi, yaitu keadaan ketika penelusuran berlanjut di atas koleksi yang bentuknya sudah berubah.

Kesalahan umum#

Mengiterasi koleksi tanpa & padahal isinya masih dibutuhkan. Perulangan for n in koleksi memakai into_iter() dan memindahkan seluruh elemen keluar dari koleksi, sehingga pemakaian koleksi setelahnya ditolak dengan error E0382. Bentuk for n in &koleksi cukup dipakai untuk pembacaan, dan iter_mut() mengisi kebutuhan pengubahan di tempat tanpa mengorbankan kepemilikannya. Pesan errornya menyebut baris tempat kepemilikan berpindah beserta baris pemakaian ulangnya, sehingga sumber masalah cepat tertunjuk.

Menulis collect() tanpa tujuan tipe yang jelas. Banyak koleksi dapat dibangun dari iterator yang sama, sehingga let data = (0..5).collect() ditolak dengan error E0283 yang meminta anotasi tipe. Nyatakan tujuannya pada variabel penerima, misalnya let data: Vec<u32> = (0..5).collect(), agar kompilator tidak perlu menebak di antara banyak kandidat. Pesan errornya mencantumkan kandidat yang memenuhi batasan, sehingga daftar tujuan yang sah terlihat langsung dari pesannya.

Ringkasan#

  • Perulangan for memanggil into_iter() pada sumbernya: &koleksi menghasilkan elemen pinjaman, sedangkan koleksi memindahkan kepemilikannya.
  • Tiga rasa iterasi dibedakan kepemilikannya: iter() menghasilkan &T, iter_mut() menghasilkan &mut T, dan into_iter() menghabiskan koleksi.
  • collect() membangun koleksi baru dari iterator yang malas, dengan tujuan yang dinyatakan lewat anotasi tipe.
  • drain(rentang) memindahkan elemen keluar dari rentang koleksi, dan retain(predikat) menyaring elemen di tempat dengan urutan relatif yang dijaga.
  • Perubahan koleksi dari dalam badan perulangan yang meminjamnya ditolak dengan error E0502, dan perbaikannya berupa pengumpulan perubahan atau iterasi berbasis indeks.

Referensi#