Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Kapanışlarda Ayrık Yakalama

Özet

  • || a.x + 1, artık a yerine yalnızca a.x değerini yakalar.
  • Bu durum, değerlerin farklı zamanlarda düşmesine neden olabilir ya da kapanışların Send veya Clone gibi 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çin let _ = &a benzeri ifadeler ekler.

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:

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ı
}