Modülerliği ve Hata İşlemeyi Geliştirmek için Yeniden Düzenleme

Programımızı iyileştirmek için, programın yapısı ve olası hataları nasıl ele aldığı ile ilgili dört sorunu çözeceğiz. İlk olarak, ma'n fonksiyonumuz artık iki görevi yerine getiriyor: argümanları ayrıştırıyor ve dosyaları okuyor. Programımız büyüdükçe, ana fonksiyonun yerine getirdiği ayrı görevlerin sayısı artacaktır. Bir fonksiyon sorumluluk kazandıkça, hakkında mantık yürütmek daha zor, test etmek daha zor ve parçalarından birini bozmadan değiştirmek daha zor hale gelir. Her fonksiyonun tek bir görevden sorumlu olması için işlevleri ayırmak en iyisidir.

Bu konu aynı zamanda ikinci sorunla da bağlantılıdır: sorgu ve dosya adı programımız için yapılandırma değişkenleri olsa da, içerik gibi değişkenler programın mantığını gerçekleştirmek için kullanılır. main ne kadar uzun olursa, o kadar çok değişkeni kapsama almamız gerekecektir; ne kadar çok değişkeni kapsama alırsak, her birinin amacını takip etmek o kadar zor olacaktır. Amaçlarını netleştirmek için yapılandırma değişkenlerini tek bir yapıda gruplamak en iyisidir.

Üçüncü sorun, dosyayı okuma başarısız olduğunda bir hata mesajı yazdırmak için expect kullandık, ancak hata mesajı sadece Something went wrong reading the file yazıyor. Bir dosyayı okumak çeşitli şekillerde başarısız olabilir: örneğin, dosya eksik olabilir veya dosyayı açmak için iznimiz olmayabilir. Şu anda, durum ne olursa olsun, her şey için aynı hata mesajını yazdırırız ve bu da kullanıcıya hiçbir bilgi vermeyiz.

Dördüncüsü, farklı hataları işlemek için tekrar tekrar expect kullanıyoruz ve kullanıcı programımızı yeterli argüman belirtmeden çalıştırırsa, Rust'tan sorunu açıkça açıklamayan bir index out of bounds hatası alacaktır. Tüm hata işleme kodunun tek bir yerde olması en iyisidir, böylece gelecekteki bakımcılar hata işleme mantığının değişmesi gerektiğinde koda başvurmak için tek bir yere sahip olurlar. Tüm hata işleme kodunun tek bir yerde olması, son kullanıcılarımız için anlamlı olacak mesajları yazdırmamızı da sağlayacaktır.

Projemizi yeniden düzenleyerek bu dört sorunu ele alalım.

İkili Projeler için Endişelerin Ayrılması

Birden fazla görevin sorumluluğunun ana fonksiyona verilmesine ilişkin organizasyonel sorun, birçok ikili projede ortaktır. Sonuç olarak, Rust topluluğu, ana program büyümeye başladığında ikili bir programın ayrı endişelerini bölmek için yönergeler geliştirmiştir. Bu süreç aşağıdaki adımlardan oluşur:

  • Programınızı main.rs ve lib.rs olarak ayırın ve programınızın ana mantığını lib.rs'e taşıyın.
  • Komut satırı ayrıştırma mantığınız küçük olduğu sürece main.rs içinde kalabilir.
  • Komut satırı ayrıştırma mantığı karmaşıklaşmaya başladığında, main.rs'den çıkarın ve lib.rs'e taşıyın.

Bu işlemlerden sonra main fonksiyonunda kalanlar aşağıdakilerle 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ı ayarlama
  • lib.rs içinde bir run fonksiyonu çağırma
  • run bir hata döndürürse hatayı işleme

Bu model endişeleri ayırmakla ilgilidir: main.rs programı çalıştırır ve lib.rs eldeki görevin tüm mantığını ele alır. main fonksiyonunu doğrudan test edemeyeceğiniz için, bu yapı programınızın tüm mantığını lib.rs'deki fonksiyonlara taşıyarak test etmenizi sağlar. main.rs'de kalan kod, okunarak doğruluğu teyit edilebilecek kadar küçük olacaktır.

Bu süreci takip ederek programımızı yeniden düzenleyelim.

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

Komut satırı ayrıştırma mantığını src/lib.rs'ye taşımaya hazırlanmak için argümanları ayrıştırma işlevini main'in çağıracağı bir fonksiyona çıkaracağız. Liste 12-5, şimdilik src/main.rs içinde tanımlayacağımız yeni parse_config fonksiyonunu çağıran main'in yeni başlangıcını göstermektedir.

Dosya adı: src/main.rs

use std::env;
use std::fs;

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

    let (query, filename) = parse_config(&args);

    // --snip--

    println!("Searching for {}", query);
    println!("In file {}", filename);

    let contents = fs::read_to_string(filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

fn parse_config(args: &[String]) -> (&str, &str) {
    let query = &args[1];
    let filename = &args[2];

    (query, filename)
}

Liste 12-5: parse_config fonksiyonunun main'den çıkarılması

Komut satırı argümanlarını hala bir vektörde topluyoruz, ancak 1. indeksteki argüman değerini sorgu değişkenine ve 2. indeksteki argüman değerini main içindeki dosya adı değişkenine atamak yerine, tüm vektörü parse_config'e aktarıyoruz. parse_config daha sonra hangi argümanın hangi değişkene gideceğini belirleyen mantığı tutar ve değerleri main'e geri aktarır. query ve filename değişkenlerini hala main içinde oluşturuyoruz, ancak main artık komut satırı argümanlarının ve değişkenlerin nasıl karşılık geldiğini belirleme sorumluluğuna sahip değil.

Bu yeniden çalışma küçük programımız için aşırı gibi görünebilir, ancak küçük, artan adımlarla yeniden düzenliyoruz. Bu değişikliği yaptıktan sonra, argüman ayrıştırmanın hala çalıştığını doğrulamak için programı tekrar çalıştırın. İlerlemenizi sık sık kontrol etmek, ortaya çıktıklarında sorunların nedenini belirlemeye yardımcı olmak için iyidir.

Yapılandırma Değerlerini Gruplama

parse_config'i daha da geliştirmek için küçük bir adım daha atabiliriz. Şu anda bir tuple döndürüyoruz, ancak hemen ardından bu tuple'ı tekrar ayrı parçalara ayırıyoruz. Bu, belki de henüz doğru soyutlamaya sahip olmadığımızın bir işaretidir.

İyileştirme için yer olduğunu gösteren bir başka gösterge de parse_config'in config kısmıdır, bu da döndürdüğümüz iki değerin ilişkili olduğunu ve her ikisinin de bir yapılandırma değerinin parçası olduğunu ima eder. Şu anda bu anlamı, iki değeri bir tuple olarak gruplamak dışında verinin yapısında aktarmıyoruz; bunun yerine iki değeri struct içine koyacağız ve struct alanlarının her birine anlamlı bir isim vereceğiz. Bunu yapmak, bu kodun gelecekteki bakımcılarının farklı değerlerin birbirleriyle nasıl ilişkili olduğunu ve amaçlarının ne olduğunu anlamalarını kolaylaştıracaktır.

Liste 12-6, parse_config'de yapılan iyileştirmeleri göstermektedir.

Dosya adı: src/main.rs

use std::env;
use std::fs;

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

    let config = parse_config(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    // --snip--

    println!("With text:\n{}", contents);
}

struct Config {
    query: String,
    filename: String,
}

fn parse_config(args: &[String]) -> Config {
    let query = args[1].clone();
    let filename = args[2].clone();

    Config { query, filename }
}

Liste 12-6: Config yapısının örneğini döndürmek için parse_config öğesini yeniden düzenleme

query ve filename adında alanlara sahip olacak şekilde tanımlanmış Config adında bir yapı ekledik. parse_config'in imzası artık Config değeri döndürdüğünü gösteriyor. Eskiden args içindeki String değerlerine referans veren dizgi dilimleri döndürdüğümüz parse_config gövdesinde, artık Config'i String değerlerini içerecek şekilde tanımlıyoruz. main içindeki args değişkeni argüman değerlerinin sahibidir ve yalnızca parse_config fonksiyonunun bunları ödünç almasına izin verir, bu da Config'in args içindeki değerlerin sahipliğini almaya çalışması durumunda Rust'ın ödünç alma kurallarını ihlal edeceğimiz anlamına gelir.

String verilerini yönetebileceğimiz birkaç yol vardır; biraz verimsiz olsa da en kolay yol, değerler üzerinde clone metodunu çağırmaktır. Bu, Config örneğinin sahip olması için verilerin tam bir kopyasını oluşturacaktır, bu da dizgi verilerine bir referans depolamaktan daha fazla zaman ve bellek gerektirir. Bununla birlikte, verileri klonlamak kodumuzu çok basit hale getirir çünkü referansların yaşam sürelerini yönetmek zorunda değiliz; bu durumda, basitlik kazanmak için biraz performanstan vazgeçmek değerli bir değiş tokuştur.

clone Kullanmanın Getirileri

Birçok Rustsever arasında, çalışma zamanı maliyeti nedeniyle sahiplik sorunlarını çözmek için clone kullanmaktan kaçınma eğilimi vardır. Bölüm 13'te, bu tür durumlarda nasıl daha verimli yöntemler kullanacağınızı öğreneceksiniz. Ancak şimdilik, ilerlemeye devam etmek için birkaç dizgiyi kopyalamanızda bir sakınca yok çünkü bu kopyaları yalnızca bir kez yapacaksınız ve filename ve query dizginiz çok küçük. İlk geçişinizde kodu aşırı optimize etmeye çalışmaktansa biraz verimsiz çalışan bir programa sahip olmak daha iyidir. Rust ile daha deneyimli hale geldikçe, en verimli çözümle başlamak daha kolay olacaktır, ancak şimdilik clone kullanmak tamamen kabul edilebilir.

main'i, parse_config tarafından döndürülen Config örneğini config adlı bir değişkene atayacak şekilde güncelledik ve daha önce ayrı query ve filename değişkenlerini kullanan kodu güncelledik, böylece artık bunun yerine Config yapısındaki alanları kullanıyor.

Artık kodumuz query ve filename'in ilişkili olduğunu ve amaçlarının programın nasıl çalışacağını yapılandırmak olduğunu daha açık bir şekilde ifade ediyor. Bu değerleri kullanan herhangi bir kod, bunları config örneğinde amaçlarına göre adlandırılmış alanlarda bulmayı bilir.

Config için Bir Yapıcı Oluşturma

Şimdiye kadar, komut satırı argümanlarını ayrıştırmaktan sorumlu mantığı main'den çıkardık ve parse_config fonksiyonuna yerleştirdik. Bunu yapmak, query ve filename değerlerinin ilişkili olduğunu ve bu ilişkinin kodumuzda aktarılması gerektiğini görmemize yardımcı oldu. Daha sonra query ve filename'in amaçlarını adlandırmak ve parse_config fonksiyonundan değerlerin adlarını struct alan adları olarak döndürebilmek için bir Config struct'ı ekledik.

Artık parse_config fonksiyonunun amacı bir Config örneği oluşturmak olduğuna göre, parse_config'i düz bir fonksiyondan Config yapısıyla ilişkili new adlı bir fonksiyona dönüştürebiliriz. Bu değişikliği yapmak kodu daha deyimsel hale getirecektir. Standart kütüphanedeki String gibi türlerin örneklerini String::new fonksiyonunu çağırarak oluşturabiliriz. Benzer şekilde, parse_config'i Config ile ilişkili yeni bir fonksiyona dönüştürerek, Config::new'i çağırarak Config'in örneklerini 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 args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);

    // --snip--
}

// --snip--

struct Config {
    query: String,
    filename: String,
}

impl Config {
    fn new(args: &[String]) -> Config {
        let query = args[1].clone();
        let filename = args[2].clone();

        Config { query, filename }
    }
}

Liste 12-7: parse_config'i Config::new ile değiştirme

main'de parse_config çağrısı yaptığımız yeri Config::new çağrısı yapacak şekilde güncelledik. parse_config'in adını new olarak değiştirdik ve yeni fonksiyonu Config ile ilişkilendiren impl bloğunun içine taşıdık. Çalıştığından emin olmak için bu kodu tekrar derlemeyi deneyin.

Hata İşlemeyi Düzeltme

Şimdi hata işlememizi düzeltmeye çalışacağız. args vektöründeki değerlere indeks 1 veya indeks 2'den erişmeye çalışmanın, vektör üçten az öğe içeriyorsa programın paniklemesine neden olacağını hatırlayın. Programı herhangi bir argüman olmadan çalıştırmayı deneyin; şöyle görünecektir:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`
thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src/main.rs:27:21
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

index out of bounds: the len is 1 but the index is 1, programcılara yönelik detaylı bir hata mesajıdır. Son kullanıcılarımızın bunun yerine ne yapmaları gerektiğini anlamalarına yardımcı olmaz. Şimdi bunu düzeltelim.

Hata Mesajını İyileştirme

Liste 12-8'de, yeni fonksiyona 1. ve 2. dizine erişmeden önce dilimin yeterince uzun olduğunu doğrulayacak bir kontrol ekliyoruz. Dilim yeterince uzun değilse, program paniğe kapılır ve daha iyi bir hata mesajı görüntüler.

Dosya adı: src/main.rs

use std::env;
use std::fs;

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

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

struct Config {
    query: String,
    filename: String,
}

impl Config {
    // --snip--
    fn new(args: &[String]) -> Config {
        if args.len() < 3 {
            panic!("not enough arguments");
        }
        // --snip--

        let query = args[1].clone();
        let filename = args[2].clone();

        Config { query, filename }
    }
}

Liste 12-8: Bağımsız değişken sayısı için bir kontrol ekleme

Bu kod, Liste 9-13'te yazdığımız Guess::new fonksiyonuna benzer; burada value bağımsız değişkeni geçerli değerler aralığının dışında kaldığında panic! yapar. Bir değer aralığını kontrol etmek yerine, args uzunluğunun en az 3 olduğunu kontrol ediyoruz ve fonksiyonun geri kalanı bu koşulun sağlandığı varsayımı altında çalışabilir. Eğer args üçten az öğeye sahipse, bu koşul doğru olur ve programı hemen sonlandırmak için panic! makrosunu çağırırız.

Bu ekstra birkaç satırlık yeni kodla, hatanın şimdi nasıl göründüğünü görmek için programı herhangi bir argüman olmadan tekrar çalıştıralım:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`
thread 'main' panicked at 'not enough arguments', src/main.rs:26:13
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. Ancak, kullanıcılarımıza vermek istemediğimiz gereksiz bilgilere de sahibiz. Belki de Liste 9-13'te kullandığımız tekniği burada kullanmak en iyisi değildir: Bölüm 9'da tartışıldığı gibi, panic! çağrısı bir kullanım probleminden ziyade bir programlama problemi için daha uygundur. Bunun yerine, Bölüm 9'da öğrendiğiniz diğer tekniği kullanacağız— başarı ya da hatayı gösteren Result'u döndürmek.

panic! Yerine new'den Result Döndürme

Bunun yerine, başarılı durumda bir Config örneği içeren ve hata durumunda sorunu açıklayan bir Result değeri döndürebiliriz. Config::new main ile iletişim kurarken, bir sorun olduğunu belirtmek için Result türünü kullanabiliriz.

Daha sonra main'i, panic!'in neden olduğu main iş parçacığı ve RUST_BACKTRACE ile ilgili çevreleyen metin olmadan kullanıcılarımız için Err varyantını daha pratik bir hataya dönüştürmek için değiştirebiliriz.

Liste 12-9, Config::new'in dönüş değerinde ve Result döndürmek için gereken fonksiyonun gövdesinde yapmamız gereken değişiklikleri göstermektedir. Bunun main'i de güncellemeden derlenmeyeceğini unutmayın, ki bunu bir sonraki listede yapacağız.

Dosya adı: src/main.rs

use std::env;
use std::fs;

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

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

struct Config {
    query: String,
    filename: String,
}

impl Config {
    fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

Liste 12-9: Config::new'den Result Döndürme

Yeni fonksiyonumuz artık başarı durumunda bir Config örneği ve hata durumunda &'static str içeren bir Result döndürmektedir. Hata değerlerimiz her zaman 'static ömüre sahip dizgi değişmezleri olacaktır.

Yeni fonksiyonun gövdesinde iki değişiklik yaptık: kullanıcı yeterli argüman iletmediğinde panic! çağrısı yapmak yerine, artık bir Err değeri döndürüyoruz ve Config dönüş değerini bir Ok içinde tuttuk. Bu değişiklikler fonksiyonun yeni tür imzasına uygun olmasını sağlar.

Config::new'den Err değeri döndürmek, main fonksiyonun yeni fonksiyondan dönen Result değerini işlemesini ve hata durumunda süreçten daha temiz bir şekilde çıkmasını sağlar.

Config::new Çağrısı ve Hataların İşlenmesi

Hata durumunu ele almak ve kullanıcı dostu bir mesaj yazdırmak için, Liste 12-10'da gösterildiği gibi, Config::new tarafından döndürülen Result'u ele almak üzere main'i güncellememiz gerekir. Ayrıca, komut satırı aracından sıfır olmayan bir hata koduyla çıkma sorumluluğunu panic!'ten alacağız ve elle sürekleyeceğiz. Sıfır olmayan bir çıkış durumu, programımızı çağıran sürece programın bir hata durumuyla çıktığını bildirmek için kullanılan bir kuraldır.

Dosya adı: src/main.rs

use std::env;
use std::fs;
use std::process;

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

    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    // --snip--

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

struct Config {
    query: String,
    filename: String,
}

impl Config {
    fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

Liste 12-10: Yeni bir Config oluşturma başarısız olursa bir hata koduyla çıkmak

Bu listelemede, henüz ayrıntılı olarak ele almadığımız bir metod kullandık: standart kütüphane tarafından Result<T, E> üzerinde tanımlanan unwrap_or_else. unwrap_or_else'i kullanmak, panik yaratmayan bazı özel hata işleme yöntemleri tanımlamamızı sağlar. Eğer Result Ok değerinde ise, bu metodun davranışı unwrap'a benzer: Ok'un sarmaladığı iç değeri döndürür. Ancak, değer Err değeriyse, bu metod, tanımladığımız ve unwrap_or_else'ye argüman olarak aktardığımız anonim bir fonksiyon olan kapanış ifadesindeki kodu çağırır. Kapanış ifadelerini Bölüm 13'te daha ayrıntılı olarak ele alacağız. Şimdilik, unwrap_or_else'nin Err'nin iç değerini, yani bu durumda Liste 12-9'da eklediğimiz "not enough arguments" statik dizgisini, dikey aktarmalar arasında görünen err argümanı içinde kapanış ifadesini aktaracağını bilmeniz yeterlidir. Daha sonra kapanış ifadesi içindeki kod çalıştığında err değerini kullanabilir.

Standart kütüphaneden process'i kapsam içine almak için yeni bir use satırı ekledik. Hata durumunda çalıştırılacak kapanış ifadesi içindeki kod yalnızca iki satırdır: err değerini yazdırırız ve ardından process::exit'i çağırırız. process::exit fonksiyonu programı hemen durduracak ve çıkış durum kodu olarak aktarılan sayıyı döndürecektir. Bu, Liste 12-8'de kullandığımız panic!-tabanlı işleme benzer, ancak artık tüm ekstra çıktıları almıyoruz. Hadi deneyelim:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments

Harika! Bu çıktı, kullanıcılarımız için çok daha güzel.

main'den Mantığı Çıkarma

Yapılandırma ayrıştırmasını yeniden düzenlemeyi bitirdiğimize göre, şimdi programın mantığına dönelim.

“İkili Projeler için İşlerin Ayrılması” bölümünde belirttiğimiz gibi, yapılandırmayı ayarlamak veya hataları ele almakla ilgili olmayan main'de şu anda bulunan tüm mantığı tutacak run adlı bir fonksiyon çıkaracağız. İşimiz bittiğinde, main kısa ve öz olacak ve inceleme yoluyla doğrulanması kolay olacak ve diğer tüm mantık için testler yazabileceğiz.

Liste 12-11 ayıklanmış çalıştırma fonksiyonunu göstermektedir. Şimdilik sadece fonksiyonu ayıklayarak küçük ve aşamalı bir iyileştirme yapıyoruz. Fonksiyonu src/main.rs içinde tanımlıyoruz.

Dosya adı: src/main.rs

use std::env;
use std::fs;
use std::process;

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

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

    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    run(config);
}

fn run(config: Config) {
    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

// --snip--

struct Config {
    query: String,
    filename: String,
}

impl Config {
    fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

Liste 12-11: Program mantığının geri kalanını içeren run'ın çıkarılması

run fonksiyonu artık dosyanın okunmasından başlayarak main'den kalan tüm mantığı içerir. run fonksiyonu Config örneğini bir argüman olarak alır.

run Fonksiyonundan Hata Döndürme

Kalan program mantığının run fonksiyonuna ayrılmasıyla, Liste 12-9'da Config::new ile yaptığımız gibi hata işlemeyi geliştirebiliriz. Bir şeyler ters gittiğinde programın expect'i çağırarak paniklemesine izin vermek yerine, run fonksiyonu Result<T, E> döndürecektir. Bu, hataları ele alma mantığını main'de kullanıcı dostu bir şekilde daha da birleştirmemizi sağlayacaktır. Liste 12-12, run'ın 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 args: Vec<String> = env::args().collect();

    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    run(config);
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    println!("With text:\n{}", contents);

    Ok(())
}

struct Config {
    query: String,
    filename: String,
}

impl Config {
    fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

Liste 12-12: Result döndürmek için run'ı değiştirme

Burada üç önemli değişiklik yaptık. İlk olarak, run fonksiyonunun dönüş tipini Result<(), Box<dyn Error>> olarak değiştirdik. Bu fonksiyon daha önce birim tipi olan ()'i döndürüyordu ve bunu Ok durumunda döndürülen değer olarak tutuyoruz.

Hata türü için Box<dyn Error> tanım nesnesini kullandık (ve std::error::Error'ı en üstte bir use deyimiyle kapsama aldık). Tanım nesnelerini Bölüm 17'de ele alacağız. Şimdilik, Box<dyn Error>'un fonksiyonun Error tanımını sürekleyen tür döndüreceği anlamına geldiğini bilin, ancak dönüş değerinin hangi tür olacağını belirtmek zorunda değiliz. Bu bize farklı hata durumlarında farklı türlerde olabilecek hata değerleri döndürme esnekliği sağlar. dyn anahtar sözcüğü “dinamikin” kısaltmasıdır.

İkinci olarak, Bölüm 9'da bahsettiğimiz gibi ? operatörü için expect çağrısını kaldırdık. Bir hatada panic! yerine, ? işleci çağıranın işlemesi için geçerli fonksiyondan hata değerini döndürecektir.

Üçüncü olarak, run fonksiyonu artık başarı durumunda bir Ok değeri döndürmektedir. İmzada run fonksiyonunun başarı tipini () olarak bildirdik, bu da birim tür değerini Ok değerine sarmamız gerektiği anlamına geliyor. Bu Ok(()) söz dizimi ilk başta biraz garip görünebilir, ancak ()'yi bu şekilde kullanmak, run'ı yalnızca yan etkileri için çağırdığımızı belirtmenin deyimsel yoludur; ihtiyacımız olan bir değer döndürmez.

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

$ cargo run the poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
warning: unused `Result` that must be used
  --> src/main.rs:19:5
   |
19 |     run(config);
   |     ^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` on by default
   = note: this `Result` may be an `Err` variant, which should be handled

warning: `minigrep` (bin "minigrep") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.71s
     Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

Rust bize kodumuzun Result değerini göz ardı ettiğini ve Result değerinin bir hata oluştuğunu gösterebileceğini söyler. Ancak bir hata olup olmadığını kontrol etmiyoruz ve derleyici bize muhtemelen burada bazı hata işleme kodlarına sahip olmamız gerektiğini hatırlatıyor! Şimdi bu sorunu düzeltelim.

main'de Çalıştırmadan Dönen Hataları İşleme

Hataları kontrol edeceğiz ve bunları Liste 12-10'da Config::new ile kullandığımıza benzer bir teknik kullanarak ele alacağız, ancak küçük bir farkla:

Dosya adı: src/main.rs

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

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

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

    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    if let Err(e) = run(config) {
        println!("Application error: {}", e);

        process::exit(1);
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    println!("With text:\n{}", contents);

    Ok(())
}

struct Config {
    query: String,
    filename: String,
}

impl Config {
    fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

run'ın bir Err değeri döndürüp döndürmediğini kontrol etmek ve döndürürse process::exit(1)'i çağırmak için unwrap_or_else yerine if let kullanırız. run fonksiyonu, Config::new'in Config örneğini döndürdüğü gibi unwrap etmek istediğimiz bir değer döndürmez. run başarı durumunda () değerini döndürdüğü için, yalnızca bir hatayı tespit etmekle ilgileniyoruz, bu nedenle unwrap_or_else'in yalnızca () değerini döndürmesine gerek yok.

if let ve unwrap_or_else fonksiyonlarının gövdeleri her iki durumda da aynıdır: hatayı yazdırır ve çıkarız.

Kodu Kütüphane Kasasına Bölme

minigrep projemiz şu ana kadar iyi görünüyor! Şimdi src/main.rs dosyasını böleceğiz ve src/lib.rs dosyasına bazı kodlar koyacağız. Bu şekilde kodu test edebilir ve daha az sorumluluğu olan bir src/main.rs dosyasına sahip olabiliriz.

main fonksiyonu olmayan tüm kodları src/main.rs dosyasından src/lib.rs dosyasına taşıyalım:

  • run fonksiyonu tanımı
  • İlişkili use deyimleri
  • Config'in tanımı
  • Config::new fonksiyon tanımı

src/lib.rs dosyasının içeriği Liste 12-13'te gösterilen imzalara sahip olmalıdır (kısa olması için fonksiyonların gövdelerini atladık). Liste 12-14'te src/main.rs'yi değiştirene kadar bunun derlenmeyeceğini unutmayın.

Dosya adı: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        // --snip--
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    // --snip--
    let contents = fs::read_to_string(config.filename)?;

    println!("With text:\n{}", contents);

    Ok(())
}

Liste 12-13: Config ve runsrc/lib.rs'e taşıma

pub anahtar sözcüğünü bolca kullandık: Config üzerinde, Config'in alanları üzerinde, new metodu üzerinde ve run fonksiyonu üzerinde. Artık test edebileceğimiz herkese açık bir API'ye sahip bir kütüphane kasamız var!

Şimdi src/lib.rs dosyasına taşıdığımız kodu, Liste 12-14'te gösterildiği gibi src/main.rs dosyasındaki ikili kasanın kapsamına almamız gerekiyor.

Dosya adı: src/main.rs

use std::env;
use std::process;

use minigrep::Config;

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

    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    if let Err(e) = minigrep::run(config) {
        // --snip--
        println!("Application error: {}", e);

        process::exit(1);
    }
}

Liste 12-14: src/main.rs içinde minigrep kütüphanesini kullanma

Config türünü kütüphane kasasından ikili kasanın kapsamına getirmek için use minigrep::Config satırı ekliyoruz ve run fonksiyonunun önüne kasa adımızı ekliyoruz. Artık tüm fonksiyonlar birbirine bağlı olmalı ve çalışmalıdır. Programı cargo run ile çalıştırın ve her şeyin doğru çalıştığından emin olun.

Vay be! Sanki bu seni ve beni çok yormuş gibi duruyor, ancak gelecekte başarılı olmak için bunları yapmalıydık. Artık hataları ele almak çok daha kolay ve kodu daha modüler hale getirdik. Bundan sonraki neredeyse tüm işlerimiz src/lib.rs içinde yapılacak.

Eski kodla zor olan ancak yeni kodla kolay olan bir şeyi yaparak bu yeni modülerlikten yararlanalım: bazı testler yazacağız!