Data di dunia nyata jarang berdiri sendiri. Satu baris data mahasiswa memuat nama, umur, dan IPK sekaligus; satu rangkaian nilai ujian tersimpan sebagai barisan angka berurutan. Tipe skalair yang kita pelajari sebelumnya hanya menampung satu nilai, sehingga Rust menyediakan tipe komposit untuk mengelompokkan beberapa nilai ke dalam satu variable. Rust memiliki dua tipe komposit dasar: tuple untuk kumpulan nilai dengan tipe yang boleh berbeda, dan array untuk kumpulan nilai dengan tipe yang sama dan jumlah yang tetap. Pada Chapter ini kita akan memakai keduanya, mengurai isinya, dan menyaksikan bagaimana Rust menjaga batas koleksi tetap aman.

Tuple #

Tuple mengelompokkan sejumlah nilai menjadi satu kesatuan, dan setiap anggotanya boleh bertipe berbeda. Tuple dideklarasikan dengan menuliskan nilainya dalam tanda kurung, dipisah koma. Ukuran tuple bersifat tetap sejak dideklarasi: kita tidak bisa menambah atau mengurangi jumlah anggota setelah tuple terbentuk. Mengakses anggota tuple dilakukan lewat tanda titik diikuti posisi anggotanya, misalnya .0 untuk anggota pertama (Rust mulai menghitung dari nol, bukan satu).

Berikut tuple yang menyimpan data seorang mahasiswa:

rust
fn main() {
    let mahasiswa = ("Budi Santoso", 20, 3.75);

    println!("Nama: {}", mahasiswa.0);
    println!("Umur: {}", mahasiswa.1);
    println!("IPK: {}", mahasiswa.2);
}

Output yang Dihasilkan:

text
Nama: Budi Santoso
Umur: 20
IPK: 3.75

Mari kita bedah kode di atas baris per baris:

  1. Baris 2 (let mahasiswa = (...)): variable mahasiswa mengikat tuple berisi tiga nilai: string "Budi Santoso", bilangan bulat 20, dan bilangan desimal 3.75; tipe tuple tersebut adalah (&str, i32, f64).
  2. Baris 4 (println!("Nama: {}", mahasiswa.0);): tanda .0 membaca anggota pertama tuple, yaitu nama mahasiswa.
  3. Baris 5 (println!("Umur: {}", mahasiswa.1);): tanda .1 membaca anggota kedua, yaitu umur bertipe bilangan bulat.
  4. Baris 6 (println!("IPK: {}", mahasiswa.2);): tanda .2 membaca anggota ketiga, yaitu IPK bertipe desimal.

Tuple cocok dipakai sebagai nilai kembalian sementara atau pembungkus data ringkas. Ketika isi tuple perlu dipakai berulang kali, menulis .0 atau .2 terus-menerus membuat kode sulit dibaca, sehingga Rust menyediakan cara yang lebih rapi.

Mengurai Tuple dengan Destructuring #

Destructuring memecah isi tuple sekaligus ke dalam beberapa variable baru. Pola penulisannya menempatkan tanda kurung berisi nama-nama variable di sisi kiri tanda sama dengan, dan tuple di sisi kanan:

rust
fn main() {
    let produk = ("Kopi Gayo", 2, 85000);

    let (nama, jumlah, harga_satuan) = produk;

    println!("Produk: {}", nama);
    println!("Jumlah: {} paket", jumlah);
    println!("Harga satuan: Rp{}", harga_satuan);
}

Output yang Dihasilkan:

text
Produk: Kopi Gayo
Jumlah: 2 paket
Harga satuan: Rp85000

Mari kita amati mekanisme di balik kode tersebut:

  1. Baris 2 (let produk = ("Kopi Gayo", 2, 85000);): tuple berisi tiga nilai dibuat dan diikat ke variable produk.
  2. Baris 4 (let (nama, jumlah, harga_satuan) = produk;): kompilator mencocokkan pola di kiri dengan struktur tuple di kanan; anggota pertama masuk ke nama, anggota kedua masuk ke jumlah, dan anggota ketiga masuk ke harga_satuan, masing-masing membawa tipenya sendiri.
  3. Baris 6-8 (println!(...)): ketiga variable hasil urai kini berdiri sendiri dengan nama yang bermakna, sehingga kode di bawahnya terbaca seperti variable biasa.

Array #

Array menyimpan sejumlah nilai bertipe sama dengan jumlah anggota yang tetap sejak kompilasi. Kita menuliskan tipenya sebagai [tipe; jumlah], misalnya [i32; 4] untuk empat bilangan bulat. Seluruh elemen array berdampingan di satu blok memori, sehingga akses elemen berdasarkan index sangat cepat. Sama seperti tuple, panjang array tidak bisa berubah setelah dideklarasikan:

