Rust'ta closure, bir değişkende saklayabildiğiniz ya da başka bir fonksiyona argüman olarak geçirebildiğiniz isimsiz bir fonksiyondur. Sıradan fn fonksiyonlarından en önemli farkı, tanımlandığı kapsamdaki değişkenleri yakalayabilmesidir: |x| x + oran yazdığınızda oran dışarıdaki bir değişken olabilir. Rust bu değişkeni değiştirmeden ödünç mü alacağına, değiştirilebilir ödünç mü alacağına yoksa sahipliğini mi devralacağına closure gövdesine bakarak kendisi karar verir ve bu karar closure'ın hangi Fn trait'ini uyguladığını belirler.

Bu yazıda closure sözdizimini, tip çıkarımının nasıl çalıştığını, üç yakalama biçimini, move anahtar kelimesini, Fn, FnMut ve FnOnce arasındaki farkı, closure döndürmeyi ve fonksiyon işaretçilerini ele alıyoruz. Bütün örnekler Rust 1.99.1 ile derlendi; hata örneklerinde derleyicinin verdiği gerçek ilk satırı yorum olarak bıraktık. Sahiplik ve ödünç alma kurallarını henüz oturtmadıysanız önce ownership yazısına ve referans ve borrowing yazısına göz atmanız işinizi kolaylaştırır.

Closure sözdizimi ve tip çıkarımı

Bir closure iki dikey çizgi arasına yazılan parametre listesiyle başlar, ardından gövde gelir. Gövde tek bir ifadeyse süslü parantez gerekmez. Parametre ve dönüş tiplerini yazmak isteğe bağlıdır; yazarsanız sözdizimi fonksiyona çok benzer: |fiyat: u32| -> u32 { ... }. Fonksiyonlarda tipleri yazmak zorunludur çünkü imza, kodu kullanan herkesle yapılmış açık bir sözleşmedir. Closure'lar ise genellikle kısa ömürlü ve dar bir bağlamda kullanılır, kimseye arayüz olarak sunulmaz; bu yüzden derleyici tipleri kullanım yerinden çıkarabilir.

rust
fn main() {
    let kdv_orani = 20;
    let kdvli = |fiyat: u32| -> u32 { fiyat + fiyat * kdv_orani / 100 };
    let iki_kat = |x| x * 2;

    println!("{}", kdvli(150));
    println!("{}", iki_kat(21));

    let tercih: Option<&str> = None;
    let varsayilan = String::from("mavi");
    let renk = tercih.unwrap_or_else(|| varsayilan.as_str());
    println!("Seçilen renk: {renk}");
}
// çıktı: 180
// çıktı: 42
// çıktı: Seçilen renk: mavi

Örnekte kdvli tamamen tiplendirilmiş, iki_kat ise hiç tip yazılmadan tanımlanmış. İkisi de geçerli. Dikkat edilecek nokta kdvli içindeki kdv_orani: bu değişken closure'ın parametresi değil, dış kapsamdan yakalanan bir değerdir. Aynı şeyi bir fn ile yapamazsınız; iç içe tanımlanmış bir fonksiyon bile çevresindeki yerel değişkenleri göremez. Son satırdaki unwrap_or_else da standart kütüphanenin closure alan tipik bir metodu: Option değeri None ise verdiğiniz closure çağrılır ve sonucu kullanılır, Some ise closure hiç çalışmaz.

Tip çıkarımının bir sınırı var: tiplenmemiş bir closure için derleyici her parametreye ve dönüş değerine tek bir somut tip atar. İlk çağrıda hangi tip kullanıldıysa closure ona kilitlenir. Aşağıdaki kod bu yüzden derlenmez; closure önce String ile çağrıldığı için ikinci çağrıdaki tam sayı reddedilir. Closure'lar generic değildir; birden fazla tiple çalışan bir yapı istiyorsanız generic bir fonksiyon yazmanız gerekir (bkz. generics yazısı).

rust
fn main() {
    let ornek = |x| x;
    let s = ornek(String::from("merhaba"));
    let n = ornek(5);
}
// error[E0308]: mismatched types
// (not: expected because the closure was earlier called with an argument of type `String`)

