panic!'lemek mi, panic!'lememek mi?

Peki ne zaman panik yapacağınıza ve ne zaman Result'a geri döneceğinize nasıl karar vereceksiniz? Kod panik yaptığında, kurtarmanın bir yolu yoktur. Kurtarmanın olası bir yolu olsun ya da olmasın, herhangi bir hata durumu için panic! çağrısı yapabilirsiniz, ancak o zaman çağıran kod adına bir durumun kurtarılamaz olduğuna karar vermiş olursunuz. Bir Result değeri döndürmeyi seçtiğinizde, çağıran koda seçenekler sunarsınız. Çağıran kod, kendi durumuna uygun bir şekilde kurtarma girişiminde bulunmayı seçebilir veya bu durumda bir Err değerinin kurtarılamaz olduğuna karar verebilir, böylece panic! çağrısı yapabilir ve kurtarılabilir hatanızı kurtarılamaz bir hataya dönüştürebilir. Bu nedenle, başarısız olabilecek bir fonksiyon tanımlarken Result döndürmek iyi bir varsayılan seçimdir.

Örnekler, prototip kodu ve testler gibi durumlarda, Result döndürmek yerine panikleyen kod yazmak daha uygundur. Nedenini inceleyelim, ardından derleyicinin başarısızlığın imkansız olduğunu söyleyemediği, ancak insan olarak sizin söyleyebildiğiniz durumları tartışalım. Bölüm, kütüphane kodunda panik yapıp yapmamaya nasıl karar verileceğine ilişkin bazı genel yönergelerle sona erecektir.

Örnekler, Prototip Kod ve Testler

Bir kavramı açıklamak için bir örnek yazarken, sağlam hata işleme kodu da eklemek örneği daha az anlaşılır hale getirebilir. Örneklerde, unwrap gibi panik yaratabilecek bir metoda yapılan çağrının, uygulamanızın hataları nasıl ele almasını istediğinize yönelik bir yer tutucu olduğu anlaşılır; bu da kodunuzun geri kalanının ne yaptığına bağlı olarak farklılık gösterebilir.

Benzer şekilde, unwrap ve expect metodları, hataları nasıl ele alacağınıza karar vermeye hazır olmadan önce prototip oluştururken çok kullanışlıdır. Programınızı daha sağlam hale getirmeye hazır olduğunuzda kodunuzda net işaretler bırakırlar.

Bir testte bir metod çağrısı başarısız olursa, bu yöntem test edilen fonksiyon olmasa bile tüm testin başarısız olmasını istersiniz. panic! bir testin başarısız olarak işaretlenme şekli olduğundan, unwrap veya expect çağrısı tam olarak olması gereken şeydir.

Derleyiciden Daha Fazla Bilgiye Sahip Olduğunuz Durumlar

Ayrıca, Result'un Ok değerine sahip olmasını sağlayan başka bir mantığınız olduğunda unwrap veya expect çağrısı yapmak da uygun olacaktır, ancak bu mantık derleyicinin anlayabileceği bir şey değildir. Hala işlemeniz gereken bir Result değeriniz olacaktır: çağırdığınız işlem, sizin özel durumunuzda mantıksal olarak imkansız olsa bile, genel olarak başarısız olma olasılığına sahiptir. Kodu manuel olarak inceleyerek hiçbir zaman bir Err varyantına sahip olmayacağınızdan emin olabiliyorsanız, unwrap'i çağırmak tamamen kabul edilebilir ve hatta expect metninde hiçbir zaman bir Err varyantına sahip olmayacağınızı düşünmenizin nedenini belgelemek daha iyidir. İşte bir örnek:

fn main() {
    use std::net::IpAddr;

    let home: IpAddr = "127.0.0.1"
        .parse()
        .expect("Hardcoded IP address should be valid");
}

