ch13-01-closures
Kapsayıcılar: Ortamlarını Yakalamayı Başaran Anonim Fonksiyonlar
Rust'taki kapsayıcılar, bir değişkende saklayabileceğiniz veya diğer fonksiyonlara argüman olarak geçebileceğiniz anonim fonksiyonlardır. Kapsayıcıyı bir yerde oluşturabilir ve başka bir yerde onu farklı bir bağlamda değerlendirmek için çağırabilirsiniz. Fonksiyonların aksine, kapsayıcılar tanımlandıkları kapsamdan değerleri yakalayabilirler. Bu kapsayıcı özelliklerinin kod yeniden kullanımı ve davranış özelleştirmesi için nasıl olanak sağladığını göstereceğiz.
Kapsayıcılar ile Ortamı Yakalamak
Öncelikle, kapsayıcıları tanımlandıkları ortamdan değerleri yakalamak için nasıl kullanabileceğimizi inceleyeceğiz. İşte senaryo: Belirli aralıklarla, tişört şirketimiz, bir promosyon olarak posta listemizdeki birine özel, sınırlı sayıda üretilmiş bir tişört hediye eder. Posta listesinde bulunan kişiler, profillerine tercih ettikleri rengi isteğe bağlı olarak ekleyebilirler. Ücretsiz tişört kazanan kişinin tercih ettiği bir renk varsa, o renkte bir tişört alır. Kişi tercih ettiği bir rengi belirtmemişse, şirketin şu anda en fazla bulundurduğu renkten bir tişört alır.
Bu örneğin uygulanması için ShirtColor
adında bir enum ve Inventory
adında bir struct kullanacağız.
Bunu uygulamanın birçok yolu vardır. Bu örnek için, basitlik amacıyla Red
ve Blue
varyantlarına sahip olan ShirtColor
adında bir enum kullanacağız. Şirketin envanterini temsil etmek için, mevcut tişört renklerini temsil eden Vec
içeren shirts
adında bir alanı olan bir Inventory
struct'ı kullanıyoruz. Inventory
üzerinde tanımlanan giveaway
yöntemi, ücretsiz tişört kazanıcısının isteğe bağlı tişört rengi tercihini alır ve kişinin alacağı tişört rengini döner. Bu kurulum Şekil 13-1'de gösterilmektedir:
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-01/src/main.rs}}
main
içinde tanımlanan store
, bu sınırlı sayıda üretilmiş promosyon için dağıtmak üzere iki mavi tişört ve bir kırmızı tişört bulundurmaktadır. Kırmızı tişört tercih eden bir kullanıcı ve hiçbir tercihi olmayan bir kullanıcı için giveaway
yöntemini çağırıyoruz.
Bu kod birçok şekilde uygulanabilir; burada kapsayıcılara odaklanmak için, giveaway
yönteminin gövdesi dışında öğrendiğiniz kavramlara sadık kaldık.
giveaway
yönteminde, kullanıcı tercihini Option
türünde bir parametre olarak alırız ve user_preference
üzerinde unwrap_or_else
metodunu çağırırız. unwrap_or_else
metodu üzerinde Option
standart kütüphane tarafından tanımlanmıştır. Bu metod bir argüman alır: argümanları olmayan ve bir T
değeri döndüren bir kapsayıcı (bu durumda Option
'nin Some
varyantında saklanan aynı tür, ShirtColor
). Eğer Option
Some
varyantıysa, unwrap_or_else
içinde bulunan değeri döndürür. Eğer Option
None
varyantıysa, unwrap_or_else
kapsayıcıyı çağırır ve kapsayıcının döndürdüğü değeri döndürür.
Kapsayıcı ifadesini || self.most_stocked()
olarak unwrap_or_else
metoduna argüman olarak belirtiyoruz. Bu, kendisi hiçbir parametre almayan bir kapsayıcıdır (eğer kapsayıcının parametreleri olsaydı, iki dikey çubuk arasında görünürdü). Kapsayıcının gövdesi self.most_stocked()
çağrısını içerir. Burada kapsayıcıyı tanımlıyoruz ve unwrap_or_else
'in implementasyonu daha sonra sonuç gerektiğinde kapsayıcıyı değerlendirecektir.
Bu kodu çalıştırmak şunları yazdırır:
{{#include ../listings/ch13-functional-features/listing-13-01/output.txt}}
Burada ilginç bir yön, self.most_stocked()
çağıran bir kapsayıcıyı mevcut Inventory
örneğine geçmemizdir. Standart kütüphane, tanımladığımız Inventory
veya ShirtColor
türleri hakkında hiçbir şey bilmek zorunda değildi ya da bu senaryoda kullanmak istediğimiz mantığa dair bir bilgiye ihtiyaç duymadı. Kapsayıcı, self
Inventory
örneğine değiştirilemeyen bir referans alır ve bunu belirttiğimiz kodla birlikte unwrap_or_else
metoduna geçirir. Öte yandan, fonksiyonlar çevrelerini bu şekilde yakalayamazlar.
Kapsayıcı Türü Çıkarımı ve Notasyon
Fonksiyonlar ile kapsayıcılar arasında daha fazla fark vardır. Kapsayıcıların genellikle parametrelerin veya dönüş değerinin türlerini fn
fonksiyonları gibi belirtmeleri gerekmez. Fonksiyonlarda tür notasyonları zorunludur çünkü türler, kullanıcılarınıza sunulan açıklayıcı bir arayüzün parçasıdır. Bu arayüzü katı bir şekilde tanımlamak, herkesin bir fonksiyonun kullandığı ve döndürdüğü değer türleri üzerinde hemfikir olmasını sağlamak için önemlidir. Kapsayıcılar ise, bu tür bir arayüzde kullanılmazlar: değişkenlerde saklanır ve isimlendirilmeden ve kütüphanemizin kullanıcılarına ifşa edilmeden kullanılırlar.
Kapsayıcılar genellikle kısa ve dar bir bağlam içinde geçerlidir, herhangi bir keyfi senaryoda değil. Bu sınırlı bağlamlar içinde, derleyici, parametrelerin ve dönüş türünün türlerini çıkarabilir, çoğu değişkenin türlerini çıkardığı gibi (derleyicinin nadir durumlarda kapsayıcı tür notasyonlarına ihtiyaç duyduğu yerler vardır).
Değişkenlerle olduğu gibi, istiyorsak açıklığı ve netliği artırmak için tür notasyonları ekleyebiliriz.
Kapsayıcı için tür notasyonları eklemek, Şekil 13-2'deki tanımda gösterilen gibi görünecektir. Bu örnekte, bir kapsayıcı tanımlıyor ve onu, Şekil 13-1'de yaptığımız gibi bir argüman olarak geçtiğimiz noktada tanımlamak yerine bir değişkende saklıyoruz.
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-02/src/main.rs:here}}
Tür notasyonları eklendiğinde, kapsayıcı sözdizimi, fonksiyonların sözdizimine daha benzer hale gelir. Burada, parametreine 1 ekleyen bir fonksiyon tanımlıyoruz ve karşılaştırma için aynı davranışı sergileyen bir kapsayıcı tanımlıyoruz. İlgili kısımları hizalamak için alanlar ekledik. Bu, kapsayıcı sözdiziminin, boru kullanımından ve gerekli sözdizimi miktarından başka bir farkın yanı sıra fonksiyon sözdizimiyle benzer olduğunu göstermektedir:
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
İlk satır bir fonksiyon tanımını, ikinci satır ise tam olarak notasyon yapılmış bir kapsayıcı tanımını göstermektedir. Üçüncü satırda, kapsayıcı tanımından tür notasyonlarını kaldırıyoruz. Dördüncü satırda ise, kapsayıcı gövdesinde yalnızca bir ifade olduğu için isteğe bağlı olan parantezleri kaldırıyoruz. Çağrıldıklarında aynı davranışları üretecek olan geçerli tanımların hepsi bunlardır. add_one_v3
ve add_one_v4
satırları, kapsayıcıların değerlendirilmesi için derlenmeleri gerektiği için, türler kullanımlarından çıkarılacaktır. Bu, let v = Vec::new();
ifadesinin derleyicinin türü çıkarabilmesi için tür notasyonlarına veya belirli bir türde değerlerin Vec
içine eklenmesine ihtiyaç duymasıyla benzerdir.
Kapsayıcı tanımları için, derleyici her bir parametreleri ve dönüş değerleri için bir somut tür çıkarır. Örneğin, Şekil 13-3'te yalnızca aldığı değeri döndürdüğü kısa bir kapsayıcının tanımı gösterilmektedir. Bu kapsayıcı, yalnızca bu örneğin amaçları için pek faydalı değildir. Tanımda herhangi bir tür notasyonu eklemediğimizi unutmayın. Tür notasyonu olmadığı için, kapsayıcıyı herhangi bir tür ile çağırabiliriz; burada ilk kez String
ile yaptık. Ardından example_closure
'ı bir tamsayı ile çağırmaya çalışırsak, bir hata alırız.
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-03/src/main.rs:here}}
Derleyici bize bu hatayı verir:
{{#include ../listings/ch13-functional-features/listing-13-03/output.txt}}
String
değer ile ilk kez example_closure
'ı çağırdığımızda, derleyici x
türünü ve kapsayıcının dönüş türünü String
olarak çıkarır. Bu türler, ardından example_closure
içinde kapsayıcıya kilitlenir ve farklı bir türü aynı kapsayıcı ile kullanmaya çalıştığımızda tür hatası alırız.
Referansları Yakalamak veya Sahipliği Taşımak
Kapsayıcılar, ortamlarından değerleri üç şekilde yakalayabilirler; bu, bir fonksiyonun bir parametre almalarının üç yoluyla doğru biçimde eşleşir: değiştirilemeyen borç alma, değiştirilebilir borç alma ve sahipliği alma. Kapsayıcı, yakalanan değerlerle fonksiyon gövdesinin ne yaptığına bağlı olarak hangisini kullanacaklarını belirler.
Şekil 13-4'te, yalnızca değeri yazdırmak için değiştirilemeyen bir referansa ihtiyaç duyduğu için list
adındaki diziye değiştirilemeyen bir referans yakalayan bir kapsayıcı tanımlıyoruz:
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-04/src/main.rs}}
Bu örnek, bir değişkenin bir kapsayıcı tanımına bağlı olabileceğini ve daha sonra kapsayıcıyı, değişken ismini ve parantezleri kullanarak sanki değişken adı bir fonksiyon adıymış gibi çağırabileceğimizi göstermektedir.
list
üzerinde aynı anda birden fazla değiştirilemeyen referansa sahip olabileceğimiz için, list
, kapsayıcı tanımından önceki koddan, kapsayıcı tanımının yanındaki koddan ve kapsayıcı çağrıldıktan sonraki koddan erişilebilir.
Bu kod derlenir, çalıştırılır ve aşağıdakileri yazdırır:
{{#include ../listings/ch13-functional-features/listing-13-04/output.txt}}
Sonraki adımda, Şekil 13-5'te, kapsayıcı gövdesini list
dizisine bir eleman ekleyecek şekilde değiştiriyoruz. Kapsayıcı artık bir değiştirilebilir referansı yakalar:
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-05/src/main.rs}}
Bu kod derlenir, çalıştırılır ve aşağıdakileri yazdırır:
{{#include ../listings/ch13-functional-features/listing-13-05/output.txt}}
Artık borrows_mutably
kapsayıcısının tanımı ile çağrılması arasında println!
yoktur: borrows_mutably
tanımlandığında, list
üzerinde değiştirilebilir bir referans yakalar. Kapsayıcı çağrıldıktan sonra bir kez daha kapsayıcıyı kullanmadığımız için, değiştirilebilir borç sona erer. Kapsayıcı tanımı ile çağrılması arasında, yalnızca bir yazdırma işlemi olduğu için değiştirilemeyen bir borç verilmez; çünkü değiştirilebilir bir borç olduğunda başka borçlar verilmez. Oraya bir println!
eklemeyi denerseniz, hangi hata mesajını alabileceğinizi görebilirsiniz!
Kapsayıcının, ortamındaki değerlerin sahipliğini almasını, kapsayıcının gövdesinin sahipliğe sıkı bir şekilde ihtiyaç duymadığı durumlarda zorlamak istiyorsanız, parametreler listesinin önüne move
anahtar kelimesini ekleyebilirsiniz.
Bu teknik, genellikle yeni bir iş parçacığına verileri taşımak için kapsayıcıyı geçirirken kullanışlıdır.
Bu nedenle, veriler yeni iş parçacığına ait olur. İş parçacıkları ve neden onları kullanmak isteyeceğimiz hakkında detayları 16. bölümde eşzamanlılık üzerine konuşurken ele alacağız, ancak şimdilik, move
anahtar kelimesini gerektiren bir kapsayıcı kullanarak yeni bir iş parçacığı oluşturmaya kısaca göz atalım. Şekil 13-6, Şekil 13-4'ün iş parçacığında bir vektörü yazdırmak için değiştirildiğini göstermektedir:
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-06/src/main.rs}}
Yeni bir iş parçacığı başlatıyoruz ve iş parçacığına çalıştırması için bir kapsayıcı veriyoruz. Kapsayıcı gövdesi listeyi yazdırır. Şekil 13-4'te, kapsayıcı yalnızca list
'i değiştirilemeyen bir referans olarak yakalamıştır, çünkü bunu yazdırmak için gereken en az erişimdir. Bu örnekte, kapsayıcı gövdesi hâlâ yalnızca değiştirilemeyen bir referansa ihtiyaç duyduğunda, kapsayıcı tanımının başına move
anahtar kelimesini koyarak list
'in kapsayıcıya taşınmasını belirtmek zorundayız. Yeni iş parçacığı, ana iş parçacığı tamamlanmadan önce sona erebilir veya ana iş parçacığı daha önce tamamlanabilir. Eğer ana iş parçacığı list
'in sahipliğini sürdürür ancak yeni iş parçacığı bittikten önce sona ererse ve list
'i bırakırsa, iş parçacığındaki değiştirilemeyen referans geçersiz olur. Bu nedenle, derleyici list
'in yeni iş parçacığına taşınmasının zorunlu hale gelmesini gerektirir; böylece referans geçerli olacak şekilde. move
anahtar kelimesini kaldırarak veya kapsayıcı tanımlandıktan sonra main
iş parçacığında list
'i kullanarak hangi derleyici hatalarını alacağınızı görmek için deneyin!
Kapanan Değerleri Kapatmalardan ve Fn
Özelliklerinden Çıkarma
Bir kapanma, kapanmanın tanımlandığı ortamdan bir değerin sahibini veya bir referansı yakaladıktan sonra (bu durumda, kapanmaya nelerin taşınacağını etkileyerek), kapanmanın gövdesindeki kod, kapanma daha sonra değerlendirildiğinde referanslara veya değerlere ne olacağını tanımlar (bu, kapanmadan nelerin taşınacağını etkileyerek). Bir kapanma gövdesi aşağıdakilerden herhangi birini yapabilir:
- Kapanmadan yakalanan bir değeri çıkarmak
- Yakalanan değeri değiştirmek
- Değeri ne taşıyabilir ne de değiştirmek
- Başlangıçta ortamdan hiçbir şey yakalamamak
Kapanmanın ortamdan değerleri yakalaması ve işlemesi, kapanmanın hangi özellikleri uyguladığını etkiler ve özellikler, işlevlerin ve yapıların hangi tür kapanmaları kullanabileceğini belirtmek için kullanılır.
Kapanmalar, kapanmanın gövdesinin değerleri nasıl ele aldığınıza bağlı olarak, otomatik olarak bir, iki veya bu üç Fn
özelliğini ekleyerek uygulayabilir:
FnOnce
: Bir kez çağrılabilen kapanmalara uygulanır. Tüm kapanmalar en azından bu özelliği uygular, çünkü tüm kapanmalar çağrılabilir. Kapanmadan yakalanan değerleri gövdesinden çıkaran bir kapanma yalnızcaFnOnce
özelliğini uygular ve diğerFn
özelliklerinden hiçbirini uygulamaz, çünkü yalnızca bir kez çağrılabilir.FnMut
: Gövdesinden yakalanan değerleri çıkarmayan ancak yakalanan değerleri değiştirebilecek kapanmalara uygulanır. Bu kapanmalar birden fazla kez çağrılabilir.Fn
: Gövdesinden yakalanan değerleri çıkarmayan ve yakalanan değerleri değiştirmeyen kapanmalara ve ortamlarından hiçbir şey yakalamayan kapanmalara uygulanır. Bu kapanmalar ortamlarını değiştirmeden birden fazla kez çağrılabilir, bu da bir kapanmayı birden fazla kez birbirleriyle eş zamanlı çağırmak gibi durumlarda önemlidir.
Option
Üzerindeki unwrap_or_else
Metodunun Tanımı
Kod örneği:
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(),
}
}
}
T
'nin, bir Option
'ın Some
varyantındaki değerin türünü temsil eden genel tür olduğunu hatırlayın. Bu T
türü, unwrap_or_else
fonksiyonunun dönüş türüdür: örneğin, bir Option
üzerinde unwrap_or_else
çağıran kod, bir String
alır.
unwrap_or_else
fonksiyonunun ek bir genel tür parametresi F
olduğunu not edin. F
tipi, unwrap_or_else
çağrıldığında sağladığımız kapanma olan f
adındaki parametrenin türüdür.
Genel tür F
üzerindeki sınırlama, FnOnce() -> T
olup, bu, F
'nin bir kez çağrılabilmesi, argüman almaması ve T
döndürmesi gerektiği anlamına gelir. Trait sınırlaması üzerinde FnOnce
kullanmak, unwrap_or_else
fonksiyonunun en fazla bir kez f
'yi çağıracağına dair kısıtlamayı ifade eder. unwrap_or_else
gövdesinde, Option
'ın Some
olması durumunda f
'nin çağrılmayacağını görebiliriz. Option
None
ise, f
bir kez çağrılacaktır. Tüm kapanmalar FnOnce
'yi uyguladığı için, unwrap_or_else
üç tür kapanmayı da kabul eder ve mümkün olduğunca esnektir.
Not: İşlevler de tüm
Fn
özelliklerini uygulayabilir. Ne yapmak istediğimiz, ortamdan bir değeri yakalamayı gerektirmiyorsa,Fn
özelliklerinden birini uygulayan bir şeye ihtiyaç duyduğumuzda bir kapanma yerine bir işlevin adını kullanabiliriz. Örneğin, birOption>
değeri üzerinde,None
değerini aldığında yeni, boş bir dizi elde etmek içinunwrap_or_else(Vec::new)
çağırabiliriz.
Şimdi, dilimlerin üzerinde tanımlı standart kütüphane yöntemi sort_by_key
'ye bakalım, bunun unwrap_or_else
'den nasıl farklı olduğunu ve neden sort_by_key
için trait sınırı olarak FnMut
kullandığını görelim. Kapanma, değerlendirilen dilimdeki mevcut öğeye bir referans şeklinde bir argüman alır ve sıralanabilir bir K
türünde bir değer döndürür. Bu işlev, her bir öğenin belirli bir niteliğine göre bir dilimi sıralamak istediğinizde yararlıdır. Listing 13-7'de, Rectangle
örneklerinin bir listesini aldık ve bunları width
niteliği ile düşükten yükseğe doğru sıralamak için sort_by_key
kullandık:
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-07/src/main.rs}}
Bu kod şu çıktıyı verir:
{{#include ../listings/ch13-functional-features/listing-13-07/output.txt}}
sort_by_key
'in bir FnMut
kapanmasını alacak şekilde tanımlanmasının nedeni, kapanmayı birden fazla kez çağırmasıdır: dilimdeki her bir öğe için bir kez. Kapanma |r| r.width
ortamdan hiçbir şey yakalamadığı, değiştirmediği veya çıkarmadığı için trait sınırlama gerekliliklerini karşılar.
Buna karşılık, Listing 13-8, sadece FnOnce
özelliğini uygulayan bir kapanma örneği gösterir, çünkü bu ortamdan bir değeri çıkarır. Derleyici, bu kapanmayı sort_by_key
ile kullanmamıza izin vermez:
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-08/src/main.rs}}
Bu, list
'i sıralarken sort_by_key
'ın kapanmayı kaç kez çağırdığını saymaya çalışmanın bir çarpıtılmış, karmaşık yoludur (bu çalışmaz). Bu kod, value
'ı — kapanmanın ortamından bir String
— sort_operations
vektörüne iterek bu sayımı yapmayı dener. Kapanma value
'ı yakalar ve ardından value
'ın sahipliğini sort_operations
vektörüne transfer ederek value
'ı kapanmadan çıkarır. Bu kapanma yalnızca bir kez çağrılabilir; ikinci kez çağırmaya çalışmak işe yaramaz çünkü value
artık ortamda sort_operations
'a tekrar itilecek durumda değildir! Bu nedenle, bu kapanma yalnızca FnOnce
'yi uygular. Bu kodu derlemeye çalıştığımızda, value
'ın kapanmadan çıkarılamayacağına dair bir hata alırız çünkü kapanmanın FnMut
özelliğini uygulaması gerekir:
{{#include ../listings/ch13-functional-features/listing-13-08/output.txt}}
Hata, value
'ı ortamdan çıkaran kapanma gövdesindeki satıra işaret eder. Bunu düzeltmek için, kapanma gövdesini ortamdan değerleri çıkarmayacak şekilde değiştirmemiz gerekir. Kapanmanın kaç kez çağrıldığını saymanın daha basit bir yolu, ortamda bir sayaç tutmak ve kapanma gövdesinde değerini artırmaktır. Listing 13-9'daki kapanma, num_sort_operations
sayacına yalnızca değişken bir referans yakaladığı için sort_by_key
ile çalışır ve bu yüzden birden fazla kez çağrılabilir:
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-09/src/main.rs}}
Fn
özellikleri, kapanmaları kullanan işlevler veya türler tanımlarken veya kullanırken önemlidir. Bir sonraki bölümde, yineleyicileri tartışacağız. Birçok yineleme yöntemi kapanma argümanları alır, bu nedenle bu kapanma ayrıntılarını unutmadan devam edelim!