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:
fn main() {
let mahasiswa = ("Budi Santoso", 20, 3.75);
println!("Nama: {}", mahasiswa.0);
println!("Umur: {}", mahasiswa.1);
println!("IPK: {}", mahasiswa.2);
}Output yang Dihasilkan:
Nama: Budi Santoso
Umur: 20
IPK: 3.75Mari kita bedah kode di atas baris per baris:
- Baris 2 (
let mahasiswa = (...)): variablemahasiswamengikat tuple berisi tiga nilai: string"Budi Santoso", bilangan bulat20, dan bilangan desimal3.75; tipe tuple tersebut adalah(&str, i32, f64). - Baris 4 (
println!("Nama: {}", mahasiswa.0);): tanda.0membaca anggota pertama tuple, yaitu nama mahasiswa. - Baris 5 (
println!("Umur: {}", mahasiswa.1);): tanda.1membaca anggota kedua, yaitu umur bertipe bilangan bulat. - Baris 6 (
println!("IPK: {}", mahasiswa.2);): tanda.2membaca 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:
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:
Produk: Kopi Gayo
Jumlah: 2 paket
Harga satuan: Rp85000Mari kita amati mekanisme di balik kode tersebut:
- Baris 2 (
let produk = ("Kopi Gayo", 2, 85000);): tuple berisi tiga nilai dibuat dan diikat ke variableproduk. - Baris 4 (
let (nama, jumlah, harga_satuan) = produk;): kompilator mencocokkan pola di kiri dengan struktur tuple di kanan; anggota pertama masuk kenama, anggota kedua masuk kejumlah, dan anggota ketiga masuk keharga_satuan, masing-masing membawa tipenya sendiri. - 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:
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:
Nilai pertama: 85
Nilai terakhir: 90
Banyak nilai: 4
Seluruh nilai: [85, 92, 78, 90]Mari kita bedah baris-baris pentingnya:
- Baris 2 (
let nilai_ujian: [i32; 4] = [85, 92, 78, 90];): anotasi[i32; 4]menyatakan array berisi tepat empat nilai bertipei32; isi array ditulis dalam tanda kurung siku dan dipisah koma. - Baris 4 (
println!("Nilai pertama: {}", nilai_ujian[0]);): index0membaca elemen pertama, yaitu85. - Baris 5 (
println!("Nilai terakhir: {}", nilai_ujian[3]);): index3membaca elemen keempat alias terakhir; index tertinggi selalu panjang array dikurangi satu. - Baris 6 (
println!("Banyak nilai: {}", nilai_ujian.len());): methodlen()melaporkan jumlah elemen array, dalam kasus ini4. - Baris 7 (
println!("Seluruh nilai: {:?}", nilai_ujian);): placeholder{:?}mencetak array utuh dalam format debug; tanda?memberi tahuprintln!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:
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:
Suhu hari ke-1: 30.5 derajat
Suhu hari ke-7: 30.5 derajat
Banyak hari: 7Penulisan [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:
fn main() {
let nilai_ujian = [85, 92, 78, 90];
println!("Nilai ke-5: {}", nilai_ujian[4]);
}Kompilator menolak kode di atas dengan pesan berikut:
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 errorKarena 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:
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:
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 backtraceProgram 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.