Kodlanmış bir dizeyi ayrıştırarak bir IpAddr örneği oluşturuyoruz. 127.0.0.1'in geçerli bir IP adresi olduğunu görebiliyoruz, bu nedenle burada expect kullanmak kabul edilebilir. Ancak, sabit kodlu, geçerli bir dizeye sahip olmak, parse metodunun dönüş türünü değiştirmez: hala bir Result değeri alırız ve derleyici, bu dizginin her zaman geçerli bir IP adresi olduğunu görecek kadar akıllı olmadığından, Err varyantı bir olasılıkmış gibi Result'u işlememizi sağlar. IP adresi dizgisi programa kodlanmak yerine bir kullanıcıdan gelseydi ve bu nedenle hata olasılığı olsaydı, bunun yerine Result'u kesinlikle daha sağlam bir şekilde ele almak isterdik. Bu IP adresinin sabit kodlu olduğu varsayımından bahsetmek, gelecekte IP adresini başka bir kaynaktan almamız gerekirse, daha iyi hata işleme kodu beklentisini değiştirmemizi sağlayacaktır.

Hata İşleme Yönergeleri

Kodunuzun kötü bir duruma düşme olasılığı olduğunda kodunuzun paniğe kapılması tavsiye edilir. Bu bağlamda kötü durum, geçersiz değerler, çelişkili değerler veya eksik değerlerin kodunuza aktarılması gibi bazı varsayımların, garantilerin, sözleşmelerin veya değişmezlerin ihlal edilmesi ve ayrıca aşağıdakilerden bir veya daha fazlasının gerçekleşmesi durumudur:

  • Kötü durum, kullanıcının yanlış formatta veri girmesi gibi ara sıra meydana gelebilecek bir durumun aksine beklenmedik bir durumdur. Bu noktadan sonra kodunuzun her adımda sorunu kontrol etmek yerine bu kötü durumda olmamaya güvenmesi gerekir. Kullandığınız türlerde bu bilgiyi kodlamanın iyi bir yolu yoktur. Bölüm 17'deki “Durumları ve Davranışları Türler Olarak Kodlama” kısmında ne demek istediğimizi bir örnekle açıklayacağız.

  • Birisi kodunuzu çağırır ve mantıklı olmayan değerler girerse, yapabiliyorsanız bir hata döndürmek en iyisidir, böylece kütüphane kullanıcısı bu durumda ne yapmak istediğine karar verebilir. Ancak, devam etmenin güvensiz veya zararlı olabileceği durumlarda, en iyi seçim panic! çağrısı yapmak ve kütüphanenizi kullanan kişiyi kodlarındaki hata konusunda uyarmak olabilir, böylece geliştirme sırasında düzeltebilirler. Benzer şekilde, kontrolünüz dışında olan harici bir kodu çağırıyorsanız ve bu kod düzeltme imkanınızın olmadığı geçersiz bir durum döndürüyorsa panic! çağrısı yapılmalıdır.

  • Başarısızlık beklendiğinde, panic! çağrısı yapmaktansa bir Result döndürmek daha uygundur. Örnekler arasında, hatalı biçimlendirilmiş verilerin verildiği bir ayrıştırıcı veya bir hız sınırına ulaştığınızı gösteren bir durum döndüren bir HTTP isteği yer alır. Bu durumlarda, Result döndürmek, başarısızlığın, çağıran kodun nasıl ele alacağına karar vermesi gereken beklenen bir olasılık olduğunu gösterir.

Kodunuz, geçersiz değerler kullanılarak çağrıldığında kullanıcıyı riske atabilecek bir işlem gerçekleştirdiğinde, kodunuz önce değerlerin geçerli olduğunu doğrulamalı ve değerler geçerli değilse paniklemelidir. Bu çoğunlukla güvenlik nedenleriyle yapılır: geçersiz veriler üzerinde işlem yapmaya çalışmak kodunuzu güvenlik açıklarına maruz bırakabilir. Sınır dışı bir bellek erişimi denediğinizde standart kütüphanenin panic! çağrısı yapmasının ana nedeni budur: mevcut veri yapısına ait olmayan belleğe erişmeye çalışmak yaygın bir güvenlik sorunudur. Fonksiyonların genellikle sözleşmeleri vardır: davranışları yalnızca girdilerin belirli gereksinimleri karşılaması durumunda garanti edilir. Sözleşme ihlal edildiğinde paniklemek mantıklıdır çünkü bir sözleşme ihlali her zaman çağıran tarafında bir hata olduğunu gösterir ve çağıran kodun açıkça ele almasını istediğiniz bir hata türü değildir. Aslında, çağıran kodun hatayı telafi etmesinin makul bir yolu yoktur; çağıran programcıların kodu düzeltmesi gerekir. Bir fonksiyon için sözleşmeler, özellikle de bir ihlal paniğe neden olacaksa, işlevin API belgelerinde açıklanmalıdır.

