Chapter sebelumnya menunjukkan bahwa kompilator menangkap lingkungan dengan cara seringan mungkin: membaca cukup dengan reference biasa, mengubah cukup dengan reference mutable. Cara ringan itu punya umur terbatas — reference menuntut variabel aslinya tetap hidup selama closure hidup. Masalah muncul ketika closure harus hidup lebih lama daripada variabel yang ditangkap: closure dikirim ke thread lain yang bekerja setelah fungsi pembuatnya selesai, atau dikembalikan dari fungsi sementara tangkapannya variabel lokal. Kata kunci move menyelesaikan konflik umur dengan cara Rust yang khas: memindahkan kepemilikan. Pada Chapter ini kita pelajari kapan move wajib, kapan move tidak mengubah apa pun, dan harga E0382 yang menyertainya.

Pinjaman yang Terlalu Pendek untuk Thread #

Kita mulai dari masalah nyatanya. Fungsi std::thread::spawn menjalankan closure di thread baru, dan thread baru itu bisa hidup lebih lama daripada fungsi yang membuatnya. Karena itu Rust menuntut closure untuk thread tidak menyimpan pinjaman apa pun dari scope pembuatnya — tuntutan yang lazim ditulis sebagai batasan lifetime 'static. Kode berikut mencoba mengirim nama_tim ke thread tanpa move:

rust
use std::thread;

fn main() {
    let nama_tim = String::from("Tim Nusantara");

    let pekerja = thread::spawn(|| {
        println!("{} mulai memproses pesanan", nama_tim);
    });

    pekerja.join().unwrap();
    println!("Semua pesanan selesai diproses");
}

Kompilasi gagal dengan pesan asli berikut:

text
error[E0373]: closure may outlive the current function, but it borrows `nama_tim`, which is owned by the current function
 --> main.rs:6:33
  |
