Kita telah meminjam potongan array di Chapter pertama dan potongan teks di Chapter kedua — sekarang saatnya melengkapi kotak alatnya. Dua wadah data yang paling sering kita jumpai adalah array dengan ukuran tetap di stack dan Vec yang tumbuh menyusut di heap, dan kabar baiknya: keduanya menyimpan elemen secara berurutan, sehingga keduanya bisa dipotong dengan satu keluarga notasi yang sama. Pada Chapter ini kita akan memetakan seluruh variasi rentang — mulai dari [1..3] sampai [..=2] — lalu membuat slice dari Vec, mengubah isi data lewat mutable slice, dan menutupnya dengan dua kegagalan yang paling sering dialami pemula: rentang yang melebihi data dan push yang dilakukan saat slice masih dipinjam.

Notasi Rentang pada Slice #

Rentang pada slice sebenarnya adalah keluarga notasi yang sudah kita kenal dari for dan range di section Kontrol Alur, sekarang dipakai di dalam tanda kurung siku. Tabel berikut merangkum kelima bentuk yang tersedia:

NotasiElemen yang dipinjamKeterangan
&data[1..3]index 1 sampai 2batas akhir eksklusif
&data[..=2]index 0 sampai 2batas akhir inklusif
&data[3..]index 3 sampai akhirbatas akhir dihitung otomatis
&data[..2]index 0 sampai 1batas awal otomatis dari 0
&data[..]semua elemenseluruh data

Kita uji keempat bentuk pertama sekaligus pada satu array:

rust
fn main() {
    let numbers = [10, 20, 30, 40, 50];

    let view_a = &numbers[1..3];
    let view_b = &numbers[..=2];
    let view_c = &numbers[3..];
    let view_d = &numbers[..];

    println!("numbers[1..3] = {:?}", view_a);
    println!("numbers[..=2] = {:?}", view_b);
    println!("numbers[3..]  = {:?}", view_c);
    println!("numbers[..]   = {:?}", view_d);
}

Output yang Dihasilkan:

text
numbers[1..3] = [20, 30]
numbers[..=2] = [10, 20, 30]
numbers[3..]  = [40, 50]
numbers[..]   = [10, 20, 30, 40, 50]

Mari kita bedah kode di atas:

  1. Baris 2 (let numbers = [10, 20, 30, 40, 50];): array lima elemen tetap di stack, pemiliknya numbers.
  2. Baris 4 (let view_a = &numbers[1..3];): meminjam elemen index 1 dan 2 — nilai 20 dan 30. Batas akhir 3 tidak termasuk, sehingga 40 di index 3 tertinggal.
  3. Baris 5 (let view_b = &numbers[..=2];): tanda ..= memakai batas akhir inklusif, jadi elemen index 0, 1, dan 2 ikut — tiga elemen pertama. Bandingkan hasilnya dengan view_a: satu karakter = mengubah hasil pinjaman.
  4. Baris 6 (let view_c = &numbers[3..];): tanpa batas akhir, slice meminjam dari index 3 sampai elemen terakhir — 40 dan 50.
  5. Baris 7 (let view_d = &numbers[..];): tanpa batas di kedua sisi, slice meminjam array utuh. Bentuk [..] ini berguna saat kita hanya ingin mengubah tampilan wadah menjadi slice tanpa memotong apa pun.
  6. Baris 9–12 (println! berempat): format {:?} mencetak isi slice sebagai daftar nilai — format yang sudah kita kenal dari tipe data komposit.

Slice dari Vector #

Vec menyimpan elemennya di heap, tetapi dari sudut pandang slice tampilannya sama saja: elemen tersusun berurutan di satu wilayah memori, sehingga notasi rentang bekerja identik. Kita potong isi keranjang belanja fiktif:

rust
fn main() {
    let cart = vec![12000, 45000, 8000, 33000];
    let expensive = &cart[1..3];

    println!("Item mahal: {:?}", expensive);
    println!("Panjang slice: {}", expensive.len());
}

Output yang Dihasilkan:

text
Item mahal: [45000, 8000]
Panjang slice: 2

Mari kita bedah kode di atas:

  1. Baris 2 (let cart = vec![12000, 45000, 8000, 33000];): vec! membangun Vec<i32> berisi empat harga di heap; cart adalah pemiliknya dengan data pengelola di stack — susunan yang sama seperti String.
  2. Baris 3 (let expensive = &cart[1..3];): slice meminjam elemen index 1 dan 2 langsung dari buffer heap milik Vec. Pointer slice menunjuk byte pertama elemen 45000, dan panjangnya 2 elemen.
  3. Baris 5–6 (println! berdua): isi dan panjang slice dicetak. {:?} menampilkan [45000, 8000], dan expensive.len() menghasilkan 2 — bukan panjang Vec yang berisi empat elemen.

Batas Rentang yang Melebihi Data #

Seperti dijanjikan Chapter pertama, rentang pembuatan slice diperiksa saat program berjalan. Batas akhir yang melampaui jumlah elemen membuat program berhenti sebelum slice terbentuk:

rust
fn main() {
    let numbers = [10, 20, 30];
    let view = &numbers[1..5];

    println!("Isi slice: {:?}", view);
}

Pesan panic saat runtime:

