Closures: Fungsi Anonim yang Bisa Menangkap Lingkungannya
Closures di Rust adalah fungsi anonim (tanpa nama) yang bisa kita simpan di dalam sebuah variabel atau diteruskan sebagai argumen ke fungsi lain. Kita bisa membuat sebuah closure di satu tempat lalu memanggil closure tersebut di tempat lain untuk dievaluasi dalam konteks yang berbeda. Tidak seperti fungsi biasa, closures bisa menangkap (capture) nilai-nilai dari scope tempat mereka didefinisikan. Kita bakal mendemonstrasikan gimana fitur-fitur closure ini memungkinkan penggunaan ulang kode dan kustomisasi perilaku.
Menangkap Lingkungan Menggunakan Closures
Pertama-tama kita bakal meneliti gimana kita bisa memakai closures buat menangkap nilai dari lingkungan tempat mereka didefinisikan untuk dipakai nanti. Berikut skenarionya: sesekali, perusahaan kaos kita membagikan kaos edisi terbatas eksklusif kepada seseorang di mailing list kita sebagai bentuk promosi. Orang-orang di mailing list bisa secara opsional menambahkan warna favorit mereka ke profilnya. Kalau orang yang terpilih untuk dapat kaos gratis itu sudah menge-set warna favoritnya, dia bakal dapat kaos dengan warna itu. Tapi kalau orang tersebut belum menentukan warna favorit, dia bakal dapat warna apa pun yang saat itu stoknya paling banyak di perusahaan.
Ada banyak cara buat mengimplementasikan ini. Buat contoh ini, kita bakal
memakai sebuah enum bernama ShirtColor yang punya varian Red (merah) dan
Blue (biru) (kita membatasi jumlah warna yang ada biar simpel). Kita mewakili
stok barang milik perusahaan dengan sebuah struct Inventory yang punya field
bernama shirts yang berisi sebuah Vec<ShirtColor> yang merepresentasikan
warna-warna kaos yang saat ini ada di stok. Method giveaway yang didefinisikan
pada Inventory menerima preferensi warna kaos opsional dari pemenang kaos
gratis, lalu mengembalikan warna kaos yang bakal didapat oleh orang tersebut.
Persiapan ini ditunjukkan di Listing 13-1.
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
Red,
Blue,
}
struct Inventory {
shirts: Vec<ShirtColor>,
}
impl Inventory {
fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
user_preference.unwrap_or_else(|| self.most_stocked())
}
fn most_stocked(&self) -> ShirtColor {
let mut num_red = 0;
let mut num_blue = 0;
for color in &self.shirts {
match color {
ShirtColor::Red => num_red += 1,
ShirtColor::Blue => num_blue += 1,
}
}
if num_red > num_blue {
ShirtColor::Red
} else {
ShirtColor::Blue
}
}
}
fn main() {
let store = Inventory {
shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
};
let user_pref1 = Some(ShirtColor::Red);
let giveaway1 = store.giveaway(user_pref1);
println!(
"The user with preference {:?} gets {:?}",
user_pref1, giveaway1
);
let user_pref2 = None;
let giveaway2 = store.giveaway(user_pref2);
println!(
"The user with preference {:?} gets {:?}",
user_pref2, giveaway2
);
}
Toko (store) yang didefinisikan di fungsi main punya dua kaos biru dan satu
kaos merah yang tersisa untuk dibagikan dalam promosi edisi terbatas ini. Kita
memanggil method giveaway untuk seorang user dengan preferensi kaos merah
dan seorang user tanpa preferensi sama sekali.
Sekali lagi, kode ini bisa saja diimplementasikan dengan banyak cara, dan di
sini, untuk fokus ke closures, kita cuma memakai konsep-konsep yang sudah
kita pelajari, kecuali buat body dari method giveaway yang memakai sebuah
closure. Di method giveaway, kita menerima preferensi user sebagai sebuah
parameter bertipe Option<ShirtColor> lalu memanggil method unwrap_or_else
pada user_preference. Method unwrap_or_else pada Option<T>
didefinisikan oleh standard library. Method ini menerima satu argumen: sebuah
closure tanpa argumen apa pun yang mengembalikan sebuah nilai bertipe T
(tipe yang sama dengan yang disimpan di varian Some dari Option<T>, yang
mana di kasus ini adalah ShirtColor). Kalau Option<T> itu adalah varian
Some, unwrap_or_else bakal mengembalikan nilai dari dalam Some tersebut.
Tapi kalau Option<T> adalah varian None, unwrap_or_else bakal memanggil
closure-nya dan mengembalikan nilai yang dikembalikan oleh closure tersebut.
Kita menentukan ekspresi closure || self.most_stocked() sebagai argumen
untuk unwrap_or_else. Ini adalah sebuah closure yang tidak menerima parameter
apa pun (kalau closure-nya punya parameter, parameternya bakal muncul di antara
dua garis vertikal). Body dari closure ini memanggil self.most_stocked().
Kita mendefinisikan closure-nya di sini, dan implementasi dari unwrap_or_else
nanti bakal mengevaluasi closure ini kalau hasilnya memang dibutuhkan.
Menjalankan kode ini bakal mencetak output berikut:
$ cargo run
Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue
Satu aspek yang menarik di sini adalah kita sudah meneruskan sebuah closure
yang memanggil self.most_stocked() pada instance Inventory saat ini.
Standard library tidak perlu tahu apa-apa tentang tipe Inventory atau
ShirtColor yang kita definisikan, ataupun logika yang mau kita pakai di
skenario ini. Closure ini menangkap sebuah referensi immutable ke instance
Inventory self dan meneruskannya bersama kode yang kita tentukan ke method
unwrap_or_else. Di sisi lain, fungsi biasa tidak bisa menangkap lingkungan
mereka dengan cara seperti ini.
Inference dan Anotasi Tipe untuk Closure
Ada lebih banyak perbedaan antara fungsi biasa dan closures. Closures
biasanya tidak mengharuskan kita untuk menganotasi tipe dari parameter atau nilai
kembalian seperti yang diwajibkan oleh fungsi fn. Anotasi tipe diwajibkan
pada fungsi karena tipe-tipe tersebut adalah bagian dari antarmuka eksplisit yang
diekspos ke para pengguna fungsi tersebut. Mendefinisikan antarmuka ini secara
kaku penting untuk memastikan bahwa semua orang sepakat mengenai tipe-tipe nilai
yang dipakai dan dikembalikan oleh sebuah fungsi. Sebaliknya, closures tidak
dipakai dalam antarmuka yang terekspos seperti itu: mereka disimpan di dalam
variabel dan dipakai tanpa menamai mereka atau mengeksposnya ke pengguna library
kita.
Closures biasanya berukuran pendek dan hanya relevan di dalam konteks yang sempit, bukan di sembarang skenario acak. Di dalam batasan konteks ini, compiler bisa menebak (infer) tipe-tipe parameternya dan tipe kembaliannya, mirip dengan bagaimana ia bisa menebak tipe dari sebagian besar variabel (walaupun ada kasus-kasus langka di mana compiler juga membutuhkan anotasi tipe untuk closure).
Sama halnya dengan variabel, kita bisa menambahkan anotasi tipe kalau kita mau meningkatkan kejelasan secara eksplisit walau akibatnya kode kita bakal sedikit lebih bertele-tele (verbose) dari yang sebenarnya diperlukan. Menganotasi tipe buat sebuah closure bakal kelihatan seperti definisi yang ditunjukkan di Listing 13-2. Di contoh ini, kita mendefinisikan sebuah closure dan menyimpannya di dalam sebuah variabel, bukannya mendefinisikan closure tersebut tepat di tempat kita meneruskannya sebagai argumen seperti yang kita lakukan di Listing 13-1.
use std::thread;
use std::time::Duration;
fn generate_workout(intensity: u32, random_number: u32) {
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
if intensity < 25 {
println!("Today, do {} pushups!", expensive_closure(intensity));
println!("Next, do {} situps!", expensive_closure(intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_closure(intensity)
);
}
}
}
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(simulated_user_specified_value, simulated_random_number);
}
Dengan menambahkan anotasi tipe, sintaks closures jadi kelihatan lebih mirip
dengan sintaks fungsi biasa. Di sini, kita mendefinisikan sebuah fungsi yang
menambahkan 1 ke parameternya dan sebuah closure yang punya perilaku yang sama,
sebagai perbandingan. Kita sudah menambahkan sedikit spasi supaya bagian-bagian
yang relevan sejajar. Ini mengilustrasikan gimana sintaks closure itu mirip
dengan sintaks fungsi, kecuali di penggunaan garis vertikal (|) dan seberapa
banyak sintaks yang sifatnya opsional:
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
Baris pertama menunjukkan sebuah definisi fungsi dan baris kedua menunjukkan
definisi closure yang dianotasi secara penuh. Di baris ketiga, kita membuang
anotasi tipe dari definisi closure. Di baris keempat, kita membuang kurung
kurawal, yang mana jadi opsional karena body dari closure ini hanya punya
satu ekspresi. Ini semua adalah definisi yang valid dan bakal menghasilkan
perilaku yang sama saat mereka dipanggil. Baris add_one_v3 dan add_one_v4
mewajibkan closures tersebut untuk dievaluasi agar kodenya bisa di-compile,
karena tipe-tipenya bakal ditebak berdasarkan gimana closures tersebut dipakai.
Ini mirip dengan bagaimana let v = Vec::new(); membutuhkan entah anotasi tipe
atau adanya nilai dengan tipe tertentu yang dimasukkan ke dalam Vec agar Rust
bisa menebak tipenya.
Buat definisi closure, compiler bakal menebak satu tipe konkret untuk masing-
masing parameternya dan juga untuk nilai kembaliannya. Misalnya, Listing 13-3
menunjukkan definisi closure singkat yang cuma mengembalikan nilai yang dia
terima sebagai parameter. Closure ini sebenarnya tidak terlalu berguna kecuali
buat tujuan contoh ini. Perhatikan bahwa kita tidak menambahkan anotasi tipe apa
pun di definisinya. Karena tidak ada anotasi tipe, kita bisa memanggil closure
ini dengan tipe apa pun, yang mana kita lakukan di sini dengan tipe String
untuk panggilan pertama. Kalau kita kemudian mencoba memanggil example_closure
dengan sebuah integer, kita bakal dapat error.
fn main() {
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
}
Compiler ngasih kita error ini:
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
--> src/main.rs:5:29
|
5 | let n = example_closure(5);
| --------------- ^ expected `String`, found integer
| |
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `String`
--> src/main.rs:4:29
|
4 | let s = example_closure(String::from("hello"));
| --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
| |
| in this closure call
note: closure parameter defined here
--> src/main.rs:2:28
|
2 | let example_closure = |x| x;
| ^
help: try using a conversion method
|
5 | let n = example_closure(5.to_string());
| ++++++++++++
For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error
Saat pertama kali kita memanggil example_closure memakai nilai String,
compiler menebak kalau tipe dari x dan tipe kembalian dari closure itu
adalah String. Tipe-tipe itu kemudian “terkunci” ke dalam closure di
example_closure, dan kita bakal dapat type error (error tipe) saat kita
mencoba memakai tipe yang berbeda dengan closure yang sama.
Menangkap Referensi atau Memindahkan Kepemilikan (Ownership)
Closures bisa menangkap nilai dari lingkungannya memakai tiga cara, yang mana berkorelasi langsung dengan tiga cara sebuah fungsi bisa menerima parameter: meminjam secara immutable, meminjam secara mutable, dan mengambil kepemilikan (taking ownership). Closure bakal memutuskan cara mana yang mau dipakai berdasarkan apa yang dilakukan oleh isi fungsinya terhadap nilai-nilai yang ditangkapnya.
Di Listing 13-4, kita mendefinisikan sebuah closure yang menangkap referensi
immutable ke vector bernama list karena closure itu cuma butuh referensi
immutable buat mencetak nilainya.
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
let only_borrows = || println!("From closure: {list:?}");
println!("Before calling closure: {list:?}");
only_borrows();
println!("After calling closure: {list:?}");
}
Contoh ini juga mengilustrasikan kalau sebuah variabel bisa diikat ke definisi sebuah closure, dan nanti kita bisa memanggil closure tersebut memakai nama variabel dan tanda kurung, seolah-olah nama variabel itu adalah nama sebuah fungsi.
Karena kita bisa punya banyak referensi immutable ke list di waktu yang
bersamaan, list masih bisa diakses dari kode sebelum definisi closure, di
antara definisi closure namun sebelum closure-nya dipanggil, dan setelah
closure-nya dipanggil. Kode ini berhasil di-compile, jalan, dan mencetak:
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]
Selanjutnya, di Listing 13-5, kita ngubah body closure-nya biar dia nambahin
sebuah elemen ke vector list. Closure ini sekarang menangkap referensi
mutable.
fn main() {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
let mut borrows_mutably = || list.push(7);
borrows_mutably();
println!("After calling closure: {list:?}");
}
Kode ini berhasil di-compile, jalan, dan mencetak:
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]
Perhatikan bahwa sekarang tidak ada lagi println! di antara definisi dan
pemanggilan closure borrows_mutably: ketika borrows_mutably didefinisikan,
ia menangkap referensi mutable ke list. Kita tidak lagi menggunakan closure
ini setelah ia dipanggil, jadi peminjaman mutable itu pun berakhir. Di antara
definisi closure dan pemanggilannya, kita tidak diizinkan buat melakukan
peminjaman immutable untuk mencetaknya karena peminjaman lain tidak
diperbolehkan selama masih ada peminjaman mutable. Coba tambahkan println!
di situ buat melihat pesan error apa yang bakal kita dapatkan!
Kalau kita mau memaksa closure buat mengambil ownership dari nilai yang dia
pakai dari lingkungannya, biarpun isi closure-nya sebenarnya tidak
mewajibkan ownership, kita bisa menambahkan keyword move sebelum daftar
parameternya.
Teknik ini biasanya sangat berguna pas kita mau meneruskan sebuah closure ke
thread baru untuk memindahkan datanya supaya ia dimiliki oleh thread baru
tersebut. Kita bakal bahas threads dan kenapa kita mau menggunakannya secara
mendetail di Bab 16 saat kita ngomongin soal konkurensi (concurrency), tapi buat
sekarang, mari kita eksplor secara singkat cara bikin thread baru yang memakai
sebuah closure yang membutuhkan keyword move. Listing 13-6 menampilkan kode
dari Listing 13-4 yang diubah buat mencetak isi vector di sebuah thread baru,
bukannya di thread utama (main thread).
use std::thread;
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
thread::spawn(move || println!("From thread: {list:?}"))
.join()
.unwrap();
}
move buat memaksa closure di thread baru untuk mengambil kepemilikan dari listKita membuat thread baru, sambil memberikan sebuah closure buat dijalankan
sebagai argumen untuk thread tersebut. Isi closure-nya mencetak daftar list.
Di Listing 13-4, closure tersebut cuma menangkap list dengan memakai
referensi immutable karena itulah hak akses minimal yang dibutuhkan ke list
buat mencetaknya. Di contoh ini, meskipun isi closure-nya masih cuma butuh
referensi immutable, kita harus menentukan secara spesifik bahwa list
seharusnya dipindahkan (moved) ke dalam closure dengan menaruh keyword move
di awal definisi closure-nya. Kalau thread utamanya melakukan lebih banyak
operasi sebelum memanggil join pada thread barunya, thread baru tersebut
bisa saja selesai sebelum thread utamanya selesai, atau sebaliknya thread
utama bisa saja selesai lebih dulu. Kalau thread utama masih mempertahankan
ownership dari list tapi dia berakhir lebih dulu sebelum thread barunya
dan men-drop (membuang) list tersebut, referensi immutable di thread baru
bakal jadi tidak valid. Maka dari itu, compiler mewajibkan list untuk
dipindahkan ke dalam closure yang diberikan ke thread baru supaya
referensinya bakal dipastikan valid. Coba hapus keyword move-nya atau
coba pakai variabel list di thread utama setelah closure itu didefinisikan
buat melihat error compiler apa yang bakal muncul!
Memindahkan Nilai yang Ditangkap ke Luar Closures dan Traits Fn
Begitu sebuah closure sudah menangkap referensi atau mengambil kepemilikan dari sebuah nilai dari lingkungan tempat closure itu didefinisikan (yang artinya, hal itu memengaruhi apa yang dipindahkan ke dalam closure-nya), kode di dalam isi closure-nya bakal menentukan apa yang terjadi sama referensi atau nilai tersebut pas closure-nya dievaluasi nantinya (yang artinya, hal ini memengaruhi apa yang dipindahkan ke luar dari closure-nya).
Isi dari sebuah closure bisa ngelakuin mana aja dari hal-hal berikut: memindahkan nilai yang ditangkap ke luar dari closure, memutasi (mengubah) nilai yang ditangkap, tidak memindahkan atau memutasi nilainya, atau dari awal emang tidak menangkap apa pun dari lingkungannya.
Cara sebuah closure menangkap dan menangani nilai dari lingkungannya
memengaruhi trait mana yang diimplementasikan oleh closure tersebut, dan
traits adalah cara bagaimana fungsi dan struct bisa menentukan jenis closures
apa yang bisa mereka terima. Closures bakal secara otomatis mengimplementasikan
satu, dua, atau ketiga traits Fn ini secara aditif, tergantung dari gimana isi
closure-nya menangani nilai-nilai tersebut:
FnOnceberlaku untuk closures yang bisa dipanggil sekali saja. Semua closures minimal mengimplementasikan trait ini karena semua closures itu bisa dipanggil. Closure yang memindahkan nilai yang ditangkap keluar dari isi kodenya cuma bakal mengimplementasikanFnOncedan bukan traitFnlainnya karena ia cuma bisa dipanggil satu kali.FnMutberlaku untuk closures yang tidak memindahkan nilai yang ditangkap keluar dari isinya, tapi yang mungkin memutasi nilai-nilai tersebut. Closures jenis ini bisa dipanggil lebih dari sekali.Fnberlaku untuk closures yang tidak memindahkan nilai yang ditangkap keluar dari isinya dan tidak memutasi nilai-nilai tersebut, serta berlaku juga buat closures yang memang tidak menangkap apa pun dari lingkungannya. Closures seperti ini bisa dipanggil lebih dari sekali tanpa memutasi lingkungannya, yang mana ini penting buat kasus-kasus seperti saat kita memanggil sebuah closure berkali-kali secara konruen (bersamaan).
Mari kita lihat definisi dari method unwrap_or_else pada Option<T> yang
kita pakai di Listing 13-1:
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
Ingat kembali bahwa T adalah tipe generik yang merepresentasikan tipe dari
nilai di dalam varian Some milik sebuah Option. Tipe T itu juga adalah
tipe kembalian (return type) dari fungsi unwrap_or_else: kode yang memanggil
unwrap_or_else pada sebuah Option<String>, misalnya, bakal mendapatkan sebuah
String.
Selanjutnya, perhatikan bahwa fungsi unwrap_or_else juga punya parameter tipe
generik tambahan F. Tipe F adalah tipe dari parameter bernama f, yang
mana itu adalah closure yang kita kasih saat memanggil unwrap_or_else.
Trait bound yang ditentukan pada tipe generik F adalah FnOnce() -> T, yang
berarti F harus bisa dipanggil sekali, tidak menerima argumen apa pun, dan
mengembalikan sebuah T. Memakai FnOnce di trait bound ini mengekspresikan
batasan bahwa unwrap_or_else cuma bakal memanggil f maksimal satu kali saja.
Di dalam isi unwrap_or_else, kita bisa melihat kalau Option-nya itu Some,
f tidak bakal dipanggil. Kalau Option-nya itu None, f bakal dipanggil
sekali. Karena semua closures mengimplementasikan FnOnce, unwrap_or_else
bisa menerima ketiga jenis closures dan dia fleksibel sekali.
Catatan: Kalau hal yang mau kita lakukan tidak mewajibkan kita menangkap nilai dari lingkungannya, kita bisa aja memakai nama sebuah fungsi sebagai ganti dari closure di tempat kita butuh sesuatu yang mengimplementasikan salah satu dari trait
Fn. Contohnya, pada nilaiOption<Vec<T>>, kita bisa manggilunwrap_or_else(Vec::new)buat mendapatkan vector baru yang kosong kalau nilainya adalahNone. Compiler bakal secara otomatis mengimplementasikan traitFnyang paling pas buat definisi fungsi tersebut.
Sekarang mari kita lihat method standard library sort_by_key, yang
didefinisikan pada slices, buat melihat bedanya dari unwrap_or_else dan
kenapa sort_by_key memakai FnMut dan bukannya FnOnce buat trait bound-nya.
Closure ini menerima satu argumen berupa referensi ke item saat ini yang ada
di slice yang lagi diperiksa, lalu mengembalikan nilai bertipe K yang bisa
diurutkan (ordered). Fungsi ini berguna pas kita mau mengurutkan sebuah slice
berdasarkan atribut spesifik dari setiap itemnya. Di Listing 13-7, kita punya
daftar berisi instance Rectangle dan kita memakai sort_by_key buat mengurutkan
mereka berdasarkan atribut width (lebar) mereka dari yang terkecil sampai
terbesar.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
list.sort_by_key(|r| r.width);
println!("{list:#?}");
}
sort_by_key buat mengurutkan persegi panjang berdasarkan lebarnyaKode ini mencetak:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/rectangles`
[
Rectangle {
width: 3,
height: 5,
},
Rectangle {
width: 7,
height: 12,
},
Rectangle {
width: 10,
height: 1,
},
]
Alasan kenapa sort_by_key didefinisikan buat menerima closure FnMut adalah
karena method itu memanggil closure-nya berkali-kali: satu kali untuk setiap
item di slice-nya. Closure |r| r.width tidak menangkap, memutasi, atau
memindahkan apa pun keluar dari lingkungannya, jadi ia memenuhi syarat trait
bound-nya.
Sebaliknya, Listing 13-8 menunjukkan contoh closure yang cuma mengimplementasikan
trait FnOnce, karena dia memindahkan sebuah nilai ke luar dari lingkungannya.
Compiler tidak bakal ngebiarin kita pakai closure ini dengan sort_by_key.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut sort_operations = vec![];
let value = String::from("closure called");
list.sort_by_key(|r| {
sort_operations.push(value);
r.width
});
println!("{list:#?}");
}
FnOnce bersama sort_by_keyIni adalah cara aneh yang dibikin-bikin (dan tidak bisa jalan) buat mencoba
menghitung berapa kali sort_by_key memanggil closure-nya saat ia mengurutkan
list. Kode ini mencoba melakukan penghitungan ini dengan memasukkan (pushing)
value—sebuah String dari lingkungan closure-nya—ke dalam vector
sort_operations. Closure ini menangkap value lalu memindahkan value
tersebut keluar dari closure dengan mentransfer kepemilikan (ownership)
value ke vector sort_operations. Closure ini bisa dipanggil satu kali;
mencoba memanggilnya untuk kedua kalinya tidak bakal berhasil karena value
sudah tidak ada lagi di lingkungan closure-nya buat dimasukkan ke dalam
sort_operations! Maka dari itu, closure ini cuma mengimplementasikan
FnOnce. Pas kita mencoba men-compile kode ini, kita bakal dapat error yang
bilang kalau value tidak bisa dipindahkan keluar dari closure karena
closure-nya harus mengimplementasikan FnMut:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
--> src/main.rs:18:30
|
15 | let value = String::from("closure called");
| ----- ------------------------------ move occurs because `value` has type `String`, which does not implement the `Copy` trait
| |
| captured outer variable
16 |
17 | list.sort_by_key(|r| {
| --- captured by this `FnMut` closure
18 | sort_operations.push(value);
| ^^^^^ `value` is moved here
|
help: consider cloning the value if the performance cost is acceptable
|
18 | sort_operations.push(value.clone());
| ++++++++
For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error
Error ini menunjuk ke baris di dalam isi closure yang memindahkan value
keluar dari lingkungannya. Buat memperbaikinya, kita harus mengubah isi closure
agar ia tidak memindahkan nilai-nilai keluar dari lingkungannya. Menyimpan sebuah
variabel counter di lingkungan dan menambah nilainya dari dalam closure adalah
cara yang jauh lebih masuk akal buat menghitung berapa kali closure tersebut
dipanggil. Closure di Listing 13-9 berhasil jalan dengan sort_by_key karena
ia cuma menangkap referensi mutable ke counter num_sort_operations dan
makanya bisa dipanggil lebih dari sekali:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut num_sort_operations = 0;
list.sort_by_key(|r| {
num_sort_operations += 1;
r.width
});
println!("{list:#?}, sorted in {num_sort_operations} operations");
}
FnMut bersama sort_by_key diperbolehkanTraits Fn penting pas kita mendefinisikan atau memakai fungsi atau tipe yang
memakai closures. Di bagian selanjutnya, kita bakal membahas iterators. Banyak
method iterator yang menerima argumen closure, jadi tetap ingat detail-detail
tentang closures ini ya saat kita lanjut belajar!