Rust'ta başarısız olabilecek bir işlem Result<T, E> döndürür: işlem başarılıysa sonuç Ok(T), başarısızsa hata bilgisi Err(E) içindedir. Hatayı match ile ele alır, ? operatörüyle çağırana iletir ya da emin olduğunuz durumlarda unwrap veya expect ile değeri çıkarırsınız. Kurtarılamaz durumlar için ise panic! makrosu vardır. Rust'ta istisna (exception) mekanizması yoktur; hatalar dönüş değerinin bir parçasıdır.

Bu yazıda Result ile çalışmanın bütün temel araçlarını, ? operatörünün tam olarak ne yaptığını, main'den nasıl hata döndürüleceğini ve The Rust Book'un "ne zaman panik, ne zaman Result" sorusuna verdiği cevabı çalışan örneklerle göreceksiniz. Kendi hata tiplerinizi tanımlamak ayrı bir konu olduğu için Rust özel hata tipi ve Error trait yazısında ele alınıyor. Serinin tamamı için Rust öğrenme rehberi.

İki tür hata: panic! ve Result

İki tür hata: panic! ve Result

Standart kütüphanenin std::error belgesi Rust'ta birbirini tamamlayan iki sistem olduğunu anlatır. Birincisi panik sistemidir ve çoğunlukla programda tespit edilmiş hataları, yani bug'ları temsil eder. Bir dizinin sınırı dışına çıkmak ya da panic! çağırmak programı varsayılan olarak durdurur: Rust yığıtı geri sararak (unwinding) her fonksiyonun verisini temizler ve süreci sonlandırır. Kitap, ikili dosyayı küçültmek gibi nedenlerle bunun yerine doğrudan sonlandırmayı (abort) Cargo.toml'daki profil ayarıyla seçebileceğinizi de belirtir.

İkinci sistem Result, Error trait'i ve kullanıcı tanımlı hata tipleridir; bunlar programın çalışırken karşılaşması beklenen başarısızlıkları temsil eder. Olmayan bir dosya, hatalı biçimde girilmiş bir sayı ya da kopan bir ağ bağlantısı bu türdendir. Result prelude'da olduğu için Ok ve Err varyantlarını Result:: öneki olmadan doğrudan yazabilirsiniz. Tanımı son derece basittir:

rust
enum Result<T, E> {
    Ok(T),
    Err(E),
}

match ile Result'ı ele almak

En temel yöntem match ile iki varyantı ayrı ayrı ele almaktır. Aşağıdaki örnek, metni sayıya çeviren parse metodunu farklı girdilerle çağırıyor. parse::<i32>() bir Result<i32, ParseIntError> döndürür ve hata değerinin Display çıktısı neyin yanlış gittiğini anlatır:

rust
fn main() {
    for girdi in ["42", "abc", "", "99999999999"] {
        match girdi.parse::<i32>() {
            Ok(n) => println!("{girdi:?} sayı oldu: {n}"),
            Err(e) => println!("{girdi:?} hata: {e}"),
        }
    }
}
// çıktı: "42" sayı oldu: 42
// çıktı: "abc" hata: invalid digit found in string
// çıktı: "" hata: cannot parse integer from empty string
// çıktı: "99999999999" hata: number too large to fit in target type

Aynı hata tipi farklı nedenleri ayırt edebilir: geçersiz karakter, boş metin ve sığmayan sayı. Hatanın türüne göre farklı davranmak da yaygındır. Kitaptaki örnekte dosya yoksa oluşturulur, başka bir hata varsa program durdurulur. io::Error bunun için kind() metoduyla bir ErrorKind değeri verir:

rust
use std::fs::File;
use std::io::ErrorKind;

fn main() {
    match File::open("olmayan_dosya.txt") {
        Ok(_) => println!("açıldı"),
        Err(e) => match e.kind() {
            ErrorKind::NotFound => println!("dosya yok, varsayılan ayar kullanılacak"),
            ErrorKind::PermissionDenied => println!("izin yok"),
            diger => println!("başka bir hata: {diger:?}"),
        },
    }
}
// çıktı: dosya yok, varsayılan ayar kullanılacak

İç içe match'ler hızla uzar. Kitap aynı mantığın closure alan unwrap_or_else gibi metotlarla daha kısa yazılabileceğini gösterir; bu metotlara aşağıda değineceğiz. match'in ayrıntıları için Rust match ve if let, dosya işlemleri için Rust dosya okuma ve yazma yazısına bakın.

unwrap ve expect: hata durumunda panik

