Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Modülerliği ve Hata Yönetimini İyileştirmek İçin Yeniden Düzenleme

Programımızı iyileştirmek için, programın yapısıyla ve olası hataları nasıl ele aldığıyla ilgili dört sorunu çözeceğiz. Birincisi, main fonksiyonumuz şu anda iki ayrı işi yerine getiriyor: Argümanları ayrıştırıyor ve dosyaları okuyor. Programımız büyüdükçe main fonksiyonunun üstlendiği bağımsız görevlerin sayısı da artacaktır. Bir fonksiyonun sorumlulukları çoğaldıkça nasıl çalıştığını anlamak, onu test etmek ve bir parçasını bozmadan üzerinde değişiklik yapmak zorlaşır. En doğrusu işlevselliği her fonksiyon yalnızca tek bir görevden sorumlu olacak şekilde ayırmaktır.

Bu durum aynı zamanda ikinci sorunla da bağlantılıdır: sorgu ve dosya_yolu programımızın yapılandırma değişkenleriyken, icerik gibi değişkenler programın asıl mantığını yürütmek için kullanılır. main uzadıkça kapsama dahil etmemiz gereken değişken sayısı artar; kapsamda ne kadar çok değişken bulunursa her birinin amacını takip etmek o kadar güçleşir. Amaçlarını netleştirmek adına yapılandırma değişkenlerini tek bir yapıda gruplamak en doğrusudur.

Üçüncü sorun, dosya okuma işlemi başarısız olduğunda hata mesajı yazdırmak için expect kullanmış olmamız, ancak bu mesajın yalnızca Dosya okunamadı yazdırmasıdır. Oysa bir dosyayı okumak pek çok farklı nedenden başarısız olabilir: Örneğin dosya hiç olmayabilir ya da dosyayı açma iznimiz bulunmayabilir. Şu anda koşul ne olursa olsun her hata için aynı mesajı yazdırıyoruz; bu da kullanıcıya sorunun kaynağı hakkında doğrudan anlaşılır bir bilgi vermez!

Dördüncüsü, hataları ele almak için expect kullanıyoruz ve kullanıcı programımızı yeterli argüman belirtmeden çalıştırırsa Rust’tan sorunu açıkça anlatmayan bir index out of bounds (indeks sınırların dışında) hatası alır. Hata yönetimi kodunun tamamı tek bir yerde toplansaydı, ileride hata yönetimi mantığının değişmesi gerektiğinde koda bakım yapan geliştiricilerin bakacağı tek bir nokta olurdu. Tüm hata yönetimi kodunu tek bir yerde toplamak, son kullanıcılarımız için gerçekten anlamlı mesajlar yazdırdığımızdan emin olmamızı da sağlar.

Projemizi yeniden düzenleyerek (refactoring) bu dört sorunu adım adım ele alalım.

İkili Projelerde İlgi Alanlarını Ayırma

main fonksiyonuna birden fazla görevin sorumluluğunu yüklemek, pek çok ikili projede karşılaşılan yaygın bir organizasyon sorunudur. Bu nedenle Rust topluluğu, main fonksiyonu büyümeye başladığında ikili bir programdaki ilgi alanlarının ayrılması (separation of concerns) için kullanışlı bir yol geliştirmiştir. Bu süreç şu adımlardan oluşur:

  • Programınızı bir main.rs ve bir lib.rs dosyasına bölün ve programınızın asıl mantığını lib.rs dosyasına taşıyın.
  • Komut satırı ayrıştırma mantığınız küçük olduğu sürece main fonksiyonunda kalabilir.
  • Komut satırı ayrıştırma mantığı karmaşıklaşmaya başladığında, onu main fonksiyonundan çıkarıp başka fonksiyonlara veya türlere taşıyın.

Bu süreçten sonra main fonksiyonunda kalan sorumluluklar yalnızca şunlarla sınırlı olmalıdır:

  • Komut satırı ayrıştırma mantığını argüman değerleriyle çağırmak
  • Diğer yapılandırmaları hazırlamak
  • lib.rs içindeki bir calistir fonksiyonunu çağırmak
  • calistir bir hata döndürürse bu hatayı ele almak

Bu desen tamamen ilgi alanlarını ayırmakla ilgilidir: main.rs programın çalıştırılmasını üstlenirken, lib.rs eldeki görevin tüm iş mantığını barındırır. main fonksiyonunu doğrudan test edemediğiniz için bu yapı, programınızın tüm mantığını main fonksiyonunun dışına taşıyarak test etmenize olanak tanır. main fonksiyonunda kalan kod ise yalnızca okuyarak doğruluğunu teyit edebileceğiniz kadar küçük olacaktır. Bu süreci izleyerek programımızı yeniden yapılandıralım.

Argüman Ayrıştırıcısını Çıkarma

Öncelikle argümanları ayrıştırma işlevselliğini, main fonksiyonunun çağıracağı ayrı bir fonksiyona çıkaracağız. Liste 12-5, src/main.rs içinde tanımlayacağımız yeni yapilandirma_ayristir fonksiyonunu çağıran main fonksiyonunun güncellenmiş başlangıcını göstermektedir.

Dosya adı: src/main.rs
use std::env;
use std::fs;

fn main() {
    let argumanlar: Vec<String> = env::args().collect();

    let (sorgu, dosya_yolu) = yapilandirma_ayristir(&argumanlar);

    // --snip--

    println!("Aranan: {sorgu}");
    println!("Dosya: {dosya_yolu}");

    let icerik = fs::read_to_string(dosya_yolu).expect("Dosya okunamadı");

    println!("Metin içeriği:\n{icerik}");
}

fn yapilandirma_ayristir(argumanlar: &[String]) -> (&str, &str) {
    let sorgu = &argumanlar[1];
    let dosya_yolu = &argumanlar[2];

    (sorgu, dosya_yolu)
}
Liste 12-5: main fonksiyonundan yapilandirma_ayristir fonksiyonunu çıkarma

