Lewati ke konten

Error Rust E0373: closure may outlive the current function (pakai move)

E0373 artinya sebuah closure (fungsi anonim) meminjam variabel lokal, padahal closure itu mungkin masih berjalan (atau masih ada) setelah fungsi pemilik variabel itu selesai. Biasanya kamu bertemu error ini pertama kali saat memanggil thread::spawn atau mengembalikan closure dari sebuah fungsi. Kabar baiknya: compiler sudah memberitahu perbaikannya, dan hampir selalu cuma satu kata, move. Halaman ini menunjukkan perbaikannya dulu, baru kemudian menjelaskan alasannya. Untuk error lain di seri ini, lihat Penjelasan error compiler Rust.

error[E0373]: closure may outlive the current function, but it borrows `scores`, which is owned by the current function
--> src/main.rs:5:32
|
5 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `scores`
6 | println!("Scores: {scores:?}"); // ❌ hanya meminjam scores
| ------ `scores` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:5:18
|
5 | let handle = thread::spawn(|| {
| __________________^
6 | | println!("Scores: {scores:?}"); // ❌ hanya meminjam scores
7 | | });
| |______^
help: to force the closure to take ownership of `scores` (and any other referenced variables), use the `move` keyword
|
5 | let handle = thread::spawn(move || {
| ++++

Ini program yang menghasilkan error tersebut:

use std::thread;
fn main() {
let scores = vec![90, 85, 77];
let handle = thread::spawn(|| {
println!("Scores: {scores:?}"); // ❌ hanya meminjam scores
});
handle.join().unwrap();
}

Di kode async, kamu akan melihat error yang sama dengan kata-kata sedikit berbeda: “async block may outlive the current function, but it borrows user_id”. Itu error yang sama, dan perbaikannya juga sama (async move).

Baca label-labelnya dari atas ke bawah:

  1. ^^ may outlive borrowed value menunjuk ke closure ||. Rust tidak bisa menjamin closure itu sudah selesai sebelum main lanjut ke baris berikutnya.
  2. `scores` is borrowed here menunjuk ke tempat closure memakai scores. Karena println! hanya perlu membacanya, closure itu menangkap sebuah referensi ke scores, bukan vektornya sendiri.
  3. function requires argument type to outlive 'static menjelaskan kenapa itu jadi masalah: thread::spawn hanya menerima closure yang bisa hidup selama apa pun program membutuhkannya. 'static artinya “tidak meminjam apa pun yang bisa hilang”.

Bayangkan kamu meminjamkan buku catatanmu ke teman dan bilang “kembalikan kalau sudah selesai”, padahal kamu sebentar lagi pindah ke kota lain. Kalau temanmu lambat, buku itu tidak punya rumah untuk dikembalikan. Rust menolak kesepakatan seperti itu dan memintamu memberikan buku itu saja.

Closure menangkap variabel dengan cara paling hemat yang masih berfungsi. Kalau ia hanya membaca sebuah variabel, ia meminjamnya dengan &. Itu bagus untuk closure yang langsung dijalankan, seperti yang kamu berikan ke .map() atau .filter(). Kamu bisa membaca lebih lanjut di halaman Closure.

Thread baru itu berbeda. Ia bisa berjalan selama berapa pun, dan main tidak menunggunya kecuali kamu memanggil join. Kalau thread itu memegang referensi ke scores, lalu main selesai duluan, scores akan di-drop dan thread-nya membaca memori yang sudah dibebaskan. Itulah “dangling reference”, salah satu bug paling umum di kode thread C dan C++. Rust menghentikannya saat kompilasi.

Hal yang sama terjadi saat kamu mengembalikan closure. Variabel lokal fungsi (dan parameternya) di-drop saat fungsi itu selesai. Closure yang masih meminjamnya akan menunjuk ke ketiadaan.

Perbaikan 1: Tambahkan move (jawaban yang biasa)

Section titled “Perbaikan 1: Tambahkan move (jawaban yang biasa)”

Pilih ini hampir setiap saat. Inilah yang disarankan rustc. move menyuruh closure mengambil ownership (kepemilikan) atas variabel-variabel yang dipakainya, jadi variabel itu ikut pergi bersama closure-nya:

use std::thread;
fn main() {
let scores = vec![90, 85, 77];
let handle = thread::spawn(move || {
let total: i32 = scores.iter().sum();
println!("Nilai: {scores:?}, total {total}");
});
handle.join().unwrap();
}

Output:

Nilai: [90, 85, 77], total 252
▶ Coba sendiri

Perbaikan yang sama berlaku untuk fungsi yang mengembalikan closure. Tanpa move, |x| x + n meminjam parameter n, yang sudah hilang begitu make_adder selesai. Dengan move, setiap closure menyimpan salinan n-nya sendiri:

fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
fn main() {
let add_five = make_adder(5);
let add_ten = make_adder(10);
println!("{}", add_five(1));
println!("{}", add_ten(1));
}

Output:

6
11
▶ Coba sendiri

move tidak mengubah trait closure yang kamu dapat. Closure move yang hanya membaca datanya tetap Fn dan bisa dipanggil berkali-kali.

Perbaikan 2: Clone dulu, kalau kamu masih butuh nilainya

Section titled “Perbaikan 2: Clone dulu, kalau kamu masih butuh nilainya”

Setelah move, variabelnya menjadi milik closure. Kalau main memakainya lagi nanti, kamu akan mendapat E0382 (“borrow of moved value”), dan rustc menyarankan untuk meng-clone nilainya sebelum dipindahkan. Buat salinan untuk closure dan simpan aslinya:

use std::thread;
fn main() {
let guests = vec![String::from("Ana"), String::from("Ben")];
let guests_for_thread = guests.clone();
let handle = thread::spawn(move || {
println!("Thread melihat {} tamu", guests_for_thread.len());
});
handle.join().unwrap();
println!("Main masih punya: {guests:?}");
}

Output:

Thread melihat 2 tamu
Main masih punya: ["Ana", "Ben"]
▶ Coba sendiri

Meng-clone daftar kecil itu murah dan sama sekali tidak masalah. Kalau datanya besar, atau banyak thread membutuhkannya, pakai Arc (Perbaikan 3) supaya semuanya berbagi satu salinan.

Perbaikan 3: Bagikan dengan Arc ke beberapa thread

Section titled “Perbaikan 3: Bagikan dengan Arc ke beberapa thread”

Pilih ini kalau beberapa thread perlu membaca data yang sama. Arc (“atomically reference counted”) membuat banyak pemilik bisa berbagi satu nilai. Arc::clone hanya menyalin pointer dan menaikkan sebuah penghitung; datanya sendiri tidak disalin:

use std::sync::Arc;
use std::thread;
fn main() {
let prices = Arc::new(vec![4.5, 2.0, 3.5]);
let mut handles = Vec::new();
for discount in [0.0, 0.1, 0.5] {
let prices = Arc::clone(&prices); // satu handle per thread
handles.push(thread::spawn(move || {
let total: f64 = prices.iter().sum();
total * (1.0 - discount)
}));
}
for handle in handles {
println!("Total: {:.2}", handle.join().unwrap());
}
println!("Harga: {prices:?}");
}

Output:

Total: 10.00
Total: 9.00
Total: 5.00
Harga: [4.5, 2.0, 3.5]
▶ Coba sendiri

Arc saja hanya memberi akses baca bersama. Kalau thread-thread itu juga perlu mengubah datanya, bungkus menjadi Arc<Mutex<T>>. Lihat Konkurensi dan Shared state.

Perbaikan 4: Pinjam dengan aman memakai std::thread::scope

Section titled “Perbaikan 4: Pinjam dengan aman memakai std::thread::scope”

Pilih ini kalau thread-thread itu hanya butuh datanya untuk pekerjaan singkat dan kamu lebih suka meminjam daripada memindahkan atau meng-clone. Thread yang dimulai di dalam thread::scope dijamin selesai sebelum scope selesai, jadi meminjam variabel lokal itu aman dan tidak perlu move:

use std::thread;
fn main() {
let scores = vec![90, 85, 77, 64];
let (first, second) = scores.split_at(2);
let (a, b) = thread::scope(|s| {
let h1 = s.spawn(|| first.iter().sum::<i32>());
let h2 = s.spawn(|| second.iter().sum::<i32>());
(h1.join().unwrap(), h2.join().unwrap())
}); // kedua thread sudah selesai di sini
println!("{a} + {b} = {}", a + b);
println!("Masih milik kita: {scores:?}");
}

Output:

175 + 141 = 316
Masih milik kita: [90, 85, 77, 64]
▶ Coba sendiri

Scoped thread adalah jawaban paling rapi untuk pola “bagi pekerjaannya, lalu pakai hasilnya”. Pelajaran Shared state dimulai dengan topik ini.

Membuat thread di dalam perulangan. Bahkan angka biasa pun dipinjam kalau closure-nya hanya membacanya:

for worker in 1..=3 {
handles.push(thread::spawn(|| {
println!("Worker {worker} started"); // ❌ meminjam `worker`
}));
}

Perbaikan: thread::spawn(move || ...). worker adalah i32, jadi move cukup menyalinnya ke setiap thread.

Mengembalikan closure dalam Box. Box::new(|| greeting.clone()) di dalam fungsi yang mengembalikan Box<dyn Fn() -> String> meminjam variabel lokal greeting, dan rustc menambahkan note: closure is returned here. Perbaikan: Box::new(move || greeting.clone()). Selengkapnya di Fn, FnMut, dan FnOnce.

Task async (Tokio dan sejenisnya). tokio::spawn bekerja seperti thread::spawn: task-nya bisa berjalan lama setelah fungsi saat ini selesai, jadi ia butuh data 'static. Menulis tokio::spawn(async { ... }) dan memakai variabel lokal di dalamnya memberi “async block may outlive the current function”. Perbaikan: tokio::spawn(async move { ... }), lalu clone atau Arc::clone apa pun yang masih kamu butuhkan di luar task. Lihat Async/Await.

Kuis singkat

1. Sebuah closure thread mencetak name tanpa move. Kenapa kamu mendapat E0373?

2. Kamu sudah menambahkan move, tapi main masih perlu mencetak vektornya setelah spawn. Apa perbaikan paling sederhana?

3. Alat mana yang membuat thread bisa meminjam variabel lokal tanpa move?

Skor: 0 / 3