Jika Anda baru belajar Rust, biasanya kebingungan mulai muncul setelah mengenal ownership. Konsep bahwa setiap nilai punya satu pemilik memang cukup jelas di awal, tetapi ketika data ingin dipakai oleh fungsi lain tanpa dipindahkan, muncul pertanyaan: bagaimana caranya?

Di sinilah reference dan borrowing berperan. Keduanya memungkinkan kita memakai data tanpa mengambil kepemilikan penuh. Dengan kata lain, kita bisa “meminjam” data untuk dibaca atau diubah, sambil tetap menjaga aturan keselamatan memori yang menjadi kekuatan utama Rust.

Artikel ini melanjutkan konsep ownership dengan fokus pada penggunaan & dan &mut secara praktis. Tujuannya bukan hanya agar Anda tahu sintaksnya, tetapi juga memahami kenapa Rust membatasi akses tertentu dan bagaimana memanfaatkan aturan tersebut agar kode lebih aman dan mudah dipelihara.

Mengapa Borrowing Diperlukan?

Dalam Rust, ketika sebuah nilai dipindahkan ke variabel atau fungsi lain, pemilik sebelumnya tidak bisa lagi menggunakannya. Ini mencegah masalah seperti double free dan penggunaan memori yang sudah tidak valid. Namun dalam praktik, kita sering ingin:

  • membaca isi data tanpa mengambil alih kepemilikannya,
  • mengirim data ke fungsi utilitas tanpa membuat salinan mahal,
  • atau mengubah data yang sama dari tempat tertentu dengan tetap aman.

Jika semua hal itu harus dilakukan lewat move, kode akan cepat menjadi tidak nyaman. Borrowing hadir sebagai solusi. Dengan borrowing, kita meminjam akses ke data untuk sementara waktu. Pemilik asli tetap ada, dan setelah peminjaman selesai, data tetap bisa digunakan lagi oleh pemilik tersebut.

Contoh sederhana:

fn main() {
    let nama = String::from("Rust");
    print_text(&nama);
    println!("{}", nama);
}

fn print_text(teks: &String) {
    println!("Isi teks: {}", teks);
}

Pada contoh di atas, nama tidak dipindahkan ke fungsi print_text. Yang dipinjam hanyalah referensinya melalui &nama. Karena itu, setelah fungsi dipanggil, nama masih bisa digunakan.

Inilah manfaat paling praktis dari reference untuk pemula: Anda bisa memakai data tanpa kehilangan kepemilikannya.

Apa Itu Reference di Rust?

Reference adalah cara untuk merujuk ke suatu nilai tanpa memilikinya. Secara sintaks, Rust menggunakan tanda & untuk membuat reference.

Misalnya:

let pesan = String::from("Halo");
let r = &pesan;

Variabel r bukan pemilik dari string tersebut. Ia hanya menunjuk ke data milik pesan. Karena itu, saat r dipakai, Rust memastikan bahwa data asli masih valid selama reference tersebut hidup.

Di sinilah pentingnya memahami bahwa borrowing bukan sekadar fitur sintaks. Rust memakai sistem ini untuk memastikan beberapa hal:

  • tidak ada reference yang menunjuk ke data yang sudah dihapus,
  • tidak ada perubahan data yang berpotensi membuat pembacaan jadi tidak konsisten,
  • dan tidak ada dua akses berbahaya yang terjadi bersamaan.

Dengan kata lain, reference di Rust bukan pointer bebas seperti di bahasa sistem lain. Reference diatur ketat oleh borrow checker, komponen compiler Rust yang memverifikasi keamanan akses data saat kompilasi.

Catatan: Saat belajar Rust, error dari borrow checker sering terasa menyulitkan. Namun sebenarnya compiler sedang menunjukkan lokasi akses data yang berpotensi tidak aman sebelum program dijalankan.

Borrowing Immutable: Meminjam untuk Dibaca

Bentuk borrowing yang paling sederhana adalah immutable borrowing, yaitu meminjam data hanya untuk dibaca. Ini ditulis dengan &T, di mana T adalah tipe data.

Contohnya:

fn panjang(teks: &String) -> usize {
    teks.len()
}

fn main() {
    let judul = String::from("Belajar Rust");
    let hasil = panjang(&judul);

    println!("Panjang: {}", hasil);
    println!("Judul tetap bisa dipakai: {}", judul);
}

Fungsi panjang menerima &String, bukan String. Artinya fungsi hanya meminjam string untuk dibaca, bukan memilikinya. Ini berguna untuk banyak kasus sehari-hari, seperti:

  • menghitung panjang string,
  • memvalidasi data,
  • membaca isi konfigurasi,
  • atau mencetak data ke log.