Komut satırı argümanlarını hâlâ bir vektörde topluyoruz; ancak main fonksiyonunun içinde 1 numaralı indeksteki argüman değerini sorgu değişkenine, 2 numaralı indeksteki argüman değerini de dosya_yolu değişkenine atamak yerine vektörün tamamını (&argumanlar yazımıyla, bir dilim olarak) yapilandirma_ayristir fonksiyonuna iletiyoruz. Hangi argümanın hangi değişkene karşılık geleceğini belirleyen mantığı artık yapilandirma_ayristir fonksiyonu barındırır ve elde ettiği değerleri main fonksiyonuna geri döndürür. sorgu ve dosya_yolu değişkenlerini hâlâ main içinde oluşturuyoruz, fakat komut satırı argümanları ile değişkenlerin nasıl eşleşeceğini belirleme sorumluluğu artık main fonksiyonuna ait değildir.

Bu değişiklik küçük programımız için ilk bakışta fazladan iş gibi görünebilir; ancak yeniden düzenlemeyi küçük ve artımlı adımlarla gerçekleştiriyoruz. Bu değişikliği yaptıktan sonra argüman ayrıştırmanın hâlâ sorunsuz çalıştığını doğrulamak için programı tekrar çalıştırın. İlerlemenizi sık sık kontrol etmek, bir sorun ortaya çıktığında nedenini hızla tespit etmenize yardımcı olur.

Yapılandırma Değerlerini Gruplama

yapilandirma_ayristir fonksiyonunu daha da iyileştirmek için küçük bir adım daha atabiliriz. Şu anda bir demet döndürüyor, hemen ardından bu demeti tekrar tekil değişkenlere ayırıyoruz. Bu durum, henüz doğru soyutlamaya ulaşmadığımızın bir işaretidir.

İyileştirme fırsatı olduğunu gösteren bir diğer ipucu da yapilandirma_ayristir adındaki yapilandirma sözcüğüdür: Bu isim, döndürdüğümüz iki değerin birbiriyle yakından ilişkili olduğunu ve tek bir yapılandırma bilgisinin parçaları olduğunu ima eder. Şu anda bu iki değeri bir demet içinde gruplamak dışında verinin yapısında bu anlamı ifade etmiyoruz. Bunun yerine iki değeri tek bir struct içine koyacak ve struct alanlarının her birine anlamlı bir ad vereceğiz. Böylece ileride bu kod üzerinde çalışacak geliştiriciler, farklı değerlerin birbiriyle nasıl ilişkili olduğunu ve ne işe yaradığını çok daha kolay anlayacaktır.

Liste 12-6, yapilandirma_ayristir fonksiyonunda yapılan bu iyileştirmeleri göstermektedir.

Dosya adı: src/main.rs
use std::env;
use std::fs;

fn main() {
    let argumanlar: Vec<String> = env::args().collect();

    let yapilandirma = yapilandirma_ayristir(&argumanlar);

    println!("Aranan: {}", yapilandirma.sorgu);
    println!("Dosya: {}", yapilandirma.dosya_yolu);

    let icerik =
        fs::read_to_string(yapilandirma.dosya_yolu).expect("Dosya okunamadı");

    // --snip--

    println!("Metin içeriği:\n{icerik}");
}

struct Yapilandirma {
    sorgu: String,
    dosya_yolu: String,
}

fn yapilandirma_ayristir(argumanlar: &[String]) -> Yapilandirma {
    let sorgu = argumanlar[1].clone();
    let dosya_yolu = argumanlar[2].clone();

    Yapilandirma { sorgu, dosya_yolu }
}
Liste 12-6: yapilandirma_ayristir fonksiyonunu bir Yapilandirma struct’ı örneği döndürecek şekilde yeniden düzenleme

sorgu ve dosya_yolu adında alanlara sahip Yapilandirma adlı bir struct tanımladık. yapilandirma_ayristir fonksiyonunun imzası artık bir Yapilandirma değeri döndürdüğünü belirtiyor. Fonksiyonun gövdesinde, eskiden argumanlar içindeki String değerlerine referans veren string dilimleri döndürürken, artık Yapilandirma struct’ını sahiplenilmiş String değerleri tutacak şekilde tanımlıyoruz. main içindeki argumanlar değişkeni argüman değerlerinin sahibidir ve yapilandirma_ayristir fonksiyonunun bu değerleri yalnızca ödünç almasına izin verir; dolayısıyla Yapilandirma struct’ı argumanlar içindeki değerlerin sahipliğini doğrudan almaya çalışsaydı Rust’ın ödünç alma kurallarını ihlal etmiş olurduk.

String verilerini yönetmenin birkaç farklı yolu vardır; bunların en kolayı —biraz verimsiz olsa da— değerler üzerinde clone metodunu çağırmaktır. Bu yöntem, Yapilandirma örneğinin sahip olabilmesi için verinin tam bir kopyasını üretir; bu da string verisine bir referans saklamaya kıyasla biraz daha fazla zaman ve bellek gerektirir. Buna karşılık veriyi klonlamak, referansların ömürlerini yönetmek zorunda kalmayacağımız için kodumuzu son derece sade tutar; bu koşullar altında sadelik kazanmak uğruna az miktarda performanstan feragat etmek yerinde bir ödünleşimdir (trade-off).

clone Kullanımının Ödünleşimleri

