Ders 08 / 15
Başarısızlığın Dönüş Değeri
Bir işlemin başarı ve başarısızlık yollarının kaynakta ayrı gövdeler olarak yazılması ve aynı işlev art arda çağrıldığında toplam çağrı sayısı sabit kalırken bu iki kolun kaç kez çalıştığının ayrışması.
Yayın Güncelleme
Bu derste
- Başarı ve başarısızlık kollarının ayrı gövdeler olduğunu koşturarak gösterir
- Toplam çağrı sayısı sabitken kol sayılarının nasıl ayrıştığını sayarak doğrular
- Kaynakta yazılı bir kolun bir koşumda hiç çalışmayabileceğini örnekle ayırt eder
İçindekiler
None bir nedeni yoktur — bir değerin bulunmadığını söyler, ama neden bulunmadığını
söylemez. Bir dosya açma çağrısı başarısız olduğunda, çağıranın bilmesi gereken yalnız
“sonuç yok” değildir; dosyanın bulunamadığı mı, izin reddedildiği mi, yoksa başka bir
neden mi olduğu da gerekir. Bu ders, bir nedeni de taşıyan bir sonucun — Result<T, E>in
— kaç kez hangi kolu çalıştırdığını ölçüyor.
Bu konunun ilk dersinde ölçülen eksen (hazır mı tembel mi çalışıyor) burada geçerliliğini
korumuyor; Ok ve Err arasında “biri diğerinin daha ucuz biçimi” gibi bir ilişki yok.
Bunun yerine yeni bir soru devreye giriyor: kaynakta yan yana duran iki farklı gövdeden
hangisinin çalıştığı, girdiye göre nasıl değişiyor ve bu değişim koşum boyunca nasıl
birikiyor?
Arayüzler, Hatalar ve Genellikler kursu (M09/K02), bir hatanın M09’un üç bağ türünden
üçüncüsüne, değerin içine, yerleştiğini kurmuştu: hata ne bir bildirimde ne bir kullanım
yerinde, doğrudan dönüş değerinin kendisinde taşınıyordu. Bu ders o kararı miras alıyor —
Result da tam olarak bu düzeni izliyor — ama farklı bir soru soruyor: aynı işlev art
arda çağrıldığında, başarı ve başarısızlık kollarının her biri kaç kez çalışıyor?
Yokluğun Tiple İfadesi dersi, hazır ve tembel iki yazımın aynı işi yaptığını, yalnız
kaç kez çalıştıklarıyla ayrıştığını ölçmüştü. Bu derste ölçülen fark başka bir yerde
duruyor: Ok ve Err kolları aynı işi yapmıyor, tamamen farklı iki gövde çalıştırıyor.
Soru artık “aynı iş kaç kez koşuyor” değil, “hangi iş, ne sıklıkla koşuyor” — kaynakta
yazılan iki farklı gövdenin koşumdaki payı ne.
Bir Sonuç, İki Yol
Result<T, E>, önceki kursta kurulan numaralandırma düzeneğinin bir başka standart
kütüphane örneğidir: Ok(T) başarılı sonucu, Err(E) başarısızlığın nedenini taşır.
Optiondan farkı, Noneun aksine Errin kendi başına bir değer taşımasıdır — başarısızlık
burada boş bir durum değil, kendi tipi olan bir veridir. Option “değer var mı” sorusuna
yanıt verir, Result “işlem neden başarısız oldu” sorusuna da yanıt verebilir.
HT4. Bu iki tip aynı numaralandırma düzeneğini kullanır, ama farklı bir bilgiyi taşırlar:
Optionın Noneu hiçbir veri taşımaz, Resultın Erri taşır. Bu, iki tipin birbirinin
yerine geçemeyeceği anlamına da gelir: bir arama sonucunun bulunup bulunmadığını
söylemek için Option yeterlidir, ama bir işlemin neden başarısız olduğunu söylemek
gerekiyorsa Result gerekir. Bir kod tabanında ikisi birlikte de görülür — örneğin bir
Result<Option<T>, E>, hem işlemin başarısız olabileceğini hem de başarılı olsa bile bir
değer bulamayabileceğini aynı anda taşır; bu birleşim bu derste ölçülmüyor, ama iki tipin
neden ayrı tutulduğunu netleştiriyor. Bir işlevin dönüş tipi
Result<i32, SayiHatasi> ise, çağıran taraf yalnız “başarısız mı oldu” diye sormaz, aynı
zamanda hangi hatanın oluştuğunu okuyabilir. Bu derste kullanılan SayiHatasi, hiçbir alan
taşımayan boş bir yapıdır — hatanın kendisi bu derste önemli değil, önemli olan hangi
kolun ne zaman çalıştığı. Özel Hata Tipleri dersi hatanın kendisine, taşıdığı bilgiye
odaklanacak.
Yukarıdaki matchte Err kolu hatanın kendisini bir ada bağlamıyor, Err(_) yazıyor —
alt çizgi, Desen Eşleme dersinde (M10/K01) kurulan yakalayıcı dal (catch-all) deseninin bir
biçimi ve “bu değeri okumayacağım” demenin kaynaktaki karşılığı. SayiHatasi hiçbir alan
taşımadığı için burada okunacak bir şey de yok; sıradaki iki derste hata bir bilgi
taşımaya başlayınca, Err(_) yerini hatayı gerçekten bağlayan bir desene bırakacak.
Optionın _or / _or_else adlandırma deseni Result üzerinde de sürüyor:
unwrap_or, unwrap_or_else, map_or, and_then — hepsinin Result karşılığı var ve
hepsi aynı hazır/tembel ayrımını taşıyor. Ama bu ders o yöntemleri ölçmüyor; Resultın
kendine özgü olan tarafını, iki kolun birbirinden farklı işler yaptığı durumunu
ölçüyor. unwrap_or gibi bir yöntemde her iki “taraf” da aynı tipten aynı işi yapardı
(bir değer üretmek); burada Ok kolu bir hesaplamayı sürdürür, Err kolu hesaplamayı
durdurup bir neden üretir — ikisi farklı işler.
Bir işlevin Result döndürmesi, o işlevin kendi hatasını kendi içinde çözmesi gerektiği
anlamına gelmez. cozumle kendi Errini üretiyor ve döndürüyor; çağıran taraf bu değeri
kullanmaya, görmezden gelmeye ya da kendi dönüş değerine olduğu gibi taşımaya karar
verebilir. Bu üçüncü seçenek — bir hatayı işlemeden yukarı taşımak — art arda birkaç
işlev sınırını geçebilir ve her geçişte kaynakta yeni bir kol yazmak gerekmez. Bu ders bu
taşımayı ölçmüyor; yalnız tek bir sınırda, tek bir match üzerinde, iki kolun kaç kez
çalıştığını sayıyor. Taşımanın kendisi Hata Yayılımının Kısa Yazımı dersinin konusu.
Kollardan Hangisi Çalışıyor
Bir işlev Result döndürdüğünde, çağıran taraf genellikle bir match ile iki kolu ayırır.
İki kol da sıradan Rust kodudur — biri diğerinden ayrı bir mekanizmayla çalışmaz, ikisi de
aynı işlevin dönüş değerine bakan aynı match ifadesinin bir parçasıdır. Bu, kolların
ikisinin de derlenmiş ikili dosyada yan yana durduğu, hangisinin çalışacağının yalnız
dönen değere (Ok mu Err mi) bağlı olduğu anlamına gelir. Aşağıdaki koşum aynı işlevi üç
farklı girdiyle çağırıyor ve her çağrıda hangi kolun çalıştığını sayıyor.
// hatakontrol.rs — basari ve basarisizlik kollarinin ayri govdeler olmasi
pub type Defter = std::rc::Rc<std::cell::RefCell<std::collections::BTreeMap<String, u32>>>;
pub fn say(defter: &Defter, ad: &str) {
*defter.borrow_mut().entry(ad.to_string()).or_insert(0) += 1;
}
pub fn kere(defter: &Defter, ad: &str) -> u32 {
*defter.borrow().get(ad).unwrap_or(&0)
}
#[derive(Debug)]
pub struct SayiHatasi;
pub fn cozumle(metin: &str) -> Result<i32, SayiHatasi> {
match metin.parse::<i32>() {
Ok(sayi) => Ok(sayi * 2),
Err(_) => Err(SayiHatasi),
}
}
// ikikol.rs — ayni islev uc kez cagriliyor, hangi kolun costugu sayiliyor
mod hatakontrol;
use hatakontrol::{Defter, say, kere, cozumle};
use std::cell::RefCell;
use std::rc::Rc;
use std::collections::BTreeMap;
fn main() {
let defter: Defter = Rc::new(RefCell::new(BTreeMap::new()));
for metin in ["8", "yirmibir", "15"] {
match cozumle(metin) {
Ok(sonuc) => {
say(&defter, "basari-kolu");
println!("{:<10} basari kolu, sonuc {}", metin, sonuc);
}
Err(_) => {
say(&defter, "basarisizlik-kolu");
println!("{:<10} basarisizlik kolu", metin);
}
}
}
println!();
let toplam = kere(&defter, "basari-kolu") + kere(&defter, "basarisizlik-kolu");
println!("toplam cagri: {}", toplam);
println!("basari-kolu: {} kez", kere(&defter, "basari-kolu"));
println!("basarisizlik-kolu: {} kez", kere(&defter, "basarisizlik-kolu"));
}
8 basari kolu, sonuc 16 yirmibir basarisizlik kolu 15 basari kolu, sonuc 30 toplam cagri: 3 basari-kolu: 2 kez basarisizlik-kolu: 1 kez
cozumle üç kez çağrılıyor ve toplam çağrı sayısı sabit: üç. Ama bu üç çağrının kolları
aynı biçimde dağılmıyor: iki çağrı basari-koluna, bir çağrı basarisizlik-koluna
düşüyor. HT5. Bu, kaynakta yazılan iki gövdenin — Ok kolunun gövdesi ve Err kolunun
gövdesi — birbirinden bağımsız çalıştığını gösteriyor: toplam sabit kalırken, hangi kolun
kaç kez seçildiği yalnız girdiye bağlı. "yirmibir" metni bir tam sayı olarak
ayrıştırılamadığı için Err kolu çalışıyor; "8" ve "15" ayrıştırılabildiği için Ok
kolu çalışıyor. Kaynakta iki kol da eşit uzunlukta yazılı, ama koşumda hangisinin ne kadar
çalıştığı girdiye bağlı bir sayı.
Bu ayrım, bir Optionda görülenden farklı bir yerde duruyor. Önceki derste ölçülen fark
hazır ve tembel bir gövde arasındaydı — ikisi de aynı işi görebilirdi, farkları
çalışma zamanlamasıydı. Burada ise iki kol tamamen farklı işler yapıyor: biri sonucu
ikiye katlıyor, öbürü bir hata değeri üretiyor. Sayının kendisi bu farkı değiştirmiyor —
kere fonksiyonu hangi kolun ne iş yaptığını bilmiyor, yalnız kaç kez çağrıldığını
sayıyor. Ölçüm burada da aynı kalıyor: kaynakta yazılan bir gövdenin koşumda kaç kez
çalıştığı.
Bu üç girdi bir doğrulama işlem hattını temsil ediyor: bir kullanıcının girdiği üç ayrı
alan, sırayla ayrıştırılıyor. Gerçek bir doğrulama işlem hattında girdiler tek tek değil,
bir liste olarak gelir ve aynı cozumle çağrısı her alan için tekrarlanır. Yukarıdaki
for döngüsü bu düzeni küçük ölçekte gösteriyor: üç metin, aynı işlevden geçiyor, her biri
kendi kolunu seçiyor. Döngünün kendisi bir “koşum” değil — üç ayrı çağrının toplamı. Bu
ders için önemli olan, döngünün her adımının bağımsız bir çağrı olması ve her çağrının
kendi kolunu, önceki çağrılardan etkilenmeden seçmesi: ikinci çağrının Err dönmesi,
üçüncü çağrının hangi kolu seçeceğini hiçbir şekilde etkilemiyor. Sayaç defteri bu
bağımsızlığı doğal olarak koruyor — her say çağrısı yalnız kendi anahtarını artırıyor,
başka bir anahtara dokunmuyor.
match ifadesinin kendisi de Denetim Akışı dersinde kurulan ilkeyi taşıyor:
match cozumle(metin) { ... } bir ifadedir ve bir değer üretir — burada üretilen değer
()dir, çünkü her iki kol da yalnızca bir yan etki (sayaç kaydı ve yazdırma) üretip
görünürde bir değer döndürmüyor. Bu, matchin kendisinin ifade olmasıyla, kollarının hangi
değeri ürettiği arasındaki farkı yeniden gösteriyor: Denetim Akışı dersi (M10/K01)
matchin bir ifade olduğunu ve bütün dallarının aynı tipi vermesi gerektiğini kurmuştu;
burada bütün dallar () üretiyor, ama her dalın içindeki gövde birbirinden tamamen
farklı. Bir ifadenin kendisi tek bir değer üretse de, o değere varmak için çalışan kod dal
dal ayrışabilir.
Sınırlayıcı Ölçüm
İki kol da kaynakta yazılıdır, ama bir çağrıda yalnız biri çalışır. Aşağıdaki koşum tek bir çağrıyla bunu somutlaştırıyor.
// tekcagri.rs — iki kol da kaynakta yazili, ama tek cagride yalniz biri kosuyor
mod hatakontrol;
use hatakontrol::{Defter, say, kere, cozumle};
use std::cell::RefCell;
use std::rc::Rc;
use std::collections::BTreeMap;
fn main() {
let defter: Defter = Rc::new(RefCell::new(BTreeMap::new()));
match cozumle("100") {
Ok(sonuc) => {
say(&defter, "basari-kolu");
println!("sonuc: {}", sonuc);
}
Err(_) => {
say(&defter, "basarisizlik-kolu");
println!("hata olustu");
}
}
println!("basari-kolu: {} kez", kere(&defter, "basari-kolu"));
println!("basarisizlik-kolu: {} kez", kere(&defter, "basarisizlik-kolu"));
}
sonuc: 200 basari-kolu: 1 kez basarisizlik-kolu: 0 kez
HT6. cozumle("100") çağrısı Ok sonucu verdiği için yalnız basari-kolu çalışıyor;
basarisizlik-kolu sıfır kalıyor. Ama bu sıfır, Err kolunun var olmadığı anlamına
gelmiyor — kod ikili dosyasında o kol duruyor, derleyici onu üretti, match bütünlük
gereği onu kapsamak zorunda kaldı. Yalnız bu belirli koşumda hiç seçilmedi. Bir kolun
kaynakta yazılı olması ile bir koşumda çalışmış olması iki ayrı olgu: biri kaynak kodun
bir özelliği, öbürü belirli bir girdiyle yapılan belirli bir koşumun sonucu. “Kapsanmış
kod” — derleyicinin kabul ettiği, bütünlük ilkesini sağlayan kod — ile “koşmuş kod” —
belirli bir çalıştırmada gerçekten işlemci tarafından yürütülen kod — arasındaki bu ayrım,
bir programın kaynağını okumakla bir koşumunu ölçmek arasındaki farkın da özeti.
Bu ayrımın önemi, bütünlük ilkesinin ne sağladığını düşününce daha net görünüyor. Bütünlük
ilkesi olmasaydı — yani match yalnız Ok kolunu yazıp Err durumunu görmezden gelmeye
izin verseydi — kaynakta basarisizlik-kolu diye bir gövde hiç bulunmayabilirdi ve
"yirmibir" gibi bir girdi geldiğinde program tanımsız bir duruma düşerdi: ne bir hata
değeri üretilirdi ne de akış nereye gideceğini bilirdi. Bütünlük ilkesi bunu engelliyor —
Err kolu kaynakta bulunmak zorunda, ister o koşumda hiç çalışmasın ister koşumun
tamamını devralsın. Sıfır kez çalışmış olması, orada gerekli olmadığı anlamına gelmiyor;
tam tersine, sıfırın ölçülebilmesinin nedeni, kolun zaten orada, çalışmaya hazır durmasıdır.
Bu fark küçük bir örnekte belirgin görünmeyebilir, ama bir işlevin çağrıldığı yer sayısı
büyüdükçe önem kazanıyor. Bir işlev binlerce kez çağrılıyorsa ve girdilerin çoğu geçerliyse,
Err kolu kaynakta yer kaplar, derlenir, doğrulanır — ama koşumda neredeyse hiç
çalışmayabilir. Tam tersi de mümkün: bir işlev çoğunlukla geçersiz girdilerle çağrılıyorsa,
Ok kolu kaynakta dursa da koşumda ender çalışır. Kaynağın kendisi hangi kolun daha sık
çalışacağını söylemez; bunu yalnız gerçek bir koşum söyler.
Yukarıdaki ikinci koşumda üç girdiden ikisi başarılı, biri başarısızdı — basari-kolu iki,
basarisizlik-kolu bir kez çalıştı. Aynı işlev bin girdiyle çağrılsaydı ve girdilerin
yalnız birkaçı geçersiz olsaydı, oran da buna göre kayardı: basari-kolu binin yakınında,
basarisizlik-kolu tek haneli bir sayıda kalabilirdi. Kaynakta yazılan iki kolun uzunluğu
bu oranla ilgisizdir — Err kolu tek satırlık bir gövde olabilir ve yine de koşumun
neredeyse tamamını devralabilir, girdiler öyle gerektiriyorsa. Bu ders bu oranı kestirmeye
çalışmıyor; yalnız oranın kaynaktan değil, koşumdan okunması gerektiğini gösteriyor.
Yukarıdaki iki koşum birlikte, bu konunun açılışında sorulan soruyu kısmen yanıtlıyor:
Optionda yedek gövdenin çalışıp çalışmadığı hazır mı tembel mi yazıldığına bağlıydı;
Resultda bir kolun çalışıp çalışmadığı, o kolun hangi girdiye karşılık geldiğine bağlı.
İkisi de aynı ölçüm çerçevesine — kaynakta yazılan bir gövdenin koşumda kaç kez çalıştığı —
oturuyor, ama her biri kendi nedenine göre “her seferinde”, “yalnız gerektiğinde” ya da
“hiç” sınıfına giriyor.
Özet
Result<T, E>,Optionın aksine başarısızlığın nedenini de taşır:Ok(T)veErr(E)ayrı bilgiler içerir;Optionın_or/_or_elseadlandırma deseni burada da geçerlidir.- Başarı ve başarısızlık kollarının gövdeleri kaynakta ayrı yazılır ve birbirinden bağımsız çalışır; aynı işlev art arda çağrıldığında toplam çağrı sayısı sabit kalır, ama kol sayıları girdiye göre ayrışır.
- Bir kolun kaynakta yazılı olması onun her koşumda çalıştığı anlamına gelmez — tek bir çağrıda iki kolun yalnız biri çalışır, öbürü o koşumda hiç çalışmaz.
- “Kapsanmış kod” (derleyicinin kabul ettiği, bütünlük gereği yazılmış kod) ile “koşmuş kod” (belirli bir koşumda gerçekten çalışan kod) iki ayrı olgudur.
Err(_)gibi bir yakalayıcı desen, hatanın içeriğini okumadan yalnız hangi kolun çalıştığını saymaya yeter; hatanın taşıdığı bilgi sıradaki derslerin konusudur.- Hangi kolun ne sıklıkla çalışacağı kaynaktan okunamaz; yalnız gerçek girdilerle yapılan bir koşumdan okunabilir. Bu koşumda iki kol da yalnız gerektiğinde çalıştı: her çağrı kendi girdisinin gerektirdiği kolu koşturdu, öbürü o çağrıda hiç koşmadı.
Sonraki Adım
Yukarıdaki her iki koşumda da başarı kolu aynı üç satırı tekrarlıyordu: değeri ayrıştır,
hata varsa bildir, yoksa devam et. Bu düzen, art arda birkaç ayrıştırma yapan bir işlevde
hızla tekrar eden bir kalıba dönüşür — özellikle bir işlev kendi içinde başka bir
ayrıştırma çağırıp onun sonucunu da denetlemek zorunda kaldığında, aynı match iskeleti
birkaç kez art arda yazılır. Hata Yayılımının Kısa Yazımı dersi, bu kalıbı tek bir
işaretle kısaltan bir yazımı ölçüyor — ve o kısaltmanın kaynağı kısalttığını, ama koşumu
değiştirmediğini, yani basari-kolu ile basarisizlik-kolunun bu derste görülenle aynı
sayıları verdiğini gösteriyor. Kısa yazım, ölçülen sayıyı değil, o sayıya varmak için
yazılması gereken satır sayısını azaltıyor — bu ders yazılan match iskeletini elle
kurdu, sıradaki ders aynı iskeletin kaynakta ne kadar kısalabildiğini, koşumu
değiştirmeden, ölçüyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.