Rust'ta deyimsel geri aramalar


100

C / C ++ 'da normal olarak düz bir işlev işaretçisi ile geri çağırmalar yaparım, belki bir void* userdataparametre de iletirim. Bunun gibi bir şey:

typedef void (*Callback)();

class Processor
{
public:
    void setCallback(Callback c)
    {
        mCallback = c;
    }

    void processEvents()
    {
        for (...)
        {
            ...
            mCallback();
        }
    }
private:
    Callback mCallback;
};

Rust'ta bunu yapmanın deyimsel yolu nedir? Özellikle, işlevim hangi türleri almalı setCallback()ve ne tür olmalıdır mCallback? Bir almalı Fnmı? Belki FnMut? Kaydediyor muyum Boxed? Bir örnek harika olurdu.

Yanıtlar:


195

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 fntür olarak bildirmek olacaktır . C ++ 'ın işlev işaretçileri gibi fn, fnanahtar 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(); // hello world!
}

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_callbackdiğ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 callbackalanı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 userdatageri aramaya eşlik etmesi gerekmez ; arayan tarafından sağlanan kapanış, set_callbackihtiyaç 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:

  • Fnyalnı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 mutdeğişkene yazarak verileri değiştiren kapanışlardır . Aynı zamanda birden çok kez çağrılabilir, ancak paralel olarak değil. ( FnMutBirden ç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 FnOncetersine, 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_eventsMutasyon 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 Processorbir 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_callbackfarklı 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ış Fnile bir Fnözellik nesnesi arasındaki farkı, genel işlev nesneleri ve std::functionC ++' 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, Processorgeri 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 Processormağazalarda Box<dyn FnMut()>, artık genel olması gerekir ancak set_callback yöntem artık genel bir kabul cbir yoluyla impl Traitargüman . Bu nedenle, durumla kapatmalar da dahil olmak üzere her türlü çağrılabilir türü kabul edebilir ve Processor. set_callbackKabul edilen geri aramanın türü Processoryapı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ü

'staticTürüne bağlı ömür boyu ctarafından kabul argüman set_callbackolduğ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_callbackartık derlenmez. Bunun nedeni set_callback, yeni bir kutu oluşturması ve bunu olarak callbacktanı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. sArtık, dizginin işlemciden daha uzun ömürlü olmasını sağlamak için tanımının tanımından önce yerleştirilmesi movekoşuluyla, kapanış yerel nesneye atıfta bulunabilir , yani artık olması gerekmez.sp


15
Vay canına, sanırım bu bir SO sorusuna aldığım en iyi cevap! Teşekkür ederim! Mükemmel bir şekilde açıklandı. Yine de anlamadığım küçük bir şey - neden son örnekte CBolmak zorunda 'static?
Timmmm

9
Box<FnMut()>Yapı alanı vasıtası kullanılır Box<FnMut() + 'static>. Kabaca "Kutulu özellik nesnesi hiçbir referans / içerdiği son (veya eşit) referans içermez 'static". Geri aramanın yerelleri referans alarak yakalamasını engeller.
bluss

Ah anlıyorum, sanırım!
Timmmm

1
Diğer ayrıntılar'ı @Timmmm 'staticbir bağlanmış ayrı blog post .
user4815162342

3
Bu harika bir cevap, sağladığınız için teşekkür ederiz @ user4815162342.
Dash83
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.