Ancak, tüm fonksiyonlarınızda çok sayıda hata kontrolü olması ayrıntılı ve can sıkıcı olacaktır. Neyse ki, Rust'ın tür sistemini (ve dolayısıyla derleyici tarafından yapılan tür kontrolünü) kontrollerin çoğunu sizin için yapmak için kullanabilirsiniz. Fonksiyonunuz parametre olarak belirli bir türe sahipse, derleyicinin zaten geçerli bir değere sahip olduğunuzdan emin olduğunu bilerek kodunuzun mantığına devam edebilirsiniz. Örneğin, bir Option yerine bir türünüz varsa, programınız hiçbir şey yerine bir şey olmasını bekler. Bu durumda kodunuz Some ve None varyantları için iki durumla uğraşmak zorunda kalmaz: kesinlikle bir değere sahip olmak için yalnızca bir durum olacaktır. Fonksiyonunuza hiçbir şey iletmemeye çalışan kod derlenmez bile, bu nedenle fonksiyonunuzun çalışma zamanında bu durumu kontrol etmesi gerekmez. Başka bir örnek de u32 gibi işaretsiz bir tam sayı türü kullanmaktır, bu da parametrenin asla negatif olmamasını sağlar.

Doğrulama için Özel Tipler Oluşturma

Geçerli bir değere sahip olduğumuzdan emin olmak için Rust'ın tür sistemini kullanma fikrini bir adım daha ileri götürelim ve doğrulama için özel bir tür oluşturmaya bakalım. Bölüm 2'de kodumuzun kullanıcıdan 1 ile 100 arasında bir sayı tahmin etmesini istediği tahmin oyununu hatırlayın. Gizli sayımızla karşılaştırmadan önce kullanıcının tahmininin bu sayılar arasında olduğunu doğrulamadık; yalnızca tahminin pozitif olduğunu doğruladık. Bu durumda, sonuçlar çok vahim değildi: “Too high” veya "Too low" çıktılarımız yine de doğru olacaktı. Ancak, kullanıcıyı geçerli tahminlere yönlendirmek ve bir kullanıcı aralık dışında bir sayı tahmin ettiğinde, bunun yerine örneğin harfler yazdığında farklı bir davranışa sahip olmak yararlı bir geliştirme olacaktır.

Bunu yapmanın bir yolu, potansiyel olarak negatif sayılara izin vermek için tahmini yalnızca bir u32 yerine bir i32 olarak ayrıştırmak ve ardından sayının aralıkta olup olmadığına dair bir kontrol eklemek olabilir:

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
        // --snip--

        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: i32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        if guess < 1 || guess > 100 {
            println!("The secret number will be between 1 and 100.");
            continue;
        }

        match guess.cmp(&secret_number) {
            // --snip--
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

if ifadesi, değerimizin aralık dışında olup olmadığını kontrol eder, kullanıcıya sorun hakkında bilgi verir ve döngünün bir sonraki yinelemesini başlatmak ve başka bir tahmin istemek için continue çağrısı yapar. if ifadesinden sonra, tahminin 1 ile 100 arasında olduğunu bilerek tahmin ile gizli sayı arasındaki karşılaştırmalara devam edebiliriz.

Ancak, bu ideal bir çözüm değildir: programın yalnızca 1 ile 100 arasındaki değerler üzerinde çalışması kesinlikle kritikse ve bu gereksinime sahip birçok fonksiyonu varsa, her işlevde bunun gibi bir kontrol yapmak sıkıcı olacaktır (ve performansı etkileyebilir).

Bunun yerine, yeni bir tür oluşturabilir ve doğrulamaları her yerde tekrarlamak yerine türün bir örneğini oluşturmak için doğrulamaları bir fonksiyona koyabiliriz. Bu şekilde, fonksiyonların imzalarında yeni türü kullanmaları ve aldıkları değerleri güvenle kullanmaları güvenli olur. Liste 9-13, yalnızca yeni fonksiyon 1 ile 100 arasında bir değer alırsa Guess'in bir örneğini oluşturacak bir Guess türü tanımlamanın bir yolunu göstermektedir.


#![allow(unused)]
fn main() {
pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess value must be between 1 and 100, got {}.", value);
        }

        Guess { value }
    }

    pub fn value(&self) -> i32 {
        self.value
    }
}
}