Mengapa Bisa Ada Banyak Immutable Reference?

Rust mengizinkan beberapa immutable reference aktif pada saat yang sama, karena membaca data dari banyak tempat tidak menimbulkan konflik.

fn main() {
    let data = String::from("config");

    let r1 = &data;
    let r2 = &data;
    let r3 = &data;

    println!("{} | {} | {}", r1, r2, r3);
}

Ketiga reference di atas aman, karena semuanya hanya membaca. Tidak ada yang mencoba mengubah isi data.

Prinsipnya sederhana: banyak pembaca sekaligus tidak masalah.

Borrowing Mutable: Meminjam untuk Mengubah

Selain membaca, kadang kita ingin mengubah data tanpa memindahkan kepemilikannya. Untuk itu, Rust menyediakan mutable reference dengan sintaks &mut T.

Contoh:

fn tambah_tanda_seru(teks: &mut String) {
    teks.push('!');
}

fn main() {
    let mut pesan = String::from("Halo");
    tambah_tanda_seru(&mut pesan);
    println!("{}", pesan);
}

Ada dua hal penting di sini:

  1. Variabel asli harus dideklarasikan dengan mut, karena datanya memang akan diubah.
  2. Fungsi penerima harus mendeklarasikan parameter sebagai &mut String, karena ia meminjam hak ubah sementara.

Mutable borrowing sangat berguna untuk:

  • menambahkan elemen ke Vec,
  • memperbarui status dalam struct,
  • mengisi buffer,
  • atau memodifikasi string tanpa membuat objek baru.

Mengapa Mutable Reference Dibatasi?

Rust hanya mengizinkan satu mutable reference aktif pada satu waktu. Misalnya kode berikut akan gagal dikompilasi:

fn main() {
    let mut nilai = String::from("data");

    let r1 = &mut nilai;
    let r2 = &mut nilai;

    println!("{} {}", r1, r2);
}

Alasannya bukan karena Rust ingin mempersulit, tetapi untuk mencegah data race dan perubahan yang tidak terkoordinasi. Jika dua pihak bisa mengubah data yang sama pada waktu bersamaan, hasil akhirnya bisa sulit diprediksi.

Rust merangkum aturannya seperti ini:

  • boleh banyak immutable reference, atau
  • boleh satu mutable reference,
  • tetapi tidak boleh keduanya aktif bersamaan.

Aturan ini sering disebut sebagai prinsip aliasing XOR mutation: jika sebuah data dibagi ke banyak referensi, data itu tidak boleh dimodifikasi; jika sedang dimodifikasi, tidak boleh ada pembaca lain yang aktif.

Kenapa Rust Melarang Immutable dan Mutable Reference Bersamaan?

Perhatikan contoh berikut:

fn main() {
    let mut teks = String::from("Halo");

    let r1 = &teks;
    let r2 = &teks;
    let r3 = &mut teks;

    println!("{} {} {}", r1, r2, r3);
}

Kode ini akan ditolak compiler. Secara logika, r1 dan r2 sedang meminjam data untuk dibaca, sementara r3 ingin mengubah data yang sama. Jika perubahan diizinkan saat ada pembaca aktif, pembaca bisa melihat data dalam kondisi yang berubah di tengah proses.

Dalam bahasa lain, situasi seperti ini bisa memunculkan bug halus yang sulit dilacak. Rust memilih mencegahnya dari awal. Pembatasan ini memberi beberapa keuntungan nyata:

  • perilaku program lebih mudah diprediksi,
  • compiler bisa memberikan jaminan keamanan memori tanpa garbage collector,
  • dan bug terkait konkurensi atau perubahan data lebih cepat terdeteksi saat kompilasi.

Untuk pemula, aturan ini mungkin terasa kaku. Tetapi dalam jangka panjang, aturan tersebut membantu Anda menulis API yang jelas: fungsi mana yang hanya membaca, dan fungsi mana yang memang mengubah data.

Contoh Fungsi yang Menerima Reference agar Data Tidak Dipindahkan

Salah satu pola yang paling sering dipakai di Rust adalah membuat fungsi menerima reference jika fungsi tersebut tidak perlu memiliki data secara penuh.

Contoh membaca data

fn tampilkan_user(nama: &String) {
    println!("User: {}", nama);
}

fn main() {
    let user = String::from("Alya");
    tampilkan_user(&user);
    println!("Masih bisa dipakai: {}", user);
}

