00 Exception'sız dünya
C'de hata bir dönüş kodu ya da global errno; gözden kaçırması serbest. C++'ta exception gizli bir çıkış kapısı. Rust hatayı tip sistemine taşır: hata bir değerdir, derleyici onu yok saymana izin vermez.
C'nin sessiz tuzağı
C'de hatayı kontrol etmek bir nezaket meselesidir. Dönüş kodunu okumayı unutursan derleyici tek kelime etmez:
// fopen NULL dönebilir; kontrol etmeyi unutmak derlenir, çalışır, çöker.
FILE *f = fopen("yok.txt", "r");
fread(buf, 1, 64, f); // f == NULL → segfault, errno hiç okunmadıerrno bir thread-local global; çağrı ile kontrol arasına giren herhangi bir libc fonksiyonu onu ezebilir. Kontrol akışı ile veri akışı birbirinden ayrı, bu yüzden senkronize tutmak programcının sorumluluğunda.
C++ exception: görünmez çıkış
Exception'lar kontrol akışını kod metninde görünmez kılar. foo() çağrısının fonksiyondan fırlayıp çıkabileceğini imzasından anlayamazsın (noexcept dışında imza sessizdir). Bu, "exception-safe" kod yazmayı ve doğru noktalarda yakalamayı zorlaştırır; üstelik fırlatma anında stack unwinding maliyeti gizlidir.
Rust'ın ayrımı: recoverable vs unrecoverable
Rust hatayı ikiye böler. Recoverable (kurtarılabilir) hatalar Result<T, E> ile tipte taşınır — dosya yok, parse başarısız. Unrecoverable (kurtarılamaz) durumlar panic! ile programı sonlandırır — invariant ihlali, dizilim sınırı aşımı. Exception yoktur; hata akışı sıradan değer akışıdır.
C errno / dönüş kodu → kontrol opsiyonel, gözden kaçar C++ exception → gizli kontrol akışı, görünmez çıkış Rust Result / Option → hata = değer, derleyici zorlar
Rust'ta "hata fırlatmak" diye bir şey yoktur. Hata, fonksiyonun normal dönüş değerinin parçasıdır. Bu yüzden imzaya bakarak fonksiyonun hata üretip üretmediğini her zaman görürsün.
Bu bölümde
- C'de dönüş kodu/errno kontrolü opsiyonel ve gözden kaçmaya açık
- C++ exception kontrol akışını imzada görünmez kılar, unwinding maliyeti gizli
- Rust hatayı tip sistemine taşır: hata bir değerdir, yok sayılamaz
- recoverable → Result/Option, unrecoverable → panic! ayrımı
01 Option<T> — null'a veda
C'de "değer yok" durumu null pointer veya sihirli bir değerle (-1, NULL) anlatılır ve kontrolü unutursun. Rust'ta yokluk tip seviyesinde: Option<T> ya Some(v) ya da None'dur, ve T'ye dokunmadan önce hangisi olduğunu çözmen gerekir.
Tanım
Option standart kütüphanede sıradan bir enum'dur — dil sihri değil:
enum Option<T> {
None, // değer yok — C'deki NULL'ın güvenli karşılığı
Some(T), // değer var, içinde T taşıyor
}Kritik fark: Option<&str> ile &str farklı tiplerdir. Bir Option'ı doğrudan referans gibi kullanamazsın; önce Some/None ayrımını yapmak zorundasın. Null deref derleme zamanında imkânsız hale gelir.
std'de Option dönen tipik fonksiyonlar
let v = mut; // (örnek için) vec![10, 20, 30]
let v = vec![10, 20, 30];
// get: indeks taşarsa None döner — C'de v[99] UB iken burada güvenli
let a: Option<&i32> = v.get(1); // Some(&20)
let b: Option<&i32> = v.get(99); // None
// iterator find: koşulu sağlayan ilk eleman ya da None
let c = v.iter().find(|&&x| x > 15); // Some(&20)
// HashMap::get da Option döner
use std::collections::HashMap;
let mut m = HashMap::new();
m.insert("k", 1);
let d = m.get("yok"); // None — anahtar yoksa çökmezOption<Box<T>> ve benzeri "nullable pointer" durumlarında Rust niche optimization yapar: None sıfır pointer ile temsil edilir, dolayısıyla bellek maliyeti çıplak pointer ile aynıdır. Güvenlik bedava gelir.
Bu bölümde
- Option<T> = Some(v) | None; "değer yok" durumunu tip seviyesinde kodlar
- Null deref derleme zamanında imkânsız — Option ile T farklı tiplerdir
- get, find, HashMap::get gibi std fonksiyonları Option döndürür
- Niche optimization sayesinde Option<Box<T>> çıplak pointer kadar ucuz
02 Result<T, E>
Option "var mı yok mu" der ama neden yok bilgisini taşımaz. Hatanın sebebi önemliyse Result<T, E> kullanılır: başarıda Ok(T), başarısızlıkta hata bilgisini taşıyan Err(E). Hata, dönüş tipinin bir parçasıdır.
Tanım ve #[must_use]
// std'deki tanım; #[must_use] sonucu yok saymayı uyarıya çevirir.
#[must_use]
enum Result<T, E> {
Ok(T), // başarı: T değeri
Err(E), // hata: E tipinde sebep
}C'de bir fonksiyonun dönüş kodunu kullanmadan bırakmak sessizdir. Rust'ta Result #[must_use] ile işaretlidir: sonucu hiç kullanmadan atarsan derleyici uyarır.
use std::fs::File;
fn main() {
File::create("log.txt"); // warning: unused `Result` that must be used
}#[must_use] yalnızca uyarıdır, hata değil. let _ = ... ile bilinçli olarak susturulabilir; ama bunu yaparken hatayı gerçekten görmezden gelmeyi kabul ettiğini belgelemiş olursun. C'deki sessiz yok saymadan farkı: artık niyet açık.
Sebep taşıyan E tipi
E herhangi bir tip olabilir: bir String, bir enum, std::io::Error ya da kendi tanımladığın hata tipi. C++ exception hiyerarşisinin aksine E tek bir somut tiptir ve imzada açıkça görünür:
Bu bölümde
- Result<T, E> = Ok(T) | Err(E); hatanın sebebini de tipte taşır
- #[must_use] sonucu yok saymayı derleyici uyarısına çevirir
- E somut, imzada görünen tek bir tiptir — exception hiyerarşisi yok
- Result<(), E> sadece başarı/başarısızlık bildiren işlemler için
03 match ile ele alma
Option ya da Result içindeki değere ulaşmanın temel yolu match'tir. match exhaustive'dir: her olası varyantı ele almak zorundasın, aksi halde derlenmez. Bu, C'de bir switch'te default'u veya bir case'i unutmanın karşılığını imkânsız kılar.
Result'ı açmak
use std::fs;
fn oku(yol: &str) {
match fs::read_to_string(yol) {
Ok(icerik) => println!("{} bayt okundu", icerik.len()),
Err(e) => eprintln!("okunamadı: {e}"),
}
// İki kolu da yazmazsan: error[E0004] non-exhaustive patterns
}Derleyici her iki kolu da ele almaya zorlar. Err kolunu silersen kod derlenmez. C'de hatayı kontrol etmemek serbestti; burada unutmak imkânsız.
Option'ı açmak ve içe bakış
fn ilk_kelime(s: &str) -> &str {
match s.split_whitespace().next() {
Some(k) => k,
None => "", // boş girdi durumu açıkça ele alındı
}
}
// if let: yalnızca tek kola ilgileniyorsan match'in kısası
if let Some(k) = "merhaba dünya".split_whitespace().next() {
println!("ilk: {k}");
}Match desen eşleştirmesiyle değeri aynı anda bağlar: Ok(icerik) kolu hem Ok olduğunu doğrular hem içeriği icerik değişkenine açar. C++'ta if (result.has_value()) { result.value(); } iki ayrı adımken Rust'ta tek deyimdir, ve None dalında değere erişmek mümkün değildir.
Bu bölümde
- match exhaustive: tüm varyantları ele almazsan derlenmez
- Ok/Err ve Some/None kollarının ikisini de yazmak zorunludur
- Desen eşleştirme değeri doğrular ve aynı anda bağlar
- Tek kola ilgileniyorsan if let, match'in kısa biçimidir
04 ? operatörü
Her çağrıda match yazmak boilerplate üretir. ? operatörü bunu tek karaktere indirir: Ok ise içindeki değeri çıkarır, Err ise hatayı erken dönüşle çağırana yayar. C'deki "kontrol et, hatalıysa return" kalıbının dildeki karşılığıdır.
match boilerplate'inden ?'a
use std::fs::File;
use std::io::{self, Read};
// Önce: elle match — gürültülü
fn oku_uzun(yol: &str) -> Result<String, io::Error> {
let mut f = match File::open(yol) {
Ok(f) => f,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
// Sonra: ? ile aynı mantık, dört satır
fn oku_kisa(yol: &str) -> Result<String, io::Error> {
let mut f = File::open(yol)?; // Err ise hemen return Err(...)
let mut s = String::new();
f.read_to_string(&mut s)?; // Err ise hemen return Err(...)
Ok(s)
}? için fonksiyon Result/Option dönmeli
? erken dönüş yaptığı için, kullanıldığı fonksiyonun dönüş tipi uyumlu olmalıdır. Result içinde ? kullanan fonksiyon Result dönmeli; Option içinde ? kullanan fonksiyon Option dönmeli.
// Option üzerinde ?: None ise erken None döner
fn ilk_harf(s: &str) -> Option<char> {
let kelime = s.split_whitespace().next()?; // None → return None
kelime.chars().next() // Option<char>
}?'u fn main() içinde kullanmak istiyorsan main'in dönüş tipini Result<(), E> yapmalısın. Aksi halde "the ? operator can only be used in a function that returns Result or Option" hatası alırsın. main için tipik desen: fn main() -> Result<(), Box<dyn Error>>.
Bu bölümde
- ? operatörü Ok/Some değerini çıkarır, hata durumunda erken döner
- match boilerplate'ini tek karaktere indirir, hata yayma kalıbını dile gömer
- Result içinde Result, Option içinde Option döndüren fonksiyon gerektirir
- main'de ? kullanmak için dönüş tipi Result<(), E> olmalı
05 unwrap / expect
unwrap ve expect kestirme yoldur: Ok/Some ise değeri verir, değilse panic! eder. Hızlıdır ama hata akışını kısa devre yapar. Üretim kütüphanesinde nadir, prototip ve testte yaygındır.
İkisi arasındaki fark
let n: i32 = "42".parse().unwrap();
// parse başarısızsa: panicked at 'called `Result::unwrap()` on an `Err` value: ...'
let n: i32 = "42".parse()
.expect("yapılandırma sayısı geçerli olmalı");
// expect aynı işi yapar ama panik mesajı senin bağlamını taşırexpect daima unwrap'a tercih edilir: panik mesajı "neden burada bunun başarılı olmasını bekliyordum" sorusunu yanıtlamalı. İyi bir expect mesajı, koşulun neden tutması gerektiğini anlatır — hatanın kendisini değil.
Ne zaman kabul edilebilir?
| Durum | unwrap/expect? |
|---|---|
| Prototip / keşif | Kabul edilebilir, hızlı ilerleme |
| Test kodu | Kabul edilebilir, panik = test başarısız |
| Gerçekten imkânsız durum | expect + gerekçe yorumu ile |
| Kütüphane API'si | Kaçın — Result döndür, kararı çağırana bırak |
| Kullanıcı girdisine bağlı | Asla — saldırı/çökme yüzeyi |
// "Gerçekten imkânsız" durumda gerekçeli expect:
let re = Regex::new(r"^\d+$")
.expect("sabit literal regex; derleme zamanı doğru");
// unwrap_or: panik yerine varsayılan — çoğu zaman daha iyi seçim
let port: u16 = std::env::var("PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(8080);unwrap, hatayı kontrol etmeyi unuttuğun C koduna benzer bir çökme riski getirir — ama burada bilinçli ve görünür. Yine de production'da çağrılan her unwrap potansiyel bir panik noktasıdır. clippy::unwrap_used lint'i ile bunları derlemede yasaklayabilirsin.
Bu bölümde
- unwrap/expect Ok/Some'u açar, aksi halde panic! eder
- expect daima tercih edilir: anlamlı, bağlam taşıyan panik mesajı verir
- Prototip/test/gerçekten imkânsız durumda kabul; kütüphanede ve girdiye bağlı yerlerde kaçın
- unwrap_or / unwrap_or_else panik yerine varsayılana düşmenin yolu
06 panic! ne zaman?
panic! kurtarılamaz hatalar içindir: programın bir invariant'ı ihlal edildi ve devam etmek anlamsız ya da tehlikeli. C'deki abort() veya assert() başarısızlığının karşılığıdır — ama Rust'ta varsayılan davranış stack'i temiz biçimde geri sarar.
Tipik panik kaynakları
let v = vec![1, 2, 3];
let x = v[99]; // panic: index out of bounds — C'de UB, burada kontrollü çökme
let a: i32 = 1;
let b: i32 = 0;
let c = a / b; // panic: attempt to divide by zero
fn hesapla(adim: u32) {
if adim == 0 {
panic!("adim sıfır olamaz — çağıran tarafta hata var");
}
// ... invariant artık garanti
}Dizilim sınırı aşımı C'de tanımsız davranıştır (sessiz bellek bozulması, exploit yüzeyi). Rust'ta panik ile kontrollü biçimde durur; bellek güvenliği korunur.
panic = unwind vs panic = abort
Panik anında iki strateji var. Bunlar Cargo.toml'da profil bazında seçilir:
[profile.release]
panic = "abort" # anında sonlandır; unwind kodu üretilmez
# panic = "unwind" (varsayılan): stack geri sarılır, Drop'lar çalışır| Strateji | Davranış |
|---|---|
| unwind | Stack geri sarılır, her frame'in Drop'u çalışır (C++ unwinding gibi). catch_unwind ile yakalanabilir. |
| abort | Anında sonlandırır, Drop çalışmaz. Daha küçük ikili, gömülü/no_std'de yaygın. |
Kütüphane yazarken panic!'ten kaçın. Panik çağıranın kararıdır — kütüphane hatayı Result ile bildirmeli, programı sonlandırma yetkisini gasp etmemeli. Panik yalnızca "bu noktaya gelmek programlama hatasıdır" diyebileceğin invariant ihlallerinde meşrudur.
Bu bölümde
- panic! kurtarılamaz invariant ihlalleri içindir; assert/abort'un karşılığı
- Index out of bounds ve sıfıra bölme C'de UB iken Rust'ta kontrollü panik
- panic = unwind (Drop çalışır, yakalanabilir) vs abort (anında, küçük ikili)
- Kütüphanede panic'ten kaçın, hatayı Result ile çağırana bırak
07 Hata tiplerini dönüştürmek
Bir fonksiyon farklı kaynaklardan hata alabilir — IO, parse, ağ. ? operatörü, From trait'i sayesinde kaynak hatayı fonksiyonun hata tipine otomatik dönüştürür. Böylece çeşitli hatalar tek bir tipte toplanır.
? + From ile otomatik dönüşüm
?, Err(e) dönerken aslında Err(From::from(e)) çağırır. Hedef hata tipi için From<KaynakHata> tanımlıysa dönüşüm bedava gelir:
use std::num::ParseIntError;
use std::io;
// Tüm hataları toplayan kendi enum'umuz
enum UygHata {
Io(io::Error),
Parse(ParseIntError),
}
// From impl'leri ?'un dönüşümü için gereken köprü
impl From<io::Error> for UygHata {
fn from(e: io::Error) -> Self { UygHata::Io(e) }
}
impl From<ParseIntError> for UygHata {
fn from(e: ParseIntError) -> Self { UygHata::Parse(e) }
}
fn oku_sayi(yol: &str) -> Result<i32, UygHata> {
let s = std::fs::read_to_string(yol)?; // io::Error → UygHata
let n = s.trim().parse::<i32>()?; // ParseIntError → UygHata
Ok(n)
}Box<dyn Error> ile basit toplama
Her hata tipi için enum + From yazmak istemiyorsan, Box<dyn Error> bir kestirmedir. Error trait'ini uygulayan her tip otomatik olarak buna dönüşür:
use std::error::Error;
// Box<dyn Error>: "Error uygulayan herhangi bir hata" — type erasure
fn oku_sayi(yol: &str) -> Result<i32, Box<dyn Error>> {
let s = std::fs::read_to_string(yol)?; // io::Error → Box<dyn Error>
let n = s.trim().parse::<i32>()?; // ParseIntError → Box<dyn Error>
Ok(n)
}Tercih kuralı: kütüphanelerde somut/enum hata tipi (çağıran hatayı ayırt edebilsin, thiserror bunu kolaylaştırır), uygulamalarda Box<dyn Error> ya da anyhow::Error (hızlı, ayrıntı gerekmez). Box<dyn Error> type erasure yapar: hangi somut hata olduğunu kaybedersin, sadece mesajını alırsın.
Bu bölümde
- ? operatörü Err'i From::from ile hedef hata tipine otomatik dönüştürür
- From<KaynakHata> impl'leri farklı hataları tek enum'da toplamanın köprüsü
- Box<dyn Error> type erasure ile From yazmadan hızlı toplama sağlar
- Kütüphanede somut/enum hata, uygulamada Box<dyn Error>/anyhow tercih edilir
08 Kombinatorlar
match her zaman gerekmez. Option ve Result, akıcı zincirleme için bir dizi kombinatör metodu sunar: map, map_err, and_then, or_else, ok_or, unwrap_or ve dostları. Bunlar hata akışını tek bir ifadeye sıkıştırır.
Sık kullanılanlar
Zincirleme: match'siz akıcı stil
// " 42 " → 84; her adım önceki Ok'a uygulanır, ilk Err zinciri keser
fn iki_kati(s: &str) -> Result<i32, String> {
s.trim()
.parse::<i32>() // Result<i32, ParseIntError>
.map(|n| n * 2) // Ok değerini ikiye katla
.map_err(|e| format!("geçersiz: {e}")) // hata tipini String'e çevir
}
// Option zinciri: config map'inden oku, yoksa varsayılan
fn port(cfg: &std::collections::HashMap<String, String>) -> u16 {
cfg.get("port") // Option<&String>
.and_then(|s| s.parse().ok()) // Option<u16> (parse hatası → None)
.unwrap_or(8080) // yoksa 8080
}
// ok_or: yokluğu anlamlı bir hataya çevir, sonra ? ile yay
fn ilk(v: &[i32]) -> Result<i32, String> {
let &x = v.first().ok_or("boş dilim".to_string())?;
Ok(x)
}unwrap_or argümanını her zaman hesaplar; pahalı bir varsayılan için unwrap_or_else(|| ...) kullan, böylece kapanış yalnızca gerçekten gerektiğinde çalışır. Aynı tembellik ayrımı C++'taki value_or ile elden hesaplama arasındaki farkın karşılığıdır.
Bu bölümde
- map/map_err değeri ya da hatayı dönüştürür; varyantı değiştirmez
- and_then zincirleme (flatMap), or_else alternatif üretme içindir
- ok_or Option→Result köprüsü; unwrap_or/unwrap_or_else paniksiz varsayılan
- Kombinatörler match boilerplate'ini akıcı tek ifadeye indirir
09 Gerçek örnek
Öğrendiklerimizi tek bir programda birleştirelim: bir dosyayı oku, satırları parse et, her satırı sayıya çevir, topla. Her adımda ? ile hata yay; tüm hataları main() -> Result<(), Box<dyn Error>> deseninde topla.
Akış
dosya oku → satırlara böl → her satırı parse et → topla → yazdır
? (iter) ? (her satır) ΣTam program
use std::error::Error;
use std::fs;
// sayilar.txt: her satırda bir tam sayı. Toplamlarını döndürür.
fn dosyadan_topla(yol: &str) -> Result<i64, Box<dyn Error>> {
let icerik = fs::read_to_string(yol)?; // io::Error yayılır
let mut toplam: i64 = 0;
for (i, satir) in icerik.lines().enumerate() {
let t = satir.trim();
if t.is_empty() { continue; } // boş satırları atla
// parse hatasını satır numarasıyla zenginleştir, sonra ? ile yay
let n: i64 = t.parse()
.map_err(|e| format!("satır {}: '{}' sayı değil ({e})", i + 1, t))?;
toplam += n;
}
Ok(toplam)
}
// main de Result döner → fonksiyon gövdesinde ? serbest
fn main() -> Result<(), Box<dyn Error>> {
let toplam = dosyadan_topla("sayilar.txt")?;
println!("toplam = {toplam}");
Ok(()) // başarı; main Err dönerse süreç != 0 ile çıkar ve hatayı basar
}Bu programda hata yönetiminin tüm parçaları bir arada: read_to_string'in io::Error'ı ve parse'ın ParseIntError'ı, Box<dyn Error> sayesinde tek bir dönüş tipinde toplandı; map_err ile hata mesajı satır bağlamıyla zenginleşti; ? hata yaymayı sade tuttu. C eşdeğeri için her fopen, fgets, strtol sonrası elle errno/dönüş kontrolü ve manuel temizlik gerekirdi.
main'in Err dönmesi durumunda Rust runtime hatayı Debug formatıyla stderr'e basar ve süreç sıfırdan farklı bir kodla çıkar — tıpkı C'de return EXIT_FAILURE gibi, ama hata içeriği otomatik raporlanır. Daha zengin bağlam ve hata zincirleri için anyhow krate'i bu deseni genişletir.
Bu bölümde
- read → parse → topla akışında her adımda ? ile hata yayıldı
- Farklı hata tipleri Box<dyn Error> altında tek dönüş tipinde toplandı
- map_err ile hata mesajı satır numarası bağlamıyla zenginleştirildi
- main() -> Result<(), Box<dyn Error>> deseni hatayı runtime'a teslim eder