Rust:”Error Handling” Anlamak
panic! ile Kurtarılamaz Hatalar
Bazen kodunuzda kötü şeyler olur ve bu konuda yapabileceğiniz hiçbir şey yoktur. Bu gibi durumlarda, Rust panic!
makrosunu kullanabilirsiniz. Pratikte paniğe neden olmanın iki yolu vardır: kodumuzun paniklemesine neden olan bir eylemde bulunarak (sondan sonra bir diziye erişmek gibi) veya panic!
makrosunu açıkça çağırarak. Her iki durumda da programımızda paniğe neden oluyoruz. Varsayılan olarak, bu panikler bir hata iletisi yazdırır, gevşetir, yığını temizler ve çıkın. Bir ortam değişkeni aracılığıyla, paniğin kaynağını izlemeyi kolaylaştırmak için panik oluştuğunda Rust'un çağrı yığınını görüntülemesini de sağlayabilirsiniz.
Bir panic!’e Tepki Olarak Yığını Çözme veya İptal Etme
Rust’ta bir panik olursa, program varsayılan olarak yığını (stack) açmaya başlar, yani Rust yığından yukarı doğru ilerler ve karşılaştığı her işlevden veriyi temizler. Ancak, bu yığın geriye doğru ilerleme ve temizleme çok fazla çalışmayı gerektirir. Bu nedenle, Rust, programı hemen durdurarak (aborting) alternatifi seçmenize izin verir. Bu, verilerin temizlenmediği bir şekilde programı sonlandırır.
Bu durumda, program tarafından kullanılan bellek işletim sistemi tarafından temizlenmelidir. Projenizde çıktı olarak oluşacak ikili dosyayı mümkün olan en küçük hale getirmek istiyorsanız, Cargo.toml dosyanızdaki uygun [profil] bölümlerine panic = ‘abort’ ekleyerek yığını açmak yerine panik durumunda durdurmayı (aborting) seçebilirsiniz. Örneğin, panik durumunda release modunda durdurmayı (aborting) seçmek isterseniz, aşağıdaki gibi ekleyin:
Bir basit programda panic! çağrısını deneyelim:
Programı çalıştırdığınızda, aşağıdaki gibi bir şey göreceksiniz:
panic! çağrısı son iki satırda bulunan hata mesajını oluşturur. Birinci satır panik mesajımızı gösterir ve kaynak kodumuzda panik olma olayının olduğu yeri: src/main.rs:2:5, ikinci satırın, src/main.rs dosyamızın beşinci karakterini gösterir.
Bu durumda, gösterilen satır kodumuzun bir parçasıdır ve bu satıra gittiğimizde panic! makro çağrısını görebiliriz. Diğer durumlarda, panic! çağrısı kodumuz tarafından çağrılan kodun bir parçası olabilir ve hata mesajı tarafından rapor edilen dosya adı ve satır numarası, panik! makro çağrısının yapıldığı bir başkasının kodu olabilir, değil de panik! çağrısına nihayetinde sebep olan kodumuzun satırı. Sorunu neden olan kodumuzun hangi parçası olduğunu anlamaya yardımcı olmak için panic! çağrısının hangi fonksiyonlardan geldiğini gösteren backtrace’i kullanabiliriz. Backtrace’leri bir sonraki adımda daha ayrıntılı olarak ele alacağız.
Panic! Backtrace Kullanımı
Bir panik! çağrısının, kodumuzdan direkt olarak makro çağrısı yapmak yerine bir kütüphane tarafından nasıl olacağını görmek için başka bir örnek inceleyelim. Listing 9–1, bir vektörün geçerli dizinlerin dışına erişmeyi dener.
Bu kod panik atar!
Listing 9–1: Vektörün sonuna giden bir elemana erişme girişimi, panik! çağrısına sebep olacaktır.
Burada, vektörümüzün 100. elemanına (vektörler sıfırdan başlayarak indekslendiğinden indeks 99) erişmeyi deniyoruz, ancak vektörün sadece 3 elemanı var. Bu durumda, Rust panik atar. [] kullanımı bir eleman döndürmesi gerekir, ancak geçersiz bir indeks verirseniz, Rust’ın burada doğru olan bir eleman döndürebileceği yoktur.
C’de, bir veri yapısının sonuna okuma yapmaya çalışmak belirsiz bir davranıştır. Veri yapısının o elemanına ait olmayan bir bellek konumundaki veriyi alabilirsiniz, ancak bellek o veri yapısına ait değil. Bu, bir buffer overread olarak adlandırılır ve bir saldırganın veri yapısının sonundaki veriye erişmesine izin vermeyi amaçlayan bir dizin böyle bir şekilde manipüle edebilirse güvenlik açıklarına neden olabilir.
Bu tür bir güvenlik açığından programınızı korumanız için, yok olmayan bir indekse eleman okumaya çalışırsanız, Rust çalışmayı durdurur ve devam etmeyi reddeder. Deneyelim ve görelim:
Bu hatayı main.rs’nin dördüncü satırında index 99'a erişme girişimimiz gösterir. Bir sonraki not satırı, neyin olup bittiğini ayrıntılı olarak gösteren bir backtrace almak için RUST_BACKTRACE ortam değişkenini ayarlamamızı söyler. Backtrace, bu noktaya ulaştıran tüm çağrılan fonksiyonların listesidir. Rust’ta backtrace’ler diğer dillerde olduğu gibi çalışır: okumayı anlamaya yardımcı olan anahtar, en üstten başlayarak yazdığınız dosyalara kadar okumaktır. Bu, sorunun kaynağıdır. Sorunun kaynağının üstündeki satırlar kodunuz tarafından çağrılan kod, altındaki satırlar ise kodunuzu çağıran kodtur. Bu öncesi ve sonrası satırlar, çekirdek Rust kodu, standart kütüphane kodu veya kullandığınız crate’leri içerebilir. RUST_BACKTRACE ortam değişkenini 0 dışında herhangi bir değere ayarlayarak bir backtrace almayı deneyelim. Listing 9–2, göreceğiniz çıktıya benzer bir şeyleri gösterir.
Bu çok fazla çıktı! Gördüğünüz tam çıktı, işletim sisteminiz ve Rust sürümüne göre farklı olabilir. Bu tür bilgilerle backtrace’ler almak için hata simgeleri etkinleştirilmelidir. Hata simgeleri, cargo build veya cargo run kullanırken, — release bayrağı olmadan varsayılan olarak etkinleştirilir, bizim burada yaptığımız gibi.
Listing 9–2'de çıktıda, backtrace’nin altıncı satırı, sorunu oluşturan satırı gösterir: src/main.rs’nin dördüncü satırı. Eğer programımızın panik atmamasını istiyorsak, ilk satır tarafından işaret edilen konumdan araştırmaya başlamalıyız. Listing 9–1'de, kasıtlı olarak panik atacak kod yazdık, panikten kurtulmanın yolu, vektör indekslerinin dışına çıkmayan bir eleman istememektir. İleride kodunuz panik atarsa, panik atmaya sebep olan kodun ne yaptığını ve ne yapması gerektiğini bulmanız gerekecektir.
Daha sonra, bu bölümde “To panic! or Not to panic!” bölümünde, panik! ve panik! kullanımının nasıl olması gerektiği konusuna döneceğiz. Sonra, Result kullanarak bir hata durumundan nasıl kurtulunacağını inceleyeceğiz.
Sonuç ile Geri Dönülebilir Hatalar
Birçok hata, programın tamamen durmasını gerektirmez. Bazen, bir işlev başarısız olur ve bu nedeni kolayca yorumlayıp yanıt verebilirsiniz. Örneğin, bir dosya açmayı denerseniz ve bu işlem dosya olmadığı için başarısız olursa, dosyayı oluşturmayı tercih edebilirsiniz.
2. Bölümdeki “Result Kullanılarak Olası Hataların İşlenmesi” bölümünden hatırlayın ki, Result adlı tür iki varyant, Ok ve Err, olarak tanımlanmıştır:
T ve E adında iki tür parametresi vardır. Bu parametreleri daha detaylı olarak 10. Bölümde inceleyeceğiz. Şu anda bilmeniz gereken şey, T, Ok varyantı içinde başarılı bir durumda döndürülecek değerin türünü temsil ederken, E ise Err varyantı içinde başarısız bir durumda döndürülecek hata değerinin türünü temsil eder. Result’ın bu tür parametreleri olması nedeniyle, başarılı değer ve hata değerlerinin farklı olabileceği çok farklı durumlarda Result türünü ve bu tür üzerinde tanımlanan işlevleri kullanabiliriz.
Bir Result değerini döndüren bir işlevi çağıralım. Listing 9–3'te bir dosya açmayı deniyoruz.
Lising 9–3: Dosya açma
File::open işlevinin döndürdüğü tür Result<T, E>’dir. T adlı tür parametresi, File::open işlevinin uygulamasıyla doldurulmuştur ve başarılı değerin türü std::fs::File’tir, bu da bir dosya işaretçisidir. E adlı tür parametresi ise hata değerinde kullanılmıştır ve std::io::Error’tur. Bu döndürülen tür, File::open işlevinin başarılı olup okuma veya yazma işlemleri için bir dosya işaretçisi döndürebileceği anlamına gelir. İşlev çağrısı aynı zamanda başarısız da olabilir: Örneğin, dosya olmayabilir veya dosyaya erişim izni olmayabilir. File::open işlevinin bizlere başarılı olduğunu veya başarısız olduğunu söyleme ve aynı zamanda dosya işaretçisi veya hata bilgisi verme bir yolu olması gerekir. Bu bilgi, Result adlı türün taşıdığı bilgidir.
File::open işlevinin başarılı olduğu durumda, greeting_file_result değişkenindeki değer bir Ok örneği olacaktır ve bu örnek bir dosya işaretçisi içerecektir. İşlev başarısız olduğunda ise, greeting_file_result değişkenindeki değer bir Err örneği olacak ve bu örnekte oluşan hata türü hakkında daha fazla bilgi içerecektir.
Listing 9–3'te yer alan kodu, File::open işlevinin döndürdüğü değere göre farklı işlemler yapmak için genişletmemiz gerekiyor. Listing 9–4, Chapter 6'ta ele aldığımız match ifadesi adında basit bir araç kullanarak Result’ı işleme şeklinde bir yöntemi gösterir.
Listing 9–4: Result’ın olabilir döndürdüğü varyantları işlemek için match ifadesi kullanımı
Option adlı tür gibi, Result adlı tür ve varyantları prelude tarafından çağrılmıştır, bu nedenle match braketlerinde Ok ve Err varyantları öncesine Result:: yazmamıza gerek yoktur.
Sonuç Ok ise, bu kod içeriğini Ok varyantından çıkaracak ve daha sonra bu dosya işaretçisi değerini greeting_file değişkenine atayacaktır. Match’ten sonra, dosya işaretçisini okuma veya yazma işlemleri için kullanabiliriz.
Match’in diğer braketi, File::open işlevinden Err değeri döndüğü durumu işler. Bu örnekte, panic! makrosunu çağırdık. Eğer mevcut dizinimizde hello.txt adlı bir dosya yoksa ve bu kodu çalıştırırsak, panic! makrosundan aşağıdaki çıktıyı göreceğiz:
Her zaman olduğu gibi, bu çıktı bize tam olarak neyin yanlış gittiğini söyler.
Farklı Hataları Eşleştirme
Listing 9–4'de yer alan kod, File::open neden başarısız olursa olsun panic! edecektir. Ancak, farklı hatalar için farklı işlemler gerçekleştirmek istiyoruz: Eğer File::open, dosya olmadığı için başarısız olursa, dosyayı oluşturmak ve yeni dosyanın işaretçisini döndürmek istiyoruz. Eğer File::open başka bir nedenden dolayı başarısız olursa — örneğin dosyayı açmak için izin olmadığı için — hala kodun Listing 9–4'te olduğu gibi panic! etmesini istiyoruz. Bunun için, Listing 9–5'te gösterildiği gibi bir iç match ifadesi ekliyoruz.
Listing 9–5: Farklı türde hataları farklı şekillerde işleme
File::open işlevinin Err varyantı içinde döndürdüğü değerin türü io::Error’dir, bu da standart kütüphane tarafından sağlanan bir yapıdır. Bu yapının bir kind adlı işlevi vardır ve bu işlevi çağırarak io::ErrorKind değerine ulaşabiliriz. io::ErrorKind adlı tür standart kütüphane tarafından sağlanan bir türdür ve bir io işleminin sonucunda oluşabilecek farklı hataları temsil eden varyantları vardır. Kullanmak istediğimiz varyant ErrorKind::NotFound’dur ve bu, açmaya çalıştığımız dosyanın henüz olmadığını gösterir. Bu nedenle, greeting_file_result’a eşleşiriz ancak aynı zamanda error.kind() üzerinde de bir iç eşleşme yaparız.
İç eşleşmede kontrol etmek istediğimiz koşul, error.kind() tarafından döndürülen değerin ErrorKind adlı türün NotFound varyantı olup olmadığıdır. Eğer bu varyant ise, File::create ile dosyayı oluşturmayı deneriz. Ancak, File::create de başarısız olabilir, bu nedenle iç eşleşme ifadesinin ikinci braketine ihtiyacımız vardır. Dosya oluşturulamadığında, farklı bir hata mesajı yazdırılır. Dış eşleşmenin ikinci braketi aynı kalır, bu nedenle program eksik dosya hatası dışındaki herhangi bir hatada panic! eder.
Result<T, E> ile birlikte match Kullanımının Alternatifleri
Bu çok fazla match! Match ifadesi çok yararlıdır ancak aynı zamanda çok da primitif bir yöntemdir. Bölüm 13'te, Result<T, E> ile birlikte kullanılan kapamaları öğreneceksiniz. Bu yöntemler, Result<T, E> değerlerini kodunuzda işlerken match kullanımından daha kısa olabilir.
Örneğin, Listing 9–5'te gösterilen aynı mantığı burada kapamalar ve unwrap_or_else yöntemini kullanarak yazabiliriz:
Bu kodun Listing 9–5 ile aynı davranışı vardır, ancak içinde hiç match ifadesi yoktur ve okuması daha temizdir. Bölüm 13'ü okuduktan sonra bu örneğe geri dönün ve standart kütüphane dokümantasyonunda unwrap_or_else yöntemini araştırın. Hatalarla uğraşırken, daha fazla sayıda bu yöntem büyük iç içe geçmiş match ifadelerini temizleyebilir.
Hata Durumunda Panic İçin Kısayollar: unwrap ve expect
Match kullanımı yeterince işe yarar, ancak biraz uzun ve her zaman niyeti iyi iletlemez. Result<T, E> adlı tür üzerinde, çeşitli daha spesifik görevleri gerçekleştirmek için çeşitli yardımcı işlevler tanımlanmıştır. Unwrap adlı işlev, Listing 9–4'te yazdığımız match ifadesine benzer şekilde kısaltılmış bir işlevdir. Eğer Result değeri Ok varyantı ise, unwrap içerdeki Ok değerini döndürür. Eğer Result Err varyantı ise, unwrap bizim için panic! makrosunu çağırır. Burada unwrap’ın nasıl çalıştığını gösteren bir örnek:
Eğer hello.txt dosyası olmadan bu kodu çalıştırırsak, unwrap işlevinin çağırdığı panic! çağrısından bir hata mesajı görürüz:
Benzer şekilde, expect işlevi de bizlere panic! hata mesajı seçme imkanı verir. Unwrap yerine expect kullanımı ve iyi hata mesajları sağlama, niyetimizi iletlemeyi ve bir panic’in kaynağını takip etmeyi daha kolay hale getirir. Expect’in sözdizimi şöyle görünür:
Expect’i unwrap ile aynı şekilde kullanırız: dosya işaretçisini döndürmek veya panic! makrosunu çağırmak için. Expect tarafından panic! çağrısında kullanılan hata mesajı, unwrap tarafından kullanılan varsayılan panic! mesajı yerine expect’e geçirilen parametre olacaktır. Bu şöyle görünür:
Üretim kalitesinde kodlarda, çoğu Rustaceler unwrap yerine expect tercih eder ve işlemin her zaman başarılı olması beklendiği nedeni hakkında daha fazla bağlam verir. Böylece, varsayımlarınızın bir gün yanlış çıktığında, hata ayıklama için daha fazla bilgiye sahip olursunuz.
Hataların İletilmesi
Bir işlevin uygulaması başarısız olma ihtimali olan bir şey çağırdığında, işlevin içinde hatayı ele almak yerine, hatayı çağıran koda geri döndürebilirsiniz. Bu, hatanın iletilmesi olarak bilinir ve çağıran kodun daha fazla kontrolünü sağlar, çünkü hatanın nasıl ele alınmasını belirleyen daha fazla bilgi veya mantık olabilir, kodunuzun içeriğinde mevcut olanlardan daha fazla.
Örneğin, Listing 9–6 bir kullanıcı adını bir dosyadan okuyan bir işlevi gösterir. Eğer dosya mevcut değil veya okunamıyorsa, bu işlev işlevi çağıran kod için bu hataları geri döndürür.
Bu işlev çok daha kısa bir şekilde yazılabilir, ancak hataları ele almayı keşfetmek için önce çok şeyi elle yapacağız; sonunda daha kısa yolu göstereceğiz. Önce işlevin dönüş türüne bakalım: Result<String, io::Error>. Bu, işlevin T tipinde genel parametre olarak String ve E tipinde genel tip olarak io::Error’u doldurulmuş Result<T, E> türünde bir değer döndürdüğü anlamına gelir.
Bu işlev herhangi bir sorun olmadan başarılı olursa, bu işlevi çağıran kod Ok değeri olarak bir String alır — bu işlevin dosyadan okuduğu kullanıcı adı. Bu işlev herhangi bir sorunla karşılaşırsa, çağıran kod Err değeri olarak bir io::Error örneği içeren hatalar hakkında daha fazla bilgi içeren bir Err değeri alır. Bu işlevin dönüş türü olarak io::Error’u seçtik çünkü bu işlevin vücutunda çağırdığımız ve başarısız olma ihtimali olan iki işlemin hatalarının dönüş türü: File::open işlevi ve read_to_string yöntemi.
İşlevin vücudu, File::open işlevini çağırarak başlar. Daha sonra, Listing 9–4'te yaptığımız gibi Result değerini bir eşleşmeyle ele alırız. Eğer File::open başarılı olursa, eşleşme değişkeni dosya el ileki username_file değişkeninde değer olarak dosya el ileği olur ve işlev devam eder. Err durumunda, panic! yerine, işlevin tamamen dışına çıkmak için return anahtar kelimesini kullanarak işlevin tamamen dışına çıkarız ve File::open’dan gelen hataları, şimdi değişken e’de olan, çağıran kodun hatası olarak bu işlevin hatası olarak geri döndürürüz.
Bu nedenle, username_file içinde bir dosya el ileğimiz varsa, işlev daha sonra username adında yeni bir String oluşturur ve username_file içindeki dosya el ileğine read_to_string yöntemini çağırarak dosyanın içeriğini username içine okur. Read_to_string yöntemi de, File::open’ın başarılı olduğu halde başarısız olma ihtimali olan bir Result döndürür. Bu nedenle, read_to_string’in sonucunu ele almak için başka bir eşleşmeye ihtiyaç duymaz.
Böylece, işlevimiz başarılı olursa, dosyadan okunan username, Ok içinde döndürülür. Eğer read_to_string başarısız olursa, aynı şekilde File::open’ın döndürdüğü hataları döndüren bir şekilde hatayı döndürürüz. Ancak, return’u açıkça söylemeye gerek yoktur, çünkü bu işlevin son ifadesidir.
Bu kodu çağıran kod, daha sonra bir username içeren Ok değeri veya bir io::Error içeren bir Err değeri alıp bunları ele almaya çalışır. İşte bu değerlerle ne yapılacağı çağıran kodun elindedir. Eğer çağıran kod bir Err değeri alırsa, programı çökertmek için panic! çağırabilir, varsayılan bir kullanıcı adı kullanabilir veya örneğin bir dosyadan değil, başka bir yerden kullanıcı adını arayabilir. İşte çağıran kodun gerçekte ne yapmaya çalıştığını bilemediğimiz için, tüm başarı veya hataları bilgilerini yukarı doğru iletiriz ve bu bilgileri uygun şekilde ele alabilsin.
Bu hataları iletme deseni, Rust’ta o kadar yaygındır ki, Rust, bu işlemi daha kolay hale getirmek için soru işareti işaretleyicisini sağlar.
Hata Yakalamak İçin Bir Kısayol: “?” Operatörü
Listing 9–7, Listing 9–6'daki fonksiyonun aynı özelliklere sahip bir uygulamasını gösterir, ancak bu uygulama “?” operatörünü kullanır.
Listing 9–7: “?” operatörünü kullanan bir fonksiyon, hata değerlerini çağrı yapılan koda geri döndürür
“?” operatörü, Listing 9–6'daki match ifadesinin yaptığı şeylerle neredeyse aynı şekilde çalışan bir şekilde tanımlanmış bir Result değerinden sonra yer alır. Eğer Result değeri bir Ok ise, Ok içindeki değer bu ifade içinden döndürülür ve program devam eder. Eğer değer bir Err ise, Err fonksiyonun tamamından döndürülür, bu da return anahtar sözcüğünü kullanmış gibi olur ve hata değeri çağrı yapılan koda yayılır.
Listing 9–6'daki match ifadesi ile “?” operatörü arasında bir fark vardır: “?” operatörü üzerinde çağrılan hata değerleri, standart kütüphane içinde From trait’i tarafından tanımlanan from fonksiyonu üzerinden geçirilir. Bu fonksiyon, bir tipteki değerlerin başka bir tipe dönüştürülmesine yarar. “?” operatörü from fonksiyonunu çağırdığında, alınan hata türü, mevcut fonksiyonun dönüş türünde tanımlanan hata türüne dönüştürülür. Bu, bir fonksiyonun, hata yüzünden hata olma ihtimali olan birçok neden için tüm hata nedenlerini temsil etmek üzere bir hata türü döndüğünde yararlıdır.
Örneğin, Listing 9–7'deki read_username_from_file fonksiyonunu, tanımladığımız OurError adlı özel bir hata türünü döndüren bir şekilde değiştirebiliriz. Ayrıca, io::Error’dan OurError nesneleri oluşturmak için impl Fromio::Error for OurError’ı da tanımlarsak, read_username_from_file fonksiyonunun içeriğinde yer alan “?” operatörleri from çağırarak hata türlerini dönüştürür ve fonksiyona ekstra kod eklemeye gerek kalmaz.
Listing 9–7 bağlamında, File::open çağrısının sonundaki “?” ifadesi, username_file değişkenine Ok içindeki değeri döndürür. Eğer bir hata oluşursa, “?” operatörü fonksiyonun tamamından erken döner ve çağrı yapılan koda herhangi bir Err değerini verir. read_to_string çağrısının sonundaki “?” için de aynı şey geçerlidir.
“?” operatörü, çok sayıda tekrarlayan kodu ortadan kaldırır ve bu fonksiyonun uygulamasını daha basit hale getirir. Bu kodu daha da kısaltabiliriz, “?” sonrası hemen bir dizi metod çağrısıyla dağıtımı, Listing 9–8'de gösterildiği gibi.
Listing 9–9: Dosyayı açmadan önce okumak yerine fs::read_to_string kullanımı
Bir dosyayı bir dizeye okuma, oldukça yaygın bir işlemdir, bu yüzden standart kütüphane, dosyayı açar, yeni bir String oluşturur, dosya içeriğini okur, içeriği o String’e koyar ve geri döndürür şekilde kullanışlı fs::read_to_string fonksiyonunu sağlar. Tabii ki, fs::read_to_string kullanımı, bizim tüm hata ayıklama aşamalarını açıklamaya fırsat vermez, bu yüzden önce uzun yolu yaptık.
“?” Operatörünün Kullanılabileceği Yerler
“?”, sadece dönüş türü, “?” ile kullanılan değerle uyumlu olan fonksiyonlarda kullanılabilir. Bu, “?” operatörünün fonksiyondan değerin erken dönüşünü gerçekleştirme şeklinde tanımlanmasındandır, Listing 9–6'da tanımladığımız match ifadesine benzer şekilde. Listing 9–6'da, match, bir Result değeri kullanıyordu ve erken dönüş kolu bir Err(e) değeri döndürüyordu. Fonksiyonun dönüş türü, bu dönüşle uyumlu olabilecek bir Result olmalıdır.
Listing 9–10'da, dönüş türü ile “?” operatörünü kullandığımız değerin türü uyumsuz bir ana fonksiyonda “?” operatörünü kullandığımızda alacağımız hata bakalım:
Listing 9–10: () döndüren ana fonksiyonda “?” kullanmayı denemek derlenmez
Bu kod, başarısız olabilen bir dosya açar. “?” operatörü, File::open tarafından döndürülen Result değerini takip eder, ancak bu ana fonksiyonun dönüş türü (), Result değil. Bu kodu derlediğimizde, aşağıdaki hata mesajını alırız:
Bu hata, “?” operatörünün yalnızca “Result”, “Option” veya “FromResidual”ı uygulayan başka bir tür döndüren bir fonksiyonda kullanılabileceğini gösterir.
Hata gidermek için iki seçeneğiniz var. Bir seçenek, “?” operatörünü kullandığınız değerle uyumlu olarak fonksiyonun dönüş türünü değiştirmektir, bu sınırlamalar olmadıkça. Diğer teknik, uygun olduğu şekilde Result<T, E> değerini işlemeyi sağlayan bir match veya Result<T, E> yöntemlerinden birini kullanmaktır.
Hata mesajı ayrıca “?”’nın Option<T> değerleriyle de kullanılabileceğini de belirtti. “?”’nın Result üzerinde kullanımı gibi, Option üzerinde de “?”’yı yalnızca bir Option döndüren bir fonksiyonda kullanabilirsiniz. “?” operatörünün Option<T> üzerinde çağrılışı, “Result<T, E>” üzerinde çağrılışına benzer şekilde, değer None ise, o noktada None fonksiyondan erken döner. Değer Some ise, Some içindeki değer ifadenin sonucu olan değerdir ve fonksiyon devam eder. Listing 9–11, verilen metinde ilk satırın son karakterini bulan bir fonksiyon:
Listing 9–11: Option<T> değerine “?” operatörünün kullanımı
Bu fonksiyon, bir karakterin olup olmayacağının bilinmediğinden Option<char> döndürür. Bu kod, metin dizeleri argümanını alır ve onun üzerinde lines yöntemini çağırır, bu yöntem metin içindeki satırların bir iteratörünü döndürür. Bu fonksiyon ilk satırı incelemek istediğinden, iteratörden ilk değeri almak için next çağırır. Eğer text boş bir dize ise, next bu çağrısı None döndürecektir, bu durumda last_char_of_first_line’dan None döndürmek için “?”’yı kullanırız. Eğer text boş bir dize değilse, next metin içindeki ilk satırın bir dize kesiti içeren bir Some değerini döndürür.
“?”, dize kesitini çıkarır ve o dize kesitinin karakterlerinin bir iteratörünü almak için chars çağırabiliriz. Biz ilk satırdaki son karakterle ilgileniyoruz, bu yüzden iteratördeki son öğeyi döndürmek için last çağırırız. Bu, Option olarak işaretlenmiştir çünkü ilk satırın boş bir dize olma ihtimali vardır, örneğin text boş bir satırla başlıyorsa ancak diğer satırlarda karakterler varsa, “\nhi” gibi. Ancak, eğer ilk satırda bir son karakter varsa, Some varyantında döndürülecektir. “?” operatörünün ortasında, bu mantığı açıkça ifade etmemize yardımcı olan kısa bir yoldur ve bu fonksiyonu tek bir satırda gerçekleştirme imkanı verir. Eğer Option üzerinde “?” operatörünü kullanamazsım, bu mantığı daha fazla yöntem çağrısı veya bir match ifadesi kullanarak gerçekleştirmek zorunda kalırdık.
Not: “?” operatörünü bir Result’ün üzerinde Result döndüren bir fonksiyonda kullanabilir ve Option’ın üzerinde Option döndüren bir fonksiyonda “?” operatörünü kullanabilirsiniz, ancak bunları karıştıramazsınız. “?” operatörü otomatik olarak Result’ü bir Option’a veya tersine dönüştürmez; bu durumlarda, ok yöntemi gibi Result üzerinde ve ok_or yöntemi gibi Option üzerinde esnek bir şekilde dönüştürme yapmak için yöntemleri kullanabilirsiniz.
Bu noktaya kadar, kullandığımız tüm ana fonksiyonlar () döndürmüştür. Ana fonksiyon özel bir fonksiyondur çünkü çalıştırılabilir programların giriş ve çıkış noktasıdır ve beklenen şekilde çalışması için dönüş türünde birkaç sınırlama vardır.
Ne yazık ki, main aynı zamanda Result<(), E> döndürebilir. Listing 9–12, Listing 9–10 kodunu içerir ancak main’in dönüş türünü Result<(), Box<dyn Error>> olarak değiştirdik ve sonuna Ok(()) değerini ekledik. Bu kod şimdi derlenecektir:
Listing 9–12: Result<(), E> döndüren main’in kullanımı, Result değerlerine “?” operatörünün kullanılmasına izin verir
Listing 9–12: Result<(), E> döndüren main’in kullanımı, Result değerlerine “?” operatörünün kullanılmasına izin verir
Box<dyn Error> türü bir trait nesnesidir, bu konuyu “Chapter 17” bölümünde “Değişen Türlerdeki Değerleri İzin Veren Trait Nesneleri Kullanma” bölümünde tartışacağız. Şimdilik, Box<dyn Error>’ü “herhangi bir hata türü” olarak okuyabilirsiniz. std::io::Error tipinde hataları döndüren bir main fonksiyonunun vücutunda, Error türü Box<dyn Error> olarak belirtilerek, ? operatörünün Result değerine kullanılmasına izin verilir çünkü bu, herhangi bir Err değerinin erken döndürülmesine izin verir. Bu main fonksiyonunun vücudu sadece std::io::Error tipinde hatalar döndürecek olsa bile, bu imza Box<dyn Error> olarak belirtildiğinden, main fonksiyonunun vücuduna diğer hataları döndüren daha fazla kod eklendiğinde bu imza hala doğru olacaktır.
Panic! olmak ya da olmamak
Yani ne zaman panic! çağırılması gerektiğine ve ne zaman Result döndürülmesi gerektiğine karar vermeniz gerekiyor? Kod panik olduğunda, kurtulma şansı yoktur. İstisnasız her hangi bir hata durumunda panic! çağırabilirsiniz, ancak o zaman bir durumun kurtarılamaz olduğu kararını kullanıcı kodun adına vermiş olursunuz. Bir Result değeri döndürdüğünüzde, kullanıcı koduna seçenekler sunarsınız. Kullanıcı kodu, durumuna uygun bir şekilde kurtarma girişiminde bulunabilir veya bu durumda bir Err değerinin kurtarılamayacağını düşünebilir ve panic! çağırarak kurtarılabilir bir hata durumunu kurtarılamaz bir hata durumuna dönüştürebilir. Bu nedenle, bir işlevin başarısızlık durumunu düşündüğünüzde, Result döndürmek iyi bir varsayılan seçenektir.
Prototip kod, örnekler ve testler gibi durumlarda, Result döndürmek yerine kodun panik etmesi daha uygundur. Bunun nedenini açıklayalım ve sonra derleyicinin bir hata durumunun imkansız olduğunu anlamayabileceği, ancak insan olarak sizin anlayabileceğiniz durumları ele alalım. Bölüm, kitaplık kodunda panik etme konusunda genel kuralların nasıl belirleneceği konusunda bazı genel kurallar sunacaktır.
Örnekler, Prototip Kod ve Testler
Bir kavramı açıklamak için bir örnek yazarken, sağlam hata eleme kodunu da içererek örnek daha az anlaşılır hale gelebilir. Örneklerde, unwrap gibi bir yöntem çağrısının panik yapabileceği anlaşılır ve bu yerine, kodunuzun geri kalanı ne yapıyorsa o şekilde hata durumlarını ele almak istediğiniz yer tutucu olarak kullanılır.
Benzer şekilde, unwrap ve expect yöntemleri, hata durumlarını nasıl ele alacağınıza karar vermeye hazır olmadan prototip yaparken çok yararlıdır. Kodunuzda, programınızı daha sağlam hale getirmeye hazır olduğunuzu gösteren net işaretler bırakırlar.
Bir testte bir yöntem çağrısı başarısız olduğunda, testin tümünün başarısız olmasını istersiniz, ancak test edilen işlevselliğin olmadığı durumlarda bile. Çünkü panik! testin bir başarısızlık olarak işaret edilmesi şeklidir ve unwrap veya expect çağrısı yapılması tam olarak olması gereken şeydir.
Derleyiciden Daha Fazla Bilgiye Sahip Olunan Durumlar
Aynı zamanda, Result’ın Ok değerine sahip olmasını garanti eden başka bir mantık varsa, ancak derleyici tarafından anlaşılmayan bir şey, unwrap veya expect çağrısı yapmak da uygun olur. Yine de ele almanız gereken bir Result değeri olacaktır: çağırdığınız işlemin genel olarak başarısızlık durumuna sahip olma olasılığı hala var, ancak bu özel durumunuzda mantıksal olarak imkansızdır. Eğer kodu elle incelersek asla bir Err türevine sahip olmayacağımızı garanti edebilirsek, unwrap çağrısı yapmak mükemmeldir ve expect metninde neden asla bir Err türevine sahip olmayacağımızı belirtmek daha da iyidir. Burada bir örnek verilebilir:
Bu kod parçasında, bir IpAddr örneği oluşturulur ve bir sabit kodlanmış dizeyi parse() metodu ile ayrıştırılır. Sabit kodlanmış, geçerli bir dizinin olduğunu görebiliriz, bu nedenle burada expect kullanımı uygun olur. Ancak, geçerli bir dizi olmasına rağmen parse() metodunun dönüş türü değişmez: hala bir Result değeri alırız ve derleyici bizi Err türevinin olasılığı olarak ele alması gereken Result’ı ele almamızı zorunlu kılar çünkü derleyici IP adresinin bu dizinin her zaman geçerli olacağını göremez. Eğer IP adresi dizesi programa sabit kodlanmış değil, kullanıcıdan geldi ve bu nedenle gerçekten başarısızlık olasılığı varsa, elbette Result’ı daha sağlam bir şekilde ele almak isterdik. Bu IP adresinin sabit kodlandığı varsayımını belirtmek, gelecekte IP adresini başka bir kaynaktan almak zorunda kalırsak expect’i daha iyi hata eleme koduna değiştirmemizi hatırlatacaktır.
Doğrulama İçin Özel Türler Oluşturma
Rust’ın tür sistemini kullanarak geçerli bir değer elde etme fikrini bir adım daha ileriye götürerek, doğrulama için özel bir tür oluşturmayı gözden geçirelim. 2. Bölümde yaptığımız tahmin oyununu hatırlayın; kodumuz kullanıcıya 1 ile 100 arasında bir sayı tahmin etmesini isterdi. Gizli sayımıza karşı kullanıcının tahminini kontrol etmeden önce, kullanıcının tahmininin bu sayılar arasında olup olmadığını doğrulamadık; sadece tahminin pozitif olup olmadığını doğruladık. Bu durumda sonuçlar çok kötü değildi: “Çok yüksek” veya “Çok düşük” çıktımız hala doğru olacaktı.
Bu durumda, kullanıcıyı geçerli tahminlere yönlendirici ve kullanıcının bir sayının aralık dışında tahmin ettiği durumda veya örneğin harfler yazdığı durumda farklı davranış gösteren bir iyileştirme önemli olurdu. Bu yapılışın bir yolu, tahmini sadece pozitif bir u32 değil, potansiyel olarak negatif sayıları da kapsayan bir i32 olarak ayrıştırmak ve daha sonra aralıkta olup olmadığını kontrol eden bir kontrol eklemek olabilir:
if ifadesi, değerimizin aralık dışında olup olmadığını kontrol eder, kullanıcıyı problem hakkında bilgilendirir ve döngünün bir sonraki iterasyonunu başlatmak ve yeni bir tahmin istemek için continue çağırır. if ifadesinin sonrasında, tahmin ve gizli sayı arasındaki karşılaştırmaları yapabiliriz, çünkü tahmin 1 ve 100 arasındadır.
Ancak, bu idealdir: eğer programın sadece 1 ile 100 arasındaki değerler üzerinde çalıştığı çok kritikse ve bu gereksinimi olan çok sayıda fonksiyonu varsa, her fonksiyon içinde böyle bir kontrol yapmak zahmetli olacaktır (ve performansı etkileyebilir).
Bunun yerine, yeni bir tür oluşturup, doğrulamaları oluşturulan türün bir örneğini yaratacak bir fonksiyona taşıyabiliriz. Bu şekilde, fonksiyonların yeni türü imzalarında kullanmaları ve aldıkları değerleri güvenle kullanmaları güvenlidir. Listing 9–13, 1 ile 100 arasında bir değer alan yeni fonksiyonu kullanarak Guess türünün bir örneğini sadece oluşturabilecek bir Guess türünün nasıl tanımlanabileceğini göstermektedir.
Listing 9–13: 1 ve 100 arasında değerlerle devam eden bir Guess türü
Önce, bir i32 değerine sahip value isimli bir alana sahip Guess isimli bir yapı (struct) tanımlıyoruz. Bu, sayının saklanacağı yerdir.
Daha sonra, Guess türüne ait, Guess değerlerinin örneklerini oluşturan bir new isimli ilişkili fonksiyon uygulamıyoruz. new fonksiyonu, i32 türünde bir değer alan value isimli bir parametre alan ve Guess türünde bir değer döndüren bir fonksiyon olarak tanımlanır. new fonksiyonunun vücut kodu, value’nın 1 ve 100 arasında olduğundan emin olmak için value’yı test eder. Eğer value bu testi geçemezse, programcının, çağıran kodun yazdığını bildiren bir panic! çağırısı yaparız, çünkü bu aralık dışında bir değerle Guess oluşturmak, Guess::new’in güveneceği bir sözleşmeyi ihlal eder.
Sonra, self’i ödünç alan, başka hiçbir parametresi olmayan ve i32 türünde bir değer döndüren value isimli bir metodu uyguluyoruz. Bu tür metodlar bazen “getter” olarak da adlandırılır, çünkü amacı, alanlarından bazı verileri almak ve geri döndürmektir. Bu kamusal metod, Guess yapısının value alanının özel olduğu için gereklidir. Guess yapısının value alanının özel olması önemlidir, çünkü Guess yapısını kullanan kodun value’yı doğrudan ayarlamaya izin verilmemesi gerekir: modül dışındaki kod, Guess’in bir örneğini 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 olabileceği bir yol olmaması sağlanır.
Sadece 1 ile 100 arasındaki sayıları alan veya döndüren bir fonksiyon, imzasında i32 yerine Guess alan veya döndüren bir fonksiyon olarak ilan edebilir ve vücutunda herhangi bir ek kontrol yapmaya gerek kalmaz.