Kapanışlarda Ayrık Yakalama
Özet
|| a.x + 1, artıkayerine yalnızcaa.xdeğerini yakalar.- Bu durum, değerlerin farklı zamanlarda düşmesine neden olabilir ya da
kapanışların
SendveyaClonegibi trait’leri uygulayıp uygulamadığını etkileyebilir.- Olası değişiklikler tespit edilirse
cargo fix, kapanışın değişkenin tamamını yakalamasını zorlamak içinlet _ = &abenzeri ifadeler ekler.
- Olası değişiklikler tespit edilirse
Ayrıntılar
Kapanışlar,
gövdeleri içinde başvurduğunuz her şeyi otomatik olarak yakalar. Örneğin
|| a + 1, çevresindeki kapsamdan aya ait bir referansı otomatik olarak yakalar.
Rust 2018 ve öncesinde kapanışlar, yalnızca bir alanı kullansalar bile tüm
değişkeni yakalardı. Örneğin || a.x + 1, yalnızca a.xi değil anın
tamamına ait bir referansı yakalar. anın bütünüyle yakalanması, diğer
alanlarda değişiklik yapmayı ya da taşıma işlemlerini engeller. Bu yüzden
aşağıdaki kod derlenmez:
let a = BirYapi::new();
drop(a.x); // Yapının bir alanını dışarı taşır
println!("{}", a.y); // Tamam: Yapının başka bir alanı hâlâ kullanılıyor
let c = || println!("{}", a.y); // Hata: `a`nın tamamını yakalamaya çalışır
c();
Rust 2021 ile birlikte kapanış yakalamaları daha hassas hale geldi. Genellikle yalnızca gerçekten kullanılan alanlar yakalanır. Bazı özel durumlarda bundan fazlası da yakalanabilir; tüm ayrıntılar için Rust referansına bakabilirsiniz. Bu yüzden yukarıdaki örnek Rust 2021’de sorunsuz derlenir.
Ayrık yakalama, RFC 2229 kapsamında önerildi; gerekçeye dair ayrıntılar bu RFC’de yer alır.
Taşıma
2021 sürümünün bir parçası olarak, Rust 2018 kod tabanlarını Rust 2021’e
otomatik taşımaya yardımcı olmak için rust_2021_incompatible_closure_captures
lint’i eklendi.
Kodunuzu Rust 2021 ile uyumlu hale getirmek için şunu çalıştırın:
cargo fix --edition
Aşağıda, otomatik taşıma başarısız olursa ya da taşımanın nasıl çalıştığını daha iyi anlamak isterseniz, Rust 2021 ile uyumlu kapanış yakalamalarına elle nasıl geçileceği anlatılıyor.
Bir kapanışın yakaladığı değişkenleri değiştirmek, programların davranışını iki durumda değiştirebilir ya da derlemeyi durdurabilir:
- düşme sırasının, yani yıkıcıların ne zaman çalıştığının değişmesi (ayrıntılar);
- kapanışın hangi trait’leri uyguladığının değişmesi (ayrıntılar).
Aşağıdaki durumlardan biri tespit edilirse cargo fix, kapanışın değişkenin
tamamını yakalamasını zorlamak için kapanışın içine bir “yer tutucu let”
ekler:
#![allow(unused)]
fn main() {
let x = (vec![22], vec![23]);
let c = move || {
// `x`in tamamını yakalamayı zorlayan yer tutucu `let`
let _ = &x;
// Aksi halde burada yalnızca `x.0` yakalanırdı
println!("{:?}", x.0);
};
}
Bu korumacı bir analizdir; birçok durumda bu yer tutucu let ifadeleri güvenle
kaldırılabilir ve programınız düzgün çalışmaya devam eder.
Joker Desenler
Kapanışlar artık yalnızca okunması gereken verileri yakalar. Bu nedenle aşağıdaki
kapanışlar x değişkenini yakalamaz:
#![allow(unused)]
fn main() {
let x = 10;
let c = || {
let _ = x; // etkisiz işlem
};
let c = || match x {
_ => println!("Merhaba Dunya!")
};
}
Buradaki let _ = x ifadesi etkisiz bir işlemdir; çünkü _ deseni sağ taraftaki
ifadeyi tamamen yok sayar ve x bellekteki bir konuma, yani burada bir
değişkene referans verir.
Bu değişiklik tek başına, yani daha az değer yakalanması, öneri üretmez; ancak aşağıdaki “düşme sırası” değişikliğiyle birlikte öneri oluşabilir.
İnce nokta: Benzer görünen bazı ifadeler, örneğin eklediğimiz
let _ = &x türündeki yer tutucu letler, etkisiz işlem değildir. Bunun nedeni,
sağ taraftaki ifadenin (&x) bellekteki bir konuma verilen çıplak bir referans
olmaması; önce değerlendirilmesi gereken, ardından sonucu atılan bir ifade olmasıdır.
Düşme Sırası
Bir kapanış, t değişkeninden bir değerin sahipliğini aldığında, o değer artık
t kapsam dışına çıktığında değil, kapanış düştüğünde düşer:
#![allow(unused)]
fn main() {
fn move_value<T>(_: T){}
{
let t = (vec![0], vec![0]);
{
let c = || move_value(t); // `t` burada taşınır
} // `c` düşer ve beraberinde demet `t` de düşer
} // `t` burada kapsam dışına çıkar
}
Yukarıdaki kod hem Rust 2018’de hem de Rust 2021’de aynı şekilde çalışır. Ancak kapanış yalnızca bir değişkenin bir kısmının sahipliğini alıyorsa, fark oluşabilir:
#![allow(unused)]
fn main() {
fn move_value<T>(_: T){}
{
let t = (vec![0], vec![0]);
{
let c = || {
// Rust 2018'de `t`nin tamamını yakalar.
// Rust 2021'de yalnızca `t.0` yakalanır
move_value(t.0);
};
// Rust 2018'de bu bloktan çıkarken hem `c` hem de `t` düşer.
//
// Rust 2021'de ise bloktan çıkarken `c` ve `t.0` düşer.
}
// Rust 2018'de `t` içindeki değer taşınmıştır
// ve artık düşmez.
//
// Rust 2021'de `t.0` içindeki değer taşınmıştır; ama `t.1`
// yerinde kaldığı için burada düşer.
}
}
Çoğu durumda değerlerin farklı zamanlarda düşmesi yalnızca belleğin ne zaman
serbest bırakılacağını etkiler ve önemli değildir. Ancak bazı Drop
uygulamalarının, yani yıkıcıların yan etkileri vardır. Bu durumlarda düşme
sırasını değiştirmek programınızın anlamını da değiştirebilir. Böyle bir
senaryoda derleyici, tüm değişkenin yakalanmasını zorlamak için yer tutucu
bir let eklenmesini önerir.
Trait Uygulamaları
Kapanışlar, yakaladıkları değerlere bağlı olarak aşağıdaki trait’leri otomatik olarak uygular:
Clone: yakalanan tüm değerlerCloneise.- otomatik trait’ler olan
Send,SyncveUnwindSafe: yakalanan tüm değerler ilgili trait’i uyguluyorsa.
Rust 2021’de farklı değerler yakalandığı için, bir kapanışın hangi trait’leri
uyguladığı da değişebilir. Taşıma lint’leri her kapanışı sınar; önce belirli
bir trait’i uygulayıp uygulamadığına ve şimdi hâlâ uygulayıp uygulamadığına
bakar. Önceden uygulanan ama artık uygulanmayan bir trait bulurlarsa yer
tutucu let ifadeleri eklenir.
Örneğin ham işaretçileri iş parçacıkları arasında taşımak için sık kullanılan
yöntemlerden biri, bunları bir yapı içinde sarmalamak ve ardından sarmalayıcı
için Send/Sync otomatik trait’lerini uygulamaktır. thread::spawn içine
verilen kapanış, sarmalayıcının yalnızca belirli alanlarını kullansa da yine de
tüm sarmalayıcı yakalanır. Sarmalayıcı Send/Sync olduğu için kod güvenli
sayılır ve başarıyla derlenir.
Ayrık yakalama ile birlikte kapanışta anılan belirli alan yakalanır. Bu alanın
kendisi başlangıçta Send/Sync olmadığı için sarmalayıcının sağladığı koruma
da etkisiz kalır.
#![allow(unused)]
fn main() {
use std::thread;
struct Isaretci(*mut i32);
unsafe impl Send for Isaretci {}
let mut x = 5;
let px = Isaretci(&mut x as *mut i32);
let c = thread::spawn(move || {
unsafe {
*(px.0) += 10;
}
}); // Kapanış, `Send` olmayan `px.0` alanını yakaladı
}