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();
}
i32Perhatiin 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];
}
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);
}
push buat nambahin nilai ke vectorSama 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."),
}
}
get buat akses item di vectorAda 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);
}
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}");
}
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}");
}
}
for loopKita 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;
}
}
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),
];
}
enum buat nyimpen nilai dari tipe yang beda di dalem satu vectorRust 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
}
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!