rust
fn main() {
    let nilai_ujian: [i32; 4] = [85, 92, 78, 90];

    println!("Nilai pertama: {}", nilai_ujian[0]);
    println!("Nilai terakhir: {}", nilai_ujian[3]);
    println!("Banyak nilai: {}", nilai_ujian.len());
    println!("Seluruh nilai: {:?}", nilai_ujian);
}

Output yang Dihasilkan:

text
Nilai pertama: 85
Nilai terakhir: 90
Banyak nilai: 4
Seluruh nilai: [85, 92, 78, 90]

Mari kita bedah baris-baris pentingnya:

  1. Baris 2 (let nilai_ujian: [i32; 4] = [85, 92, 78, 90];): anotasi [i32; 4] menyatakan array berisi tepat empat nilai bertipe i32; isi array ditulis dalam tanda kurung siku dan dipisah koma.
  2. Baris 4 (println!("Nilai pertama: {}", nilai_ujian[0]);): index 0 membaca elemen pertama, yaitu 85.
  3. Baris 5 (println!("Nilai terakhir: {}", nilai_ujian[3]);): index 3 membaca elemen keempat alias terakhir; index tertinggi selalu panjang array dikurangi satu.
  4. Baris 6 (println!("Banyak nilai: {}", nilai_ujian.len());): method len() melaporkan jumlah elemen array, dalam kasus ini 4.
  5. Baris 7 (println!("Seluruh nilai: {:?}", nilai_ujian);): placeholder {:?} mencetak array utuh dalam format debug; tanda ? memberi tahu println! untuk menampilkan struktur koleksi, bukan satu nilai tunggal.

Mengisi Array dengan Nilai Sama #

Kadang kita perlu array yang seluruh elemennya bernilai sama, misalnya suhu awal tujuh hari berturut-turut. Rust menyediakan notasi ringkas [nilai; jumlah] untuk keperluan itu:

rust
fn main() {
    let suhu_harian = [30.5; 7];

    println!("Suhu hari ke-1: {} derajat", suhu_harian[0]);
    println!("Suhu hari ke-7: {} derajat", suhu_harian[6]);
    println!("Banyak hari: {}", suhu_harian.len());
}

Output yang Dihasilkan:

text
Suhu hari ke-1: 30.5 derajat
Suhu hari ke-7: 30.5 derajat
Banyak hari: 7

Penulisan [30.5; 7] membuat array [f64; 7] yang ketujuh elemennya berisi 30.5. Notasi ini menggabungkan deklarasi tipe dan pengisian nilai sekaligus, sehingga tidak perlu menulis [30.5, 30.5, 30.5, 30.5, 30.5, 30.5, 30.5] secara manual.

Ketika Index Berada di Luar Batas #

Kelemahan klasik bahasa lain adalah membaca index di luar koleksi: PHP diam-diam mengembalikan NULL dan peringatan, sementara C membaca memori sembarangan. Rust menolak kedua skenario tersebut dengan dua lapis pertahanan. Lapis pertama bekerja saat kompilasi ketika index berupa angka literal:

rust
fn main() {
    let nilai_ujian = [85, 92, 78, 90];

    println!("Nilai ke-5: {}", nilai_ujian[4]);
}

Kompilator menolak kode di atas dengan pesan berikut:

text
error: this operation will panic at runtime
 --> main.rs:4:32
  |
4 |     println!("Nilai ke-5: {}", nilai_ujian[4]);
  |                                ^^^^^^^^^^^^^^ index out of bounds: the length is 4 but the index is 4
  |
  = note: `#[deny(unconditional_panic)]` on by default

error: aborting due to 1 previous error

Karena panjang array dan index literal diketahui saat kompilasi, kompilator memastikan kombinasi mustahil seperti ini tidak pernah lolos. Lapis kedua menjaga kasus ketika index berasal dari variable yang nilainya baru diketahui saat program berjalan:

rust
fn main() {
    let nilai_ujian = [85, 92, 78, 90];
    let indeks = 4;

    println!("Nilai ke-{}: {}", indeks, nilai_ujian[indeks]);
}

Kali ini program lolos kompilasi, lalu berhenti total saat dijalankan:

text
thread 'main' (6110) panicked at main.rs:5:41:
index out of bounds: the len is 4 but the index is 4
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Program berhenti lewat mekanisme panic alih-alih membaca memori yang tidak sah. Angka dalam tanda kurung setelah kata main adalah nomor thread sistem operasi dan pasti berbeda setiap kali program dijalankan.

Ringkasan #

Tuple mengelompokkan nilai dengan tipe berbeda dan diakses lewat posisi .0, .1, dan seterusnya, atau diurai sekaligus lewat destructuring. Array menyimpan nilai bertipe sama dengan jumlah tetap, ditulis [tipe; jumlah], diakses lewat index, dan dilengkapi method len() untuk mengetahui panjangnya. Akses index di luar batas ditolak saat kompilasi untuk index literal, dan dihentikan lewat panic saat runtime untuk index dinamis. Dengan dua tipe komposit ini, kita siap mempelajari teknik shadowing untuk mengolah nilai secara bertahap.