İçeriğe geç
Educora
Orta18 dk6 / 10

Hata yönetimi: panic, Result ve ?

Kurtarılamaz hataları (`panic!`) kurtarılabilir olanlardan (`Result`) ayır, `unwrap` ve `expect`'in risklerini kavra, `?` operatörüyle kısa kod yaz ve kendi hata türünü oluştur.

Kendini test et
Bu derste öğreneceklerin
  • Panik ile Result arasında doğru seçim yapmak
  • Result'ı match, unwrap_or ve expect ile işlemek
  • Hatayı ? operatörüyle çağıran fonksiyona iletmek
  • Display uygulayan kendi hata enum'unu oluşturmak

Kullanıcıya yaşını soruyorsun, o da abc yazıyor. Bir dosyayı okumak istiyorsun ama dosya yok. Bunlar programdaki hatalar değil, beklenen durumlardır; program bunlara çökmeden yanıt vermelidir. Rust, olası tüm başarısızlıkları türlerde göstermeni ister ve derleyici onları unutmana izin vermez.

İki tür hata

Kurtarılamaz bir hata, programda bir bug, yani programcı hatası olduğunu gösterir: dizinin sınırını aşmak ya da “asla olmaması gereken” bir durum. Böyle bir durumda Rust panikler: bir hata mesajı yazdırır ve iş parçacığını durdurur. panic!("mesaj") çağrısını kendin de yapabilirsin. Kurtarılabilir hatalar ise Result<T, E> türüyle döndürülür ve çağıran kod tarafından işlenir.

Rust
fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("division by zero: {a} / {b}");
    }
    a / b
}

fn main() {
    println!("{}", divide(10, 2));
    println!("{}", divide(1, 0));
    println!("never printed");
}
Beklenen çıktı
5
thread 'main' panicked at src/main.rs:3:9:
division by zero: 1 / 0
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Panik mesajı standart hata akışına (stderr) yazılır ve dosyayı, satırı, sütunu gösterir; son satıra hiç ulaşılmaz. Dizinin sınırını aşınca da benzer bir mesaj görünür: index out of bounds: the len is 3 but the index is 10.

Result türü

Result<T, E>, iki varyantlı bir enum'dur: başarıda Ok(T), başarısızlıkta Err(E). Örneğin metni sayıya çeviren parse metodu Result döndürür, çünkü metin bir sayı olmayabilir. match ile iki durumu ayrı ayrı işleriz:

Rust
fn main() {
    let inputs = ["42", "abc", ""];
    for input in inputs {
        match input.parse::<i32>() {
            Ok(n) => println!("{input:?} -> {}", n * 2),
            Err(e) => println!("{input:?} -> error: {e}"),
        }
    }
}
Beklenen çıktı
"42" -> 84
"abc" -> error: invalid digit found in string
"" -> error: cannot parse integer from empty string
parse::<i32>() yazımındaki ::<i32> kısmı hangi türe dönüştürüleceğini belirtir (buna “turbofish” denir). Hata {e} ile insanın okuyabileceği bir metin olarak yazdırılır.

unwrap, expect ve diğer kısayollar

Rust
fn main() {
    let a: i32 = "7".parse().unwrap();
    let b: i32 = "8".parse().expect("b must be a number");
    let c: i32 = "x".parse().unwrap_or(0);
    let d: i32 = "".parse().unwrap_or_default();
    println!("{}", a + b + c + d);
}
Beklenen çıktı
15
c ve d için dönüştürme başarısız oldu ama program durmadı: unwrap_or(0) ve unwrap_or_default() yedek bir değer (0) döndürdü. 7 + 8 + 0 + 0 = 15.
YöntemErr durumundaNe zaman kullanılır
matchher durum için kendi koduntam kontrol gerektiğinde
?hatayı çağırana döndürürfonksiyonun kendisi Result döndürdüğünde
unwrap_or(v)v döndürürmakul bir yedek değer varsa
expect("...")senin mesajınla panikbaşarısızlık bir programlama hatası anlamına geliyorsa
unwrap()panikyalnızca örneklerde ve testlerde

? operatörü