6 |     let pekerja = thread::spawn(|| {
  |                                 ^^ may outlive borrowed value `nama_tim`
7 |         println!("{} mulai memproses pesanan", nama_tim);
  |                                                -------- `nama_tim` is borrowed here
  |
note: function requires argument type to outlive `'static`
 --> main.rs:6:19
  |
6 |       let pekerja = thread::spawn(|| {
  |  ___________________^
7 | |         println!("{} mulai memproses pesanan", nama_tim);
8 | |     });
  | |______^
help: to force the closure to take ownership of `nama_tim` (and any other referenced variables), use the `move` keyword
  |
6 |     let pekerja = thread::spawn(move || {
  |                                 ++++

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0373`.

Pesan E0373 menggambarkan konflik umur dengan gamblang: closure boleh hidup lebih lama dari fungsi ini (may outlive the current function), tetapi closure meminjam nama_tim yang dimiliki fungsi ini. Pinjaman pasti putus sebelum main berakhir, sementara thread belum tentu selesai — dan Rust menolak menebak nasib data. Baris help langsung menawarkan solusi pastinya: tambahkan kata kunci move sebelum pipe. Detail thread kita bahas tuntas di Section Concurrency; saat ini skenario ini cukup sebagai kasus nyata yang memaksa closure memiliki datanya sendiri.

Memaksa Kepemilikan dengan move #

Menambahkan kata kunci move sebelum daftar parameter mengubah mode penangkapan semua variabel menjadi kepemilikan penuh. Nilai-nilai tersebut pindah ke dalam struct anonim closure saat closure dibuat, dan closure tidak lagi tergantung pada umur scope asalnya:

rust
use std::thread;

fn main() {
    let nama_tim = String::from("Tim Nusantara");

    let pekerja = thread::spawn(move || {
        println!("{} mulai memproses pesanan", nama_tim);
    });

    pekerja.join().unwrap();
    println!("Semua pesanan selesai diproses");
}

Output yang Dihasilkan:

text
Tim Nusantara mulai memproses pesanan
Semua pesanan selesai diproses

Mari kita bedah perubahan yang terjadi:

  1. Baris 6 (let pekerja = thread::spawn(move || {): kata kunci move berdiri di depan ||. Penempatannya selalu di situ — move || — dan berlaku untuk seluruh penangkapan sekaligus, bukan per variabel.
  2. Baris 6 (perpindahan nama_tim): begitu ekspresi move || { ... } dievaluasi, kepemilikan nama_tim pindah ke struct anonim closure — closure kini membawa teks tersebut sebagai miliknya sendiri, tuntutan 'static terpenuhi, dan E0373 hilang.
  3. Baris 10 (pekerja.join().unwrap();): pemanggilan join() menunggu thread selesai sebelum main melanjutkan, sehingga urutan dua baris cetak di output pasti seperti di atas.
  4. Baris 11: main tetap mencetak penutup setelah thread selesai, tetapi nama_tim sudah tidak bisa dicetak di main — kepemilikannya pindah ke closure.

Pola thread::spawn(move || ...) akan kita lihat berulang di Section Concurrency; pola serupa dipakai saat closure dikembalikan dari fungsi — topik Chapter Mengembalikan Closure.

move pada Tipe Copy: Tanpa Kehilangan #

Move semantics pada tipe yang mengimplementasikan Copy berjalan lewat penyalinan, bukan perpindahan. Akibatnya, move closure atas variabel bertipe u32, i64, atau bool tidak membuat variabel asli mati:

rust
fn main() {
    let batas_stok = 5;
    let cek_stok = move |jumlah| jumlah > batas_stok;

    println!("Stok 8 melebihi batas? {}", cek_stok(8));
    println!("Batas masih bisa dipakai : {}", batas_stok);
}

Output yang Dihasilkan:

text
Stok 8 melebihi batas? true
Batas masih bisa dipakai : 5

Mari kita bedah:

  1. Baris 2 (let batas_stok = 5;): variabel bertipe i32 — tipe yang mengimplementasikan Copy sesuai pembahasan kita di Section Ownership dan Memory.
  2. Baris 3 (let cek_stok = move |jumlah| jumlah > batas_stok;): move meminta kepemilikan batas_stok, tetapi karena i32 bersifat Copy, compiler menyalin nilainya ke struct anonim closure.
  3. Baris 6 (println!("Batas masih bisa dipakai : {}", batas_stok);): variabel asli batas_stok tetap hidup dan terbaca normal — penyalinan tidak mematikan sumbernya. Bandingkan dengan contoh nama_tim bertipe String yang benar-benar pindah.

Perilaku ini membuat move pada angka kecil terasa “gratis”: data disalin ke struct anonim closure tanpa alokasi heap, dan variabel asli tidak terganggu.

Harga yang Harus Dibayar: E0382 #

move pada tipe non-Copy seperti String benar-benar memindahkan kepemilikan, dan aturan move semantics berlaku penuh: variabel yang sudah pindah tidak boleh dipakai lagi. Kode berikut mencoba menampilkan kode voucher setelah dipindahkan ke dalam closure:

rust
fn main() {
    let kode_voucher = String::from("VOUCHER-HARI-RAYA");

    let klaim_voucher = move || {
        println!("Voucher {} ditukarkan", kode_voucher);
    };

    klaim_voucher();
    println!("Kode voucher: {}", kode_voucher);
}

Kompilasi gagal dengan pesan asli berikut:

text
error[E0382]: borrow of moved value: `kode_voucher`
 --> main.rs:9:34
  |
2 |     let kode_voucher = String::from("VOUCHER-HARI-RAYA");
  |         ------------ move occurs because `kode_voucher` has type `String`, which does not implement the `Copy` trait
3 |
4 |     let klaim_voucher = move || {
  |                         ------- value moved into closure here
5 |         println!("Voucher {} ditukarkan", kode_voucher);
  |                                           ------------ variable moved due to use in closure
...
9 |     println!("Kode voucher: {}", kode_voucher);
  |                                  ^^^^^^^^^^^^ value borrowed here after move
  |
help: consider cloning the value before moving it into the closure
  |
4 ~     let value = kode_voucher.clone();
5 ~     let klaim_voucher = move || {
6 ~         println!("Voucher {} ditukarkan", value);
  |

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0382`.

Pesan E0382 menceritakan kronologinya: kode_voucher dipindahkan ke closure di baris 4 (value moved into closure here), lalu dipakai lagi di baris 9 (value borrowed here after move). Ini galat move semantics klasik dari Section Ownership dan Memory — hanya kali ini penerima kepemilikannya adalah closure. Saran perbaikannya juga layak dicatat: bila kedua pihak butuh nilai yang sama, clone() sebelum move membuat salinan mandiri untuk closure; bila closure tidak perlu hidup lebih lama dari variabel, hapus move dan biarkan closure meminjam biasa.

Kapan Memakai move #

Rangkuman situasi berikut membantu memutuskan kapan move dipakai:

  • Closure dikirim ke thread::spawn atau mekanisme lain yang menuntut 'static — move biasanya wajib, seperti contoh E0373 di awal Chapter.
  • Closure dikembalikan dari fungsi sementara tangkapannya variabel lokal fungsi — move wajib, dibahas di Chapter Mengembalikan Closure.
  • Closure disimpan lama di struct atau koleksi dengan umur variabel asli yang tidak pasti — move memberi kemandirian.
  • Closure dipakai singkat di scope yang sama — biarkan penangkapan otomatis bekerja; move hanya menambah perpindahan yang tidak perlu.

Ringkasan #

Kata kunci move sebelum || memaksa closure mengambil kepemilikan semua variabel yang ditangkap, memutus ketergantungan pada umur scope asal. Thread yang menuntut lifetime 'static menolak pinjaman dengan E0373 dan langsung menyarankan move; pada tipe Copy seperti i32, move cukup menyalin nilai sehingga variabel asli tetap hidup; pada tipe non-Copy seperti String, perpindahan nyata membuat pemakaian variabel setelahnya memicu E0382. Aturan praktisnya: move hanya saat closure harus hidup lebih lama dari tangkapannya; penangkapan otomatis menangani sisanya. Selanjutnya kita bawa closure keluar dari main — menjadikannya parameter fungsi.