Ortamı yakalamanın üç yolu

Ortamı yakalamanın üç yolu: Değiştirilebilir yakalama ve ödünç kuralları, Alan bazında yakalama

Closure'lar dış değişkenleri, bir fonksiyonun parametre alabildiği üç yolla yakalar: değiştirmeden ödünç alma (&T), değiştirilebilir ödünç alma (&mut T) ve sahipliği alma (değerin kendisi). Hangisinin seçileceğini siz söylemezsiniz; derleyici gövdeye bakar ve işi görecek en hafif yolu seçer. Rust Reference bu sırayı açıkça tanımlar: önce paylaşılan ödünç, sonra benzersiz ödünç, sonra değiştirilebilir ödünç ve en son taşıma denenir. Seçim closure'ın çevresindeki koddan etkilenmez, yalnızca gövdedeki kullanım belirleyicidir.

rust
use std::thread;

fn main() {
    let liste = vec![1, 2, 3];
    let yazdir = || println!("Closure içinde: {liste:?}");
    println!("Çağırmadan önce: {liste:?}");
    yazdir();

    let mut sayilar = vec![1, 2, 3];
    let mut ekle = || sayilar.push(4);
    ekle();
    ekle();
    println!("Ekledikten sonra: {sayilar:?}");

    let veri = vec![10, 20];
    let h = thread::spawn(move || println!("Thread içinde: {veri:?}"));
    h.join().unwrap();
}
// çıktı: Çağırmadan önce: [1, 2, 3]
// çıktı: Closure içinde: [1, 2, 3]
// çıktı: Ekledikten sonra: [1, 2, 3, 4, 4]
// çıktı: Thread içinde: [10, 20]

İlk closure liste değişkenini yalnızca yazdırdığı için değiştirmeden ödünç alır; aynı anda birden çok okuma referansı olabileceğinden closure tanımlandıktan sonra da liste serbestçe okunabilir. İkinci closure push çağırdığı için sayilar üzerinde değiştirilebilir ödünç alır. Bu yüzden closure değişkeninin kendisi de let mut ekle olarak tanımlanmalıdır: closure'ı çağırmak, içinde tuttuğu değiştirilebilir referansı kullanmak anlamına gelir. Üçüncü örnekte gövde sadece okuma yapsa bile move yazdık; çünkü yeni thread ana thread'den daha uzun yaşayabilir ve referans geçersiz kalabilir. move, closure'ı yakaladığı her şeyin sahipliğini almaya zorlar. Thread tarafını thread::spawn ve join yazısında ayrıntılı işliyoruz.

Değiştirilebilir yakalama ve ödünç kuralları

Değiştirilebilir yakalayan bir closure tanımlandığı andan son kullanımına kadar o değişken üzerinde tek erişim hakkını tutar. Bu aralıkta değişkeni okumaya çalışırsanız, normal referanslarda gördüğünüz ödünç hatasını alırsınız. Aşağıdaki kodda println! closure'ın tanımı ile çağrısı arasına girdiği için derleyici itiraz eder. Çözüm basit: yazdırmayı closure'ın son çağrısından sonraya taşıyın. Closure'ın son kullanımı bittiğinde ödünç de biter; bu, derleyicinin ödünç sürelerini kullanım noktalarına göre hesaplamasının doğal sonucudur.

rust
fn main() {
    let mut sayilar = vec![1, 2, 3];
    let mut ekle = || sayilar.push(4);
    println!("{sayilar:?}");
    ekle();
}
// error[E0502]: cannot borrow `sayilar` as immutable because it is also borrowed as mutable

Alan bazında yakalama

Rust 2021 edition'dan itibaren closure'lar bir struct'ın tamamını değil, gövdede kullanılan alan yolunu yakalar. Reference bu yola capture path adını verir: o.puan kullanılıyorsa yakalanan şey o değil o.puan olur. 2018 ve önceki edition'larda değişkenin tamamı yakalanıyordu. Bu inceliğin pratik faydası, closure bir alanı değiştirirken diğer alanları okumaya devam edebilmenizdir. Aşağıdaki örnek, closure puan alanını değiştirilebilir ödünç almışken ad alanını yazdırıyor ve sorunsuz derleniyor.

