Kapanışlar (Closures)
Rust’taki kapanışlar (closures), bir değişkende saklayabileceğiniz veya başka fonksiyonlara argüman olarak aktarabileceğiniz anonim fonksiyonlardır. Kapanışı bir yerde oluşturup daha sonra farklı bir bağlamda çalıştırmak üzere başka bir yerde çağırabilirsiniz. Normal fonksiyonların aksine kapanışlar, tanımlandıkları kapsamdaki değerleri yakalayabilir (capture). Bu özelliklerin kodun yeniden kullanımına ve davranışların özelleştirilmesine nasıl olanak tanıdığını birlikte inceleyeceğiz.
Çevreyi Yakalamak
İlk olarak, tanımlandıkları çevredeki değerleri daha sonra kullanmak üzere yakalamak için kapanışlardan nasıl yararlanabileceğimizi inceleyeceğiz. Senaryomuz şöyle: Gömlek şirketimiz zaman zaman promosyon olarak e-posta listesindeki bir kişiye özel, sınırlı sayıda üretilmiş bir gömlek hediye ediyor. E-posta listesindeki kişiler isteğe bağlı olarak profillerine favori renklerini ekleyebiliyor. Ücretsiz gömlek için seçilen kişi favori rengini belirtmişse o renkteki gömleği alıyor. Favori bir renk belirtmemişse şirketin stoklarında o an en fazla hangi renk varsa onu alıyor.
Bunu uygulamanın pek çok yolu vardır. Bu örnekte, basitlik adına mevcut renk sayısını sınırlayarak Kirmizi ve Mavi varyantlarına sahip GomlekRengi adlı bir enum kullanacağız. Şirketin envanterini ise stokta bulunan gömlek renklerini tutan Vec<GomlekRengi> türünde gomlekler adlı bir alana sahip Envanter struct’ı ile temsil ediyoruz. Envanter üzerinde tanımlanan hediye_et metodu, ücretsiz gömlek kazanan kişinin isteğe bağlı renk tercihini alır ve alacağı gömlek rengini döndürür. Bu yapı Liste 13-1’de gösterilmiştir.
#[derive(Debug, PartialEq, Copy, Clone)]
enum GomlekRengi {
Kirmizi,
Mavi,
}
struct Envanter {
gomlekler: Vec<GomlekRengi>,
}
impl Envanter {
fn hediye_et(&self, kullanici_tercihi: Option<GomlekRengi>) -> GomlekRengi {
kullanici_tercihi.unwrap_or_else(|| self.en_cok_stoklanan())
}
fn en_cok_stoklanan(&self) -> GomlekRengi {
let mut kirmizi_sayisi = 0;
let mut mavi_sayisi = 0;
for renk in &self.gomlekler {
match renk {
GomlekRengi::Kirmizi => kirmizi_sayisi += 1,
GomlekRengi::Mavi => mavi_sayisi += 1,
}
}
if kirmizi_sayisi > mavi_sayisi {
GomlekRengi::Kirmizi
} else {
GomlekRengi::Mavi
}
}
}
fn main() {
let magaza = Envanter {
gomlekler: vec![GomlekRengi::Mavi, GomlekRengi::Kirmizi, GomlekRengi::Mavi],
};
let kullanici_tercihi1 = Some(GomlekRengi::Kirmizi);
let hediye1 = magaza.hediye_et(kullanici_tercihi1);
println!(
"{:?} tercihine sahip kullanıcı {:?} alıyor",
kullanici_tercihi1, hediye1
);
let kullanici_tercihi2 = None;
let hediye2 = magaza.hediye_et(kullanici_tercihi2);
println!(
"{:?} tercihine sahip kullanıcı {:?} alıyor",
kullanici_tercihi2, hediye2
);
}
main fonksiyonunda tanımlanan magaza, bu sınırlı üretim promosyonunda dağıtılmak üzere kalan iki mavi ve bir kırmızı gömleğe sahiptir. hediye_et metodunu kırmızı gömlek tercih eden bir kullanıcı ve herhangi bir tercihi olmayan başka bir kullanıcı için çağırıyoruz.
Bu kod da pek çok farklı şekilde yazılabilirdi; burada doğrudan kapanışlara odaklanmak adına, kapanış kullanan hediye_et metodunun gövdesi dışında daha önce öğrendiğiniz kavramlara bağlı kaldık. hediye_et metodunda kullanıcı tercihini Option<GomlekRengi> türünde bir parametre olarak alıyor ve kullanici_tercihi üzerinde unwrap_or_else metodunu çağırıyoruz. Option<T> üzerindeki unwrap_or_else metodu standart kütüphane tarafından tanımlanmıştır. Tek bir argüman alır: kendisi hiçbir argüman almayan ve T türünde (Option<T>’nin Some varyantında saklanan tür, yani bu örnekte GomlekRengi) bir değer döndüren bir kapanış. Option<T> değeri Some varyantıysa unwrap_or_else, Some içindeki değeri doğrudan döndürür. Option<T> değeri None varyantıysa unwrap_or_else bu kapanışı çağırır ve kapanışın ürettiği değeri döndürür.
unwrap_or_else metoduna argüman olarak || self.en_cok_stoklanan() kapanış ifadesini veriyoruz. Bu, hiçbir parametre almayan bir kapanıştır (kapanışın parametreleri olsaydı iki dikey çubuk |...| arasında yer alırdı). Kapanışın gövdesi self.en_cok_stoklanan() metodunu çağırır. Kapanışı burada tanımlıyoruz; unwrap_or_else uygulaması ise sonuca gerçekten ihtiyaç duyulursa (yani değer None ise) kapanışı daha sonra çalıştıracaktır.
Bu kodu çalıştırdığımızda aşağıdaki çıktıyı alırız:
$ cargo run
Compiling gomlek-sirketi v0.1.0 (file:///projects/gomlek-sirketi)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/gomlek-sirketi`
Some(Kirmizi) tercihine sahip kullanıcı Kirmizi alıyor
None tercihine sahip kullanıcı Mavi alıyor
Buradaki dikkat çekici nokta, mevcut Envanter örneği üzerinde self.en_cok_stoklanan() metodunu çağıran bir kapanış aktarmış olmamızdır. Standart kütüphanenin tanımladığımız Envanter veya GomlekRengi türleri ya da bu senaryoda çalıştırmak istediğimiz mantık hakkında hiçbir şey bilmesine gerek kalmadı. Kapanış, self (Envanter örneği) için değiştirilemez bir referans yakalar ve bunu belirttiğimiz kodla birlikte unwrap_or_else metoduna aktarır. Normal fonksiyonlar ise çevrelerini bu şekilde yakalayamaz.
Kapanış Türlerini Çıkarsamak ve Açıklamak
Fonksiyonlar ile kapanışlar arasında başka farklar da vardır. Kapanışlar genellikle fn fonksiyonlarında olduğu gibi parametrelerin veya dönüş değerinin türlerini açıkça belirtmenizi (tür açıklaması / type annotation eklemenizi) gerektirmez. Fonksiyonlarda tür açıklamaları zorunludur; çünkü bu türler kullanıcılarınıza sunulan açık arayüzün bir parçasıdır. Bu arayüzü katı bir şekilde tanımlamak, bir fonksiyonun hangi türde değerler aldığı ve döndürdüğü konusunda herkesin mutabık kalmasını sağlamak açısından önemlidir. Kapanışlar ise böyle dışa açık bir arayüzde kullanılmaz: değişkenlerde saklanır ve bir isim verilmeden ya da kütüphanemizin kullanıcılarına açılmadan kullanılırlar.
Kapanışlar genellikle kısadır ve her yerde geçerli genel senaryolardan ziyade yalnızca dar bir bağlamda anlam taşır. Bu sınırlı bağlamlar içinde derleyici, çoğu değişkenin türünü çıkarsayabildiği gibi kapanışların parametre türlerini ve dönüş türünü de çıkarsayabilir (derleyicinin kapanışlarda da tür açıklamasına ihtiyaç duyduğu nadir durumlar vardır).
Değişkenlerde olduğu gibi, zorunlu olandan daha ayrıntılı yazma pahasına açıklığı ve okunabilirliği artırmak istersek kapanışlara da tür açıklamaları ekleyebiliriz. Tür açıklamaları eklenmiş bir kapanış, Liste 13-2’deki tanıma benzer. Bu örnekte kapanışı, Liste 13-1’de yaptığımız gibi argüman olarak aktardığımız noktada tanımlamak yerine ayrı tanımlayıp bir değişkende saklıyoruz.
use std::thread;
use std::time::Duration;
fn egzersiz_olustur(yogunluk: u32, rastgele_sayi: u32) {
let pahali_kapanis = |sayi: u32| -> u32 {
println!("yavaşça hesaplanıyor...");
thread::sleep(Duration::from_secs(2));
sayi
};
if yogunluk < 25 {
println!("Bugün {} şınav çekin!", pahali_kapanis(yogunluk));
println!("Ardından {} mekik çekin!", pahali_kapanis(yogunluk));
} else {
if rastgele_sayi == 3 {
println!("Bugün ara verin! Bol su içmeyi unutmayın!");
} else {
println!(
"Bugün {} dakika koşun!",
pahali_kapanis(yogunluk)
);
}
}
}
fn main() {
let simule_edilmis_kullanici_degeri = 10;
let simule_edilmis_rastgele_sayi = 7;
egzersiz_olustur(simule_edilmis_kullanici_degeri, simule_edilmis_rastgele_sayi);
}
Tür açıklamaları eklendiğinde kapanışların sözdizimi fonksiyonların sözdizimine oldukça benzer. Karşılaştırmak amacıyla aşağıda parametresine 1 ekleyen bir fonksiyon ile aynı davranışa sahip kapanış tanımlarını görüyoruz. İlgili kısımları hizalamak için fazladan boşluklar ekledik. Bu örnek, dikey çubukların (|...|) kullanımı ve isteğe bağlı sözdizimi esnekliği dışında kapanış sözdiziminin fonksiyon sözdizimine ne kadar benzediğini gösterir:
fn bir_ekle_v1 (x: u32) -> u32 { x + 1 }
let bir_ekle_v2 = |x: u32| -> u32 { x + 1 };
let bir_ekle_v3 = |x| { x + 1 };
let bir_ekle_v4 = |x| x + 1 ;
İlk satır bir fonksiyon tanımını, ikinci satır ise türleri tamamen belirtilmiş bir kapanış tanımını gösterir. Üçüncü satırda kapanış tanımından tür açıklamalarını kaldırıyoruz. Dördüncü satırda ise kapanış gövdesi tek bir ifadeden oluştuğu için isteğe bağlı olan süslü parantezleri kaldırıyoruz. Bunların tümü çağrıldıklarında aynı davranışı üretecek geçerli tanımlardır. bir_ekle_v3 ve bir_ekle_v4 satırlarının derlenebilmesi için bu kapanışların bir yerde çağrılması gerekir; çünkü parametre ve dönüş türleri kullanımlarından çıkarsanacaktır. Bu durum, Rust’ın türü çıkarsayabilmesi için ya tür açıklamasına ya da içine belirli türden değerler eklenmesine ihtiyaç duyan let v = Vec::new(); deyimine benzer.
Kapanış tanımlarında derleyici, her bir parametre ve dönüş değeri için tek bir somut tür çıkarsar. Örneğin Liste 13-3, yalnızca parametre olarak aldığı değeri olduğu gibi döndüren kısa bir kapanış tanımını göstermektedir. Bu kapanış, bu örneğin amacı dışında pek işlevsel değildir. Tanıma herhangi bir tür açıklaması eklemediğimize dikkat edin. Tür açıklaması bulunmadığı için kapanışı herhangi bir türle çağırabiliriz; burada ilk çağrıyı String ile yapıyoruz. Ardından ornek_kapanis’ı bir tam sayıyla çağırmaya çalışırsak derleme hatası alırız.
fn main() {
let ornek_kapanis = |x| x;
let s = ornek_kapanis(String::from("merhaba"));
let n = ornek_kapanis(5);
}
Derleyici bize şu hatayı verir:
$ cargo run
Compiling kapanis-ornek v0.1.0 (file:///projects/kapanis-ornek)
error[E0308]: mismatched types
--> src/main.rs:5:27
|
5 | let n = ornek_kapanis(5);
| ------------- ^ expected `String`, found integer
| |
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `String`
--> src/main.rs:4:27
|
4 | let s = ornek_kapanis(String::from("merhaba"));
| ------------- ^^^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
| |
| in this closure call
note: closure parameter defined here
--> src/main.rs:2:26
|
2 | let ornek_kapanis = |x| x;
| ^
help: try using a conversion method
|
5 | let n = ornek_kapanis(5.to_string());
| ++++++++++++
For more information about this error, try `rustc --explain E0308`.
error: could not compile `kapanis-ornek` (bin "kapanis-ornek") due to 1 previous error
ornek_kapanis’ı String değeriyle ilk kez çağırdığımızda derleyici, x parametresinin türünü ve kapanışın dönüş türünü String olarak çıkarsar. Bu türler artık ornek_kapanis içindeki kapanışa sabitlenir ve aynı kapanışı farklı bir türle tekrar çağırmaya çalıştığımızda tür uyuşmazlığı hatası (E0308) alırız.
Referansları Yakalamak veya Sahipliği Taşımak
Kapanışlar çevrelerindeki değerleri, bir fonksiyonun parametre alabildiği üç yolla birebir örtüşen üç şekilde yakalayabilir: değiştirilemez olarak ödünç alma (&T), değiştirilebilir olarak ödünç alma (&mut T) ve sahipliği alma (T). Kapanış, gövdesinin yakalanan değerlerle ne yaptığına bakarak bunlardan hangisini kullanacağına kendisi karar verir.
Liste 13-4’te, değeri yazdırmak için yalnızca değiştirilemez bir referans yeterli olduğundan liste adlı vektöre değiştirilemez bir referans yakalayan bir kapanış tanımlıyoruz.
fn main() {
let liste = vec![1, 2, 3];
println!("Kapanışı tanımlamadan önce: {liste:?}");
let sadece_odunc_alir = || println!("Kapanıştan: {liste:?}");
println!("Kapanışı çağırmadan önce: {liste:?}");
sadece_odunc_alir();
println!("Kapanışı çağırdıktan sonra: {liste:?}");
}
Bu örnek ayrıca bir değişkenin bir kapanış tanımına bağlanabileceğini ve daha sonra bu değişken adını sanki bir fonksiyon adıymış gibi parantezlerle (sadece_odunc_alir()) kullanarak kapanışı çağırabileceğimizi gösterir.
Aynı anda liste için birden çok değiştirilemez referans bulunabildiğinden, liste’ye hem kapanış tanımlanmadan önce hem kapanış tanımlandıktan sonra (henüz çağrılmadan önce) hem de kapanış çağrıldıktan sonra erişmeye devam edebiliriz. Bu kod derlenir, çalışır ve şunu yazdırır:
$ cargo run
Compiling kapanis-ornek v0.1.0 (file:///projects/kapanis-ornek)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/kapanis-ornek`
Kapanışı tanımlamadan önce: [1, 2, 3]
Kapanışı çağırmadan önce: [1, 2, 3]
Kapanıştan: [1, 2, 3]
Kapanışı çağırdıktan sonra: [1, 2, 3]
Bir sonraki adımda, Liste 13-5’te liste vektörüne bir öğe eklemesi için kapanış gövdesini değiştiriyoruz. Kapanış artık değiştirilebilir bir referans yakalar.
fn main() {
let mut liste = vec![1, 2, 3];
println!("Kapanışı tanımlamadan önce: {liste:?}");
let mut degistirilebilir_odunc_alir = || liste.push(7);
degistirilebilir_odunc_alir();
println!("Kapanışı çağırdıktan sonra: {liste:?}");
}
Bu kod derlenir, çalışır ve şunu yazdırır:
$ cargo run
Compiling kapanis-ornek v0.1.0 (file:///projects/kapanis-ornek)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/kapanis-ornek`
Kapanışı tanımlamadan önce: [1, 2, 3]
Kapanışı çağırdıktan sonra: [1, 2, 3, 7]
degistirilebilir_odunc_alir kapanışının tanımlanması ile çağrılması arasında artık bir println! satırı bulunmadığına dikkat edin: degistirilebilir_odunc_alir tanımlandığı anda liste’ye değiştirilebilir bir referans yakalar (kapanış çağrıldığında kendi içindeki bu değiştirilebilir referansı kullandığı için degistirilebilir_odunc_alir değişkeninin kendisi de mut olarak tanımlanmıştır). Kapanış çağrıldıktan sonra bir daha kullanılmadığı için değiştirilebilir ödünç alma sona erer. Kapanış tanımı ile kapanış çağrısı arasında yazdırma amacıyla değiştirilemez bir ödünç almaya izin verilmez; çünkü etkin bir değiştirilebilir ödünç alma varken başka hiçbir ödünç almaya izin verilmez. Hangi hata mesajını aldığınızı görmek için araya bir println! eklemeyi deneyin!
Kapanış gövdesi aslında sahipliğe ihtiyaç duymasa bile kapanışın çevreden kullandığı değerlerin sahipliğini almasını zorlamak istiyorsanız parametre listesinden önce move anahtar kelimesini kullanabilirsiniz.
Bu teknik en çok, bir kapanışı yeni bir thread’e (iş parçacığına) aktarırken verinin sahipliğinin yeni thread’e geçmesi için kullanılır. Bölüm 16’da eşzamanlılıktan bahsederken thread’leri ve onları neden kullanmak isteyeceğinizi ayrıntılı olarak ele alacağız; şimdilik move anahtar kelimesini gerektiren bir kapanışla yeni bir thread başlatmayı kısaca inceleyelim. Liste 13-6, vektörü ana thread yerine yeni bir thread içinde yazdırmak üzere Liste 13-4’ün uyarlanmış hâlini göstermektedir.
use std::thread;
fn main() {
let liste = vec![1, 2, 3];
println!("Kapanışı tanımlamadan önce: {liste:?}");
thread::spawn(move || println!("Thread'den: {liste:?}"))
.join()
.unwrap();
}
liste’nin sahipliğini almaya zorlamak için move kullanmakYeni bir thread başlatıyor (thread::spawn) ve bu thread’e çalıştırması için argüman olarak bir kapanış veriyoruz. Kapanış gövdesi listeyi yazdırır. Liste 13-4’te kapanış, yazdırma işlemi için gereken en düşük erişim düzeyi bu olduğundan liste’yi yalnızca değiştirilemez bir referansla yakalamıştı. Bu örnekte ise kapanış gövdesi hâlâ yalnızca değiştirilemez bir referansa ihtiyaç duysa da kapanış tanımının başına move anahtar kelimesini ekleyerek liste’nin kapanışa taşınması gerektiğini belirtmeliyiz. Ana thread, yeni thread üzerinde join çağrısı yapmadan önce başka işlemler gerçekleştirseydi yeni thread ana thread’in geri kalanından önce bitebileceği gibi ana thread de daha önce sonlanabilirdi. Ana thread liste’nin sahipliğini elinde tutmaya devam etse ve yeni thread bitmeden sonlanıp liste’yi bellekten düşürseydi (drop), yeni thread içindeki değiştirilemez referans geçersiz kalırdı. Bu nedenle derleyici, referansın geçerliliğini güvence altına almak için liste’nin yeni thread’e verilen kapanışa taşınmasını zorunlu kılar. Hangi derleyici hatalarını aldığınızı görmek için move anahtar kelimesini kaldırmayı veya kapanış tanımlandıktan sonra ana thread’de liste’yi kullanmayı deneyin!
Yakalanan Değerleri Kapanışların Dışına Taşımak
Bir kapanış tanımlandığı çevreden bir değerin referansını veya sahipliğini yakaladığında (dolayısıyla varsa nelerin kapanışın içine taşındığını belirlediğinde), kapanış gövdesindeki kod da kapanış daha sonra çalıştırıldığında bu referanslara veya değerlere ne olacağını tanımlar (dolayısıyla varsa nelerin kapanışın dışına taşındığını belirler).
Bir kapanış gövdesi aşağıdakilerden herhangi birini yapabilir: yakalanan bir değeri kapanışın dışına taşıyabilir, yakalanan değeri değiştirebilir, değeri dışarı taşımayıp değiştirmeden kullanabilir ya da baştan itibaren çevreden hiçbir şey yakalamayabilir.
Bir kapanışın çevredeki değerleri yakalama ve işleme biçimi, kapanışın hangi trait’leri uygulayacağını belirler; fonksiyonlar ve struct’lar da ne tür kapanışları kabul edebileceklerini bu trait’ler aracılığıyla ifade eder. Kapanışlar, gövdelerinin yakalanan değerleri nasıl işlediğine bağlı olarak bu üç Fn trait’inden birini, ikisini veya üçünü birden birikimli bir biçimde otomatik olarak uygular:
FnOnce, bir kez çağrılabilen kapanışlar için geçerlidir. Tüm kapanışlar çağrılabildiği için en azından bu trait’i uygular. Yakaladığı değerleri gövdesinin dışına taşıyan bir kapanış yalnızca bir kez çağrılabileceğinden yalnızcaFnOncetrait’ini uygular; diğerFntrait’lerini uygulamaz.FnMut, yakaladığı değerleri gövdesinin dışına taşımayan ancak bu değerleri değiştirebilen kapanışlar için geçerlidir. Bu kapanışlar birden fazla kez çağrılabilir.Fn, yakaladığı değerleri gövdesinin dışına taşımayan ve bu değerleri değiştirmeyen kapanışların yanı sıra çevresinden hiçbir şey yakalamayan kapanışlar için geçerlidir. Bu kapanışlar çevrelerini değiştirmeden birden fazla kez çağrılabilir; bu özellik, bir kapanışın eşzamanlı olarak birden çok kez çağrıldığı durumlarda büyük önem taşır.
Not: Standart kütüphanede bu üç trait birbirinin alt trait’i olarak tanımlanmıştır (Fn: FnMut ve FnMut: FnOnce). Dolayısıyla bu trait’leri uygulayan kapanış kümeleri arasında FnOnce ⊇ FnMut ⊇ Fn ilişkisi vardır: Fn uygulayan her kapanış aynı zamanda FnMut ve FnOnce beklentilerini, FnMut uygulayan her kapanış da FnOnce beklentisini otomatik olarak karşılar.
Ayrıca move anahtar kelimesi ile FnOnce arasındaki ayrıma dikkat edin:
move, kapanışın çevredeki değerleri nasıl yakalayacağını (sahipliği
kapanışın içine taşıyarak) zorlar; kapanışın hangi Fn trait’lerini
uygulayacağını ise kapanış gövdesinin yakalanan değerle ne yaptığı
belirler. Dolayısıyla move kullanan bir kapanış, yakaladığı değerleri
gövdesinden dışarı taşımıyor ve değiştirilebilir erişimle kullanmıyorsa Fn
ve FnMut trait’lerini de uygular. Yakalanan verilere değiştirilebilir
erişim gerekiyor ancak değerler dışarı taşınmıyorsa FnMut ve FnOnce
uygular. Cell gibi içsel değiştirilebilirlik türleriyle paylaşımlı erişim
üzerinden yapılan değişiklikler ise tek başına Fn uygulanmasını engellemez.
Liste 13-1’de kullandığımız Option<T> üzerindeki unwrap_or_else metodunun tanımına bakalım:
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
Hatırlayacağınız gibi T, bir Option’ın Some varyantındaki değerin türünü temsil eden jenerik türdür. Bu T türü aynı zamanda unwrap_or_else fonksiyonunun dönüş türüdür: örneğin bir Option<String> üzerinde unwrap_or_else çağıran kod sonuç olarak bir String elde eder.
Ardından, unwrap_or_else fonksiyonunun ek bir F jenerik tür parametresine sahip olduğuna dikkat edin. F türü, unwrap_or_else çağrılırken sağladığımız kapanış olan f parametresinin türüdür.
Jenerik F türü üzerinde belirtilen trait sınırı FnOnce() -> T şeklindedir; bu da F’nin bir kez çağrılabilmesi, hiçbir argüman almaması ve T türünde bir değer döndürmesi gerektiği anlamına gelir. Trait sınırında FnOnce kullanılması, unwrap_or_else metodunun f’yi en fazla bir kez çağıracağı garantisini ifade eder. unwrap_or_else gövdesine baktığımızda Option değeri Some ise f’nin hiç çağrılmayacağını, None ise yalnızca bir kez çağrılacağını görebiliriz. Tüm kapanışlar FnOnce trait’ini uyguladığından, unwrap_or_else her üç tür kapanışı da kabul eder ve olabildiğince esnek davranır.
Not: Yapmak istediğimiz iş çevreden bir değer yakalamayı gerektirmiyorsa, Fn trait’lerinden birini uygulayan bir argüman beklenen yerlerde kapanış yerine doğrudan bir fonksiyonun adını da kullanabiliriz. Örneğin bir Option<Vec<T>> değeri üzerinde, değer None olduğunda yeni ve boş bir vektör elde etmek için unwrap_or_else(Vec::new) çağrısı yapabiliriz. Derleyici, bir fonksiyon tanımı için Fn trait’lerinden hangileri uygunsa onları otomatik olarak uygular.
Not: Aynı trait sınırlarını kullanarak kapanış alan fonksiyonları kendiniz de yazabilirsiniz. Her kapanışın derleyici tarafından üretilen, adı olmayan ve kendine özgü bir türü vardır; bu yüzden bir kapanış parametresinin türünü doğrudan yazamazsınız. Bunun yerine, Bölüm 10’da gördüğümüz impl Trait sözdizimiyle ya da jenerik bir tür parametresiyle “şu imzaya sahip Fn trait’ini uygulayan herhangi bir tür” dersiniz. Örneğin aşağıdaki fonksiyon, i32 alıp i32 döndüren herhangi bir kapanışı (veya sıradan fonksiyonu) kabul eder ve onu iki kez uygular:
fn iki_kez_uygula(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
fn main() {
let carpan = 3;
let uc_kati = |x| x * carpan; // çevresinden `carpan` değerini yakalar
assert_eq!(iki_kez_uygula(uc_kati, 2), 18);
assert_eq!(iki_kez_uygula(|x| x + 1, 2), 4);
}
Fonksiyon gövdesinde f iki kez çağrıldığı için trait sınırı olarak FnOnce
kullanamazdık; bu örnekte kapanışı paylaşımlı erişimle çağırmak üzere Fn
sınırı tercih edildi. Ancak Fn, FnMut’tan daha kısıtlayıcıdır ve daha az
kapanışı kabul eder. Yakalanan durumu değiştiren kapanışlara da izin vermek
için parametreyi mut f: impl FnMut(i32) -> i32 olarak tanımlayıp
gövdeyi let ara_sonuc = f(x); f(ara_sonuc) biçiminde yazabiliriz.
İç içe f(f(x)) yazımı iki değiştirilebilir ödünç almayı çakıştıracağı
için çağrıları burada ayırırız. İki kez çağırmak için FnMut yeterlidir. Hangi Fn trait’ini isteyeceğinize
karar verirken kural basittir: Fonksiyonunuz kapanışı nasıl kullanacaksa (bir
kez mi, birden çok kez mi, eşzamanlı mı?) en az onu gerektiren trait’i seçin.
Bir kapanışı fonksiyondan döndürmek ise ek incelikler barındırır; bunu Bölüm
20’de ele alacağız.
Şimdi dilimler üzerinde tanımlanan standart kütüphane metodu sort_by_key’e bakalım; böylece unwrap_or_else metodundan nasıl ayrıldığını ve sort_by_key’in trait sınırı için neden FnOnce yerine FnMut kullandığını görebiliriz. Bu metodun aldığı kapanış, dilimde o an incelenen öğeye bir referans şeklinde tek bir argüman alır ve sıralanabilen K türünde bir değer döndürür. Bu fonksiyon, bir dilimi her bir öğenin belirli bir özelliğine göre sıralamak istediğinizde kullanışlıdır. Liste 13-7’de Dikdortgen örneklerinden oluşan bir listemiz var ve bunları genislik alanlarına göre küçükten büyüğe sıralamak için sort_by_key kullanıyoruz.
#[derive(Debug)]
struct Dikdortgen {
genislik: u32,
yukseklik: u32,
}
fn main() {
let mut liste = [
Dikdortgen { genislik: 10, yukseklik: 1 },
Dikdortgen { genislik: 3, yukseklik: 5 },
Dikdortgen { genislik: 7, yukseklik: 12 },
];
liste.sort_by_key(|r| r.genislik);
println!("{liste:#?}");
}
sort_by_key kullanmakBu kod şunu yazdırır:
$ cargo run
Compiling dikdortgenler v0.1.0 (file:///projects/dikdortgenler)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/dikdortgenler`
[
Dikdortgen {
genislik: 3,
yukseklik: 5,
},
Dikdortgen {
genislik: 7,
yukseklik: 12,
},
Dikdortgen {
genislik: 10,
yukseklik: 1,
},
]
sort_by_key metodunun bir FnMut kapanışı alacak şekilde tanımlanmasının nedeni, kapanışı birden çok kez çağırmasıdır: dilimdeki her öğe için bir kez. |r| r.genislik kapanışı çevresinden hiçbir şey yakalamaz, değiştirmez veya dışarı taşımaz; dolayısıyla trait sınırı gereksinimlerini karşılar.
Buna karşılık Liste 13-8, çevreden bir değeri dışarı taşıdığı için yalnızca FnOnce trait’ini uygulayan bir kapanış örneğini göstermektedir. Derleyici bu kapanışı sort_by_key ile kullanmamıza izin vermez.
#[derive(Debug)]
struct Dikdortgen {
genislik: u32,
yukseklik: u32,
}
fn main() {
let mut liste = [
Dikdortgen { genislik: 10, yukseklik: 1 },
Dikdortgen { genislik: 3, yukseklik: 5 },
Dikdortgen { genislik: 7, yukseklik: 12 },
];
let mut siralama_islemleri = vec![];
let deger = String::from("kapanış çağrıldı");
liste.sort_by_key(|r| {
siralama_islemleri.push(deger);
r.genislik
});
println!("{liste:#?}");
}
sort_by_key ile bir FnOnce kapanışı kullanmaya çalışmakBu örnek, liste sıralanırken sort_by_key metodunun kapanışı kaç kez çağırdığını saymak için kurgulanmış dolambaçlı (ve çalışmayan) bir yöntemdir. Kod bu sayımı, kapanışın çevresinde bulunan String türündeki deger değişkenini siralama_islemleri vektörüne ekleyerek (push) yapmaya çalışır. Kapanış deger’i yakalar ve ardından deger’in sahipliğini siralama_islemleri vektörüne aktararak onu kapanışın dışına taşır. Bu kapanış yalnızca bir kez çağrılabilir; ikinci kez çağırmaya çalışmak işe yaramaz, çünkü deger artık tekrar siralama_islemleri vektörüne eklenebilmek üzere çevrede değildir! Bu nedenle bu kapanış yalnızca FnOnce trait’ini uygular. Kodu derlemeye çalıştığımızda, kapanışın FnMut trait’ini uygulaması gerektiği için deger değişkeninin kapanış dışına taşınamayacağını belirten şu hatayı alırız:
$ cargo run
Compiling dikdortgenler v0.1.0 (file:///projects/dikdortgenler)
error[E0507]: cannot move out of `deger`, a captured variable in an `FnMut` closure
--> src/main.rs:18:33
|
15 | let deger = String::from("kapanış çağrıldı");
| ----- -------------------------------- move occurs because `deger` has type `String`, which does not implement the `Copy` trait
| |
| captured outer variable
16 |
17 | liste.sort_by_key(|r| {
| --- captured by this `FnMut` closure
18 | siralama_islemleri.push(deger);
| ^^^^^ `deger` is moved here
|
help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but `FnOnce` closures may consume them only once
--> /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/alloc/src/slice.rs:249:11
help: consider cloning the value if the performance cost is acceptable
|
18 | siralama_islemleri.push(deger.clone());
| ++++++++
For more information about this error, try `rustc --explain E0507`.
error: could not compile `dikdortgenler` (bin "dikdortgenler") due to 1 previous error
Hata mesajı, kapanış gövdesinde deger değişkenini çevrenin dışına taşıyan satırı işaret eder; yardımcı (help) satırında da Fn ve FnMut kapanışlarının yakalanan değerleri birden çok kez kullanabilmeyi gerektirdiği, FnOnce kapanışlarının ise bunları yalnızca bir kez tüketebileceği belirtilir. Bunu düzeltmek için kapanış gövdesini değerleri çevrenin dışına taşımayacak şekilde değiştirmemiz gerekir. Çevrede bir sayaç tutmak ve kapanış gövdesinde bu sayacın değerini artırmak, kapanışın kaç kez çağrıldığını saymanın çok daha doğrudan ve anlaşılır bir yoludur. Liste 13-9’daki kapanış sort_by_key ile sorunsuz çalışır; çünkü siralama_islemi_sayisi sayacına yalnızca değiştirilebilir bir referans yakalar ve dolayısıyla birden fazla kez çağrılabilir.
#[derive(Debug)]
struct Dikdortgen {
genislik: u32,
yukseklik: u32,
}
fn main() {
let mut liste = [
Dikdortgen { genislik: 10, yukseklik: 1 },
Dikdortgen { genislik: 3, yukseklik: 5 },
Dikdortgen { genislik: 7, yukseklik: 12 },
];
let mut siralama_islemi_sayisi = 0;
liste.sort_by_key(|r| {
siralama_islemi_sayisi += 1;
r.genislik
});
println!("{liste:#?}, {siralama_islemi_sayisi} işlemde sıralandı");
}
sort_by_key ile bir FnMut kapanışı kullanılmasına izin verilirKapanışlardan yararlanan fonksiyonları veya türleri tanımlarken ya da kullanırken Fn trait’leri büyük önem taşır. Sıradaki kısımda iteratörleri ele alacağız. Pek çok iteratör metodu argüman olarak kapanış alır; bu nedenle devam ederken kapanışlarla ilgili bu ayrıntıları aklınızda bulundurun!