Liste 9-13: Yalnızca 1 ile 100 arasındaki değerlerle devam edecek bir Guess türü

İlk olarak, bir i32 tutan value adlı bir üyeye sahip Guess adlı bir yapı tanımlarız. Burası sayının saklanacağı yerdir.

Ardından, Guess değerlerinin örneklerini oluşturan Guess üzerinde new adında ilişkili bir fonksiyon uyguluyoruz. new fonksiyonu, i32 türünde value adında bir parametreye sahip olacak ve bir Guess değeri döndürecek şekilde tanımlanır. new fonksiyonunun gövdesindeki kod, 1 ile 100 arasında olduğundan emin olmak için değeri test eder. Eğer value bu testi geçemezse, çağıran kodu yazan programcıyı düzeltmesi gereken bir hata olduğu konusunda uyaracak bir panic! çağrısı yaparız, çünkü bu aralığın dışında bir değere sahip bir Guess oluşturmak Guess::new'in dayandığı sözleşmeyi ihlal edecektir. Guess::new'in paniğe kapılabileceği koşullar kamuya açık API dokümantasyonunda tartışılmalıdır; Bölüm 14'te oluşturacağınız API dokümantasyonunda panic olasılığını belirten dokümantasyon kurallarını ele alacağız. Eğer value testi geçerse, value alanı value parametresine ayarlanmış yeni bir Guess yaratırız ve Guess'i döndürürüz.

Ardından, self öğesini ödünç alan, başka parametresi olmayan ve bir i32 döndüren value adlı bir metod yazarız. Bu tür yöntemlere bazen getter adı verilir, çünkü amacı alanlarından bazı verileri almak ve döndürmektir. Guess yapısının value üyesi gizli olduğu için burada yaygın metod gereklidir. Değer üyesinin gizli olması önemlidir, böylece Guess yapısını kullanan kodun değeri doğrudan ayarlamasına izin verilmez: modül dışındaki kod, bir Guess tanımı oluşturmak için Guess::new fonksiyonunu kullanmalıdır, böylece Guess'in Guess::new fonksiyonundaki koşullar tarafından kontrol edilmemiş bir değere sahip olmasının hiçbir yolu yoktur.

Parametresi olan veya yalnızca 1 ile 100 arasındaki sayıları döndüren bir fonksiyon, imzasında bir i32 yerine bir Guess aldığını veya döndürdüğünü bildirebilir ve gövdesinde herhangi bir ek kontrol yapması gerekmez.

Özet

Rust'ın hata işleme özellikleri, daha sağlam kod yazmanıza yardımcı olmak için tasarlanmıştır. panic! makrosu, programınızın üstesinden gelemeyeceği bir durumda olduğunu bildirir ve geçersiz veya yanlış değerlerle devam etmeye çalışmak yerine sürece durmasını söylemenizi sağlar. Result enum'u, işlemlerin kodunuzun kurtarabileceği bir şekilde başarısız olabileceğini belirtmek için Rust'ın tür sistemini kullanır. Result'u, kodunuzu çağıran koda olası başarı veya başarısızlığı da ele alması gerektiğini söylemek için kullanabilirsiniz. panic! ve Result'u uygun durumlarda kullanmak, kaçınılmaz sorunlar karşısında kodunuzu daha güvenilir hale getirecektir.

Standart kütüphanenin Option ve Result enum'ları ile yaygınları nasıl kullandığını gördüğünüze göre, yaygınların nasıl çalıştığından ve bunları kodunuzda nasıl kullanabileceğinizden bahsedeceğiz.