Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Nyimpen Daftar Nilai pake Vectors

Tipe koleksi pertama yang bakal kita liat adalah Vec<T>, yang juga dikenal sebagai vector. Vectors ngebolehin kita nyimpen lebih dari satu nilai di dalem satu struktur data tunggal yang naruh semua nilai itu bersebelahan di memori. Vectors cuma bisa nyimpen nilai dengan tipe yang sama. Mereka berguna pas kita punya daftar (list) item, kayak baris-baris teks di sebuah file atau harga-harga barang di keranjang belanja.

Bikin Vector Baru

Buat bikin vector baru yang kosong, kita manggil fungsi Vec::new, kayak yang ditunjukin di Listing 8-1.

fn main() {
    let v: Vec<i32> = Vec::new();
}
Listing 8-1: Bikin vector baru yang kosong buat nampung nilai tipe i32

Perhatiin ya kalau kita nambahin anotasi tipe di sini. Karena kita nggak masukin nilai apa pun ke vector ini, Rust nggak tau jenis elemen apa yang mau kita simpen. Ini poin penting. Vectors diimplementasikan pake generik (generics); kita bakal bahas cara pake generik bareng tipe kita sendiri di Bab 10. Buat sekarang, tau aja kalau tipe Vec<T> yang disediain sama standard library bisa nampung tipe apa pun. Pas kita bikin vector buat nampung tipe spesifik, kita bisa nentuin tipenya di dalem kurung siku. Di Listing 8-1, kita ngasih tau Rust kalau Vec<T> di variabel v bakal nampung elemen dengan tipe i32.

Biasanya, kita bakal bikin Vec<T> dengan nilai awal dan Rust bakal nebak (infer) tipe nilai yang mau kita simpen, jadi kita jarang sekali butuh anotasi tipe kayak gini. Rust nyediain macro yang praktis sekali, vec!, yang bakal bikin vector baru yang isinya nilai-nilai yang kita kasih. Listing 8-2 bikin Vec<i32> baru yang isinya nilai 1, 2, dan 3. Tipe integer-nya adalah i32 karena itu adalah tipe integer default, kayak yang kita bahas di bagian “Tipe Data” di Bab 3.

fn main() {
    let v = vec![1, 2, 3];
}
Listing 8-2: Bikin vector baru yang isinya nilai-nilai

Karena kita udah ngasih nilai awal i32, Rust bisa nebak kalau tipe dari v adalah Vec<i32>, dan anotasi tipenya nggak dibutuhin. Selanjutnya, kita bakal liat cara ngubah sebuah vector.

Ngubah Vector

Buat bikin vector terus nambahin elemen ke dalemnya, kita bisa pake method push, kayak yang ditunjukin di Listing 8-3.

fn main() {
    let mut v = Vec::new();

    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);
}
Listing 8-3: Pake method push buat nambahin nilai ke vector

Sama kayak variabel mana pun, kalau kita mau bisa ngubah nilainya, kita harus bikin variabelnya mutable pake keyword mut, kayak yang dibahas di Bab 3. Angka-angka yang kita taruh di dalemnya semuanya bertipe i32, dan Rust nebak ini dari datanya, jadi kita nggak perlu anotasi Vec<i32>.

Ngebaca Elemen dari Vectors

Ada dua cara buat ngerujuk ke nilai yang disimpan di sebuah vector: lewat indexing atau pake method get. Di contoh-contoh berikut, kita udah nganotasi tipe dari nilai yang dibalikin sama fungsi-fungsi ini biar lebih jelas.

Listing 8-4 nunjukin kedua cara buat akses nilai di dalem vector, pake sintaks indexing dan method get.

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let third: &i32 = &v[2];
    println!("The third element is {third}");

    let third: Option<&i32> = v.get(2);
    match third {
        Some(third) => println!("The third element is {third}"),
        None => println!("There is no third element."),
    }
}
Listing 8-4: Pake sintaks indexing dan pake method get buat akses item di vector

Ada beberapa detail yang perlu diperhatiin di sini. Kita pake nilai indeks 2 buat dapet elemen ketiga karena vectors diindeks pake angka, mulai dari nol. Pake & sama [] ngasih kita sebuah referensi ke elemen di nilai indeks tersebut. Pas kita pake method get dengan indeks yang dimasukin sebagai argumen, kita dapet Option<&T> yang bisa kita pake bareng match.

Rust nyediain dua cara buat ngerujuk elemen biar kita bisa milih gimana program kita bereaksi pas kita nyoba pake nilai indeks yang di luar rentang (range) elemen yang ada. Sebagai contoh, yuk kita liat apa yang terjadi pas kita punya vector isinya lima elemen terus kita nyoba akses elemen di indeks 100 pake kedua teknik ini, kayak yang ditunjukin di Listing 8-5.

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let does_not_exist = &v[100];
    let does_not_exist = v.get(100);
}
Listing 8-5: Nyoba akses elemen di indeks 100 di vector yang isinya lima elemen

Pas kita jalanin kode ini, metode pertama [] bakal bikin program panic karena dia ngerujuk ke elemen yang nggak ada. Metode ini paling pas dipake kalau kita mau program kita nge-crash kalau ada percobaan buat akses elemen ngelewatin akhir vector.

Pas method get dikasih indeks yang di luar vector, dia bakal balikin None tanpa bikin panic. Kita bakal pake metode ini kalau akses elemen di luar rentang vector mungkin sesekali kejadian di bawah kondisi normal. Kode kita terus bakal punya logika buat nanganin dapet Some(&element) atau None, kayak yang dibahas di Bab 6. Misalnya, indeksnya bisa jadi dateng dari orang yang masukin angka. Kalau mereka nggak sengaja masukin angka yang kegedean dan programnya dapet nilai None, kita bisa ngasih tau user berapa banyak item yang ada di vector saat ini dan ngasih mereka kesempatan lagi buat masukin nilai yang valid. Itu bakal lebih user-friendly daripada nge-crash-in program gara-gara typo!

Pas programnya punya referensi yang valid, borrow checker bakal nerapin aturan ownership dan borrowing (yang dibahas di Bab 4) buat mastiin referensi ini dan referensi apa pun lainnya ke isi vector tetep valid. Inget aturan yang bilang kalau kita nggak bisa punya referensi mutable sama immutable di scope yang sama. Aturan itu berlaku di Listing 8-6, di mana kita megang immutable reference ke elemen pertama di vector terus nyoba nambahin elemen di akhir vector. Program ini nggak bakal jalan kalau kita juga nyoba ngerujuk ke elemen itu nanti di fungsinya.

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    let first = &v[0];

    v.push(6);

    println!("The first element is: {first}");
}
Listing 8-6: Nyoba nambahin elemen ke vector sambil megang referensi ke sebuah item

Compile kode ini bakal ngasilin error ini:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:5
  |
4 |     let first = &v[0];
  |                  - immutable borrow occurs here
5 |
6 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
7 |
8 |     println!("The first element is: {first}");
  |                                      ----- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` (bin "collections") due to 1 previous error

Kode di Listing 8-6 mungkin keliatannya harusnya jalan: kenapa referensi ke elemen pertama harus peduli sama perubahan di akhir vector? Error ini terjadi karena cara kerja vectors: karena vectors naruh nilai bersebelahan satu sama lain di memori, nambahin elemen baru di akhir vector mungkin butuh ngalokasiin memori baru dan ngopi elemen-elemen lama ke ruang yang baru, kalau ternyata nggak ada ruang yang cukup buat naruh semua elemen bersebelahan di tempat vector itu saat ini disimpan. Di kasus itu, referensi ke elemen pertama bakal nunjuk ke memori yang udah di-dealokasi (deallocated memory). Aturan borrowing nyegah program berakhir di situasi kayak gitu.

Catatan: Buat detail implementasi lebih lanjut dari tipe Vec<T>, liat “The Rustonomicon”.

Iterasi Lewat Nilai-nilai di dalem Vector

Buat akses tiap elemen di vector secara bergiliran, kita bakal iterasi lewat semua elemennya bukannya pake indeks buat akses satu-satu. Listing 8-7 nunjukin cara pake for loop buat dapet immutable references ke tiap elemen di vector nilai i32 terus nyetak semuanya.

fn main() {
    let v = vec![100, 32, 57];
    for i in &v {
        println!("{i}");
    }
}
Listing 8-7: Nyetak tiap elemen di vector dengan iterasi lewat elemen pake for loop

Kita juga bisa iterasi lewat mutable references ke tiap elemen di mutable vector buat bikin perubahan ke semua elemen. for loop di Listing 8-8 bakal nambahin 50 ke tiap elemen.

fn main() {
    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }
}
Listing 8-8: Iterasi lewat mutable references ke elemen-elemen di vector

Buat ngubah nilai yang ditunjuk sama mutable reference, kita harus pake operator dereference * buat nyampe ke nilai di i sebelum kita bisa pake operator +=. Kita bakal bahas operator dereference lebih dalem di bagian “Ngikutin Pointer ke Nilai pake Operator Dereference” di Bab 15.

Iterasi lewat sebuah vector, entah itu secara immutable atau mutable, selalu aman berkat aturan borrow checker. Kalau kita nyoba insert atau remove item di body for loop di Listing 8-7 sama Listing 8-8, kita bakal dapet error compiler yang mirip kayak yang kita dapet dari kode di Listing 8-6. Referensi ke vector yang dipegang sama for loop nyegah modifikasi keseluruhan vector di waktu yang sama.

Pake Enum Buat Nyimpen Banyak Tipe

Vectors cuma bisa nyimpen nilai yang tipenya sama. Ini bisa jadi kurang nyaman; pasti ada kasus di mana kita butuh nyimpen daftar item yang tipenya beda-beda. Untungnya, varian dari sebuah enum didefinisikan di bawah tipe enum yang sama, jadi pas kita butuh satu tipe buat ngewakilin elemen dari berbagai tipe, kita bisa bikin dan pake enum!

Misalnya, katakanlah kita mau dapet nilai dari sebuah baris di spreadsheet di mana beberapa kolom di baris itu isinya integer, beberapa angka floating-point, dan beberapa lagi strings. Kita bisa mendefinisikan enum yang varian-variannya bakal nampung tipe nilai yang beda, dan semua varian enum itu bakal dianggap sebagai tipe yang sama: yaitu tipe dari enum tersebut. Terus kita bisa bikin vector buat nampung enum itu dan akhirnya bisa nampung tipe yang beda-beda. Kita udah demonstrasikan ini di Listing 8-9.

fn main() {
    enum SpreadsheetCell {
        Int(i32),
        Float(f64),
        Text(String),
    }

    let row = vec![
        SpreadsheetCell::Int(3),
        SpreadsheetCell::Text(String::from("blue")),
        SpreadsheetCell::Float(10.12),
    ];
}
Listing 8-9: Mendefinisikan sebuah enum buat nyimpen nilai dari tipe yang beda di dalem satu vector

Rust perlu tau tipe apa aja yang bakal ada di vector pas compile time biar dia tau persis berapa banyak memori di heap yang bakal dibutuhin buat nyimpen tiap elemen. Kita juga harus eksplisit soal tipe apa aja yang dibolehin di vector ini. Kalau Rust ngebolehin sebuah vector buat nampung tipe apa aja, ada kemungkinan satu atau lebih dari tipe itu bakal nyebabin error sama operasi yang dijalanin pada elemen vector-nya. Pake enum ditambah ekspresi match artinya Rust bakal mastiin pas compile time kalau setiap kasus yang mungkin terjadi itu di-handle, kayak yang dibahas di Bab 6.

Kalau kita nggak tau daftar lengkap dari tipe-tipe yang bakal didapet program pas runtime buat disimpan di vector, teknik enum ini nggak bakal jalan. Sebagai gantinya, kita bisa pake trait object, yang bakal kita bahas di Bab 18.

Sekarang setelah kita bahas beberapa cara paling umum buat pake vectors, pastiin buat cek dokumentasi API-nya buat semua method berguna yang didefinisikan pada Vec<T> sama standard library. Misalnya, selain push, ada method pop yang ngehapus dan balikin elemen terakhir.

Nge-drop Vector Bakal Nge-drop Elemennya Juga

Kayak struct lainnya, sebuah vector bakal dibebasin (freed) pas dia keluar dari scope, kayak yang dianotasi di Listing 8-10.

fn main() {
    {
        let v = vec![1, 2, 3, 4];

        // do stuff with v
    } // <- v goes out of scope and is freed here
}
Listing 8-10: Nunjukin di mana vector dan elemennya di-drop

Pas vector di-drop, semua isinya juga ikut di-drop, artinya integer-integer yang ada di dalemnya bakal dibersihin. Borrow checker mastiin kalau referensi apa pun ke isi dari vector cuma dipake selama vector itu sendiri masih valid.

Yuk kita lanjut ke tipe koleksi berikutnya: String!