unwrap, Ok ise içindeki değeri döndürür, Err ise hata değerini yazdırarak panikler. expect aynı işi yapar ama panik mesajını siz belirlersiniz. İkisinin gerçek çıktısı şöyledir:

rust
fn main() {
    let n: i32 = "12a".parse().unwrap();
    println!("{n}");
}
// panik: called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }
rust
fn main() {
    let port: u16 = "8o80"
        .parse()
        .expect("PORT ayarı geçerli bir sayı olmalı");
    println!("{port}");
}
// panik: PORT ayarı geçerli bir sayı olmalı: ParseIntError { kind: InvalidDigit }

Kitap, üretim kalitesindeki kodda çoğu Rust geliştiricisinin unwrap yerine expect seçtiğini ve işlemin neden her zaman başarılı olması gerektiğini mesajda açıkladığını söyler. std::error belgesi bu mesaj için bir üslup da önerir: mesajı hatanın tarifi olarak değil, beklenen önkoşul olarak yazın. Yani "ortam değişkeni yok" yerine "ortam değişkeni şu betik tarafından ayarlanmış olmalı" gibi. Panik çıktısı zaten kaynak hatayı mesajın arkasına eklediği için, önkoşul biçimindeki mesaj yeni bilgi verir. Belgenin önerdiği hatırlatma, mesajda "olmalı" (should) kelimesine odaklanmaktır.

? operatörü: hatayı çağırana iletmek

Bir fonksiyon içinde oluşan hatayı orada ele almak yerine çağırana geri vermek çoğu zaman daha doğrudur, çünkü ne yapılacağına karar verecek bilgi çağıranda vardır. Buna hatayı yaymak (propagation) denir. Bunu match ve return Err(e) ile elle yazmak mümkündür ama o kadar yaygın bir ihtiyaçtır ki Rust bunun için ? operatörünü sunar. Aşağıdaki iki fonksiyon aynı işi yapar:

rust
use std::num::ParseIntError;

fn topla(a: &str, b: &str) -> Result<i32, ParseIntError> {
    let x = a.trim().parse::<i32>()?;
    let y = b.trim().parse::<i32>()?;
    Ok(x + y)
}

fn topla_uzun(a: &str, b: &str) -> Result<i32, ParseIntError> {
    let x = match a.trim().parse::<i32>() {
        Ok(v) => v,
        Err(e) => return Err(e),
    };
    let y = match b.trim().parse::<i32>() {
        Ok(v) => v,
        Err(e) => return Err(e),
    };
    Ok(x + y)
}

fn main() {
    println!("{:?}", topla("10", " 32 "));
    println!("{:?}", topla("10", "otuz"));
    println!("{:?}", topla_uzun("1", "2"));
}
// çıktı: Ok(42)
// çıktı: Err(ParseIntError { kind: InvalidDigit })
// çıktı: Ok(3)

? bir Result değerinin arkasına yazıldığında, değer Ok ise içindekini ifadenin sonucu yapar ve fonksiyon devam eder; Err ise hatayı fonksiyonun tamamından return ile döndürür. Kitabın vurguladığı önemli bir fark vardır: ? hata değerini döndürmeden önce From trait'inin from fonksiyonundan geçirir. Böylece fonksiyonun dönüş tipindeki hata tipi, içeride oluşan hata tipinden farklı olabilir; yeter ki aralarında bir From dönüşümü tanımlı olsun. Bu mekanizma kendi hata tipinizi tanımladığınızda çok işe yarar ve özel hata tipi yazısında örnekleniyor. ? çağrıları zincirlenebilir de: kitaptaki File::open("x")?.read_to_string(&mut s)? örneği gibi.

? nerede kullanılabilir?

? erken dönüş yaptığı için yalnızca dönüş tipi uyumlu olan fonksiyonlarda kullanılabilir. () döndüren sıradan bir main içinde kullanırsanız derleyici şu hatayı verir:

rust
fn main() {
    let n: i32 = "5".parse()?;
    println!("{n}");
}
// error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)

Derleyicinin önerdiği çözüm main'in dönüş tipini Result<(), Box<dyn Error>> yapmak ve sona Ok(()) eklemektir. Box<dyn Error>'u şimdilik "herhangi bir hata" olarak okuyabilirsiniz. main bir Err döndürdüğünde Rust hatayı yazdırır ve program sıfırdan farklı bir çıkış koduyla biter; Ok(()) döndüğünde çıkış kodu sıfırdır:

rust
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let n: i32 = "5".parse()?;
    println!("{n}");
    let m: i32 = "beş".parse()?;
    println!("{m}");
    Ok(())
}
// çıktı: 5
// çıktı: Error: ParseIntError { kind: InvalidDigit }
// (çıkış kodu: 1)

Çıktıdaki Error: ParseIntError { kind: InvalidDigit } satırının hatanın Debug biçimi olduğuna dikkat edin; kullanıcıya daha okunur bir mesaj göstermek istiyorsanız hatayı main içinde kendiniz yakalayıp Display ile yazdırmanız gerekir. Kitaba göre main, std::process::Termination trait'ini uygulayan her tipi döndürebilir. Çıkış kodları ve eprintln! ile hata yazdırma Rust komut satırı argümanları yazısında anlatılıyor.

? Option üzerinde de çalışır: None ise fonksiyondan None ile erken çıkar. Ancak Result döndüren bir fonksiyonda Option üzerinde, ya da tersi, ? kullanamazsınız; dönüşümü ok() ve ok_or() ile açıkça yapmanız gerekir:

rust
fn ilk_satirin_son_harfi(metin: &str) -> Option<char> {
    metin.lines().next()?.chars().last()
}

fn main() {
    println!("{:?}", ilk_satirin_son_harfi("Günaydın\nNasılsınız?"));
    println!("{:?}", ilk_satirin_son_harfi(""));
    println!("{:?}", ilk_satirin_son_harfi("\nselam"));

    let v = vec!["7", "x"];
    let ilk: Result<&&str, &str> = v.first().ok_or("liste boş");
    println!("{ilk:?}");
    let sonuc: Option<i32> = "x".parse::<i32>().ok();
    println!("{sonuc:?}");
}
// çıktı: Some('n')
// çıktı: None
// çıktı: None
// çıktı: Ok("7")
// çıktı: None

Result metotları: match yazmadan dönüştürmek

Result metotları: match yazmadan dönüştürmek: unwrap(), expect(msg), unwrap_or(d), unwrap_or_else(f)

Result üzerinde tanımlı metotlar, küçük dönüşümler için match yazma ihtiyacını büyük ölçüde ortadan kaldırır. map yalnızca Ok değerini dönüştürür, map_err yalnızca hatayı. and_then başarılı değeri yine Result döndüren bir fonksiyona verir ve zincirin herhangi bir adımında hata olursa zincir orada kesilir. unwrap_or, unwrap_or_else ve unwrap_or_default hata durumunda varsayılan bir değer kullanır:

rust
fn main() {
    let iyi: Result<i32, String> = Ok(4);
    let kotu: Result<i32, String> = Err(String::from("bağlantı koptu"));

    println!("{:?}", iyi.clone().map(|n| n * 10));
    println!("{:?}", kotu.clone().map_err(|e| format!("hata: {e}")));
    println!("{}", kotu.clone().unwrap_or(0));
    println!("{}", kotu.clone().unwrap_or_else(|e| e.len() as i32));
    println!("{}", kotu.clone().unwrap_or_default());
    println!("{}", iyi.clone().is_ok_and(|n| n > 3));

    let yarim = |n: i32| if n % 2 == 0 { Ok(n / 2) } else { Err(format!("{n} tek sayı")) };
    println!("{:?}", iyi.clone().and_then(yarim).and_then(yarim));
    println!("{:?}", iyi.and_then(yarim).and_then(yarim).and_then(yarim));
}
// çıktı: Ok(40)
// çıktı: Err("hata: bağlantı koptu")
// çıktı: 0
// çıktı: 16
// çıktı: 0
// çıktı: true
// çıktı: Ok(1)
// çıktı: Err("1 tek sayı")

unwrap_or_else örneğindeki 16, "bağlantı koptu" metninin bayt uzunluğudur (ğ ve ı ikişer bayt). unwrap_or varsayılanı her durumda önceden hesaplar; varsayılan pahalıysa ya da hatanın kendisine bağlıysa closure alan unwrap_or_else tercih edilir. Bu metotlar değeri tükettiği için örnekte aynı değişkeni birden çok kez kullanabilmek amacıyla clone() çağırdık. Closure sözdizimi Rust closure yazısında anlatılıyor.

MetotOk(v) içinErr(e) için
unwrap()v dönerPanik (mesajda e)
expect(msg)v dönerPanik (msg ve e)
unwrap_or(d)v dönerd döner
unwrap_or_else(f)v dönerf(e) döner
unwrap_or_default()v dönerT::default() döner
map(f)Ok(f(v))Err(e) aynen
map_err(f)Ok(v) aynenErr(f(e))
and_then(f)f(v) (yine Result)Err(e) aynen
ok()Some(v)None
? operatörüv ifadenin değeri olurFrom::from(e) ile erken dönüş