Pek çok Rust programcısı, çalışma zamanı maliyeti nedeniyle sahiplik sorunlarını çözmek için clone kullanmaktan kaçınma eğilimindedir. Bölüm 13’te bu tür durumlarda çok daha verimli yöntemlerin nasıl kullanılacağını öğreneceksiniz. Ancak şimdilik ilerlemeye devam etmek adına birkaç string’i kopyalamakta hiçbir sakınca yoktur; çünkü bu kopyaları program başlarken yalnızca bir kez oluşturacaksınız ve hem dosya yolunuz hem de sorgu metniniz oldukça küçüktür. İlk denemede kodu aşırı optimize etmeye çalışmaktansa, biraz verimsiz de olsa çalışan bir programa sahip olmak daha iyidir. Rust’ta deneyim kazandıkça doğrudan en verimli çözümle başlamak kolaylaşacaktır; ancak şimdilik clone çağırmak tamamen kabul edilebilirdir.

main fonksiyonunu, yapilandirma_ayristir tarafından döndürülen Yapilandirma örneğini yapilandirma adlı bir değişkene yerleştirecek şekilde güncelledik ve daha önce ayrı sorgu ve dosya_yolu değişkenlerini kullanan kodu Yapilandirma struct’ının alanlarını kullanacak biçimde değiştirdik.

Artık kodumuz, sorgu ve dosya_yolu değerlerinin birbiriyle ilişkili olduğunu ve programın nasıl çalışacağını yapılandırma amacı taşıdığını çok daha net ifade ediyor. Bu değerleri kullanan herhangi bir kod, onları yapilandirma örneğinin amaçlarına göre adlandırılmış alanlarında bulacağını bilir.

Yapilandirma İçin Bir Yapıcı Oluşturma

Şimdiye kadar komut satırı argümanlarını ayrıştırmaktan sorumlu mantığı main fonksiyonundan çıkarıp yapilandirma_ayristir fonksiyonuna yerleştirdik. Bu adım, sorgu ve dosya_yolu değerlerinin birbiriyle ilişkili olduğunu ve bu ilişkinin kodumuzda yansıtılması gerektiğini görmemize yardımcı oldu. Ardından sorgu ve dosya_yolu’nun ortak amacını adlandırmak ve bu değerleri yapilandirma_ayristir fonksiyonundan anlamlı alan adlarıyla döndürebilmek için Yapilandirma struct’ını ekledik.

Artık yapilandirma_ayristir fonksiyonunun amacı bir Yapilandirma örneği oluşturmak olduğuna göre, yapilandirma_ayristir fonksiyonunu bağımsız bir fonksiyondan Yapilandirma struct’ı ile ilişkili new adında bir yapıcı (constructor) fonksiyona dönüştürebiliriz. Bu değişiklik kodu daha deyimsel hâle getirecektir. Standart kütüphanedeki String gibi türlerin örneklerini String::new çağrısıyla oluşturabiliyoruz. Benzer şekilde yapilandirma_ayristir fonksiyonunu Yapilandirma ile ilişkili bir new fonksiyonuna dönüştürdüğümüzde, Yapilandirma::new çağrısıyla Yapilandirma örnekleri oluşturabileceğiz. Liste 12-7 yapmamız gereken değişiklikleri göstermektedir.

Dosya adı: src/main.rs
use std::env;
use std::fs;

fn main() {
    let argumanlar: Vec<String> = env::args().collect();

    let yapilandirma = Yapilandirma::new(&argumanlar);

    println!("Aranan: {}", yapilandirma.sorgu);
    println!("Dosya: {}", yapilandirma.dosya_yolu);

    let icerik =
        fs::read_to_string(yapilandirma.dosya_yolu).expect("Dosya okunamadı");

    println!("Metin içeriği:\n{icerik}");

    // --snip--
}

// --snip--

struct Yapilandirma {
    sorgu: String,
    dosya_yolu: String,
}

impl Yapilandirma {
    fn new(argumanlar: &[String]) -> Yapilandirma {
        let sorgu = argumanlar[1].clone();
        let dosya_yolu = argumanlar[2].clone();

        Yapilandirma { sorgu, dosya_yolu }
    }
}
Liste 12-7: yapilandirma_ayristir fonksiyonunu Yapilandirma::new olarak değiştirme

main fonksiyonunda daha önce yapilandirma_ayristir çağırdığımız yeri Yapilandirma::new çağıracak şekilde güncelledik. yapilandirma_ayristir fonksiyonunun adını new olarak değiştirdik ve onu Yapilandirma ile ilişkilendiren bir impl bloğunun içine taşıdık. Çalıştığından emin olmak için bu kodu tekrar derlemeyi deneyin.

Hata Yönetimini Düzeltme