rust
struct Oyuncu {
    ad: String,
    puan: u32,
}

fn main() {
    let mut o = Oyuncu { ad: String::from("Deniz"), puan: 0 };
    let mut puan_ekle = || o.puan += 10;
    println!("Oyuncu: {}", o.ad);
    puan_ekle();
    puan_ekle();
    println!("{} puanı: {}", o.ad, o.puan);
}
// çıktı: Oyuncu: Deniz
// çıktı: Deniz puanı: 20

Fn, FnMut ve FnOnce: closure'ın türünü ne belirler?

Fn, FnMut ve FnOnce: closure'ın türünü ne belirler? FnOnce, FnMut, Yakaladığı değeri ne taşıyan ne değiştiren closure'lar, ayrıca hiçbir şey yakalamayanlar. Paylaşılan...

Her closure'ın kendine özgü, adı yazılamayan bir tipi vardır. Reference bu tipi kabaca yakalanan değerleri alan olarak tutan bir struct gibi tarif eder. Bu tip, gövdenin yakalanan değerlerle ne yaptığına göre bir, iki ya da üç çağrı trait'ini otomatik olarak uygular. Fonksiyonlar ve struct'lar closure kabul ederken bu trait'leri sınır olarak kullanır, dolayısıyla hangi closure'ın nereye verilebileceğini bu üçlü belirler.

  • FnOnce: En az bir kez çağrılabilen her closure bunu uygular, yani bütün closure'lar FnOnce'tır. Yakaladığı bir değeri gövdeden dışarı taşıyan closure yalnızca bunu uygular, çünkü ikinci çağrıda taşınacak değer kalmamıştır.
  • FnMut: Yakaladığı değeri dışarı taşımayan ama değiştirebilen closure'lar. Birden çok kez çağrılabilir, çağırmak için closure'a değiştirilebilir erişim gerekir.
  • Fn: Yakaladığı değeri ne taşıyan ne değiştiren closure'lar, ayrıca hiçbir şey yakalamayanlar. Paylaşılan referansla, birden çok kez ve aynı anda çağrılabilir.

Buradaki ince nokta şu: trait'i belirleyen şey yakalamanın biçimi değil, gövdenin yaptığı iştir. Reference bunu özellikle not eder; move ile yakalayan bir closure da, gövdesi değeri yalnızca okuyorsa Fn uygulayabilir. Sınır seçerken kural da buradan çıkar: bir fonksiyon closure'ı yalnızca bir kez çağıracaksa FnOnce isteyerek en geniş kümeyi kabul eder. Option::unwrap_or_else tam olarak bunu yapar. Birden çok kez çağıracaksa FnMut, aynı anda birden çok yerden çağıracaksa ya da paylaşılan referans üzerinden çağıracaksa Fn ister. Dilimlerdeki sort_by_key anahtar closure'ını her eleman için tekrar tekrar çağırdığı için FnMut sınırı kullanır.

rust
fn bir_kez<F: FnOnce() -> String>(f: F) -> String {
    f()
}

fn iki_kez<F: FnMut()>(mut f: F) {
    f();
    f();
}

fn uc_kez<F: Fn(i32) -> i32>(f: F) -> i32 {
    f(1) + f(2) + f(3)
}

fn main() {
    let ad = String::from("Ayşe");
    let teslim = move || ad;
    println!("{}", bir_kez(teslim));

    let mut sayac = 0;
    iki_kez(|| sayac += 1);
    println!("sayac = {sayac}");

    let carpan = 10;
    println!("{}", uc_kez(|x| x * carpan));

    let mut dizi = [5, 3, 8, 1];
    let mut karsilastirma = 0;
    dizi.sort_by_key(|x| {
        karsilastirma += 1;
        *x
    });
    println!("{dizi:?}, anahtar çağrısı: {karsilastirma}");
}
// çıktı: Ayşe
// çıktı: sayac = 2
// çıktı: 60
// çıktı: [1, 3, 5, 8], anahtar çağrısı: 10