Birden çok Result'ı toplamak

Bir listedeki her elemanı dönüştürürken bazıları başarısız olabilir. Rust by Example bu durum için birkaç strateji anlatır. Result, FromIterator uyguladığı için Result üreten bir iterator'ı Result<Vec<T>, E>'ye toplayabilirsiniz: hepsi başarılıysa Ok(vektör), ilk hatada ise o hata gelir ve yineleme durur. Hatalıları sessizce atlamak için filter_map ile ok(), başarılı ve hatalıları ayırmak için partition kullanılır:

rust
fn main() {
    let girdiler = vec!["3", "elma", "8"];

    let hepsi: Result<Vec<i32>, _> = girdiler.iter().map(|s| s.parse::<i32>()).collect();
    println!("{hepsi:?}");

    let gecerliler: Vec<i32> = girdiler.iter().filter_map(|s| s.parse().ok()).collect();
    println!("{gecerliler:?}");

    let (tamam, hatali): (Vec<_>, Vec<_>) = girdiler
        .iter()
        .map(|s| s.parse::<i32>())
        .partition(|r| r.is_ok());
    println!("{} başarılı, {} hatalı", tamam.len(), hatali.len());

    let temiz: Result<Vec<i32>, _> = ["1", "2"].iter().map(|s| s.parse::<i32>()).collect();
    println!("{temiz:?}");
}
// çıktı: Err(ParseIntError { kind: InvalidDigit })
// çıktı: [3, 8]
// çıktı: 2 başarılı, 1 hatalı
// çıktı: Ok([1, 2])

Result'ı yok saymak: unused_must_use uyarısı

Result tipi #[must_use] özniteliğiyle işaretlenmiştir. Belgeye göre bunun amacı, dönüş değeriyle bildirilen hataların kolayca gözden kaçmasını önlemektir: bir Result'ı hiçbir şey yapmadan bırakırsanız derleyici uyarı verir. Kod yine derlenir ve çalışır, ama hata sessizce kaybolur:

rust
fn kaydet(veri: &str) -> Result<(), String> {
    if veri.is_empty() {
        return Err(String::from("boş veri"));
    }
    Ok(())
}

fn main() {
    kaydet("");
    println!("bitti");
}
// warning: unused `Result` that must be used
// çıktı: bitti

Hatayı bilerek yok saymak istiyorsanız derleyicinin de önerdiği gibi let _ = kaydet(""); yazarak niyetinizi açıkça belirtin. Bunun dışında hatayı ele almalı, ? ile yaymalı ya da başarının garanti olduğu durumda expect ile nedenini belgelemelisiniz. Lint seviyeleriyle bu uyarıyı hataya çevirmek Rust clippy ve rustfmt yazısında anlatılıyor.

Ne zaman panic!, ne zaman Result?

The Rust Book'un bu konudaki tavsiyesi nettir: başarısız olabilecek bir fonksiyon tanımlarken Result döndürmek iyi bir varsayılandır, çünkü karar verme hakkını çağırana bırakır. Çağıran kurtarmayı deneyebilir ya da durumu kurtarılamaz sayıp kendisi panik çağırabilir. Siz panik çağırdığınızda ise bu kararı çağıranın yerine vermiş olursunuz. Hatalı biçimli veri alan bir ayrıştırıcı ya da istek sınırına takılan bir HTTP çağrısı gibi başarısızlığın beklendiği durumlarda Result doğru seçimdir.

Panik ise şu durumlarda uygundur: örnek kodlar, prototipler ve testler (test başarısızlığı zaten panik ile işaretlenir); sizin bildiğiniz ama derleyicinin anlayamadığı bir nedenle hatanın imkânsız olduğu durumlar, örneğin sabit yazılmış geçerli bir IP adresini ayrıştırmak; ve kodun bir sözleşmesinin (contract) ihlal edildiği, devam etmenin güvensiz olacağı kötü durumlar. Standart kütüphanenin sınır dışı bellek erişiminde paniklemesinin nedeni de budur: geçersiz veriyle çalışmaya devam etmek güvenlik açığına dönüşebilir. Kitap ayrıca doğrulamayı her fonksiyonda tekrarlamak yerine, geçerli değerleri yalnızca oluşturulurken kontrol eden bir tip tanımlamayı önerir. Kitaptaki Guess örneğinin Türkçe uyarlaması şöyle:

rust
pub struct Tahmin {
    deger: i32,
}

