Kısa yanıt: Maksimum esneklik için, geri aramayı FnMut
, geri arama türü üzerinde genel geri arama ayarlayıcısı ile kutulu bir nesne olarak depolayabilirsiniz . Bunun kodu, cevaptaki son örnekte gösterilmiştir. Daha ayrıntılı bir açıklama için okumaya devam edin.
"İşlev işaretçileri": geri çağırmalar fn
Sorudaki C ++ kodunun en yakın eşdeğeri, geri aramayı bir fn
tür olarak bildirmek olacaktır . C ++ 'ın işlev işaretçileri gibi fn
, fn
anahtar sözcükle tanımlanan işlevleri kapsüller :
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();
}
Bu kod Option<Box<Any>>
, işlevle ilişkili "kullanıcı verilerini" tutmak için genişletilebilir . Öyle olsa bile, deyimsel Rust olmazdı. Bir fonksiyonu ile ilişkilendirmek verilere Pas yolu isimsiz bunu çekebilmektir kapatma ++ sadece modern bir C gibi. Kapanışlar olmadığından fn
, set_callback
diğer türden işlev nesnelerini kabul etmek gerekecektir.
Genel işlev nesneleri olarak geri çağırmalar
Hem Rust hem de C ++ kapanışlarında aynı çağrı imzasına sahip, yakalayabilecekleri farklı değerleri barındırmak için farklı boyutlarda gelir. Ek olarak, her bir kapanış tanımı, kapanışın değeri için benzersiz bir anonim tür üretir. Bu kısıtlamalar nedeniyle, yapı kendi callback
alanının türünü adlandıramaz veya bir takma ad kullanamaz.
Somut bir türe atıfta bulunmadan yapı alanına bir kapanış yerleştirmenin bir yolu, yapıyı jenerik yapmaktır . Yapı, kendisine ilettiğiniz somut işlev veya kapanış için boyutunu ve geri arama türünü otomatik olarak uyarlayacaktır:
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();
}
Daha önce olduğu gibi, geri aramanın yeni tanımı, ile tanımlanan üst düzey işlevleri kabul edebilecektir fn
, ancak bu, aynı zamanda kapanışları ve || println!("hello world!")
gibi değerleri yakalayan kapanışları da kabul edecektir || println!("{}", somevar)
. Bu nedenle, işlemcinin userdata
geri aramaya eşlik etmesi gerekmez ; arayan tarafından sağlanan kapanış, set_callback
ihtiyaç duyduğu verileri ortamından otomatik olarak yakalayacak ve çağrıldığında kullanılabilir olmasını sağlayacaktır.
Ama anlaşma nedir FnMut
, neden sadece olmasın Fn
? Kapanışlar yakalanan değerleri tuttuğundan, kapanış çağrısı yapılırken Rust'un olağan mutasyon kuralları geçerli olmalıdır. Kapanışların sahip oldukları değerlerle ne yaptığına bağlı olarak, her biri bir özellik ile işaretlenmiş üç aile halinde gruplanırlar:
Fn
yalnızca verileri okuyan ve muhtemelen birden çok iş parçacığından birden çok kez güvenle çağrılabilen kapanışlardır. Her ikisi de yukarıdaki kapanışlardır Fn
.
FnMut
örneğin yakalanan bir mut
değişkene yazarak verileri değiştiren kapanışlardır . Aynı zamanda birden çok kez çağrılabilir, ancak paralel olarak değil. ( FnMut
Birden çok iş parçacığından bir kapatma çağrısı yapmak bir veri yarışına yol açacaktır, bu nedenle yalnızca bir muteksin korunmasıyla yapılabilir.) Kapanış nesnesi, çağıran tarafından değiştirilebilir olarak bildirilmelidir.
FnOnce
Örneğin, yakalanan bir değeri sahipliğini alan bir işleve taşıyarak, yakaladıkları verilerin bir kısmını tüketen kapanışlardır . Adından da anlaşılacağı gibi, bunlar yalnızca bir kez aranabilir ve arayanın sahibi olması gerekir.
Bir kapatmayı kabul eden bir nesnenin türüne bağlı bir özellik belirlerken, sezginin FnOnce
tersine, aslında en izin verici olanıdır. Genel bir geri arama türünün FnOnce
özelliği karşılaması gerektiğini beyan etmek, kelimenin tam anlamıyla herhangi bir kapatmayı kabul edeceği anlamına gelir. Ancak bunun bir bedeli vardır: bu, sahibinin yalnızca bir kez aramasına izin verildiği anlamına gelir. Yana process_events()
geri aramasını birden çok kez çağırmak için tercih edebilir ve yöntem olarak kendini bir sonraki en hoşgörülü sınırdır, kereden fazla çağrılabilir FnMut
. process_events
Mutasyon olarak işaretlememiz gerektiğine dikkat edin self
.
Genel olmayan geri çağrılar: işlev özelliği nesneleri
Geri aramanın genel uygulaması son derece verimli olsa da, ciddi arabirim sınırlamaları vardır. Her bir Processor
örneğin somut bir geri arama türüyle parametrelendirilmesini gerektirir , yani tek Processor
bir tek bir geri arama türüyle ilgilenebilir. Her bir kapanışın farklı bir türü olduğu göz önüne alındığında, jenerik Processor
, proc.set_callback(|| println!("hello"))
ardından gelen işleyemez proc.set_callback(|| println!("world"))
. Yapının iki geri arama alanını destekleyecek şekilde genişletilmesi, tüm yapının iki türe parametrelendirilmesini gerektirecektir; bu, geri arama sayısı arttıkça hızlı bir şekilde kullanışsız hale gelir. Daha fazla tür parametresi eklemek, geri arama sayısının dinamik olması gerekiyorsa, örneğin add_callback
farklı geri aramaların bir vektörünü koruyan bir işlevi uygulamak için işe yaramaz .
Tür parametresini kaldırmak için , niteliklere dayalı dinamik arabirimlerin otomatik olarak oluşturulmasına izin veren Rust'un özelliği olan özellik nesnelerinden yararlanabiliriz . Bu bazen tür silme olarak adlandırılır ve C ++ [1] [2] ' da popüler bir tekniktir, Java ve FP dillerinin terimin biraz farklı kullanımıyla karıştırılmamalıdır. C ++ 'ya aşina olan okuyucular, uygulayan bir kapanış Fn
ile bir Fn
özellik nesnesi arasındaki farkı, genel işlev nesneleri ve std::function
C ++' daki değerler arasındaki ayrıma eşdeğer olarak tanıyacaktır .
Bir özellik nesnesi, &
operatörle bir nesneyi ödünç alarak ve onu belirli bir özelliğe bir referansa atarak veya zorlayarak oluşturulur. Bu durumda, Processor
geri arama nesnesine sahip olmamız gerektiğinden, ödünç almayı kullanamayız, ancak geri aramayı , işlevsel olarak bir özellik nesnesine eşdeğer olan bir yığın ayrılmış Box<dyn Trait>
(Rust eşdeğeri std::unique_ptr
) içinde depolamalıyız .
Eğer Processor
mağazalarda Box<dyn FnMut()>
, artık genel olması gerekir ancak set_callback
yöntem artık genel bir kabul c
bir yoluyla impl Trait
argüman . Bu nedenle, durumla kapatmalar da dahil olmak üzere her türlü çağrılabilir türü kabul edebilir ve Processor
. set_callback
Kabul edilen geri aramanın türü Processor
yapıda depolanan türden ayrıştırıldığından, genel argüman işlemcinin ne tür geri aramayı kabul edeceğini sınırlamaz .
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();
}
Kutulu kapaklar içindeki referansların ömrü
'static
Türüne bağlı ömür boyu c
tarafından kabul argüman set_callback
olduğunu derleyici ikna etmek basit bir yoludur referanslar içerdiği c
çevresiyle başvuran bir kapatma olabilir ki, sadece global değerlere bakın ve bu nedenle kullanımı süresince geçerli olacak geri aramak. Ancak statik sınır da çok ağırdır: kendi nesnelerinin gayet iyi olduğu kapanışları kabul ederken (yukarıda kapatmayı yaparak bunu sağladık move
), yerel ortama atıfta bulunan kapanışları, yalnızca değerlere atıfta bulunsalar bile reddeder. işlemciden daha uzun ömürlüdür ve aslında güvenli olacaktır.
Geri aramalara yalnızca işlemci canlı olduğu sürece canlı olarak ihtiyaç duyduğumuz için, ömürlerini işlemcininkine bağlamaya çalışmalıyız ki bu daha az sıkı bir sınırdır 'static
. Ancak 'static
ömür sınırını kaldırırsak, set_callback
artık derlenmez. Bunun nedeni set_callback
, yeni bir kutu oluşturması ve bunu olarak callback
tanımlanan alana atamasıdır Box<dyn FnMut()>
. Tanım, kutulu özellik nesnesi için bir yaşam süresi belirtmediğinden 'static
, ima edilir ve atama, kullanım ömrünü etkin bir şekilde (geri aramanın adsız bir keyfi yaşam süresinden olarak 'static
) genişletir ve buna izin verilmemektedir. Düzeltme, işlemci için açık bir yaşam süresi sağlamak ve bu ömrü, hem kutudaki referanslara hem de geri aramadaki referanslara bağlamaktır 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);
}
}
Bu ömürlerin açık hale getirilmesiyle artık kullanılması gerekmiyor 'static
. s
Artık, dizginin işlemciden daha uzun ömürlü olmasını sağlamak için tanımının tanımından önce yerleştirilmesi move
koşuluyla, kapanış yerel nesneye atıfta bulunabilir , yani artık olması gerekmez.s
p
CB
olmak zorunda'static
?