Jawaban singkat: Untuk fleksibilitas maksimum, Anda dapat menyimpan callback sebagai FnMut
objek dalam kotak , dengan callback setter generik pada jenis callback. Kode untuk ini ditunjukkan pada contoh terakhir dalam jawaban. Untuk penjelasan lebih detail, baca terus.
"Function pointers": panggilan balik sebagai fn
Persamaan terdekat dari kode C ++ dalam pertanyaan akan mendeklarasikan callback sebagai sebuah fn
tipe. fn
merangkum fungsi yang ditentukan oleh fn
kata kunci, seperti pointer fungsi C ++:
type Callback = fn();
struct Processor {
callback: Callback,
}
impl Processor {
fn set_callback(&mut self, c: Callback) {
self.callback = c;
}
fn process_events(&self) {
(self.callback)();
}
}
fn simple_callback() {
println!("hello world!");
}
fn main() {
let p = Processor {
callback: simple_callback,
};
p.process_events();
}
Kode ini dapat diperpanjang untuk menyertakan Option<Box<Any>>
untuk menyimpan "data pengguna" yang terkait dengan fungsi tersebut. Meski begitu, itu bukanlah Rust idiomatik. Cara Rust untuk mengaitkan data dengan suatu fungsi adalah dengan menangkapnya dalam penutupan anonim , seperti di C ++ modern. Karena penutupan tidak fn
, set_callback
perlu menerima jenis objek fungsi lainnya.
Callback sebagai objek fungsi umum
Dalam penutupan Rust dan C ++ dengan tanda tangan panggilan yang sama datang dalam ukuran berbeda untuk mengakomodasi nilai berbeda yang mungkin mereka tangkap. Selain itu, setiap definisi closure menghasilkan tipe anonim unik untuk nilai closure. Karena batasan ini, struct tidak dapat menamai jenis callback
bidangnya, juga tidak dapat menggunakan alias.
Salah satu cara untuk menanamkan closure di field struct tanpa mengacu pada tipe konkret adalah dengan membuat struct generic . Struct akan secara otomatis menyesuaikan ukurannya dan jenis callback untuk fungsi konkret atau closure yang Anda teruskan:
struct Processor<CB>
where
CB: FnMut(),
{
callback: CB,
}
impl<CB> Processor<CB>
where
CB: FnMut(),
{
fn set_callback(&mut self, c: CB) {
self.callback = c;
}
fn process_events(&mut self) {
(self.callback)();
}
}
fn main() {
let s = "world!".to_string();
let callback = || println!("hello {}", s);
let mut p = Processor { callback: callback };
p.process_events();
}
Seperti sebelumnya, definisi baru dari callback akan dapat menerima fungsi tingkat atas yang didefinisikan dengan fn
, tetapi yang ini juga akan menerima closure sebagai || println!("hello world!")
, serta closure yang menangkap nilai, seperti || println!("{}", somevar)
. Karena itu, prosesor tidak perlu userdata
menyertai panggilan balik; penutupan yang disediakan oleh pemanggil dariset_callback
akan secara otomatis menangkap data yang dibutuhkan dari lingkungannya dan membuatnya tersedia saat dipanggil.
Tapi apa masalahnya FnMut
, mengapa tidak Fn
? Karena closure menyimpan nilai yang ditangkap, aturan mutasi biasa Rust harus diterapkan saat memanggil closure. Bergantung pada apa yang dilakukan closure dengan nilai yang mereka pegang, mereka dikelompokkan dalam tiga keluarga, masing-masing ditandai dengan ciri:
Fn
adalah closure yang hanya membaca data, dan dapat dipanggil dengan aman beberapa kali, mungkin dari beberapa thread. Kedua penutupan di atas adalahFn
.
FnMut
adalah closure yang mengubah data, misalnya dengan menulis ke mut
variabel yang ditangkap . Mereka juga dapat dipanggil beberapa kali, tetapi tidak secara paralel. (Memanggil aFnMut
penutupan dari beberapa utas akan menyebabkan perlombaan data, jadi itu hanya dapat dilakukan dengan perlindungan mutex.) Objek closure harus dinyatakan bisa berubah oleh pemanggil.
FnOnce
adalah closure yang mengkonsumsi beberapa data yang mereka ambil, misalnya dengan memindahkan nilai yang diambil ke fungsi yang mengambil kepemilikannya. Sesuai dengan namanya, ini hanya dapat dipanggil sekali, dan pemanggil harus memilikinya.
Agak berlawanan dengan intuisi, ketika menentukan sifat terikat untuk tipe objek yang menerima penutupan, FnOnce
sebenarnya adalah yang paling permisif. Menyatakan bahwa jenis panggilan balik generik harus memenuhi FnOnce
sifat tersebut berarti ia akan menerima penutupan apa pun secara harfiah. Tapi itu ada harganya: artinya pemegangnya hanya diizinkan untuk meneleponnya sekali. Karena process_events()
dapat memilih untuk memanggil callback beberapa kali, dan karena metodenya sendiri dapat dipanggil lebih dari sekali, batas paling permisif berikutnya adalah FnMut
. Perhatikan bahwa kami harus menandai process_events
sebagai bermutasi self
.
Callback non-generik: objek sifat fungsi
Meskipun implementasi umum callback sangat efisien, ia memiliki batasan antarmuka yang serius. Setiap Processor
instance harus diparameterisasi dengan jenis callback konkret, yang berarti bahwa satu instance Processor
hanya dapat menangani satu jenis callback. Mengingat bahwa setiap closure memiliki tipe yang berbeda, generik Processor
tidak dapat menangani proc.set_callback(|| println!("hello"))
diikuti oleh proc.set_callback(|| println!("world"))
. Memperluas struct untuk mendukung dua bidang callback akan membutuhkan seluruh struct untuk diparameterisasi menjadi dua jenis, yang akan dengan cepat menjadi sulit karena jumlah callback bertambah. Menambahkan lebih banyak parameter tipe tidak akan berfungsi jika jumlah callback harus dinamis, misalnya untuk mengimplementasikan add_callback
fungsi yang mempertahankan vektor callback yang berbeda.
Untuk menghapus parameter type, kita dapat memanfaatkan objek sifat , yaitu fitur Rust yang memungkinkan pembuatan antarmuka dinamis secara otomatis berdasarkan sifat. Ini kadang-kadang disebut sebagai penghapusan tipe dan merupakan teknik yang populer di C ++ [1] [2] , jangan disamakan dengan penggunaan istilah bahasa Java dan FP yang agak berbeda. Pembaca yang akrab dengan C ++ akan mengenali perbedaan antara closure yang mengimplementasikan Fn
dan Fn
objek ciri yang setara dengan perbedaan antara objek fungsi umum danstd::function
nilai dalam C ++.
Objek ciri dibuat dengan meminjam objek dengan &
operator dan menggunakan atau memaksanya untuk mengacu pada sifat tertentu. Dalam kasus ini, karena Processor
perlu memiliki objek callback, kita tidak dapat menggunakan peminjaman, tetapi harus menyimpan callback dalam heap-dialokasikan Box<dyn Trait>
(setara dengan Rust std::unique_ptr
), yang secara fungsional setara dengan objek ciri.
Jika Processor
disimpan Box<dyn FnMut()>
, itu tidak lagi perlu generik, tetapi set_callback
metode sekarang menerima generik c
melalui impl Trait
argumen . Dengan demikian, ia dapat menerima segala jenis callable, termasuk closure with state, dan mengemasnya dengan benar sebelum menyimpannya di Processor
. Argumen umum untuk set_callback
tidak membatasi jenis callback yang diterima prosesor, karena jenis callback yang diterima dipisahkan dari jenis yang disimpan di Processor
struct.
struct Processor {
callback: Box<dyn FnMut()>,
}
impl Processor {
fn set_callback(&mut self, c: impl FnMut() + 'static) {
self.callback = Box::new(c);
}
fn process_events(&mut self) {
(self.callback)();
}
}
fn simple_callback() {
println!("hello");
}
fn main() {
let mut p = Processor {
callback: Box::new(simple_callback),
};
p.process_events();
let s = "world!".to_string();
let callback2 = move || println!("hello {}", s);
p.set_callback(callback2);
p.process_events();
}
Referensi seumur hidup di dalam penutupan kotak
Batas waktu 'static
seumur hidup pada jenis c
argumen yang diterima set_callback
adalah cara sederhana untuk meyakinkan kompiler yang terdapat referensi di dalamnya c
, yang mungkin berupa closure yang merujuk ke lingkungannya, hanya merujuk ke nilai global dan oleh karena itu akan tetap valid selama penggunaan panggilan balik. Tetapi ikatan statis juga sangat berat: sementara menerima penutupan yang memiliki objek dengan baik (yang telah kami pastikan di atas dengan membuat penutupanmove
), ia menolak closure yang merujuk ke lingkungan lokal, bahkan ketika mereka hanya merujuk ke nilai yang hidup lebih lama dari prosesor dan bahkan akan aman.
Karena kita hanya perlu callback hidup selama prosesor hidup, kita harus mencoba mengikat masa pakainya dengan prosesor, yang merupakan batasan yang kurang ketat dari 'static
. Tetapi jika kita hanya menghapus 'static
batas waktu dari set_callback
, itu tidak lagi dikompilasi. Ini karena set_callback
membuat kotak baru dan menetapkannya ke callback
bidang yang ditentukan sebagai Box<dyn FnMut()>
. Karena definisi tidak menentukan masa pakai untuk objek sifat kotak, 'static
tersirat, dan tugas akan secara efektif memperluas masa pakai (dari masa pakai arbitrer yang tidak disebutkan namanya ke 'static
), yang tidak diizinkan. Perbaikannya adalah memberikan masa pakai eksplisit untuk prosesor dan mengikat masa pakai itu ke referensi di kotak dan referensi di callback yang diterima oleh set_callback
:
struct Processor<'a> {
callback: Box<dyn FnMut() + 'a>,
}
impl<'a> Processor<'a> {
fn set_callback(&mut self, c: impl FnMut() + 'a) {
self.callback = Box::new(c);
}
}
Dengan masa aktif ini dibuat eksplisit, maka tidak perlu lagi digunakan 'static
. Closure sekarang dapat merujuk ke s
objek lokal , yaitu tidak lagi harus move
, asalkan definisi s
ditempatkan sebelum definisi p
untuk memastikan bahwa string hidup lebih lama dari prosesor.
CB
harus ada'static
di contoh terakhir?