Örnekteki teslim closure'ı ad değerini döndürerek dışarı taşıdığı için yalnızca FnOnce uygular; onu iki_kez fonksiyonuna vermeye kalksaydınız derleyici reddederdi. sayac += 1 yapan closure FnMut, carpan değerini yalnızca okuyan closure ise Fn olur. Son bölümde sort_by_key anahtarı kaç kez çağırdığını sayıyoruz; bu sayı sıralama algoritmasının iç ayrıntısıdır ve dizinin içeriğine göre değişebilir, ona güvenerek kod yazmayın. Asıl gösterilmek istenen, sayaç artıran bir FnMut closure'ın bu metoda kabul edilmesidir.

Tersi durumda, yani FnMut beklenen bir yere değeri dışarı taşıyan bir closure verdiğinizde, aldığınız hata E0507 olur. Kitaptaki örneğin bir benzeri aşağıda: closure her çağrıda aynı String değerini vektöre taşımaya çalışıyor, oysa değer ilk çağrıdan sonra artık closure'da değil. Derleyici iki çözüm önerir: değeri klonlamak ya da closure'ın sayaç gibi taşımayan bir iş yapmasını sağlamak. Sayma amacındaysanız bir önceki örnekteki sayaç yaklaşımı hem doğru hem ucuzdur.

rust
fn main() {
    let mut dizi = [5, 3, 8, 1];
    let mut kayit: Vec<String> = Vec::new();
    let mesaj = String::from("çağrıldı");
    dizi.sort_by_key(|x| {
        kayit.push(mesaj);
        *x
    });
}
// error[E0507]: cannot move out of `mesaj`, a captured variable in an `FnMut` closure
TraitGövde ne yapabilirKaç kez çağrılırTipik kullanan API
FnOnceYakalanan değeri dışarı taşıyabilirEn az bir kezOption::unwrap_or_else, thread::spawn
FnMutYakalanan değeri değiştirebilir, taşıyamazBirden çok kezsort_by_key, Iterator::map, for_each
FnYalnızca okur ya da hiçbir şey yakalamazBirden çok kez, paylaşılan referanslaAynı anda birden çok yerden çağrılan geri çağırmalar

Closure döndürmek ve saklamak

Her closure'ın tipi benzersiz ve isimsiz olduğundan bir fonksiyonun dönüş tipine closure'ın kendi tipini yazamazsınız. En yaygın çözüm impl Fn(i32) -> i32 gibi bir impl Trait dönüşüdür: çağıran taraf somut tipi bilmez, yalnızca bu trait'i uyguladığını bilir. Ancak her impl Trait dönüşü ayrı bir opak tip üretir. İki farklı fonksiyondan gelen closure'ları aynı vektöre koymak istediğinizde tipler uyuşmaz. O zaman closure'ları Box<dyn Fn(i32) -> i32> içine alıp bir trait object'e dönüştürürsünüz; bu durumda çağrı dinamik dağıtımla yapılır. Trait object'lerin maliyetini ve kurallarını dyn Trait yazısında, kutulamayı Box yazısında anlatıyoruz.

rust
fn toplayici(baslangic: i32) -> impl Fn(i32) -> i32 {
    move |x| x + baslangic
}

fn islemler() -> Vec<Box<dyn Fn(i32) -> i32>> {
    vec![Box::new(|x| x + 1), Box::new(|x| x * x), Box::new(toplayici(100))]
}

fn uygula(f: fn(i32) -> i32, deger: i32) -> i32 {
    f(deger)
}

fn main() {
    let on_ekle = toplayici(10);
    println!("{}", on_ekle(5));

    for f in islemler() {
        println!("{}", f(7));
    }

    let ciftle = |x| x * 2;
    println!("{}", uygula(ciftle, 21));

    let metinler: Vec<String> = [1, 2, 3].iter().map(ToString::to_string).collect();
    println!("{metinler:?}");
}
// çıktı: 15
// çıktı: 8
// çıktı: 49
// çıktı: 107
// çıktı: 42
// çıktı: ["1", "2", "3"]