?, bir Result'ın arkasına yazılır. Sonuç Ok ise ? içindeki değeri çıkarır ve kod devam eder. Err ise geçerli fonksiyon o hatayı hemen döndürür. Böylece her adımda match yazmadan hatalar çağıran koda iletilir. Koşul: fonksiyonun kendisi Result (ya da Option) döndürmelidir.

Rust
use std::num::ParseIntError;

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

fn main() {
    println!("{:?}", sum_pair("10", " 32 "));
    println!("{:?}", sum_pair("10", "ten"));
    match sum_pair("5", "") {
        Ok(total) => println!("total = {total}"),
        Err(e) => println!("failed: {e}"),
    }
}
Beklenen çıktı
Ok(42)
Err(ParseIntError { kind: InvalidDigit })
failed: cannot parse integer from empty string
trim() kenarlardaki boşlukları siler. {:?} hatanın iç yapısını, {e} ise okunaklı mesajını gösterir.
? olmadan
use std::num::ParseIntError;

fn read_age(s: &str) -> Result<u8, ParseIntError> {
    let age = match s.parse::<u8>() {
        Ok(value) => value,
        Err(e) => return Err(e),
    };
    Ok(age)
}
? ile
use std::num::ParseIntError;

fn read_age(s: &str) -> Result<u8, ParseIntError> {
    let age = s.parse::<u8>()?;
    Ok(age)
}
İki fonksiyon da aynı işi yapar. cargo clippy soldaki yazımı görünce onu ? ile değiştirmeyi önerir (question_mark kuralı).
E0277 hatası
fn main() {
    let n: i32 = "5".parse()?;
    println!("{n}");
}
Doğru: `5` yazdırır
use std::num::ParseIntError;

fn main() -> Result<(), ParseIntError> {
    let n: i32 = "5".parse()?;
    println!("{n}");
    Ok(())
}
Sıradan bir main hiçbir şey döndürmez; bu yüzden derleyici şunu yazar: the ? operator can only be used in a function that returns Result or Option. main de Result döndürebilir; Err durumunda hata yazdırılır ve program başarısız bir çıkış koduyla biter.

Kendi hata türün

Büyük bir programda hataların kendi adları olmalıdır. Hata çeşitlerini bir enum olarak yaz ve ona Display trait'ini uygula ki hata kullanıcıya okunaklı bir mesajla gösterilsin. map_err metodu bir hata türünü başka birine çevirir; genellikle ? ile birlikte kullanılır. (Trait'leri 8. derste ayrıntılı öğreneceksin.)

Rust
use std::fmt;

#[derive(Debug)]
enum AgeError {
    NotANumber,
    TooOld(u32),
}

impl fmt::Display for AgeError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AgeError::NotANumber => write!(f, "age must be a number"),
            AgeError::TooOld(n) => write!(f, "{n} is too old"),
        }
    }
}

fn parse_age(s: &str) -> Result<u32, AgeError> {
    let age: u32 = s.parse().map_err(|_| AgeError::NotANumber)?;
    if age > 150 {
        return Err(AgeError::TooOld(age));
    }
    Ok(age)
}

fn main() {
    for s in ["15", "abc", "200"] {
        match parse_age(s) {
            Ok(age) => println!("ok: {age}"),
            Err(e) => println!("error: {e} / {e:?}"),
        }
    }
}
Beklenen çıktı
ok: 15
error: age must be a number / NotANumber
error: 200 is too old / TooOld(200)
{e} bizim Display uygulamamızı, {e:?} ise #[derive(Debug)]'ın ürettiği metni kullanır. |_|, parametresini yok sayan kısa bir fonksiyondur (closure, 7. ders).

Önemli noktalar

  • panic! bir programlama hatasında programı durdurur; beklenen hatalar Result<T, E> ile döndürülür.
  • Result ya Ok(değer) ya da Err(hata) tutar ve match ile işlenir.
  • unwrap ve expect, Err durumunda panikler; üretim kodunda ?, match ya da unwrap_or tercih edilir.
  • ?, Ok değerini çıkarır, Err'i ise hemen çağırana döndürür; yalnızca Result ya da Option döndüren fonksiyonlarda çalışır.
  • Kendi hata türün için bir enum yaz, ona Display uygula ve hataları map_err ile dönüştür.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
Sonuç Err olduğunda ? operatörü ne yapar?