Lewati ke konten

Error Rust E0499: cannot borrow as mutable more than once at a time

Error E0499 artinya kamu mencoba punya dua referensi mutable (&mut) ke nilai yang sama pada saat bersamaan. Ini salah satu error borrow checker (pemeriksa peminjaman) yang paling umum, dan perbaikannya biasanya kecil: sering kali kamu cukup memindahkan satu baris. (Mencari error lain? Lihat penjelasan error compiler.)

error[E0499]: cannot borrow `balance` as mutable more than once at a time
--> src/main.rs:5:20
|
4 | let deposit = &mut balance;
| ------------ first mutable borrow occurs here
5 | let withdraw = &mut balance;
| ^^^^^^^^^^^^ second mutable borrow occurs here
6 |
7 | *deposit += 50;
| -------------- first borrow later used here

Dalam bahasa sederhana: “balance tidak bisa dipinjam sebagai mutable lebih dari sekali pada saat yang sama”.

Ini program yang menghasilkan error tersebut:

fn main() {
let mut balance = 100;
let deposit = &mut balance;
let withdraw = &mut balance;
*deposit += 50;
*withdraw -= 30;
println!("Balance: {balance}");
}

Compiler menunjuk tiga tempat, dan bersama-sama ketiganya menceritakan semuanya:

  1. Baris 4: deposit menjadi mutable borrow atas balance.
  2. Baris 5: withdraw mencoba menjadi mutable borrow kedua atas balance.
  3. Baris 7: deposit masih dipakai setelah baris 5. Jadi borrow pertama masih hidup saat borrow kedua dimulai.

Label ketiga itulah yang penting. Dua borrow &mut hanya jadi masalah kalau masa hidupnya tumpang tindih. Kalau deposit sudah selesai dipakai sebelum baris 5, tidak akan ada error.

Bayangkan sebuah dokumen bersama dengan satu “kunci edit”. Siapa pun yang memegang kunci itu boleh mengubah dokumennya. Rust tidak mau menggandakan kuncinya: orang kedua harus menunggu sampai orang pertama mengembalikannya.

Aturan borrowing (peminjaman) di Rust: pada satu saat, kamu boleh punya banyak pembaca (&) atau satu penulis (&mut), tidak pernah lebih dari satu penulis. Referensi &mut adalah sebuah janji: “selama aku ada, tidak ada orang lain yang boleh menyentuh nilai ini.”

Janji itu mencegah bug sungguhan. Kalau dua bagian kode bisa mengubah nilai yang sama sekaligus, yang satu bisa mengubah ukuran sebuah Vec sementara yang lain masih menunjuk ke memori lamanya. Di bahasa lain, itu berarti crash atau data rusak tanpa ketahuan. Antar-thread, itu namanya data race.

Berkat aturan ini, compiler bisa yakin bahwa sebuah &mut adalah satu-satunya jalan untuk mencapai datanya selama ia hidup. Kamu mendapat keamanan itu tanpa biaya runtime sama sekali. Harganya: kadang kamu harus menunjukkan ke compiler dengan lebih jelas bahwa dua borrow-mu sebenarnya tidak bertabrakan.

Perbaikan 1: Selesaikan borrow pertama sebelum memulai yang kedua

Section titled “Perbaikan 1: Selesaikan borrow pertama sebelum memulai yang kedua”

Coba ini dulu. Cara ini memperbaiki sebagian besar error E0499. Sebuah borrow bertahan sampai pemakaian terakhirnya, bukan sampai akhir blok. Jadi kerjakan semuanya dengan referensi pertama, baru buat yang kedua:

fn main() {
let mut balance = 100;
let deposit = &mut balance;
*deposit += 50; // pemakaian terakhir `deposit`: borrow-nya berakhir di sini
let withdraw = &mut balance; // ✅ sekarang tidak ada yang meminjam
*withdraw -= 30;
println!("Saldo: {balance}");
}

Output:

Saldo: 120
▶ Coba sendiri

Sering kali kamu bahkan tidak butuh variabel tambahan itu: balance += 50; lalu balance -= 30; juga bekerja dengan baik.

Perbaikan 2: Pinjam bagian-bagian yang tidak tumpang tindih

Section titled “Perbaikan 2: Pinjam bagian-bagian yang tidak tumpang tindih”

Kadang kamu memang butuh dua &mut sekaligus, tapi keduanya menunjuk ke bagian yang berbeda dari data. Compiler tidak selalu bisa melihatnya, jadi kamu yang menunjukkannya.

Untuk dua elemen sebuah Vec atau slice, tasks[0] dan tasks[1] sama-sama meminjam seluruh tasks. Pakai split_at_mut (bahasa Inggris) untuk memotongnya menjadi dua bagian yang dijamin tidak tumpang tindih:

fn main() {
let mut tasks = vec![String::from("write"), String::from("test")];
// Belah Vec menjadi dua bagian yang tidak tumpang tindih
let (left, right) = tasks.split_at_mut(1);
let first = &mut left[0];
let second = &mut right[0];
std::mem::swap(first, second);
println!("{tasks:?}");
}

Output:

["test", "write"]
▶ Coba sendiri

(Khusus untuk menukar, tasks.swap(0, 1) bahkan lebih sederhana. split_at_mut untuk saat kamu butuh kedua referensi itu untuk pekerjaan sungguhan.)