toplayici fonksiyonundaki move zorunludur: closure baslangic parametresini kullanıyor ve fonksiyon döndüğünde bu parametre yok olacak. move olmadan closure, ömrü bitecek bir yerel değişkene referans tutmaya çalışırdı ve derleyici buna izin vermezdi. Örneğin geri kalanı fonksiyon işaretçilerini gösteriyor. Küçük harfle yazılan fn(i32) -> i32 bir trait değil, somut bir tiptir. Hiçbir şey yakalamayan closure'lar bu tipe dönüştürülebilir; ciftle bu yüzden uygula fonksiyonuna verilebildi. Bir şey yakalayan closure ise fonksiyon işaretçisine dönüşemez, çünkü taşıması gereken bir veri vardır.

Ters yön her zaman serbesttir: fonksiyon işaretçileri üç closure trait'ini de uygular. Bu yüzden kitabın önerisi, kendi API'lerinizde mümkünse generic bir parametre ve closure trait'i sınırı kullanmanızdır; böylece fonksiyonunuz hem closure hem de isimli fonksiyon kabul eder. Yalnızca fn kabul etmeniz gereken başlıca durum, closure kavramı olmayan C gibi dillerle konuşmaktır. Son satırdaki map(ToString::to_string) da aynı fikrin bir uygulaması: closure yazmak yerine var olan bir fonksiyonun yolunu veriyorsunuz. İki yazım da aynı koda derlenir; okunabilir bulduğunuzu seçin.

Sık yapılan hatalar

  • Closure değişkenini mut yapmamak. Değiştirilebilir yakalayan bir closure'ı çağırabilmek için let mut f = || ... yazmalısınız. Unutursanız derleyici closure'ı değiştirilebilir ödünç alamadığını söyler.
  • move kelimesini her derde deva sanmak. move yalnızca yakalama biçimini değiştirir. Taşınan değeri closure'dan sonra kullanmak istiyorsanız önce klonlayıp klonu taşıyın; Copy tipler için ise move değeri kopyalar ve orijinal kullanılabilir kalır.
  • Gereğinden dar trait sınırı seçmek. Closure'ı bir kez çağıran bir fonksiyon Fn isterse, değer taşıyan closure'ları boşuna reddeder. Sınırı ihtiyaca göre FnOnce, FnMut, Fn sırasıyla daraltın.
  • Tiplenmemiş closure'ı farklı tiplerle çağırmak. İlk kullanım tipi kilitler. Generic davranış gerekiyorsa generic fonksiyon yazın.
  • Farklı closure'ları aynı koleksiyona koymaya çalışmak. Her closure ayrı tiptir; ortak bir yapı için Box<dyn Fn...> kullanın.

Sık Sorulan Sorular

Closure ile fonksiyon arasında performans farkı var mı?

Generic bir parametre üzerinden geçirilen closure, derleme zamanında kendi somut tipiyle derlenir; generic kodun her somut tip için ayrı üretilmesi monomorphization olarak bilinir. Kitap, closure ve iterator'ların çalışma zamanı maliyeti eklemeyen soyutlamalar olduğunu vurgular. Box<dyn Fn> kullandığınızda ise heap ayırma ve dinamik dağıtım devreye girer.

move yazdığım closure neden hâlâ Fn olabiliyor?

Çünkü uygulanan trait, değerlerin closure'a nasıl girdiğine değil, gövdenin onlarla ne yaptığına göre belirlenir. Sahipliği alınmış bir değeri yalnızca okuyan closure, değeri değiştirmediği ve dışarı taşımadığı için Fn uygular.

Bir closure kendini özyinelemeli olarak çağırabilir mi?

Closure'ın tipi isimsiz olduğu için gövdesinden kendisine doğrudan isimle ulaşamazsınız. Özyineleme gerekiyorsa sıradan bir fn yazmak en sade yoldur; dış değişkenlere ihtiyaç varsa onları parametre olarak geçirin.

Closure'ı struct alanında nasıl saklarım?

Generic bir tip parametresi ve sınır kullanabilirsiniz: struct Onbellek<F: Fn(u32) -> u32> { hesap: F }. Farklı closure'ları aynı alanda değiştirmeniz gerekiyorsa alanı Box<dyn Fn(u32) -> u32> yapın.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Rust öğrenme rehberi · Iterator: map, filter, collect · thread::spawn ve join · Trait object ve dyn · Trait tanımlama ve impl · Rust nedir?