00 C++ lambda'dan Rust closure'a
C++'ta lambda kapanışı derleyicinin ürettiği anonim bir operator() sınıfıdır; Rust'ta closure da aynen öyle — ama capture davranışı kodda değil tipte ve trait'lerde kodlanır.
C/C++ tarafından geldiğin için manzarayı zaten biliyorsun. Function pointer (int (*)(int)) durum taşıyamaz, sadece kod adresidir. C++11 lambda bunu değiştirir: [capture](args){ ... } ifadesi, derleyicinin ürettiği isimsiz bir closure type'tır — capture listesini saklayan alanları olan, operator() aşırı yüklemesine sahip bir sınıf. std::function<int(int)> ise bu tipi heap'e kutulayan (genelde) bir type-erasure sarmalayıcısıdır.
Rust closure'ı kavramsal olarak C++ lambda'sının birebir karşılığıdır: derleyici, capture ettiğin değişkenleri alan olarak tutan anonim bir struct üretir ve ona bir çağırma metodu (call) ekler. Aradaki en kritik fark şudur: her closure'ın benzersiz, isimlendirilemez bir tipi vardır. İki closure aynı imzaya sahip olsa bile farklı tiplerdir — tıpkı C++'ta iki ayrı lambda'nın farklı closure type üretmesi gibi.
// C++: factor'ü değerle yakalayan anonim closure type üretir.
int factor = 3;
auto mul = [factor](int x){ return x * factor; };
int y = mul(10); // 30
fn main() {
let factor = 3;
// factor otomatik olarak (referansla) yakalanır.
let mul = |x: i32| x * factor;
let y = mul(10); // 30
println!("{y}");
}
Bu benzersiz-tip kuralı pratikte iki sonuç doğurur. Birincisi: bir closure'ın tipini elle yazamazsın, sadece çıkarsanmasına izin verir veya bir trait ardına gizlersin (impl Fn, Box<dyn Fn>). İkincisi: closure'ların nasıl çağrılabileceği üç trait ile tanımlanır — Fn, FnMut, FnOnce. C++'ta bu ayrımları const / mutable / move-only yakalama ile el yordamıyla yaparsın; Rust'ta derleyici otomatik çıkarır ve trait sınırlarıyla zorunlu kılar.
Rust'ta closure tipi isimlendirilemez demek "anonim struct" demektir; ama yine de sıfır-maliyetlidir. std::function'ın aksine Rust closure'ı varsayılan olarak heap'e kutulanmaz — kutulamak istersen bunu açıkça Box<dyn Fn> ile sen söylersin.
Bu bölümde
- Rust closure'ı = C++ lambda'sı: capture'ları alan olarak tutan anonim, derleyici-üretimi bir struct.
- Her closure'ın benzersiz ve isimlendirilemez bir tipi vardır; aynı imza bile farklı tip demektir.
- Çağrılabilirlik
Fn/FnMut/FnOncetrait'leriyle modellenir; C++'taki const/mutable ayrımının dildeki karşılığı. - Varsayılanda kutulama yoktur;
std::functionbenzeri type-erasure'ı senBox<dyn Fn>ile istersin.
01 Closure sözdizimi
Pipe sözdizimi |args| gövde; tip çıkarımı C++'ın auto lambda'sından daha agresiftir — çoğu zaman ne argüman ne dönüş tipi yazman gerekir.
En kısa biçim tek ifadeli gövdedir; süslü parantez gerekmez. Daha karmaşık gövde için blok açarsın. İsteğe bağlı olarak argüman ve dönüş tiplerini de yazabilirsin — ama genelde yazmazsın, çünkü closure'ın tipi kullanım yerinden çıkarsanır.
let add = |x| x + 1; // tipler tamamen çıkarsanır
let add2 = |x: i32| -> i32 { x + 1 }; // tam açık biçim
let noop = || {}; // argümansız, () döner
println!("{}", add(2)); // 3 — fonksiyon gibi çağrılır
println!("{}", add2(2)); // 3
Tip çıkarımının bir inceliği var: closure'ın argüman tipi ilk kullanımda sabitlenir. Bir closure'ı önce i32 ile sonra String ile çağıramazsın — C++'ın template'lenmiş generic lambda'sının ([](auto x){...}) aksine Rust closure'ı tek somut imzaya kilitlenir.
let id = |x| x;
let a = id("merhaba"); // tip burada &str olarak sabitlendi
// let b = id(42); // HATA: beklenen &str, bulunan integer
fn ile fark
Bir fn (named function) capture edemez — çevresindeki yerel değişkenlere erişemez, yalnızca argümanları ve static'leri görür. Closure ise çevresini yakalar. İmza tarafında da fark var: fn'in tipi yazılabilir (fn(i32) -> i32), closure'ınki yazılamaz. Capture etmeyen bir closure, gerektiğinde bir fn pointer'a coerce olabilir (bkz. bölüm 08).
| Özellik | fn (named) | closure |
|---|---|---|
| Çevreyi yakalar | Hayır | Evet |
| Tipi yazılabilir | Evet (fn(...) -> ...) | Hayır (anonim) |
| Trait | Üçünü de implement eder | capture'a göre değişir |
Bu bölümde
- Sözdizimi
|args| ifadeveya|args| -> T { blok }; tipler genelde çıkarsanır. - Çıkarsanan argüman tipi ilk kullanımda sabitlenir; C++ generic lambda gibi çok-tipli olamaz.
fncapture edemez ve tipi yazılabilir; closure capture eder ve anonim tiptir.- Closure tıpkı fonksiyon gibi
()ile çağrılır.
02 Capture semantiği
C++'ta capture'ı [=] / [&] ile sen seçersin; Rust'ta derleyici ihtiyaca göre en az kısıtlayıcı yakalamayı kendisi seçer ve sonucu borrow checker zorlar.
Bir Rust closure çevresindeki bir değişkeni üç yoldan biriyle yakalar: paylaşımlı referansla (&T), değiştirilebilir referansla (&mut T) veya değerle (taşıyarak). Derleyici gövdede o değişkene ne yaptığına bakar ve en az kısıtlayıcı olanı seçer:
&T ile yakala (paylaşımlı).&mut T ile yakala (münhasır).Bu, C++ ile temel felsefe farkıdır. C++'ta [=] her şeyi kopyalar, [&] her şeyi referansla alır — seçim sende ve yanlış seçim (örneğin scope dışına kaçan [&] referansı) dangling reference üretebilir. Rust'ta seçimi derleyici yapar ve borrow checker, yakalanan referansın closure'dan uzun yaşamasını garanti eder; dangling capture safe kodda imkânsızdır.
fn main() {
let liste = vec![1, 2, 3];
// Sadece okuyor -> &liste ile yakalanır (paylaşımlı borrow).
let yazdir = || println!("{liste:?}");
yazdir();
yazdir();
println!("{}", liste.len()); // OK: liste hâlâ erişilebilir
}
fn main() {
let mut sayac = 0;
// Değiştiriyor -> &mut sayac ile yakalanır (münhasır borrow).
let mut arttir = || { sayac += 1; };
arttir();
arttir();
// closure burada bittiğinde &mut borrow serbest kalır.
println!("{sayac}"); // 2
}
Closure bir değişkeni &mut ile yakaladığında, o değişken closure yaşadığı sürece başka yerden erişilemez (Rust'ın tek-yazıcı kuralı). Bu yüzden closure'ı kullanmayı bitirmeden orijinal değişkene dokunmak istersen borrow checker hatası alırsın — bu hata C++'ta gözden kaçan eşzamanlı erişim hatalarının derleme-zamanı versiyonudur.
Bu bölümde
- Capture biçimini derleyici seçer: okuma →
&T, yazma →&mut T, tüketme → değer. - Her zaman en az kısıtlayıcı yakalama tercih edilir; C++'taki açık
[=]/[&]seçiminin tersine otomatiktir. &mutile yakalanan değişken, closure yaşadığı sürece başka yerden erişilemez.- Borrow checker, yakalanan referansların dangling olmasını derleme zamanında engeller.
03 move closure
Varsayılan referans-yakalamayı geçersiz kılıp her şeyi değerle almaya zorlamanın yolu move anahtar kelimesidir — C++'taki [=] + move-capture'ın net karşılığı.
Bazen referansla yakalama yeterli değildir: closure'ı, yakaladığı değişkenlerden daha uzun yaşayacak bir yere göndermek istersin — başka bir thread'e, bir Box'a, ya da bir fonksiyondan döndüreceğin bir değere. Bu durumlarda referans-yakalama derhal borrow checker hatasıyla reddedilir, çünkü yakalanan referans kaynağından uzun yaşayamaz. Çözüm move kapanışıdır: move, derleyiciye "tüm yakalananları değerle al, ownership'i closure'a taşı" der.
use std::thread;
fn main() {
let veri = vec![1, 2, 3];
// move: veri'nin ownership'i closure'a taşınır.
let handle = thread::spawn(move || {
println!("thread: {veri:?}");
});
// println!("{veri:?}"); // HATA: veri taşındı, artık burada yok
handle.join().unwrap();
}
Neden move şart? thread::spawn'ın aldığı closure 'static ömre sahip olmalıdır — çünkü oluşturan thread'den sonra da çalışabilir. Referansla yakalansaydı, ana thread veri'yi düşürünce iş parçacığı dangling referansla kalırdı. move bunu imkânsız kılar: veri tamamen closure'a aittir artık.
move ile Copy olan tipler (i32, bool, ...) kopyalanır, taşınmaz — yani move closure içinde bir i32 kullanmak orijinali geçersizleştirmez. move yalnızca yakalama biçimini "değerle" yapar; bu non-Copy tiplerde taşıma, Copy tiplerde kopya anlamına gelir.
C++ karşılaştırması nettir: move closure ≈ [=] (değerle yakala) artık std::move'lu init-capture ([v = std::move(v)]). Fark, Rust'ın bunu zorunlu kılmasıdır — referansla yakalama ömür kontrolünü geçemiyorsa kod derlenmez, oysa C++ aynı durumda sessizce dangling referans üretir.
Bu bölümde
move ||tüm yakalananları değerle alır; ownership'i closure'a taşır.- Closure kaynağından uzun yaşayacaksa (thread, dönüş değeri, saklama)
movegerekir. Copytiplermoveile kopyalanır; non-Copytipler taşınır ve orijinal geçersizleşir.- C++'ın init-capture move'unun karşılığı, ama ömür güvenliği derleyici tarafından zorunlu.
04 Fn / FnMut / FnOnce
Bir closure'ın nasıl çağrılabileceğini tanımlayan üç trait: paylaşımlı çağrı, değiştirerek çağrı, bir kez tüketerek çağrı. Hangisini implement ettiğini capture biçimi belirler.
Bölüm 02'deki üç capture biçiminin doğrudan bir karşılığı vardır. Closure'ın gövdesi yakaladıklarına ne yaptıysa, derleyici uygun trait'leri otomatik implement eder:
self alır → closure'ı tüketerek bir kez çağrılır. Yakalananları taşıyan/tüketen closure'lar yalnızca bunu implement edebilir.&mut self alır → yakaladığı durumu değiştirerek, defalarca çağrılabilir.&self alır → durumu değiştirmeden, paylaşımlı ve defalarca çağrılabilir.Bu üçü bir hiyerarşi oluşturur: Fn: FnMut: FnOnce. Yani her Fn aynı zamanda FnMut'tur, her FnMut aynı zamanda FnOnce'tur. Sezgi: paylaşımlı referansla çağırabiliyorsan, münhasır referansla da çağırabilirsin (daha güçlü erişim); münhasırla çağırabiliyorsan tüketerek de çağırabilirsin. Bu yüzden FnOnce en geneldir (en az talep eden sınır), Fn en kısıtlayıcısıdır.
Fn ⊂ FnMut ⊂ FnOnce (Fn en kısıtlayıcı, FnOnce en genel)
fn main() {
let metin = String::from("selam");
// Fn: sadece okuyor, &self ile defalarca çağrılır.
let oku = || println!("{metin}");
oku(); oku();
let mut tampon = String::new();
// FnMut: durumu değiştiriyor, &mut self ile defalarca.
let mut ekle = |s: &str| tampon.push_str(s);
ekle("a"); ekle("b");
let sahip = String::from("taşınacak");
// FnOnce: yakalananı tüketiyor, yalnızca bir kez çağrılabilir.
let tuket = move || { let x = sahip; x.len() };
let n = tuket();
// let m = tuket(); // HATA: value used here after move
println!("{n}");
}
C++ açısından: Fn ≈ const operator()'lı lambda, FnMut ≈ mutable lambda, FnOnce ≈ yakaladığı move-only kaynağı tüketen lambda (örneğin [p = std::move(unique_ptr)](){ consume(std::move(p)); }). Fark: C++ bu ayrımı tip sisteminde zorlamaz — bir FnOnce mantığındaki lambda'yı iki kez çağırırsan moved-from durumdan zehirli sonuç alırsın. Rust ikinci çağrıyı derlemez.
Bu bölümde
Fn=&self(oku, çoklu),FnMut=&mut self(değiştir, çoklu),FnOnce=self(tüket, tek).- Hiyerarşi
Fn: FnMut: FnOnce;FnOnceen genel,Fnen kısıtlayıcı sınırdır. - Hangi trait'leri implement ettiğini closure'ın gövdesi (capture'a ne yaptığı) belirler — sen değil.
- Yakaladığını tüketen closure yalnızca
FnOnce'tur ve ikinci kez çağrılamaz; bu derleme-zamanı zorlanır.
05 Closure'ı parametre olarak almak
Bir fonksiyon closure alacaksa üç yolun var: generic <F: Fn> (statik, monomorfize, hızlı), impl Fn (aynısının kısa yazımı) ve Box<dyn Fn> (dinamik dağıtım).
Closure tipleri isimlendirilemez olduğu için, parametre tipini ya bir trait sınırıyla generic yaparsın ya da bir trait nesnesinin (dyn) ardına gizlersin. İlk soru her zaman aynı: statik dağıtım mı, dinamik dağıtım mı?
Generic — <F: Fn(...)>
Trait sınırlı generic; her çağrı yeri için derleyici closure tipine özel bir kopya üretir (monomorphization). Çağrı inline'lanabilir, vtable yoktur — C++ template'inin aldığı bir functor argümanına denktir. En hızlı, en çok kod üreten yol.
// İki kez uygula: F closure'ın somut tipi (monomorfize).
fn iki_kez<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(f(x))
}
// FnMut isteyen, durum değiştiren tüketici:
fn uygula_n<F: FnMut()>(mut f: F, n: u32) {
for _ in 0..n { f(); }
}
fn main() {
println!("{}", iki_kez(|x| x + 3, 1)); // 7
let mut sayac = 0;
uygula_n(|| sayac += 1, 5);
println!("{sayac}"); // 5
}
impl Fn — aynısının özeti
fn iki_kez(f: impl Fn(i32) -> i32, x: i32) yazımı, yukarıdaki generic biçimin sözdizimsel kısaltmasıdır — derleme sonucu birebir aynı (yine monomorfize, yine statik). Tek closure parametresi olduğunda ve adlandırılmış bir tip parametresine ihtiyacın olmadığında tercih edilir.
Box<dyn Fn> — dinamik dağıtım
Closure'ı heap'e kutulayıp bir vtable üzerinden çağırırsın — bu, C++'taki std::function<int(int)>'ın birebir karşılığıdır. Farklı tipte closure'ları aynı koleksiyonda saklamak veya çalışma-zamanında seçmek gerektiğinde kaçınılmazdır. Bedeli: bir pointer dolaylaması, olası heap tahsisi ve inline kaybı.
// Farklı closure tiplerini tek Vec'te saklamak -> dyn şart.
fn main() {
let islemler: Vec<Box<dyn Fn(i32) -> i32>> = vec![
Box::new(|x| x + 1),
Box::new(|x| x * 2),
];
let mut v = 10;
for op in &islemler { v = op(v); }
println!("{v}"); // (10+1)*2 = 22
}
| Yöntem | Dağıtım | Maliyet | Ne zaman |
|---|---|---|---|
<F: Fn> | statik | sıfır (inline) | varsayılan; hot path |
impl Fn | statik | sıfır (inline) | tek param, kısa yazım |
Box<dyn Fn> | dinamik | vtable + heap | heterojen saklama / runtime seçim |
Trait sınırını her zaman gerçekten gereken en gevşek trait ile koy. Closure'ı yalnızca bir kez çağıracaksan FnOnce, durum değiştireceksen FnMut, sadece okuyacaksan Fn iste. Gereğinden sıkı sınır (örneğin gereksiz Fn) çağıranı boşyere kısıtlar — FnOnce closure'ını kabul etmez.
Bu bölümde
- Generic
<F: Fn>veimpl Fnaynı şeydir: statik dağıtım, monomorfize, inline'lanabilir. Box<dyn Fn>=std::functionbenzeri dinamik dağıtım; heterojen saklama veya runtime seçim için.- Statik yol varsayılan ve sıfır-maliyetlidir; dinamik yol bir vtable dolaylaması ekler.
- Sınırı en gevşek geçerli trait ile koy (
FnOnce<FnMut<Fn) ki çağıranı gereksiz kısıtlamayasın.
06 Closure döndürmek
Closure tipi isimlendirilemez ve boyutu çağrı yerine kadar bilinmez — bu yüzden onu çıplak döndüremezsin; impl Fn (tercih) veya Box<dyn Fn> ile sararsın.
Bir fonksiyon closure üretip döndürebilir — klasik factory deseni. Ama bir engel var: dönüş tipini yazman gerekir, oysa closure'ın tipi anonim ve isimlendirilemezdir. Üstelik fonksiyonun dönüş değeri Sized olmalı (derleme zamanında boyutu bilinmeli), bare closure ise tip değil bir trait kümesi gibi davranır. İki çözüm var.
impl Fn — tercih edilen yol
-> impl Fn(...) derleyiciye "somut ama isimsiz bir tip döndürüyorum, ayrıntısını gizle" der. Statik dağıtım korunur, heap tahsisi yoktur. Tek kısıt: fonksiyon tek somut closure tipi döndürebilir (farklı dallarda farklı closure döndüremezsin).
// Factory: çarpan'ı yakalayan bir closure üretir.
fn carpan_yap(k: i32) -> impl Fn(i32) -> i32 {
// move: k closure'a taşınır, fonksiyon dönünce yaşamaya devam eder.
move |x| x * k
}
fn main() {
let uc_kat = carpan_yap(3);
println!("{}", uc_kat(10)); // 30
}
move burada zorunlu: k fonksiyonun yerel değişkenidir, fonksiyon dönünce stack'i yok olur. Referansla yakalansaydı dönen closure dangling referans tutardı — borrow checker bunu reddeder. move ile k closure'ın içine taşınır ve onunla birlikte yaşar.
Box<dyn Fn> — farklı tipler dönmek gerektiğinde
Eğer farklı dallarda farklı closure'lar döndürmen gerekiyorsa, bunlar farklı tiplerdir ve impl Fn tek tipe izin verdiği için yetmez. Trait nesnesiyle kutularsın:
fn islem_sec(toplama: bool) -> Box<dyn Fn(i32, i32) -> i32> {
if toplama {
Box::new(|a, b| a + b)
} else {
Box::new(|a, b| a - b) // farklı tip -> impl Fn yetmez, Box gerek
}
}
fn main() {
let f = islem_sec(true);
println!("{}", f(7, 2)); // 9
}
Bu bölümde
- Closure çıplak döndürülemez: tipi isimsiz ve dönüş tipi
Sizedolmalı. -> impl Fntercih edilen yoldur: statik, heap'siz, ama tek somut tip döndürür.- Factory'de yerel değişken yakalayan closure döndürürken
movezorunludur (dangling önlenir). - Farklı dallarda farklı closure tipleri dönmek gerekiyorsa
Box<dyn Fn>kullanılır.
07 Closure'lar ve iterator'lar
Closure'ın doğal habitatı iterator adaptörleridir: map, filter, fold hepsi closure alır — ve capture'lı closure ile durum taşıyarak C++'ın elle döngülerini eler.
Iterator adaptörleri tembeldir (lazy) ve aldıkları closure'ı her eleman için çağırır. map/filter genelde FnMut ister (eleman başına çağrı, durum tutabilir), fold bir akümülatör closure'ı alır. Bunlar statik dağıtımla, çoğu zaman elle yazılmış C döngüsüyle birebir aynı makine koduna derlenir — sıfır-maliyet soyutlama.
fn main() {
let sayilar = vec![1, 2, 3, 4, 5, 6];
let sonuc: i32 = sayilar
.iter()
.filter(|&&x| x % 2 == 0) // closure: çift olanlar
.map(|&x| x * x) // closure: karesini al
.sum(); // 4 + 16 + 36 = 56
println!("{sonuc}");
}
Capture ile durum taşımak
İşin gücü, closure'ın çevresini yakalayabilmesinden gelir. Bir eşiği değişkenden yakalayıp filtreye geçirebilir, ya da FnMut bir closure ile iterasyon boyunca durum biriktirebilirsin. Bu, bölüm 02–04'teki capture/FnMut bilgisinin doğrudan uygulamasıdır.
fn main() {
let esik = 3; // closure bunu yakalayacak
let sayilar = vec![1, 5, 2, 8, 3];
// 'esik' filtreye yakalanır (paylaşımlı borrow).
let buyukler: Vec<_> = sayilar
.iter()
.filter(|&&x| x > esik)
.collect();
println!("{buyukler:?}"); // [5, 8]
// FnMut closure ile koşan toplam (durum: 'akan').
let mut akan = 0;
let kumulatif: Vec<i32> = sayilar
.iter()
.map(|&x| { akan += x; akan }) // her adımda durumu günceller
.collect();
println!("{kumulatif:?}"); // [1, 6, 8, 16, 19]
}
.iter() referans verir; bu yüzden closure parametresi &&x ya da &x gibi pattern'larla referansı söker. Değerleri tüketmek (ve move closure'larla taşımak) istiyorsan .into_iter() kullanırsın — koleksiyonun sahipliğini iterator'a geçirir.
Bu bölümde
map/filter/foldclosure alır ve her eleman için çağırır; lazy ve sıfır-maliyet derlenir.- Closure çevresini yakalayarak (örneğin bir eşik) filtre/dönüşüm parametrelerini taşır.
FnMutclosure iterasyon boyunca durum biriktirir (koşan toplam, sayaç vb.)..iter()referans,.into_iter()değer verir; pattern'da&ile referans sökülür.
08 fn pointer vs closure
Capture etmeyen bir closure, bir fn pointer'a coerce olur — bu özellik özellikle FFI ve C callback'lerinde kritiktir; çünkü dyn Fn bir C imzasına geçemez.
fn(i32) -> i32 bir function pointer tipidir — tam olarak C'deki int (*)(int). Durum taşımaz, tek bir kod adresidir, boyutu sabittir ve ABI-uyumludur. Önemli kolaylık: hiçbir şey capture etmeyen bir closure bu tipe otomatik coerce olur.
// fn pointer kabul eden bir fonksiyon.
fn uygula(f: fn(i32) -> i32, x: i32) -> i32 { f(x) }
fn iki_kat(x: i32) -> i32 { x * 2 }
fn main() {
println!("{}", uygula(iki_kat, 5)); // 10 — named fn
println!("{}", uygula(|x| x + 1, 5)); // 6 — capture'sız closure coerce oldu
// let k = 3;
// uygula(|x| x * k, 5); // HATA: k yakalandığı için fn'e coerce olmaz
}
FFI / C callback
C API'lerine callback geçirirken yalnızca extern "C" fn pointer'ları kullanabilirsin — bir Rust closure'ı (dyn Fn) doğrudan C'ye veremezsin, çünkü C tarafı tek bir kod adresi ve genelde bir void* "user data" bekler. Tipik desen: capture'sız bir extern "C" fn trampoline yaz, durumu void* üzerinden taşı.
// C imzası: void register_cb(void (*cb)(int));
extern "C" {
fn register_cb(cb: extern "C" fn(i32));
}
// Trampoline: capture etmez, C'ye geçebilir.
extern "C" fn on_event(kod: i32) {
println!("olay: {kod}");
}
fn kur() {
unsafe { register_cb(on_event); } // closure değil, fn pointer
}
Denge şu: fn pointer en hızlı ve en taşınabilir olandır ama durum taşıyamaz. Closure esnektir (durum taşır) ama soyutlandığında (dyn Fn) bir vtable maliyeti gelir ve FFI sınırından geçemez. Pratik kural: durum gerekmiyorsa ya da C ile konuşuyorsan fn; durum + saf-Rust esneklik gerekiyorsa closure trait'leri.
fn pointer | closure | |
|---|---|---|
| Durum taşır | Hayır | Evet (capture) |
| Boyut | sabit (tek adres) | capture'a göre değişken |
| FFI'ye geçer | Evet (extern "C" fn) | Hayır (trampoline gerek) |
Bu bölümde
fn(...)tipi C function pointer'ın karşılığıdır: durumsuz, sabit boyut, ABI-uyumlu.- Capture etmeyen closure otomatik olarak
fnpointer'a coerce olur; capture eden olmaz. - FFI/C callback'lerinde yalnızca
extern "C" fngeçer; closure için trampoline +void*deseni gerekir. - Denge: durumsuz/taşınabilir için
fn, durumlu/esnek saf-Rust için closure trait'leri.
09 Gerçek örnek
Tüm parçaları birleştiren bir retry-with-backoff kapanışı: bir işlemi closure olarak alıp, başarısızsa artan beklemeyle yeniden dener — closure'ı saklama, FnMut sınırı ve capture'lı durum bir arada.
Senin dünyandan tanıdık bir problem: ağ/donanım çağrısı geçici olarak başarısız olabilir ve exponential backoff ile yeniden denemek istersin. C++'ta bunu bir functor + döngü ile yazardın; Rust'ta işlemi bir closure olarak parametre alır, trait sınırıyla "değiştirerek defalarca çağrılabilir" (FnMut) deriz ve gerisini generic fonksiyon halleder.
use std::thread::sleep;
use std::time::Duration;
// İşlemi closure olarak al; başarısızsa artan beklemeyle tekrar dene.
// FnMut: deneme sayacı gibi iç durumu güncelleyebilen closure'lara izin verir.
fn retry<F, T, E>(mut islem: F, max: u32) -> Result<T, E>
where
F: FnMut() -> Result<T, E>,
{
let mut bekle = 50;
for deneme in 1..=max {
match islem() {
Ok(v) => return Ok(v),
Err(e) if deneme == max => return Err(e),
Err(_) => {
sleep(Duration::from_millis(bekle));
bekle *= 2; // exponential backoff
}
}
}
unreachable!()
}
fn main() {
let mut sayac = 0; // closure'a &mut ile yakalanacak durum
// İlk 2 deneme başarısız, 3.'de başarılı simülasyonu.
let sonuc = retry(|| {
sayac += 1;
if sayac < 3 {
Err("geçici hata")
} else {
Ok("bağlandı")
}
}, 5);
println!("{sonuc:?} ({sayac} deneme)"); // Ok("bağlandı") (3 deneme)
}
Burada her parça bir araya geliyor. retry generic F: FnMut() -> Result<T, E> alıyor — statik dağıtım, sıfır vtable. islem kapanışı sayac'ı &mut ile yakalıyor (bölüm 02), bu yüzden FnMut oluyor (bölüm 04) ve fonksiyon onu mut islem olarak alıp defalarca çağırabiliyor (bölüm 05). C++'ta aynı şeyi std::function ile yapsaydın her çağrıda vtable dolaylaması yerdin; burada closure inline'lanıp retry'a gömülüyor.
Closure'ı saklayıp sonra çağırmak
Bir başka yaygın desen: closure'ı bir struct'a koyup olay geldiğinde çağırmak — klasik event handler. dyn Fn ile saklarsın, çünkü struct'ın farklı örnekleri farklı closure tipleri tutabilir:
struct Buton {
// kutulanmış closure: handler'ı saklar, sonra çağırır.
on_click: Box<dyn Fn()>,
}
fn main() {
let etiket = String::from("Kaydet");
let btn = Buton {
// move: etiket handler'a taşınır, buton ile birlikte yaşar.
on_click: Box::new(move || println!("{etiket} tıklandı")),
};
(btn.on_click)(); // olay tetiklendiğinde: "Kaydet tıklandı"
}
Özet refleks: statik ve sıcak yol ise impl Fn / generic kullan; saklamak, heterojen tutmak veya runtime seçmek gerekiyorsa Box<dyn Fn> kullan. Closure kaynağından uzun yaşayacaksa (struct'a koymak, thread'e/döngüye geçmek) move ekle. Trait sınırını her zaman gereken en gevşek olanla (FnOnce < FnMut < Fn) seç.
Bu bölümde
- Gerçek desen: generic
FnMutclosure alan, exponential backoff ile yeniden deneyenretry. - Tüm konular birleşti: capture (&mut sayac) →
FnMut→ generic parametre → inline statik dağıtım. - Closure'ı saklayıp sonra çağırmak için struct'a
Box<dyn Fn>koyulur (event handler deseni). - Refleks: sıcak yol →
impl Fn; saklama/heterojen →Box<dyn Fn>; uzun ömür →move; sınırı en gevşek trait ile koy.