Krótka odpowiedź: aby uzyskać maksymalną elastyczność, możesz przechowywać wywołanie zwrotne jako FnMut
obiekt w ramce , z generycznym ustawiaczem wywołań zwrotnych dla typu wywołania zwrotnego. Kod do tego jest pokazany w ostatnim przykładzie odpowiedzi. Aby uzyskać bardziej szczegółowe wyjaśnienia, czytaj dalej.
„Wskaźniki funkcji”: wywołania zwrotne jako fn
Najbliższym odpowiednikiem kodu C ++ w pytaniu byłoby zadeklarowanie wywołania zwrotnego jako fn
typu. fn
hermetyzuje funkcje zdefiniowane przez fn
słowo kluczowe, podobnie jak wskaźniki funkcji w 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();
}
Ten kod można rozszerzyć, aby zawierał Option<Box<Any>>
„dane użytkownika” związane z funkcją. Mimo to nie byłaby to idiomatyczna rdza. Sposób w Rust na skojarzenie danych z funkcją polega na przechwyceniu ich w anonimowym zamknięciu , tak jak we współczesnym C ++. Ponieważ domknięcia nie są fn
, set_callback
będą musiały zaakceptować inne rodzaje obiektów funkcyjnych.
Wywołania zwrotne jako ogólne obiekty funkcji
Zarówno w Rust, jak i C ++ domknięcia z tą samą sygnaturą wywołania mają różne rozmiary, aby pomieścić różne wartości, które mogą przechwycić. Ponadto każda definicja zamknięcia generuje unikalny typ anonimowy dla wartości zamknięcia. Z powodu tych ograniczeń struktura nie może nazwać typu swojego callback
pola ani użyć aliasu.
Jednym ze sposobów osadzenia zamknięcia w polu struct bez odwoływania się do konkretnego typu jest utworzenie struktury ogólnej . Struktura automatycznie dostosuje swój rozmiar i typ wywołania zwrotnego dla konkretnej funkcji lub zamknięcia, które do niej przekażesz:
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();
}
Tak jak poprzednio, nowa definicja wywołania zwrotnego będzie mogła akceptować funkcje najwyższego poziomu zdefiniowane za pomocą fn
, ale ta będzie również akceptować domknięcia || println!("hello world!")
, a także zamknięcia, które przechwytują wartości, takie jak || println!("{}", somevar)
. Z tego powodu procesor nie musi userdata
towarzyszyć wywołaniu zwrotnemu; zamknięcie zapewniane przez dzwoniącego set_callback
automatycznie przechwytuje potrzebne dane z jego środowiska i udostępnia je po wywołaniu.
Ale o co chodzi FnMut
, dlaczego nie tylko Fn
? Ponieważ domknięcia przechowują przechwycone wartości, podczas wywoływania zamknięcia muszą obowiązywać zwykłe reguły mutacji Rusta. W zależności od tego, co zamknięcia robią z wartościami, które wyznają, są pogrupowane w trzy rodziny, z których każda jest oznaczona cechą:
Fn
są zamknięciami, które tylko odczytują dane i mogą być bezpiecznie wywoływane wiele razy, prawdopodobnie z wielu wątków. Oba powyższe zamknięcia są Fn
.
FnMut
to domknięcia, które modyfikują dane, np. poprzez zapis do przechwyconej mut
zmiennej. Można je również wywoływać wielokrotnie, ale nie równolegle. (Wywołanie FnMut
zamknięcia z wielu wątków doprowadziłoby do wyścigu danych, więc można to zrobić tylko z ochroną muteksu). Obiekt zamknięcia musi być zadeklarowany jako zmienny przez obiekt wywołujący.
FnOnce
są domknięciami, które pochłaniają część danych, które przechwytują, np. przenosząc przechwyconą wartość do funkcji, która przejmuje jej własność. Jak sama nazwa wskazuje, można je wywołać tylko raz, a dzwoniący musi je posiadać.
Nieco sprzecznie z intuicją, określając cechę związaną z typem obiektu, który akceptuje zamknięcie, FnOnce
jest w rzeczywistości najbardziej tolerancyjna. Zadeklarowanie, że ogólny typ wywołania zwrotnego musi spełniać FnOnce
cechę, oznacza, że zaakceptuje dosłownie każde zamknięcie. Ale ma to swoją cenę: oznacza to, że posiadacz może zadzwonić tylko raz. Ponieważ process_events()
może zdecydować się na wielokrotne wywołanie wywołania zwrotnego, a sama metoda może być wywoływana więcej niż raz, następną najbardziej dopuszczalną granicą jest FnMut
. Zauważ, że musieliśmy oznaczyć process_events
jako mutujący self
.
Nieogólne wywołania zwrotne: obiekty cech funkcji
Mimo że generyczna implementacja wywołania zwrotnego jest niezwykle wydajna, ma poważne ograniczenia dotyczące interfejsu. Wymaga Processor
sparametryzowania każdej instancji za pomocą konkretnego typu wywołania zwrotnego, co oznacza, że pojedynczy Processor
może obsługiwać tylko jeden typ wywołania zwrotnego. Biorąc pod uwagę, że każde zamknięcie ma inny typ, rodzaj ogólny Processor
nie może obsłużyć, proc.set_callback(|| println!("hello"))
po którym następuje proc.set_callback(|| println!("world"))
. Rozszerzenie struktury w celu obsługi dwóch pól wywołań zwrotnych wymagałoby sparametryzowania całej struktury do dwóch typów, co szybko stałoby się nieporęczne w miarę wzrostu liczby wywołań zwrotnych. Dodanie większej liczby parametrów typu nie zadziałałoby, gdyby liczba wywołań zwrotnych musiała być dynamiczna, np. W celu zaimplementowania add_callback
funkcji, która utrzymuje wektor różnych wywołań zwrotnych.
Aby usunąć parametr typu, możemy skorzystać z obiektów cech , funkcji Rusta, która umożliwia automatyczne tworzenie dynamicznych interfejsów na podstawie cech. Jest to czasami określane jako wymazywanie typu i jest popularną techniką w C ++ [1] [2] , której nie należy mylić z nieco innym użyciem tego terminu w językach Java i FP. Czytelnicy zaznajomieni z C ++ rozpoznają różnicę między zamknięciem, które implementuje, Fn
a Fn
obiektem cechy jako równoważną rozróżnieniu między ogólnymi obiektami funkcji a std::function
wartościami w C ++.
Obiekt cechy tworzony jest przez wypożyczenie obiektu od &
operatora i rzucenie lub wymuszenie odniesienia do określonej cechy. W tym przypadku, ponieważ Processor
musimy posiadać obiekt wywołania zwrotnego, nie możemy użyć wypożyczania, ale musimy przechowywać wywołanie zwrotne w przydzielonej stercie Box<dyn Trait>
(odpowiednik Rusta std::unique_ptr
), co jest funkcjonalnym odpowiednikiem obiektu cechy.
Jeśli Processor
przechowuje Box<dyn FnMut()>
, nie musi już być rodzajowy, ale set_callback
metoda akceptuje teraz rodzaj ogólny c
za pośrednictwem impl Trait
argumentu . W związku z tym może akceptować wszelkiego rodzaju wywołania, w tym zamknięcia ze stanem, i odpowiednio je zapakować przed przechowywaniem w Processor
. Ogólny argument set_callback
nie ogranicza rodzaju wywołania zwrotnego akceptowanego przez procesor, ponieważ typ zaakceptowanego wywołania zwrotnego jest oddzielony od typu przechowywanego w Processor
strukturze.
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();
}
Żywotność referencji w zamknięciach pudełkowych
Okres 'static
istnienia związany z typem c
argumentu akceptowanego przez set_callback
jest prostym sposobem na przekonanie kompilatora, że zawarte w nim odwołaniac
, które mogą być zamknięciem odnoszącym się do jego środowiska, odnoszą się tylko do wartości globalnych i dlatego pozostaną ważne przez cały czas używania oddzwonić. Ale statyczne wiązanie jest również bardzo trudne: chociaż akceptuje zamknięcia, które są właścicielami obiektów w porządku (co zapewniliśmy powyżej, wykonując zamknięcie move
), odrzuca domknięcia, które odnoszą się do środowiska lokalnego, nawet jeśli odnoszą się tylko do wartości, które przeżyje procesor i faktycznie będzie bezpieczny.
Ponieważ potrzebujemy żywych wywołań zwrotnych tylko tak długo, jak działa procesor, powinniśmy spróbować powiązać ich żywotność z czasem życia procesora, co jest mniej ścisłe niż 'static
. Ale jeśli po prostu usuniemy 'static
związany czas życia z set_callback
, to już się nie kompiluje. Dzieje się tak, ponieważ set_callback
tworzy nowe pudełko i przypisuje je do callback
pola zdefiniowanego jako Box<dyn FnMut()>
. Ponieważ definicja nie określa okresu istnienia obiektu cechy w pudełku, 'static
jest implikowana, a przypisanie skutecznie rozszerzyłoby okres istnienia (z nienazwanego dowolnego czasu życia wywołania zwrotnego do 'static
), co jest niedozwolone. Rozwiązaniem jest zapewnienie wyraźnego czasu życia procesora i powiązanie tego czasu życia zarówno z odwołaniami w polu, jak i odwołaniami w wywołaniu zwrotnym otrzymanym przez 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);
}
}
Ponieważ te okresy życia są wyraźnie określone, nie jest już konieczne używanie 'static
. Zamknięcie może teraz odnosić się do s
obiektu lokalnego , tj. Nie musi już być move
, pod warunkiem, że definicja s
zostanie umieszczona przed definicją, p
aby zapewnić, że ciąg znaków przeżyje procesor.
CB
musi być'static
w ostatnim przykładzie?Box<FnMut()>
Stosowane w środkach polowych StructBox<FnMut() + 'static>
. Z grubsza: „Obiekt cechy w ramce nie zawiera żadnych odniesień / żadnych odniesień, które zawiera, jest żywy (lub równy)'static
”. Zapobiega przechwytywaniu przez wywołanie zwrotne miejscowych przez odniesienie.'static
w osobnym poście na blogu .