impl Tahmin {
    pub fn new(deger: i32) -> Tahmin {
        if deger < 1 || deger > 100 {
            panic!("Tahmin 1 ile 100 arasında olmalı, gelen değer {deger}.");
        }
        Tahmin { deger }
    }

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

fn main() {
    let t = Tahmin::new(42);
    println!("tahmin: {}", t.deger());
    let _hatali = Tahmin::new(150);
}
// çıktı: tahmin: 42
// panik: Tahmin 1 ile 100 arasında olmalı, gelen değer 150.

deger alanı gizli (private) olduğu için modül dışındaki kod bir Tahmin'i yalnızca Tahmin::new ile oluşturabilir; bu da her Tahmin değerinin kontrolden geçmiş olmasını garanti eder. Böylece Tahmin parametre alan fonksiyonların sınırı tekrar kontrol etmesi gerekmez. Burada panik seçilmesinin nedeni, aralık dışı bir değerin çağıran kodda bir programlama hatası, yani sözleşme ihlali sayılmasıdır; kitap bu tür panik koşullarının fonksiyonun API belgesinde açıklanması gerektiğini de ekler. Değer kullanıcıdan geliyorsa aynı tipe paniklemeyen, Result<Tahmin, ...> döndüren bir oluşturucu eklemek daha doğru olur. Alan görünürlüğü Rust modül sistemi yazısında anlatılıyor.

Sık yapılan hatalar

Sık yapılan hatalar: Her yerde unwrap kullanmak, () döndüren fonksiyonda ? kullanmak, Option ile Result'ı ? ile karıştırmak, Hatayı Debug biçimiyle kullanıcıya göstermek
  • Her yerde unwrap kullanmak: Prototipte kabul edilebilir, ama kullanıcı girdisi ya da dosya gibi dış kaynaklarda program beklenmedik anda çöker. En azından expect ile nedeni yazın, mümkünse ? ile yayın.
  • () döndüren fonksiyonda ? kullanmak: E0277 hatası verir. Dönüş tipini Result yapın.
  • Option ile Result'ı ? ile karıştırmak: Otomatik dönüşüm yoktur; ok_or ve ok kullanın.
  • Hatayı Debug biçimiyle kullanıcıya göstermek: main'den dönen hata Debug ile yazılır. Kullanıcıya dönük mesajlar için hatayı yakalayıp {e} ile yazdırın.
  • Uyarıyı görmezden gelmek: unused_must_use uyarısı, kaybolan bir hatanın işaretidir.

Sık Sorulan Sorular

Result ile Option arasındaki fark nedir?

Option bir değerin var ya da yok olduğunu söyler; yokluğun nedeni yoktur. Result ise başarısızlığın nedenini Err içinde taşır. "Bulunamadı" tek bir anlam taşıyorsa Option yeterlidir; çağıranın nedeni bilmesi gerekiyorsa Result kullanın. Option'ın ayrıntıları Rust enum ve Option yazısında.

Farklı hata tiplerini aynı fonksiyonda nasıl döndürürüm?

En hızlı yol dönüş tipini Result<T, Box<dyn Error>> yapmaktır; ? her standart hata tipini otomatik olarak kutuya çevirir. Daha ayrıntılı kontrol için kendi enum hata tipinizi tanımlayıp From uygularsınız. İki yaklaşım özel hata tipi yazısında karşılaştırılıyor.

Panik yakalanabilir mi?

std::error belgesi panik sisteminin arayüzleri arasında catch_unwind ve resume_unwind'ı sayar, ama panik genel hata yönetimi için tasarlanmamıştır; beklenen hatalar için Result kullanın. Ayrıca panik ayarı abort ise geri sarma yapılmaz.

? operatörü hangi tiplerde çalışır?

Kararlı Rust'ta günlük kullanımda Result ve Option üzerinde çalışır. Derleyicinin hata mesajı, FromResidual uygulayan başka tiplerin de kabul edildiğini söyler; belge bu Try trait'lerinin henüz kısmen kararlı olduğunu belirtir. Önemli olan, ?'in kullanıldığı ifadenin tipi ile fonksiyonun dönüş tipinin uyumlu olmasıdır.

Panik olduğunda çağrı yığınını nasıl görürüm?

Panik mesajının da hatırlattığı gibi programı RUST_BACKTRACE=1 ortam değişkeniyle çalıştırmak, panik anındaki çağrı yığınını (backtrace) yazdırır.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Rust öğrenme rehberi · Özel hata tipi · Enum ve Option · match ve if let · E0277 ve E0308 tip hataları · Test yazma