Sorunun tarihi, Rust'ın kapanmaları, anonim yapıların sıfır maliyetli soyutlamalar olarak uygulama kararıyla başlar; bu yapı, çöp toplayıcı işlev nesneleri yerine geçer. JavaScript veya Python gibi dillerin aksine, Rust, sahiplik, borç verme ve değişkenlik kurallarını kapanmanın türüne doğrudan kodlamalıdır. Fn, FnMut ve FnOnce üç tür, çağrı yöntemlerindeki self parametresine dayalı olarak katı bir hiyerarşi oluşturur ve derleyicinin bir kapanmanın kullanımının, yakalanan ortamının bellek güvenliği değişmezliklerine saygı gösterdiğini derleme zamanında doğrulamasını sağlar.
Sorun, bir kapanmanın değişkenleri nasıl yakaladığı (referans olarak veya move aracılığıyla değer olarak) ve bunları içsel olarak nasıl kullandığı arasındaki ayrım etrafında dönmektedir. FnOnce, self (sahipliği tüketerek) gerektirir; böylece kapanmanın, yakalanan değişkenleri çevresinden taşımasına izin verir ancak bunu yalnızca bir kez çağırabilir. FnMut, yakalanan durumu değiştirmeye izin verir ancak kapanmanın kendisine benzersiz erişim gerektirir; &mut self gerektirir. Fn, birden çok eşzamanlı çağrıya izin verirken, yakalanan değişkenlerin değişimini yasaklar, içsel değişkenlik kullanılmadığı sürece. Bir kapanma, gövdesine taşınan bir Copy türü olmayan bir türü taşıdığında, ilk çağrı çevreyi taşınmış bir durumda bırakır, bu da sonraki çağrıları geçersiz kılar; bu nedenle FnOnce olur. Adaylar genellikle move anahtar kelimesini — ki bu yalnızca değere göre yakalamayı zorlar — FnOnce türü ile karıştırır ve yalnızca Copy türlerini içeren bir move kapanmasının hala Fn uyguladığını kabul etmez.
Çözüm, API için gerekli en az kısıtlayıcı tür sınırını seçmekle ilgilidir. Kapanma tam olarak bir kez çağrılıyorsa, çevresini tüketen herhangi bir kapanmayı kabul etmek için FnOnce kullanın. Birden fazla çağrı ile değişim gerektiğinde, FnMut kullanın. Eşzamanlı veya tekrarlanan yalnızca okuma erişimi için, Fn kullanın. Derleyici, bu uygulamaları otomatik olarak yakalama analizine dayalı olarak türetir, manuel tür uygulaması gerektirmeden.
fn apply_once<F: FnOnce()>(f: F) { f(); } fn apply_mut<F: FnMut()>(mut f: F) { f(); f(); } fn apply_fn<F: Fn()>(f: F) { f(); f(); } let data = vec![1, 2, 3]; let consume = move || drop(data); // FnOnce: Vec Copy değildir apply_once(consume); let mut count = 0; let mut increment = || { count += 1; }; // FnMut: yakalamayı değiştirir apply_mut(&mut increment); let value = 42; let print = move || println!("{}", value); // Fn: i32 Copy'dır apply_fn(print); apply_fn(print); // Geçerli: print Fn'dir
Yüksek verimli bir web sunucusunda gelen istekleri işlemek için kullanıcı tanımlı kancaları kabul eden bir asenkron görev planlayıcıyı düşünün. Planlayıcı API'si başlangıçta tüm kancaların Fn uygulamasını gerektiriyordu, böylece potansiyel paralel yürütmeye izin veriyordu.
Sorun tanımı: Yeni bir özellik, kancaların her bağlantıya özgü istatistikleri korumasını gerektiriyordu; bu da yakalanan sayaçların değiştirilmesini gerektiriyordu. Geliştiriciler, mut counter değişkenlerini yakalayan move kapanmalarını geçirmeye çalıştılar, ancak derleyici bunu reddetti çünkü Fn, kendi mut alanlarını değiştiremez.
Takım, tür sınırını gevşetme veya kanca imzasını yeniden yapılandırma arasında seçim yapmak zorunda kaldı.
Çözüm 1: Atomik Türlerle İçsel Değişkenlik:
u64 sayacını AtomicU64 ile değiştirin ve onu Arc aracılığıyla yakalayın. Kapanma Fn’yi uygular çünkü değişim, &self üzerindeki atomik işlemler yoluyla gerçekleşir ve kapanmaya mutlak erişim gerektirmez.
Artılar: Fn tür sınırını korur, planlayıcının kancaları birden fazla iş parçacığından eş zamanlı olarak yürütmesini sağlar.
Eksiler: Donanım seviyesinde atomik aşama ve bellek sıralama karmaşıklığını getirir. Tek iş parçacıklı kullanımda bile Arc tahsisi gerektirir, basit sayaçlar için sıfır maliyetli soyutlama ilkelerini bozar.
Çözüm 2: Sıralı Yürütme ile FnMut Sınırı:
Planlayıcı API'sini FnMut kapanmalarını kabul edecek şekilde değiştirin. Planlayıcı, kancaları Vec<Box<dyn FnMut()>> içinde saklar ve &mut erişimini sürdürürken bunları sıralı olarak çağırır.
Artılar: Değişim için sıfır zaman aşımı. Derleme zamanında hiçbir veri yarışı gerçekleşmeyeceğinin garantisi, çünkü tür sistemi çağrı sırasında benzersiz erişimi zorunlu kılıyor.
Eksiler: Aynı kancanın eş zamanlı çağrısını engeller ve planlayıcının iç depolamasını karmaşıklaştırır (planlayıcı üzerinde &mut self gerektirir). Mevcut Fn kancaları ile uyumluluğu kırar, boşaltılmış uygulamalar kullanılmadıkça.
Seçilen çözüm: Çözüm 2 (FnMut) seçilmiştir çünkü sunucunun mimarisi bağlantıları iş parçacığı başına işler. Eş zamanlı kanca yürütmesi gereksinimi ortadan kalkar. Takım, eş zamanlı kancaların esnekliği yerine derleme zamanı güvenliğini tercih etti ve API değişimini kırıcı ama doğru bir evrim olarak kabul etti.
Sonuç: Planlayıcı, hiçbir zaman aşımı olmaksızın, durum bilgisi olan kancaları başarıyla işledi. Tür sistemi, RefCell ile doğru senkronizasyon olmaksızın Fn kullanarak iki iş parçacığının aynı anda değişken bir sayacı artırmasını engelledi.
Kapanma tanımında move anahtar kelimesi, o kapanmanın otomatik olarak FnOnce yerine Fn veya FnMut uygulamasına yol açar mı?
Hayır. move anahtar kelimesi, yalnızca yakalanan değişkenlerin kapanmanın ortamına değer olarak taşındığını belirtir, bunun yerine borç verilerek. Tür uygulaması, kapanma gövdesinin yakalamalarını nasıl kullandığına dayanır. Eğer kapanma, bir çevreden bir Copy olmayan türü taşırsa (tüketerek), FnOnce uygular. Eğer yalnızca yakalamaları değiştirirse, FnMut uygular. Eğer yalnızca değişkeni okur veya değer olarak Copy türlerini kullanıyorsa, Fn uygular; move anahtar kelimesiyle bile. Örneğin, let x = 5; let f = move || x + 1; Fn uygular çünkü i32 Copy’dir.
Bir FnOnce kabul eden bir işlev, Fn uygulayan bir kapanma ile çağrılabilirken, neden tersine durum söz konusu değildir?
Fn, FnMut'nin bir alt türüdür, bu da FnMut, FnOnce'nin bir alt türüdür. Bu, Fn uygulayan her kapanmanın otomatik olarak FnMut ve FnOnce uyguladığı anlamına gelir, ancak bunun tersi geçerli değildir. FnOnce tarafından sınırlı bir işlev parametresi, bir kez çağrılabilecek herhangi bir kapanmayı kabul eder; bu, birden fazla kez çağrılabilenleri (uzmanlık olarak Fn ve FnMut içeren) içerir. Tersine, Fn gerektiren bir işlev, kapanmanın eşit bir referans (&self) üzerinden çağrımı desteklemesini talep eder; oysa çevresini tüketen kapanmalar (FnOnce yalnızca) bunu karşılayamaz. Bu, standart alt tür kullanımı kurallarına uyar: daha yetenekli bir tür (Fn) daha az yetenekli bir türün (FnOnce) gerektiği yerde kullanılabilir.
Derleyici, bir kapanma, çevreleyen kapsamda değişkenlere referanslar yakaladığında hangi türü uyguladığını nasıl belirler?
Derleyici, kapanmanın gövdesini analiz eder ve yakalanan değişkenlerin nasıl erişildiğini bakar. Eğer kapanma, yakalanan bir değişkeni taşırsa (ve tür Copy değilse), FnOnce uygular. Eğer yakalanan bir değişkeni değiştiriyorsa (ona atama veya &mut self yöntemlerini çağırıyorsa), FnMut (ve FnOnce olarak) uygular. Eğer yalnızca değişkeni okur veya &self yöntemlerini çağırıyorsa, Fn (ve diğerleri olarak) uygular. Referanslar yolu ile yakalamalar için (&T veya &mut T), kapanma referansları tutar. Eğer &mut T yakalarsa, genellikle FnMut uygular; çünkü çağrılması, kapalıya benzersiz erişim gerektirir ve değişken borrow'un benzersizliğini korur. Eğer &T yakalarsa, Fn uygular.