Untuk sebuah struct, error yang sama muncul saat kamu memanggil dua method yang masing-masing menerima &mut self, seperti p.name_mut() dan p.score_mut(). Setiap pemanggilan meminjam seluruh struct. Kalau kamu meminjam field-nya secara langsung, compiler bisa melihat bahwa keduanya terpisah:

struct Player {
name: String,
score: u32,
}
fn main() {
let mut p = Player { name: String::from("Ferris"), score: 0 };
// Pinjam field-nya langsung, bukan lewat method
let name = &mut p.name;
let score = &mut p.score;
name.push_str(" si Kepiting");
*score += 10;
println!("{} punya {} poin", p.name, p.score);
}

Output:

Ferris si Kepiting punya 10 poin
▶ Coba sendiri

Perbaikan 3: Pakai indeks, jangan menyimpan referensi

Section titled “Perbaikan 3: Pakai indeks, jangan menyimpan referensi”

Kasus klasik: kamu melakukan loop atas sebuah Vec dengan iter_mut() dan sekaligus mencoba push ke dalamnya di dalam loop. Iterator memegang sebuah &mut selama seluruh loop, jadi push adalah &mut yang kedua:

fn main() {
let mut queue = vec![1, 2, 3];
for job in queue.iter_mut() {
*job *= 10;
queue.push(*job); // ❌ E0499: `queue` sudah dipinjam oleh iter_mut()
}
}

Kalau loop-nya memang harus mengubah koleksinya sendiri, lakukan loop atas posisi. Indeks hanyalah angka, jadi ia tidak meminjam apa pun:

fn main() {
let mut queue = vec![1, 2, 3];
// Loop atas posisi, bukan dengan memegang referensi ke dalam `queue`
for i in 0..queue.len() {
queue[i] *= 10;
let job = queue[i];
queue.push(job);
}
println!("{queue:?}");
}

Output:

[10, 20, 30, 10, 20, 30]
▶ Coba sendiri

0..queue.len() dihitung sekali, sebelum loop dimulai, jadi item-item baru tidak dikunjungi lagi. Setiap borrow queue[i] hanya bertahan satu baris.

RefCell memindahkan pemeriksaan borrow dari waktu kompilasi ke waktu program berjalan (runtime). Kamu bisa memanggil borrow_mut() tanpa variabel mut, asalkan tidak ada dua mutable borrow yang hidup pada saat yang sama:

use std::cell::RefCell;
fn main() {
let balance = RefCell::new(100); // perhatikan: tidak perlu `mut`
*balance.borrow_mut() += 50; // pinjam, ubah, borrow berakhir di `;`
*balance.borrow_mut() -= 30;
println!("Saldo: {}", balance.borrow());
}

Output:

Saldo: 120
▶ Coba sendiri

Jujur saja: RefCell tidak menghapus aturannya, ia hanya memeriksanya belakangan. Kalau kamu membiarkan dua guard borrow_mut() hidup sekaligus, programnya lolos kompilasi tapi panic saat dijalankan:

use std::cell::RefCell;
fn main() {
let balance = RefCell::new(100);
let mut deposit = balance.borrow_mut();
let mut withdraw = balance.borrow_mut(); // 💥 `deposit` masih hidup
*deposit += 50;
*withdraw -= 30;
}
thread 'main' (12345) panicked at src/main.rs:7:32:
RefCell already borrowed

Pesan panic itu artinya “RefCell sudah sedang dipinjam”. Jadi pakai RefCell hanya kalau datanya memang dibagi-pakai (misalnya di dalam Rc<RefCell<T>>) dan Perbaikan 1–3 tidak cocok. Ini bukan cara untuk membungkam compiler.

  • Menyimpan &mut di dalam loop. handles.push(&mut counter) di dalam loop for gagal dengan pesan “mutably borrowed here in the previous iteration of the loop” (sudah dipinjam mutable di putaran sebelumnya). Setiap putaran loop menambah satu &mut lagi ke nilai yang sama. Perbaikannya: simpan indeks atau nilai yang dimiliki (owned), atau ubah nilainya langsung di dalam loop.
  • Dua pemanggilan get_mut pada HashMap. let a = stock.get_mut("apples") lalu let p = stock.get_mut("pears") meminjam stock dua kali. Perbaikannya: selesaikan urusan dengan a sebelum memanggil get_mut lagi, atau pakai stock.get_disjoint_mut(["apples", "pears"]) untuk mendapat keduanya sekaligus.
  • Dua closure yang mengubah variabel yang sama. let mut inc = || count += 1; dan let mut inc2 = || count += 2; masing-masing memegang &mut count selama closure itu hidup. Perbaikannya: pakai satu closure saja, atau panggil closure pertama untuk terakhir kalinya sebelum membuat yang kedua. Lihat Closure.

Kuis singkat

1. Kapan Rust melaporkan E0499?

2. Kamu membuat let a = &mut v;, memakai a untuk terakhir kalinya, lalu membuat let b = &mut v;. Apa yang terjadi?

3. Alat mana yang membuatmu bisa mendapat dua referensi &mut ke elemen berbeda dari slice yang sama?

Skor: 0 / 3