text
thread 'main' (15387) panicked at main.rs:3:24:
range end index 5 out of range for slice of length 3
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Pesan panic menyebut dua angka kunci: batas akhir yang diminta adalah 5, sedangkan data hanya berisi 3 elemen. Lokasi main.rs:3:24 menunjuk tepat ke ekspresi numbers[1..5]. Program keluar dengan kode status 101. Aturan serupa berlaku dua arah — batas awal pun tidak boleh melebihi batas akhir; rentang seperti [3..1] akan ditolak dengan panic slice index starts at 3 but ends at 1 karena jangkauan yang mundur tidak masuk akal.

Mengubah Isi lewat Mutable Slice #

Sejauh ini semua slice kita bersifat immutable — sekadar membaca. Slice juga punya bentuk mutable &mut [T] yang meminjam hak tulis atas potongan data, tunduk pada aturan yang sama seperti referensi mutable: selama pinjaman beredar, tidak boleh ada peminjam lain. Pola ini memungkinkan fungsi memproses sebagian data tanpa memindahkan kepemilikan:

rust
fn bump(view: &mut [i32]) {
    for position in 0..view.len() {
        view[position] += 10;
    }
}

fn main() {
    let mut numbers = [10, 20, 30, 40, 50];

    bump(&mut numbers[1..4]);
    println!("Array setelah diproses: {:?}", numbers);
}

Output yang Dihasilkan:

text
Array setelah diproses: [10, 30, 40, 50, 50]

Mari kita bedah kode di atas:

  1. Baris 1 (fn bump(view: &mut [i32])): parameter bertipe mutable slice — pinjaman berhak menulis ke elemen-elemen yang terlihat dari view. Fungsi tidak tahu dan tidak peduli slice itu bagian dari array atau Vec; fungsi hanya bekerja pada jendela yang diberikan.
  2. Baris 2–4 (for position in 0..view.len() beserta isinya): perulangan menambah 10 ke setiap elemen slice lewat view[position] += 10 — penulisan langsung ke data pemilik, bukan ke salinan.
  3. Baris 8 (let mut numbers = [10, 20, 30, 40, 50];): kata kunci mut pada pemilik wajib — data yang akan ditulis lewat pinjaman harus bisa diubah pemiliknya.
  4. Baris 10 (bump(&mut numbers[1..4]);): &mut numbers[1..4] membuka mutable slice atas elemen index 1, 2, 3 dan mengirimkannya ke fungsi. Selama bump berjalan, seluruh numbers terkunci untuk peminjam lain. Begitu panggilan selesai, pinjaman berakhir dan numbers bebas dipakai lagi.
  5. Baris 11 (println!("Array setelah diproses: {:?}", numbers);): hasilnya terlihat — elemen 20, 30, 40 naik menjadi 30, 40, 50, sementara elemen index 0 dan index 4 tidak tersentuh karena berada di luar jangkauan slice.

Pinjaman Slice vs Push #

Mutable slice dan pertumbuhan Vec adalah dua hal yang berbenturan. Metode push butuh pinjaman mutable atas Vec, dan alasan teknisnya serius: menambah elemen bisa memicu realokasi — buffer heap lama dibongkar dan diganti buffer baru yang lebih besar. Slice yang masih menunjuk buffer lama akan menggantung. Kompilator menolak skenario itu lebih dulu:

rust
fn main() {
    let mut cart = vec![12000, 45000];
    let expensive = &cart[1..];

    cart.push(8000);

    println!("Item mahal: {:?}", expensive);
}

Pesan error kompilator:

text
error[E0502]: cannot borrow `cart` as mutable because it is also borrowed as immutable
 --> main.rs:5:5
  |
3 |     let expensive = &cart[1..];
  |                      ---- immutable borrow occurs here
4 |
5 |     cart.push(8000);
  |     ^^^^^^^^^^^^^^^ mutable borrow occurs here
6 |
7 |     println!("Item mahal: {:?}", expensive);
  |                                  --------- immutable borrow later used here

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0502`.

Struktur E0502 ini sama dengan yang kita temui di Chapter String Slice, dan sekarang kita memahami alasan dalamnya: bukan sekadar aturan formal, melainkan pencegahan dangling reference — persis kelas bug yang dibahas di Chapter Dangling Reference. Jika realokasi terjadi saat expensive masih dipakai, pointer slice akan menunjuk bekas buffer yang sudah dikembalikan ke allocator. Pembenahannya sederhana: selesaikan dulu pemakaian slice — cetak, hitung, simpan hasilnya — baru panggil push setelahnya.

Ringkasan #

Satu keluarga notasi rentang — [a..b], [a..=b], [a..], [..b], dan [..] — berlaku seragam pada array di stack maupun Vec di heap, karena keduanya menyimpan elemen secara berurutan. Batas rentang diperiksa saat runtime, dan batas yang melebihi data berakhir sebagai panic yang menyebut angka yang diminta dan panjang sebenarnya. Mutable slice &mut [T] membawa hak tulis ke dalam potongan data selama pemilik terkunci, dan konfliknya dengan push menghasilkan E0502 — penjaga pagar yang mencegah slice menggantung akibat realokasi. Dengan semua notasi di tangan, Chapter berikutnya melangkah ke sisi pemanggilan: menjadikan slice sebagai parameter fungsi.