Şimdi hata yönetimimizi düzeltmeye odaklanacağız. argumanlar vektörü üçten az öğe barındırıyorsa 1 veya 2 numaralı indeksteki değerlere erişmeye çalışmanın programın paniklemesine yol açacağını hatırlayın. Programı hiçbir argüman vermeden çalıştırmayı deneyin; çıktı şu şekilde görünecektir:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`

thread 'main' (6023615) panicked at src/main.rs:27:21:
index out of bounds: the len is 1 but the index is 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Çıktıdaki index out of bounds: the len is 1 but the index is 1 (indeks sınırların dışında: uzunluk 1 ama indeks 1) satırı, programcılara yönelik teknik bir hata mesajıdır. Son kullanıcılarımızın neyi eksik yaptıklarını ve programı nasıl çalıştırmaları gerektiğini anlamalarına yardımcı olmaz. Şimdi bunu düzeltelim.

Hata Mesajını İyileştirme

Liste 12-8’de, 1 ve 2 numaralı indekslere erişmeden önce dilimin yeterince uzun olduğunu doğrulayan bir kontrolü new fonksiyonuna ekliyoruz. Dilim yeterince uzun değilse program panikler, ancak bu kez çok daha anlaşılır bir hata mesajı görüntüler.

Dosya adı: src/main.rs
use std::env;
use std::fs;

fn main() {
    let argumanlar: Vec<String> = env::args().collect();

    let yapilandirma = Yapilandirma::new(&argumanlar);

    println!("Aranan: {}", yapilandirma.sorgu);
    println!("Dosya: {}", yapilandirma.dosya_yolu);

    let icerik =
        fs::read_to_string(yapilandirma.dosya_yolu).expect("Dosya okunamadı");

    println!("Metin içeriği:\n{icerik}");
}

struct Yapilandirma {
    sorgu: String,
    dosya_yolu: String,
}

impl Yapilandirma {
    // --snip--
    fn new(argumanlar: &[String]) -> Yapilandirma {
        if argumanlar.len() < 3 {
            panic!("yeterli argüman yok");
        }
        // --snip--

        let sorgu = argumanlar[1].clone();
        let dosya_yolu = argumanlar[2].clone();

        Yapilandirma { sorgu, dosya_yolu }
    }
}
Liste 12-8: Argüman sayısı için kontrol ekleme

Bu kod, deger argümanı geçerli değer aralığının dışındaysa panic! çağırdığımız Liste 9-13’teki Tahmin::new fonksiyonuna benzer. Burada bir değer aralığını denetlemek yerine argumanlar diliminin uzunluğunun en az 3 olduğunu kontrol ediyoruz; böylece fonksiyonun geri kalanı bu koşulun sağlandığı varsayımıyla güvenle çalışabiliyor. argumanlar üçten az öğeye sahipse bu koşul true olur ve programı derhal sonlandırmak üzere panic! makrosunu çağırırız.

new fonksiyonuna eklediğimiz bu birkaç satırla birlikte hata çıktısının artık nasıl göründüğünü görmek için programı tekrar hiçbir argüman vermeden çalıştıralım:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`

thread 'main' (6023776) panicked at src/main.rs:26:13:
yeterli argüman yok
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Bu çıktı daha iyi: Artık makul bir hata mesajımız var. Ne var ki çıktıda kullanıcılarımıza göstermek istemediğimiz fazladan teknik ayrıntılar da yer alıyor. Görünüşe göre Liste 9-13’te kullandığımız teknik burası için en uygun seçim değil: Bölüm 9’da ele aldığımız üzere bir panic! çağrısı, kullanıcı kaynaklı bir kullanım hatasından ziyade kod içindeki bir programlama hatası için uygundur. Bunun yerine Bölüm 9’da öğrendiğiniz diğer tekniği —başarıyı veya hatayı ifade eden bir Result döndürmeyi— kullanacağız.

panic! Çağırmak Yerine Result Döndürme

Paniklemek yerine, başarılı durumda bir Yapilandirma örneği barındıran, hata durumunda ise sorunu açıklayan bir Result değeri döndürebiliriz. Aynı zamanda fonksiyonun adını new yerine olustur (İngilizce Rust kodlarındaki yaygın kuralıyla build) olarak değiştireceğiz; çünkü pek çok Rust programcısı new adındaki fonksiyonların asla başarısız olmamasını bekler. Yapilandirma::olustur, main fonksiyonuyla iletişim kurarken bir sorun oluştuğunu bildirmek için Result türünü kullanabilir. Ardından main fonksiyonunu, bir panic! çağrısının yol açtığı thread 'main' ve RUST_BACKTRACE gibi teknik metinler olmadan Err varyantını kullanıcılarımız için sade ve pratik bir hata mesajına dönüştürecek şekilde güncelleyebiliriz.

Liste 12-9, artık Yapilandirma::olustur adını verdiğimiz fonksiyonun dönüş türünde ve Result döndürmek için fonksiyon gövdesinde yapmamız gereken değişiklikleri göstermektedir. Bir sonraki listede main fonksiyonunu da güncelleyene kadar bu kodun derlenmeyeceğini unutmayın.

Dosya adı: src/main.rs
use std::env;
use std::fs;

fn main() {
    let argumanlar: Vec<String> = env::args().collect();

    let yapilandirma = Yapilandirma::new(&argumanlar);

    println!("Aranan: {}", yapilandirma.sorgu);
    println!("Dosya: {}", yapilandirma.dosya_yolu);

    let icerik =
        fs::read_to_string(yapilandirma.dosya_yolu).expect("Dosya okunamadı");

    println!("Metin içeriği:\n{icerik}");
}

struct Yapilandirma {
    sorgu: String,
    dosya_yolu: String,
}

impl Yapilandirma {
    fn olustur(argumanlar: &[String]) -> Result<Yapilandirma, &'static str> {
        if argumanlar.len() < 3 {
            return Err("yeterli argüman yok");
        }

        let sorgu = argumanlar[1].clone();
        let dosya_yolu = argumanlar[2].clone();

        Ok(Yapilandirma { sorgu, dosya_yolu })
    }
}
Liste 12-9: Yapilandirma::olustur fonksiyonundan Result döndürme

olustur fonksiyonumuz başarı durumunda bir Yapilandirma örneği, hata durumunda ise bir string sabiti (&'static str) içeren bir Result döndürür. Hata değerlerimiz her zaman 'static ömre sahip string sabitleri olacaktır.

Fonksiyon gövdesinde iki değişiklik yaptık: Kullanıcı yeterli sayıda argüman sağlamadığında panic! çağırmak yerine artık bir Err değeri döndürüyoruz ve dönüş değeri olan Yapilandirma örneğini Ok içine sarmalıyoruz. Bu değişiklikler fonksiyonun yeni tür imzasıyla uyumlu olmasını sağlar.

Yapilandirma::olustur fonksiyonundan bir Err değeri döndürmek, main fonksiyonunun olustur’dan dönen Result değerini ele almasına ve hata durumunda süreci çok daha temiz bir şekilde sonlandırmasına olanak tanır.

Yapilandirma::olustur’u Çağırma ve Hataları Ele Alma

Hata durumunu ele almak ve kullanıcı dostu bir mesaj yazdırmak için Liste 12-10’da gösterildiği gibi main fonksiyonunu Yapilandirma::olustur tarafından döndürülen Result değerini işleyecek şekilde güncellememiz gerekir. Ayrıca komut satırı aracından sıfır dışı (nonzero) bir hata koduyla çıkma sorumluluğunu da panic! makrosundan devralıp kendimiz uygulayacağız. Sıfır dışı bir çıkış durumu kodu, programımızı çağıran sürece programın bir hata durumuyla sonlandığını bildirmeye yarayan yerleşik bir kuraldır.

Dosya adı: src/main.rs
use std::env;
use std::fs;
use std::process;

fn main() {
    let argumanlar: Vec<String> = env::args().collect();

    let yapilandirma =
        Yapilandirma::olustur(&argumanlar).unwrap_or_else(|hata| {
            println!("Argümanları ayrıştırırken problem oluştu: {hata}");
            process::exit(1);
        });

    // --snip--

    println!("Aranan: {}", yapilandirma.sorgu);
    println!("Dosya: {}", yapilandirma.dosya_yolu);

    let icerik =
        fs::read_to_string(yapilandirma.dosya_yolu).expect("Dosya okunamadı");

    println!("Metin içeriği:\n{icerik}");
}

struct Yapilandirma {
    sorgu: String,
    dosya_yolu: String,
}

impl Yapilandirma {
    fn olustur(argumanlar: &[String]) -> Result<Yapilandirma, &'static str> {
        if argumanlar.len() < 3 {
            return Err("yeterli argüman yok");
        }

        let sorgu = argumanlar[1].clone();
        let dosya_yolu = argumanlar[2].clone();

        Ok(Yapilandirma { sorgu, dosya_yolu })
    }
}
Liste 12-10: Yapilandirma oluşturma işlemi başarısız olursa hata koduyla çıkış yapma

Bu listede henüz ayrıntılı olarak incelemediğimiz bir metot kullandık: Standart kütüphanede Result<T, E> üzerinde tanımlı olan unwrap_or_else. unwrap_or_else kullanmak, panik üretmeyen özel bir hata yönetimi tanımlamamıza olanak verir. Eğer Result değeri Ok ise bu metodun davranışı unwrap ile aynıdır: Ok varyantının sarmaladığı iç değeri döndürür. Ancak değer Err ise bu metot, tanımlayıp unwrap_or_else’e argüman olarak geçirdiğimiz isimsiz bir fonksiyon olan kapanış (closure) içindeki kodu çalıştırır. Kapanışları Bölüm 13’te daha ayrıntılı olarak ele alacağız. Şimdilik bilmeniz gereken tek şey, unwrap_or_else metodunun Err varyantının içindeki değeri —yani Liste 12-9’da eklediğimiz "yeterli argüman yok" statik string sabitini— dikey çubuklar arasında yer alan hata parametresi üzerinden kapanışımıza aktaracağıdır. Böylece kapanışın içindeki kod çalıştığında bu hata değerini kullanabilir.

Standart kütüphaneden process modülünü kapsama dahil etmek için yeni bir use satırı ekledik. Hata durumunda çalışacak kapanışın içindeki kod yalnızca iki satırdan oluşur: hata değerini yazdırıyor ve hemen ardından process::exit fonksiyonunu çağırıyoruz. process::exit fonksiyonu programı anında durdurur ve kendisine argüman olarak verilen sayıyı çıkış durum kodu olarak döndürür. Bu yaklaşım Liste 12-8’de kullandığımız panic! tabanlı yönetime benzer, ancak artık fazladan teknik çıktıların hiçbirini almayız. Deneyelim:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/minigrep`
Argümanları ayrıştırırken problem oluştu: yeterli argüman yok

Harika! Bu çıktı kullanıcılarımız için çok daha anlaşılır ve dostça.

Not: C ve POSIX geleneğini izleyen işletim sistemlerinde bir sürecin 0 çıkış koduyla sonlanması işlemin başarılı olduğunu, 1 gibi sıfırdan farklı herhangi bir tam sayıyla sonlanması ise bir hata oluştuğunu belirtir (kabukta son komutun çıkış kodunu echo $? ile görebilirsiniz). Doğru çıkış kodu döndürmek, komut satırı araçlarının betiklerde veya minigrep aranan siir.txt && diger_komut gibi kabuk zincirlerinde güvenilir biçimde kullanılabilmesini sağlar; minigrep 1 koduyla çıktığında kabuk bir hata oluştuğunu anlar ve && sonrasındaki komutu çalıştırmaz. Ayrıca process::exit, panik mekanizmasının aksine yığını geri sarmadan (stack unwinding yapmadan) ve yerel değişkenlerin Drop yıkıcılarını çalıştırmadan süreci anında sonlandırır; bu nedenle onu derinlerdeki yardımcı fonksiyonlar yerine, burada yaptığımız gibi tüm hata yönetiminin toplandığı main fonksiyonunda çağırmak en temiz yaklaşımdır.

Mantığı main’den Çıkarma

Yapılandırma ayrıştırma kısmını yeniden düzenlemeyi tamamladığımıza göre şimdi programın asıl mantığına dönelim. “İkili Projelerde İlgi Alanlarını Ayırma” bölümünde belirttiğimiz gibi, şu anda main fonksiyonunda bulunan ve yapılandırmayı hazırlamak ya da hataları ele almakla ilgisi olmayan tüm mantığı barındıracak calistir adında bir fonksiyon çıkaracağız. İşimiz bittiğinde main fonksiyonu özlü ve bakarak doğrulanması kolay bir hâle gelecek; geri kalan tüm mantık içinse testler yazabileceğiz.

Liste 12-11, calistir fonksiyonunu ayırmaya yönelik bu küçük ve artımlı adımı göstermektedir.

Dosya adı: src/main.rs
use std::env;
use std::fs;
use std::process;

fn main() {
    // --snip--

    let argumanlar: Vec<String> = env::args().collect();

    let yapilandirma =
        Yapilandirma::olustur(&argumanlar).unwrap_or_else(|hata| {
            println!("Argümanları ayrıştırırken problem oluştu: {hata}");
            process::exit(1);
        });

    println!("Aranan: {}", yapilandirma.sorgu);
    println!("Dosya: {}", yapilandirma.dosya_yolu);

    calistir(yapilandirma);
}

fn calistir(yapilandirma: Yapilandirma) {
    let icerik =
        fs::read_to_string(yapilandirma.dosya_yolu).expect("Dosya okunamadı");

    println!("Metin içeriği:\n{icerik}");
}

// --snip--

struct Yapilandirma {
    sorgu: String,
    dosya_yolu: String,
}

impl Yapilandirma {
    fn olustur(argumanlar: &[String]) -> Result<Yapilandirma, &'static str> {
        if argumanlar.len() < 3 {
            return Err("yeterli argüman yok");
        }

        let sorgu = argumanlar[1].clone();
        let dosya_yolu = argumanlar[2].clone();

        Ok(Yapilandirma { sorgu, dosya_yolu })
    }
}
Liste 12-11: Program mantığının geri kalanını içeren calistir fonksiyonunu çıkarma

calistir fonksiyonu artık dosyayı okumaktan başlayarak main içindeki kalan tüm mantığı barındırıyor. Bu fonksiyon argüman olarak Yapilandirma örneğini alır.

calistir Fonksiyonundan Hata Döndürme

Geri kalan program mantığı calistir fonksiyonuna ayrıldığına göre, Liste 12-9’da Yapilandirma::olustur ile yaptığımız gibi hata yönetimini iyileştirebiliriz. expect çağrısıyla programın paniklemesine izin vermek yerine, calistir fonksiyonu bir şeyler ters gittiğinde bir Result<T, E> değeri döndürecektir. Böylece hataları ele alma mantığını kullanıcı dostu bir biçimde main içinde birleştirmeye devam edebiliriz. Liste 12-12, calistir fonksiyonunun imzasında ve gövdesinde yapmamız gereken değişiklikleri göstermektedir.

Dosya adı: src/main.rs
use std::env;
use std::fs;
use std::process;
use std::error::Error;

// --snip--


fn main() {
    let argumanlar: Vec<String> = env::args().collect();

    let yapilandirma =
        Yapilandirma::olustur(&argumanlar).unwrap_or_else(|hata| {
            println!("Argümanları ayrıştırırken problem oluştu: {hata}");
            process::exit(1);
        });

    println!("Aranan: {}", yapilandirma.sorgu);
    println!("Dosya: {}", yapilandirma.dosya_yolu);

    calistir(yapilandirma);
}

fn calistir(yapilandirma: Yapilandirma) -> Result<(), Box<dyn Error>> {
    let icerik = fs::read_to_string(yapilandirma.dosya_yolu)?;

    println!("Metin içeriği:\n{icerik}");

    Ok(())
}

struct Yapilandirma {
    sorgu: String,
    dosya_yolu: String,
}

impl Yapilandirma {
    fn olustur(argumanlar: &[String]) -> Result<Yapilandirma, &'static str> {
        if argumanlar.len() < 3 {
            return Err("yeterli argüman yok");
        }

        let sorgu = argumanlar[1].clone();
        let dosya_yolu = argumanlar[2].clone();

        Ok(Yapilandirma { sorgu, dosya_yolu })
    }
}
Liste 12-12: calistir fonksiyonunu Result döndürecek şekilde değiştirme

Burada üç önemli değişiklik yaptık. Birincisi, calistir fonksiyonunun dönüş türünü Result<(), Box<dyn Error>> olarak değiştirdik. Bu fonksiyon daha önce birim türü olan () döndürüyordu; başarılı (Ok) durumda dönen değer olarak yine () türünü koruyoruz.

Hata türü içinse Box<dyn Error> trait nesnesini kullandık (ve dosyanın en üstüne bir use deyimi ekleyerek std::error::Error trait’ini kapsama dahil ettik). Trait nesnelerini Bölüm 18’de ayrıntılı olarak işleyeceğiz. Şimdilik Box<dyn Error> ifadesinin, fonksiyonun Error trait’ini uygulayan bir tür döndüreceği anlamına geldiğini, ancak dönen değerin somut olarak hangi tür olacağını önceden belirtmek zorunda olmadığımızı bilmeniz yeterlidir. Bu bize farklı hata durumlarında farklı türlerde hata değerleri döndürebilme esnekliği kazandırır. Buradaki dyn anahtar kelimesi dinamik (dynamic) sözcüğünün kısaltmasıdır.

Not: Box<dyn Error> yazımındaki Box sarmalayıcısına neden ihtiyaç duyduğumuzu merak edebilirsiniz. dyn Error, “Error trait’ini uygulayan herhangi bir tür” anlamına gelir; ancak bu türlerin her birinin bellekte kapladığı alan farklı olabileceğinden derleyici bir dyn Error değerinin boyutunu derleme zamanında bilemez. Oysa bir fonksiyonun dönüş değerinin boyutu sabit olmak zorundadır. Bölüm 15’te ayrıntılı ele alacağımız Box<T>, asıl hata değerini öbekte saklayıp geriye her zaman aynı boyutta olan bir işaretçi bıraktığı için bu sorunu çözer. Bu kalıp, hata türünü tek tek sıralamak istemediğiniz uygulama kodunda (özellikle main ve calistir gibi üst düzey fonksiyonlarda) son derece yaygındır; Bölüm 9’da gördüğümüz gibi main fonksiyonunun kendisi de Result<(), Box<dyn Error>> döndürebilir.

İkincisi, Bölüm 9’da incelediğimiz gibi expect çağrısını kaldırıp yerine ? operatörünü koyduk. Hata durumunda panic! çağırmak yerine ? operatörü, hata değerini çağıran kodun ele alması için mevcut fonksiyondan dışarı döndürür (örneğin fs::read_to_string’in döndürdüğü std::io::Error hatasını standart From dönüşümüyle otomatik olarak Box<dyn Error> türüne çevirerek iletir).

Üçüncüsü, calistir fonksiyonu artık başarı durumunda bir Ok değeri döndürüyor. Fonksiyon imzasında calistir’ın başarı türünü () olarak bildirdiğimiz için birim türü değerini Ok içine sarmalamamız gerekir. Bu Ok(()) sözdizimi ilk bakışta biraz tuhaf görünebilir; ancak () türünü bu şekilde kullanmak, calistir fonksiyonunu yalnızca yan etkileri için çağırdığımızı ve döndürdüğü bir değere ihtiyaç duymadığımızı belirtmenin deyimsel yoludur.

Bu kodu çalıştırdığınızda derlenecek, ancak ekranda bir uyarı görüntüleyecektir:

$ cargo run -- mezar siir.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
warning: unused `Result` that must be used
  --> src/main.rs:20:5
   |
20 |     calistir(yapilandirma);
   |     ^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: this `Result` may be an `Err` variant, which should be handled
   = note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
help: use `let _ = ...` to ignore the resulting value
   |
20 |     let _ = calistir(yapilandirma);
   |     +++++++

warning: `minigrep` (bin "minigrep") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.71s
     Running `target/debug/minigrep mezar siir.txt`
Aranan: mezar
Dosya: siir.txt
Metin içeriği:
Ne hasta bekler sabahı,
Ne taze ölüyü mezar.
Ne de şeytan, bir günahı,
Seni beklediğim kadar.

Geçti istemem gelmeni,
Yokluğunda buldum seni;
Bırak vehmimde gölgeni
Gelme, artık neye yarar?

Rust derleyicisi, kodumuzun Result değerini yok saydığını (unused Result that must be used) ve bu Result değerinin bir hata oluştuğuna işaret edebileceğini bildiriyor. Bir hata oluşup oluşmadığını kontrol etmediğimiz için derleyici burada muhtemelen bir hata yönetimi kodu yazmak istediğimizi bize hatırlatıyor! Şimdi bu sorunu giderelim.

main İçinde calistir Fonksiyonundan Dönen Hataları Ele Alma

Hataları kontrol edip ele almak için Liste 12-10’da Yapilandirma::olustur ile kullandığımız tekniğe çok benzeyen, ancak küçük bir fark barındıran bir yaklaşım kullanacağız:

Dosya adı: src/main.rs

use std::env;
use std::error::Error;
use std::fs;
use std::process;

fn main() {
    // --snip--

    let argumanlar: Vec<String> = env::args().collect();

    let yapilandirma =
        Yapilandirma::olustur(&argumanlar).unwrap_or_else(|hata| {
            println!("Argümanları ayrıştırırken problem oluştu: {hata}");
            process::exit(1);
        });

    println!("Aranan: {}", yapilandirma.sorgu);
    println!("Dosya: {}", yapilandirma.dosya_yolu);

    if let Err(e) = calistir(yapilandirma) {
        println!("Uygulama hatası: {e}");
        process::exit(1);
    }
}

fn calistir(yapilandirma: Yapilandirma) -> Result<(), Box<dyn Error>> {
    let icerik = fs::read_to_string(yapilandirma.dosya_yolu)?;

    println!("Metin içeriği:\n{icerik}");

    Ok(())
}

struct Yapilandirma {
    sorgu: String,
    dosya_yolu: String,
}

impl Yapilandirma {
    fn olustur(argumanlar: &[String]) -> Result<Yapilandirma, &'static str> {
        if argumanlar.len() < 3 {
            return Err("yeterli argüman yok");
        }

        let sorgu = argumanlar[1].clone();
        let dosya_yolu = argumanlar[2].clone();

        Ok(Yapilandirma { sorgu, dosya_yolu })
    }
}

calistir fonksiyonunun bir Err değeri döndürüp döndürmediğini kontrol etmek ve döndürdüyse process::exit(1) çağrısıyla programı sonlandırmak için unwrap_or_else yerine if let kullandık. Çünkü calistir fonksiyonu, Yapilandirma::olustur fonksiyonunun Yapilandirma örneği döndürmesi gibi unwrap ile içinden çıkarıp kullanmak isteyeceğimiz bir değer döndürmez. Başarı durumunda calistir yalnızca () döndürdüğünden, tek önemsediğimiz şey bir hatayı yakalamaktır; dolayısıyla içinden yalnızca () çıkacak bir değeri açmak için unwrap_or_else kullanmamıza gerek yoktur.

Hem if let bloğunun hem de unwrap_or_else kapanışının gövdesi iki durumda da birebir aynıdır: Hatayı yazdırır ve programdan çıkarız.

Kodu Bir Kütüphane Crate’ine Bölme

minigrep projemiz şu ana kadar oldukça iyi görünüyor! Şimdi src/main.rs dosyasını bölecek ve kodun bir kısmını src/lib.rs dosyasına koyacağız. Böylece kodu doğrudan test edebilecek ve sorumluluğu çok daha az olan bir src/main.rs dosyası elde edeceğiz.

Metin aramaktan sorumlu kodu src/main.rs yerine src/lib.rs içinde tanımlayalım; bu sayede biz (veya minigrep kütüphanemizi kullanan herhangi biri) arama fonksiyonunu minigrep ikili dosyamız dışındaki farklı bağlamlardan da çağırabileceğiz.

İlk olarak Liste 12-13’te gösterildiği gibi src/lib.rs içinde, gövdesinde unimplemented! makrosunu çağıran ara fonksiyonunun imzasını tanımlayalım. Bir sonraki bölümde fonksiyonun uygulamasını doldurduğumuzda bu imzayı çok daha ayrıntılı açıklayacağız.

Dosya adı: src/lib.rs
pub fn ara<'a>(sorgu: &str, icerik: &'a str) -> Vec<&'a str> {
    unimplemented!();
}
Liste 12-13: src/lib.rs içinde ara fonksiyonunu tanımlama

Bu kod derlenir; ancak ara fonksiyonu çağrıldığında unimplemented! makrosu panik oluşturur. Bu makro, henüz uygulamadığımız fonksiyonun gövdesi için geçici bir yer tutucudur.

ara fonksiyonunu kütüphane crate’imizin dışa açık API’sinin bir parçası olarak işaretlemek için fonksiyon tanımında pub anahtar kelimesini kullandık. Artık ikili crate’imizden kullanabileceğimiz ve doğrudan test edebileceğimiz bir kütüphane crate’imiz var!

Şimdi Liste 12-14’te gösterildiği gibi src/lib.rs içinde tanımladığımız kodu src/main.rs içindeki ikili crate’in kapsamına dahil etmemiz ve çağırmamız gerekiyor.

Dosya adı: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

// --snip--
use minigrep::ara;

fn main() {
    // --snip--
    let argumanlar: Vec<String> = env::args().collect();

    let yapilandirma =
        Yapilandirma::olustur(&argumanlar).unwrap_or_else(|hata| {
            println!("Argümanları ayrıştırırken problem oluştu: {hata}");
            process::exit(1);
        });

    if let Err(e) = calistir(yapilandirma) {
        println!("Uygulama hatası: {e}");
        process::exit(1);
    }
}

// --snip--


struct Yapilandirma {
    sorgu: String,
    dosya_yolu: String,
}

impl Yapilandirma {
    fn olustur(argumanlar: &[String]) -> Result<Yapilandirma, &'static str> {
        if argumanlar.len() < 3 {
            return Err("yeterli argüman yok");
        }

        let sorgu = argumanlar[1].clone();
        let dosya_yolu = argumanlar[2].clone();

        Ok(Yapilandirma { sorgu, dosya_yolu })
    }
}

fn calistir(yapilandirma: Yapilandirma) -> Result<(), Box<dyn Error>> {
    let icerik = fs::read_to_string(yapilandirma.dosya_yolu)?;

    for satir in ara(&yapilandirma.sorgu, &icerik) {
        println!("{satir}");
    }

    Ok(())
}
Liste 12-14: src/main.rs içinde minigrep kütüphane crate’inin ara fonksiyonunu kullanma

Kütüphane crate’indeki ara fonksiyonunu ikili crate’in kapsamına getirmek için use minigrep::ara satırını ekliyoruz. Ardından calistir fonksiyonunda dosyanın tüm içeriğini ekrana yazdırmak yerine ara fonksiyonunu çağırıyor ve &yapilandirma.sorgu değeri ile &icerik değerini argüman olarak iletiyoruz. Sonrasında calistir, ara fonksiyonundan dönen ve sorguyla eşleşen her bir satırı yazdırmak için bir for döngüsü kullanır. Ayrıca bu aşama, herhangi bir hata oluşmadığında programımızın yalnızca arama sonuçlarını yazdırması için main fonksiyonunda sorguyu ve dosya yolunu ekrana basan geçici println! çağrılarını kaldırmak için de doğru zamandır.

ara fonksiyonunun herhangi bir yazdırma işlemi gerçekleşmeden önce tüm sonuçları döndüreceği bir vektörde toplayacağına dikkat edin. Eşleşen satırlar bulundukları anda yazdırılmadığı için bu uygulama büyük dosyalarda arama yaparken sonuçları görüntülemekte yavaş kalabilir; Bölüm 13’te iteratörleri kullanarak bu durumu nasıl iyileştirebileceğimizi ele alacağız.

Not: Cargo, aynı pakette hem src/lib.rs hem de src/main.rs dosyalarını gördüğünde paket adıyla (minigrep) bir kütüphane crate’i ve aynı adla bir ikili crate derler. Rust’ta entegrasyon testleri (tests/ dizini) ve harici paketler src/main.rs içindeki öğeleri içe aktaramaz; yalnızca src/lib.rs içindeki pub öğelere (use minigrep::ara; gibi) erişebilir. Gerçek dünyadaki büyük Rust komut satırı araçlarında —bu bölümün başındaki “İkili Projelerde İlgi Alanlarını Ayırma” kılavuzunda da belirtildiği üzere— bu mimari bir adım daha ileri taşınarak Yapilandirma ve calistir dahil neredeyse tüm uygulama katmanı src/lib.rs altına yerleştirilir (biz ise bu bölümde örneği sade tutmak amacıyla yalnızca arama fonksiyonlarını src/lib.rs içinde tanımlıyoruz); src/main.rs ise yalnızca argümanları alıp kütüphaneyi çağıran ve hataları çıkış koduna dönüştüren ince bir kabuk olarak bırakılır.

Epey iş çıkardık ama gelecekteki geliştirmelerimiz için çok sağlam bir zemin hazırladık! Artık hataları ele almak çok daha kolay ve kodumuz çok daha modüler. Bundan sonraki çalışmalarımızın neredeyse tamamını src/lib.rs içinde gerçekleştireceğiz.

Yeni kazandığımız bu modülerlikten yararlanarak eski kodla zor olacak ama yeni yapıyla son derece kolaylaşan bir şey yapalım: Biraz test yazalım!