Jika tampilkan_user menerima String langsung, maka user akan dipindahkan ke fungsi. Karena fungsi hanya perlu membaca, menerima &String lebih tepat.

Contoh mengubah data

fn normalisasi_username(username: &mut String) {
    *username = username.trim().to_lowercase();
}

fn main() {
    let mut username = String::from("  Admin123  ");
    normalisasi_username(&mut username);
    println!("Hasil: {}", username);
}

Fungsi di atas mengubah isi string yang dipinjam. Dengan demikian, pemilik data tetap berada di main, tetapi perubahan dilakukan melalui mutable reference.

Kapan sebaiknya memakai reference?

Sebagai panduan praktis:

  • Gunakan &T jika fungsi hanya perlu membaca data.
  • Gunakan &mut T jika fungsi perlu mengubah data milik pemanggil.
  • Gunakan T langsung jika fungsi memang perlu mengambil kepemilikan, misalnya untuk menyimpan, mengembalikan ulang dalam bentuk lain, atau memindahkan ke thread/struktur baru.

Dengan kebiasaan ini, tanda tangan fungsi menjadi lebih informatif. Pembaca kode bisa langsung memahami apakah fungsi tersebut hanya membaca atau juga memodifikasi data.

Kesalahan Umum Pemula

1. Lupa menandai variabel sebagai mutable

let teks = String::from("Halo");
tambah_tanda_seru(&mut teks);

Ini akan gagal karena teks tidak dideklarasikan sebagai mut. Perbaiki menjadi:

let mut teks = String::from("Halo");

2. Mencoba membuat lebih dari satu mutable reference

Ini sering terjadi saat kita berpikir seperti di bahasa lain. Jika butuh beberapa operasi ubah, lakukan secara berurutan, bukan bersamaan.

3. Masih memakai variabel saat sedang dipinjam secara mutable

Saat sebuah mutable borrow aktif, akses lain ke variabel asli biasanya dibatasi sampai borrow selesai. Solusinya sering kali cukup dengan memperkecil cakupan peminjaman.

fn main() {
    let mut s = String::from("abc");

    {
        let r = &mut s;
        r.push('d');
    }

    println!("{}", s);
}

Di sini, blok tambahan membantu Rust memahami bahwa borrow selesai sebelum s dipakai lagi.

4. Terlalu cepat memakai clone()

Sebagian pemula mengatasi error borrowing dengan menyalin data menggunakan clone(). Kadang itu valid, tetapi sering kali hanya menutupi desain fungsi yang kurang tepat. Sebelum memakai clone(), cek dulu apakah cukup dengan &T atau &mut T.

Tips Praktis agar Lebih Nyaman dengan & dan &mut

  • Mulai dari pertanyaan sederhana: fungsi ini hanya membaca atau juga mengubah?
  • Gunakan immutable borrow sebagai default. Mutable borrow dipakai hanya jika benar-benar perlu.
  • Baca pesan error compiler dengan tenang. Biasanya Rust menunjukkan siapa yang meminjam, kapan, dan konflik terjadi di mana.
  • Perkecil scope borrow. Jika borrow terlalu lama, pisahkan ke blok atau panggil fungsi lebih awal.
  • Pikirkan ownership sebagai kontrak API. Tanda tangan fungsi di Rust membantu mendefinisikan siapa pemilik data dan siapa hanya peminjam.

Semakin sering Anda menulis fungsi dengan reference, semakin natural pola ini terasa. Awalnya aturan Rust tampak ketat, tetapi setelah terbiasa, aturan tersebut justru membantu menghindari banyak bug sejak awal pengembangan.

Penutup

Borrowing dan reference adalah lanjutan alami dari ownership. Jika ownership menjawab pertanyaan siapa pemilik data, maka borrowing menjawab siapa yang boleh memakai data untuk sementara. Dengan &, kita meminjam data untuk dibaca. Dengan &mut, kita meminjam data untuk diubah.

Perbedaan immutable dan mutable borrowing sangat penting karena di situlah Rust menjaga keamanan akses data. Banyak pembaca diperbolehkan karena aman, tetapi penulis harus eksklusif agar tidak muncul konflik. Aturan ini mungkin terasa baru bagi pemula, namun justru itulah yang membuat Rust kuat dalam hal keamanan memori dan kejelasan desain program.

Jika Anda masih sering terkena error dari borrow checker, itu normal. Fokuslah pada dua hal: apakah data sedang dibaca atau diubah, dan siapa yang memiliki data tersebut. Dari sana, penggunaan & dan &mut akan terasa jauh lebih masuk akal dan tidak lagi menakutkan.