Membandingkan array dengan vec di Rust
Latihan membuat instruksi pembelajaran untuk membedakan array dan vec di bahasa pemrograman Rust.
Setelah membaca dan mencoba contoh, Anda akan mampu:
Menjelaskan perbedaan konsep antara array fixed-size
[T; N]
danVec<T>
.Menjelaskan di mana masing-masing menyimpan memorinya (stack vs heap) dan implikasinya.
Mengenali situasi yang sering membingungkan pemula, mis. reallocation yang mempengaruhi pointer/referensi, panic pada indexing, stack overflow dari array besar.
Menggunakan idiom Rust yang aman (slices,
get()
,reserve()
,Box<[T]>
) untuk menghindari masalah umum.
Prasyarat
Dasar Rust: variabel, fungsi, struct
, let mut
, Vec<T>
, Option
, serta konsep ownership & borrowing secara umum. Kalau belum, silakan baca bab Ownership di The Rust Book dulu.
Gambaran ringkas: perbedaan konseptual
Array (
[T; N]
)Ukuran harus diketahui pada waktu kompilasi (
N
konstan).Elemen disimpan kontigu secara langsung; total ukuran array ikut masuk ke stack kalau variabelnya berada di stack.
Akses indeks O(1). Tidak ada
push
/pop
karena ukurannya tetap.Cocok untuk data kecil atau ukuran tetap dan deterministik.
Vec<T>
Ukuran dinamis; kapasitas dapat berubah saat runtime.
Vec
adalah struktur three machine words, diimplementasikan tiga word (misalnya 8 byte di arsitektur 64-bit) dalam bahasa mesin (pointer ke memori heap, length, capacity) yang disimpan di stack; isi elemen disimpan di heap di blok memori kontigu.Dapat
push
,pop
,insert
,remove
.push
mungkin memicu reallocation jika kapasitas tidak cukup.Umumnya pilihan bawaan untuk koleksi berukuran variabel.
Memory: stack vs heap (ilustrasi verbal)
Jika Anda memiliki
let arr: [u64; 4] = [1,2,3,4];
— seluruh 4 elemen biasanya berada di stack (atau terkadang di .data jika const).Untuk
let v: Vec<u64> = vec![1,2,3,4];
—v
(pointer + len + cap) ada di stack; empat elemen itu berada di blok memori heap yangv
tunjuk.Perbedaan penting: stack mempunyai ukuran terbatas (biasanya beberapa MB), jadi array sangat besar bisa menyebabkan stack overflow.
Vec
menghindari hal ini karena menggunakan heap.
Contoh kode: ukuran dan visualisasi sederhana
use std::mem;
fn main() {
let arr: [i32; 4] = [1, 2, 3, 4];
let v: Vec<i32> = vec![1, 2, 3, 4];
println!("size_of arr (bytes) = {}", mem::size_of_val(&arr));
println!("size_of Vec<T> struct (bytes) = {}", mem::size_of::<Vec<i32>>());
println!("Vec len = {}, capacity = {}", v.len(), v.capacity());
}
size_of_val(&arr)
mengembalikan ukuran seluruh array (4 × 4 = 16 bytes untuki32
).size_of::<Vec<i32>>()
mengembalikan ukuran structVec
sendiri (biasanya 24 bytes pada arsitektur 64-bit), bukan ukuran data di heap.
Reallocation dan dampaknya pada referensi / pointer
Skenario yang sering membuat bingung pemula:
fn main() {
let mut v = vec![1,2,3];
let r = &v[0]; // immutable borrow
// v.push(4); // ERROR: cannot borrow `v` as mutable because it is also borrowed as immutable
println!("{}", r);
}
Rust mencegah perubahan (
v.push
) saat ada referensi immutable aktif karenapush
bisa menyebabkan reallocation: data dipindahkan ke lokasi heap baru dan referensi lama akan menunjuk ke memori yang sudah tidak valid.Compiler menegakkan aturan ini waktu kompilasi sehingga program aman dari dangling pointer.
Jika Anda menggunakan unsafe
atau raw pointers, reallocation bisa menyebabkan UB, jangan lakukan itu kecuali paham risikonya.
Slices: jembatan antara array dan Vec
Slice &[T]
merepresentasikan view ke blok data berurut tanpa kepemilikan. Anda bisa mendapatkan slice dari array atau Vec
:
fn use_slice(arr: &[i32]) {
println!("slice len = {}", arr.len());
}
fn main() {
let arr = [10, 20, 30];
use_slice(&arr); // slice dari array
let v = vec![1, 2, 3, 4];
use_slice(&v[..]); // slice dari Vec
}
Slice tidak memperpanjang lifetime data; Anda tidak boleh mengembalikan slice yang menunjuk ke stack-local array yang sudah keluar scope.
Inisialisasi dan ukuran besar: jebakan stack overflow
let big: [u8; 10_000_000] = [0; 10_000_000];
— menghasilkan array ~10 MB di stack → sering menyebabkan stack overflow.Solusi: taruh di heap, mis.
let v = vec![0u8; 10_000_000];
ataulet boxed = vec![0u8; n].into_boxed_slice();
untuk array tetap di heap (Box<[T]>
).
Box<[T]>
berguna bila Anda menginginkan fixed-length data pada heap (ukuran diketahui saat runtime).
Operasi yang berbahaya / sering bikin bingung
v[index]
, panics jika index out-of-bounds. Gunakanv.get(index)
untukOption<&T>
.Meminjam data lalu memodifikasi container: kompilator menolak jika terjadi konflik borrow.
Iterasi dan modifikasi: jangan ubah
Vec
(mis. menambah elemen) saat iterasi dengan.iter()
; gunakan.drain()
atau buat salinan.
Contoh aman untuk menghapus elemen sambil iterasi:
let mut v = vec![1,2,3,4,5];
// hapus semua angka genap
v.retain(|&x| x % 2 != 0);
Copy vs Move: perbedaan ketika memindahkan array vs Vec
Memindahkan array
[T; N]
dengan ukuran kecil yang memenuhi traitCopy
akan menyalin isinya (bitwise copy). Namun untuk array besar tanpaCopy
, pemindahan akan memindahkan seluruh blok — bisa mahal.Memindahkan
Vec<T>
sendiri murah: hanya memindahkan tiga kata (pointer/len/cap). Elemen di heap tidak disalin.
Itu sebabnya untuk tipe yang besar, Vec
memindahkan kontroler cukup murah walau kopian elemen mahal.
Ketika Anda mau fixed-size on heap: Box<[T]>
Jika Anda butuh array berukuran runtime tetapi ingin representasi array (tidak Vec
), gunakan Box<[T]>
:
let v = vec![1,2,3,4];
let boxed_slice: Box<[i32]> = v.into_boxed_slice(); // sekarang ukuran tetap, data di heap
Box<[T]>
cocok jika Anda tak perlu push
/pop
lagi setelah pembuatan.
Perbandingan ringkas
Alokasi:
Array
[T; N]
: biasanya stack (ukuran compile-time).Vec<T>
: pointer on stack -> data on heap (runtime-size).
Mutabilitas ukuran:
Array: ukuran tetap; tidak ada
push
/pop
.Vec
: ukuran dinamis;push
,pop
,insert
,remove
.
Akses indeks:
Keduanya O(1), tapi
Vec
punya overhead check padav[index]
(panic if OOB).
Reallocation:
Array: tidak relevan.
Vec
: saatpush
melebihi kapasitas → reallocation, invalidates pointers intoVec
.
Move cost:
Array (besar): memindahkan seluruh blok (mahal).
Vec
: memindahkan kontroler (cheap).
Cache locality:
Keduanya kontigu: array dan Vec sama-sama cache-friendly (keduanya menyimpan elemen berurutan).
Stack overflow risk:
Array besar di stack → risiko high.
Vec → data di heap, aman dari stack overflow.
Edge cases dan best practices
Panic vs Option: Gunakan
get()
jika input tidak tepercaya untuk menghindari panic.Borrowing rules: jangan biarkan immutable borrow dan kemudian coba
push()
, compiler menolak. Pelajari pesan borrow-checker; mereka sering memberikan solusi.Large fixed data: jangan buat array besar di stack; gunakan
Vec
atauBox<[T]>
.Reallocation surprises: jika perlu referensi ke elemen sambil memodifikasi container, pertimbangkan menyimpan indeks, bukan referensi, atau gunakan struktur yang tidak memindah-mindahkan memory (mis.
VecDeque
untuk beberapa pola), atau restrukturisasi logika.Zero-sized types (ZSTs):
Vec<()>
atauVec<YourZST>
mungkin memiliki kapasitas semantics yang aneh; pahami bahwa size 0 tidak berarti tidak ada elemen.Concurrency: untuk akses bersama di thread gunakan
Arc<Vec<T>>
atau concurrent structures; kenali trade-offs.
Kapan memilih apa (praktis)
Pilih
Vec<T>
hampir selalu kecuali Anda punya alasan kuat:Anda tahu ukuran compile-time dan struktur kecil → bisa pakai
[T; N]
.Perlu fixed-size tapi besar → gunakan
Box<[T]>
untuk hindari stack overflow.Ingin performa insert/remove di tengah, dan akses acak bukan kebutuhan utama → mungkin pertimbangkan linked structure (jarang).
Anda butuh struktur thread-safe →
Arc<Vec<T>>
atau struktur khusus.
Contoh kecil yang mengilustrasikan beberapa point
fn main() {
// 1. Array kecil: ukuran di stack
let a: [u8; 4] = [10, 20, 30, 40];
println!("array bytes = {}", std::mem::size_of_val(&a));
// 2. Vec: struct di stack, data di heap
let mut v = Vec::with_capacity(2);
v.push(1);
v.push(2);
println!("vec: len={}, cap={}, size_of_vec_struct={}",
v.len(), v.capacity(), std::mem::size_of::<Vec<i32>>());
// 3. Reallocation & borrow: compiler akan menolak kalau kita lakukan ini:
let x = &v[0];
// v.push(3); // error: cannot borrow `v` as mutable because it is also borrowed as immutable
println!("first = {}", x);
}
Refleksi singkat
Walau array dan
Vec
menyimpan elemen secara kontigu, property dimana dan bagaimana mereka dialokasikan (stack vs heap, kapasitas dinamis) membuat perbedaan besar dalam penggunaan nyata.Seringkali masalah yang mengacaukan pemula bukan sekadar API, tapi pemahaman yang membekas (linger) tentang ownership & borrow rules ketika
Vec
berpotensi mereset alamat memori melalui reallocation.Mulai dengan
Vec
untuk kebutuhan koleksi dinamis. Pelajari slices untuk meminimalkan kepemilikan yang tidak perlu. GunakanBox<[T]>
bila butuh fixed-size di heap. Hindari raw pointers kecuali Anda benar-benar butuh dan mengerti unsafe.
Referensi
Klabnik, S., & Nichols, C. (eds.). The Rust Programming Language (often called “The Rust Book”). Chapter: Ownership; Chapter: Collections. URL: https://doc.rust-lang.org/book/
The Rust Project. std::vec::Vec , standard library documentation. URL: https://doc.rust-lang.org/std/vec/struct.Vec.html
The Rust Project. std::mem::size_of_val , for inspecting sizes at runtime. URL: https://doc.rust-lang.org/std/mem/fn.size_of_val.html
Rust by Example, Vec. URL: https://doc.rust-lang.org/rust-by-example/std/vec.html
Rust Unofficial. Too Many Linked Lists, tutorial exploring linked lists in Rust (practical for deeper study). URL: https://rust-unofficial.github.io/too-many-lists/