İçeriğe geç
academia.sh

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) ve Err(E) ayrı bilgiler içerir; Optionın _or / _or_else adlandı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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat