Modülerliği ve Hata Yönetimini (Error Handling) İyileştirmek İçin Yeniden Düzenleme (Refactoring)
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 görevi yerine getiriyor: Argümanları ayrıştırıyor (parses) ve dosyaları okuyor. Programımız büyüdükçe, main fonksiyonunun ele aldığı ayrı görevlerin sayısı artacaktır. Bir fonksiyon sorumluluk kazandıkça, hakkında akıl yürütmesi, test etmesi ve parçalarından birini bozmadan (breaking) değiştirmesi daha zor hale gelir. İşlevselliği ayırmak en iyisidir, böylece her fonksiyon tek bir görevden sorumlu olur.
Bu sorun aynı zamanda ikinci soruna da bağlıdır: sorgu ve dosya_yolu programımız için yapılandırma değişkenleri olmalarına rağmen, programın mantığını yerine getirmek için icerik gibi değişkenler de kullanılır. main ne kadar uzarsa, kapsama dahil etmemiz gereken değişkenler o kadar artar; kapsamda ne kadar çok değişkenimiz olursa, her birinin amacını takip etmek o kadar zor olur. Yapılandırma değişkenlerini amaçlarını netleştirmek için tek bir yapıda gruplamak en iyisidir.
Üçüncü sorun, dosya okunurken hata oluştuğunda expect ile bir hata mesajı yazdırmış olmamızdır, ancak hata mesajı yalnızca Dosya okunamadı yazar. Bir dosyayı okumak çeşitli şekillerde başarısız olabilir: Örneğin, dosya eksik olabilir veya dosyayı açma iznimiz olmayabilir. Şu anda duruma bakılmaksızın her şey için aynı hata mesajını yazdırıyoruz, bu da kullanıcıya herhangi bir bilgi vermez!
Dördüncüsü, bir hatayı ele almak için expect kullanıyoruz ve eğer 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 (indeks sınırların dışında) hatası alır. Eğer hata yönetimi (error-handling) kodunun tamamı tek bir yerde olsaydı, hata yönetimi mantığının değişmesi gerektiğinde gelecekteki bakımcıların koda bakmak için tek bir yeri olurdu. Tüm hata yönetimi kodunun tek bir yerde olması, son kullanıcılarımız için anlamlı mesajlar yazdırdığımızdan da emin olmamızı sağlar.
Projemizi yeniden düzenleyerek bu dört sorunu ele alalım.
İkili (Binary) Projelerde İlgi Alanlarını Ayırmak (Separating Concerns)
main fonksiyonuna birden fazla görevin sorumluluğunu atamaya dair bu organizasyonel sorun, pek çok ikili proje için ortaktır. Sonuç olarak pek çok Rust programcısı, main fonksiyonu büyümeye başladığında ikili bir programın ayrı ayrı ilgi alanlarını ayırmayı kullanışlı bulur. Bu sürecin aşağıdaki adımları vardır:
- Programınızı bir main.rs dosyasına ve bir lib.rs dosyasına bölün ve programınızın mantığını lib.rs dosyasına taşıyın.
- Komut satırı ayrıştırma mantığınız küçük olduğu sürece
mainfonksiyonunda kalabilir. - Komut satırı ayrıştırma mantığı karmaşıklaşmaya başladığında, onu
mainfonksiyonundan çıkarıp başka fonksiyonlara veya türlere taşıyın.
Bu süreçten sonra main fonksiyonunda kalan sorumluluklar 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ı ayarlamak
- lib.rs içindeki bir
calistirfonksiyonunu çağırmak calistirbir hata döndürürse hatayı ele almak
Bu desen (pattern) ilgi alanlarını ayırmak ile ilgilidir: main.rs programı çalıştırmayı, lib.rs ise eldeki görevin tüm mantığını halleder. 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, okuyarak doğruluğunu kanıtlayacak kadar küçük olacaktır. Bu süreci izleyerek programımızı yeniden işleyelim.
Argüman Ayrıştırıcısını (Argument Parser) Çıkarmak
Argümanları ayrıştırma işlevselliğini main’in çağıracağı bir fonksiyona çıkaracağız. Liste 12-5, src/main.rs dosyasında tanımlayacağımız yeni bir fonksiyon olan yapilandirma_ayristir’ı (parse_config) çağıran main fonksiyonunun yeni başlangıcını göstermektedir.
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)
}
main’den bir yapilandirma_ayristir fonksiyonu çıkarmakKomut satırı argümanlarını hala bir vektörde topluyoruz, ancak 1. indeksteki argüman değerini main fonksiyonu içinde sorgu değişkenine ve 2. indeksteki argüman değerini dosya_yolu değişkenine atamak yerine tüm vektörü yapilandirma_ayristir fonksiyonuna geçiriyoruz. yapilandirma_ayristir fonksiyonu daha sonra hangi argümanın hangi değişkene gideceğini belirleyen mantığı tutar ve değerleri main’e geri geçirir. sorgu ve dosya_yolu değişkenlerini hala main’de oluşturuyoruz, ancak main artık komut satırı argümanları ile değişkenlerin nasıl eşleştiğini belirleme sorumluluğuna sahip değildir.
Bu yeniden çalışma, küçük programımız için aşırı görünebilir, ancak küçük, artımlı adımlarla yeniden düzenleme yapıyoruz. Bu değişikliği yaptıktan sonra argüman ayrıştırmanın hâlâ çalıştığını doğrulamak için programı tekrar çalıştırın. Hata oluştuğunda nedenini belirlemeye yardımcı olması açısından ilerlemenizi sık sık kontrol etmek iyidir.
Yapılandırma (Configuration) Değerlerini Gruplamak
yapilandirma_ayristir fonksiyonunu daha da geliştirmek için küçük bir adım daha atabiliriz. Şu anda bir demet döndürüyoruz, ancak daha sonra bu demeti derhal yeniden ayrı parçalara bölüyoruz. Bu, belki de henüz doğru soyutlamaya sahip olmadığımızın bir işaretidir.
Geliştirme için yer olduğunu gösteren bir diğer gösterge de yapilandirma_ayristir’ın (parse_config) yapilandirma (config) kısmıdır; bu da döndürdüğümüz iki değerin ilişkili olduğunu ve her ikisinin de tek bir yapılandırma değerinin parçası olduğunu ima eder. Şu anda iki değeri bir demet içinde gruplamak haricinde bu anlamı verinin yapısında aktarmıyoruz; bunun yerine iki değeri tek bir struct içine koyacağız ve struct alanlarının her birine anlamlı bir ad vereceğiz. Bunu yapmak, bu kodun gelecekteki bakımcılarının farklı değerlerin birbiriyle nasıl ilişki kurduğunu ve amaçlarının ne olduğunu anlamasını kolaylaştıracaktır.
Liste 12-6, yapilandirma_ayristir fonksiyonundaki geliştirmeleri göstermektedir.
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 }
}
Yapilandirma struct’ının bir örneğini döndürmek için yapilandirma_ayristir fonksiyonunu yeniden düzenlemeksorgu ve dosya_yolu adında alanlara (fields) sahip olacak şekilde tanımlanmış Yapilandirma adında bir struct ekledik. yapilandirma_ayristir fonksiyonunun imzası artık bir Yapilandirma değeri döndürdüğünü gösteriyor. yapilandirma_ayristir’ın gövdesinde eskiden argumanlar’daki String değerlerine referans veren string dilimleri döndürdüğümüz yerde, artık Yapilandirma’yı sahiplenilmiş String değerleri içerecek şekilde tanımlıyoruz. main’deki argumanlar değişkeni argüman değerlerinin sahibidir ve yapilandirma_ayristir fonksiyonunun onları sadece ödünç almasına izin verir; yani Yapilandirma, argumanlar’daki değerlerin sahipliğini almaya kalksaydı Rust’ın ödünç alma kurallarını ihlal etmiş olurduk.
String verilerini yönetebileceğimizin pek çok yolu vardır; en kolayı (biraz verimsiz olsa da) değerler üzerinde clone (klonla) metodunu çağırmaktır. Bu, Yapilandirma örneğinin sahip olması için verinin tam bir kopyasını çıkaracaktır, ki bu da string verisine bir referans depolamaktan daha fazla zaman ve bellek gerektirir. Ancak verileri klonlamak, referansların ömürlerini yönetmek zorunda kalmayacağımız için kodumuzu çok daha basit hale getirir; bu durumda basitlik elde etmek için biraz performanstan vazgeçmek değerli bir takastır.
clone Kullanımının Takasları (Trade-Offs)
Birçok Rustacean arasında, çalışma zamanı maliyeti nedeniyle sahiplik sorunlarını düzeltmek için clone kullanmaktan kaçınma eğilimi vardır. Bölüm 13’te, bu tür durumlarda daha verimli metotların nasıl kullanılacağını öğreneceksiniz. Ancak şimdilik ilerlemeye devam etmek için birkaç string’i kopyalamak uygundur çünkü bu kopyaları yalnızca bir kez yapacaksınız ve dosya yolunuz ile sorgu string’iniz çok küçüktür. İlk denemenizde kodunuzu aşırı optimize etmeye çalışmaktansa biraz verimsiz de olsa çalışan bir programa sahip olmak daha iyidir. Rust’ta daha deneyimli hale geldikçe, en verimli çözümle başlamak daha kolay olacaktır ancak şimdilik clone çağırmak kesinlikle kabul edilebilirdir.
yapilandirma_ayristir tarafından döndürülen Yapilandirma örneğini yapilandirma adında bir değişkene yerleştirmesi için main fonksiyonunu güncelledik ve daha önce ayrı sorgu ve dosya_yolu değişkenlerini kullanan kodu, artık bunun yerine Yapilandirma struct’ı üzerindeki alanları kullanacak şekilde güncelledik.
Artık kodumuz, sorgu ve dosya_yolu’nun birbiriyle ilişkili olduğunu ve amaçlarının programın çalışma şeklini yapılandırmak olduğunu daha net bir şekilde yansıtıyor. Bu değerleri kullanan herhangi bir kod, onları yapilandirma örneğinde, amaçlarına göre isimlendirilmiş alanlarda (fields) bulacağını bilir.
Yapilandirma İçin Bir Yapıcı (Constructor) Oluşturmak
Şimdiye kadar, komut satırı argümanlarını ayrıştırmaktan sorumlu mantığı main’den çıkarıp yapilandirma_ayristir fonksiyonuna yerleştirdik. Bunu yapmak sorgu ve dosya_yolu değerlerinin ilişkili olduğunu ve bu ilişkinin kodumuzda aktarılması gerektiğini görmemize yardımcı oldu. Ardından sorgu ve dosya_yolu’nun ilgili amacını isimlendirmek ve değerlerin isimlerini yapilandirma_ayristir fonksiyonundan struct alanı isimleri olarak döndürebilmek için bir Yapilandirma struct’ı ekledik.
Yani artık yapilandirma_ayristir fonksiyonunun amacı bir Yapilandirma örneği oluşturmak olduğuna göre, yapilandirma_ayristir’ı sıradan bir fonksiyondan Yapilandirma struct’ı ile ilişkili new adında bir fonksiyona değiştirebiliriz. Bu değişikliği yapmak kodu daha idiyomatik hale getirecektir. String gibi standart kütüphanedeki türlerin örneklerini String::new fonksiyonunu çağırarak oluşturabiliriz. Benzer şekilde, yapilandirma_ayristir’ı Yapilandirma ile ilişkili bir new fonksiyonuna dönüştürerek Yapilandirma::new fonksiyonunu çağırıp Yapilandirma örnekleri yaratabileceğiz. Liste 12-7 yapmamız gereken değişiklikleri gösteriyor.
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 }
}
}
yapilandirma_ayristir’ı Yapilandirma::new olarak değiştirmekmain fonksiyonunda daha önce yapilandirma_ayristir çağırdığımız yeri, onun yerine Yapilandirma::new çağıracak şekilde güncelledik. yapilandirma_ayristir adını new (yeni) olarak değiştirdik ve bunu, new fonksiyonunu 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 (Error Handling) Düzeltmek
Şimdi hata yönetimimizi düzeltmeye çalışacağız. Vektör üçten daha az öge barındırıyorsa argumanlar vektöründeki değerlere 1. indeks veya 2. indeksten erişme denemesinin programın paniklemesine neden olacağını hatırlayın. Programı herhangi bir argüman olmadan çalıştırmayı deneyin; ş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' panicked at src/main.rs:27:21:
index out of bounds: the len is 1 but the index is 1
note: calistir with `RUST_BACKTRACE=1` environment variable to display a backtrace
index out of bounds: the len is 1 but the index is 1 (indeks sınırların dışında: uzunluk 1 ancak indeks 1) satırı, programcılara yönelik bir hata mesajıdır. Bu son kullanıcılarımızın bunun yerine ne yapmaları gerektiğini anlamalarına yardımcı olmayacaktır. Haydi bunu şimdi düzeltelim.
Hata Mesajını İyileştirmek
Liste 12-8’de, new fonksiyonuna 1. indeks ve 2. indekse erişmeden önce dilimin (slice) yeterince uzun olduğunu doğrulayacak bir kontrol ekliyoruz. Eğer dilim yeterince uzun değilse program panikler ve daha iyi bir hata mesajı görüntüler.
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 }
}
}
Bu kod Liste 9-13’te yazdığımız Tahmin::new fonksiyonuna benzerdir, burada deger argümanı geçerli değerler aralığı dışındaysa panic! (panik) çağırıyorduk. Burada değerlerin bir aralığını kontrol etmek yerine argumanlar’ın uzunluğunun en az 3 olduğunu kontrol ediyoruz ve fonksiyonun geri kalanı bu koşulun karşılandığı varsayımı altında çalışabiliyor. Eğer argumanlar üçten az ögeye sahipse, bu koşul true olacak ve programı anında sonlandırmak için panic! makrosunu çağıracağız.
new içindeki bu ekstra birkaç satır kodla birlikte 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` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:26:13:
yeterli argüman yok
note: calistir 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 yabancı bilgilerimiz de var. Belki de Liste 9-13’te kullandığımız teknik burada kullanmak için en iyisi değildir: panic! çağrısı, Bölüm 9’da da tartışıldığı üzere, 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—başarıyı veya hatayı belirten bir Result döndürmeyi kullanacağız.
panic! Çağırmak Yerine Bir Result Döndürmek
Bunun yerine, başarılı durumda bir Yapilandirma örneğini barındıran ve hata durumunda problemi açıklayan bir Result (sonuç) değeri döndürebiliriz. Aynı zamanda fonksiyon adını new’den olustur’a (build) değiştireceğiz, çünkü pek çok programcı new fonksiyonlarının asla başarısız olmamasını bekler. Yapilandirma::olustur main’e bilgi ilettiğinde, bir sorun olduğunu sinyallemek için Result türünü kullanabiliriz. Daha sonra bir panic! çağrısının sebep olduğu thread 'main' ve RUST_BACKTRACE ile ilgili kısımlar olmadan main’i, Err varyantını (seçeneğini) kullanıcılarımız için daha pratik bir hataya dönüştürecek şekilde değiştirebiliriz.
Liste 12-9, şimdi Yapilandirma::olustur olarak adlandırdığımız fonksiyonun dönüş değerinde yapmamız gereken değişiklikleri ve fonksiyon gövdesinde bir Result döndürmek için gerekenleri göstermektedir. Bu kodun, bir sonraki listede (listing) yapacağımız gibi main’i de güncelleyene kadar derlenmeyeceğini unutmayın.
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 })
}
}
Yapilandirma::olustur’dan bir Result döndürmekBizim olustur (build) fonksiyonumuz, başarılı (success) durumda bir Yapilandirma örneğiyle ve hatalı durumda bir string sabitiyle bir Result döndürür. Hata değerlerimiz her zaman 'static ömre (lifetime) sahip string sabitleri olacaktır.
Fonksiyonun gövdesinde iki değişiklik yaptık: Artık kullanıcı yeterince argüman aktarmadığında panic! çağırmak yerine bir Err (hata) değeri döndürüyoruz ve Yapilandirma dönüş değerini bir Ok (tamam) içine sarmalıyoruz. Bu değişiklikler fonksiyonun yeni tür imzasına uymasını sağlar.
Yapilandirma::olustur’dan bir Err değeri döndürmek, main fonksiyonunun olustur (build) fonksiyonundan döndürülen Result değerini yönetmesine ve hata durumunda işlemden daha temiz bir şekilde çıkmasına olanak tanır.
Yapilandirma::olustur’u Çağırmak ve Hataları Ele Almak (Handling Errors)
Hata durumunu ele almak ve kullanıcı dostu bir mesaj yazdırmak için Liste 12-10’da gösterildiği gibi Yapilandirma::olustur tarafından döndürülen Result’ı yönetecek şekilde main’i güncellememiz gerekir. Ayrıca, komut satırı aracından sıfır olmayan bir hata koduyla çıkma sorumluluğunu panic!’ten alacak ve bunun yerine elimizle uygulayacağız. Sıfır olmayan bir çıkış durumu, programımızı çağıran prosese (işleme), programın bir hata durumuyla çıktığını bildiren bir kuraldır.
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 })
}
}
Yapilandirma oluşturmak başarısız olursa bir hata kodu ile çıkış (exiting) yapmakBu listede henüz detaylı olarak işlemediğimiz bir metodu kullandık: unwrap_or_else, standart kütüphane tarafından Result<T, E> üzerinde tanımlanmıştır. unwrap_or_else kullanmak panic! tabanlı (non-panic!) olmayan bazı özel hata yönetimlerini tanımlamamızı sağlar. Result bir Ok değeri ise, bu metodun davranışı unwrap’e (paketi açmaya) benzer: Ok’un sarmaladığı iç değeri döndürür. Ancak değer bir Err değeri ise bu metot, tanımladığımız ve unwrap_or_else’e argüman olarak geçirdiğimiz anonim bir fonksiyon olan kapanıştaki kodu çağırır. Kapanışları Bölüm 13’te daha detaylı olarak işleyeceğiz. Şimdilik sadece unwrap_or_else’in dikey çubuklar arasında görünen hata (err) argümanındaki kapanışımıza Liste 12-9’da eklediğimiz statik "yeterli argüman yok" string’i olan Err’nin iç değerini ileteceğini bilmeniz gerekir. Daha sonra kapanışın içindeki kod çalıştığında hata değerini kullanabilir.
Standart kütüphaneden process’i (işlem) kapsama almak için yeni bir use satırı ekledik. Hata durumunda çalıştırılacak kapanışın içindeki kod sadece iki satırdan oluşuyor: hata değerini yazdırıyoruz ve ardından process::exit çağırıyoruz. process::exit fonksiyonu programı anında durduracak ve çıkış durum kodu olarak iletilen (passed) numarayı döndürecektir. Bu Liste 12-8’de kullandığımız panic! temelli yönetime benziyor, ancak artık fazladan çıktının hiçbirini almıyoruz. 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
Mükemmel! Bu çıktı kullanıcılarımız için çok daha dostane.
Mantığı (Logic) main’den Çıkarmak
Artık yapılandırma ayrıştırmasını yeniden düzenlemeyi bitirdiğimize göre, programın mantığına dönelim. “İkili Projelerde İlgi Alanlarını Ayırmak (Separating Concerns)” kısmında belirttiğimiz gibi, yapılandırmayı ayarlamak veya hataları (errors) ele almak ile ilgili olmayan, şu anda main fonksiyonunda bulunan tüm mantığı tutacak calistir adında bir fonksiyon çıkaracağız. İşimizi bitirdiğimizde main fonksiyonu kısa olacak ve incelemeyle doğrulanması kolaylaşacak, ve tüm diğer mantık için testler yazabileceğiz.
Liste 12-11, calistir fonksiyonunu ayırmanın ufak ve artımlı bir gelişimini gösteriyor.
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 })
}
}
calistir fonksiyonunu ayırmakArtık calistir fonksiyonu dosyayı okuma kısmından başlayarak main’in geri kalan mantığının tümünü kapsıyor. calistir fonksiyonu parametre (argument) olarak Yapilandirma örneğini alır.
calistir Fonksiyonundan Hata (Error) Döndürmek
Artık kalan program mantığı calistir fonksiyonuna ayrıldığına göre Liste 12-9’da Yapilandirma::olustur fonksiyonuyla yaptığımız gibi hata yönetimini iyileştirebiliriz. expect fonksiyonunu çağırıp programın panik yapmasına müsaade etmek yerine bir şeyler ters gittiğinde calistir fonksiyonu bir Result<T, E> değeri döndürecek. Bu, hataları yönetmeyle ilgili mantığı kullanıcı dostu bir yolla main’de pekiştirmemize izin verecek. Liste 12-12’de calistir’ın gövdesinde ve imzasında yapmamız gereken değişiklikler yer almaktadır.
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 })
}
}
Result değeri döndürebilmek için calistir fonksiyonunu değiştirmekBurada üç ö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, ve onu Ok (Tamam) durumu için dönen değer olarak tutuyoruz.
Hata türü için, trait (özellik) objesi olan Box<dyn Error>’ı kullandık (ve üstte bir use satırıyla std::error::Error’ı çalışma alanımıza dâhil ettik). Bölüm 18’de trait objelerini detaylı işleyeceğiz. Şimdilik yalnızca Box<dyn Error>’ın fonksiyonun Error (Hata) trait’ini uygulayan bir tür döndüreceği anlamına geldiğini bilmeniz yeterlidir, fakat dönen değerin tam olarak hangi tür olacağını belirlemek zorunda değiliz. Bu da bize farklı hata durumları için farklı türde hatalar döndürebilme esnekliğini kazandırır. dyn anahtar kelimesi dinamik kelimesinin kısaltmasıdır.
İkincisi, Bölüm 9’da bahsettiğimiz gibi ? (soru işareti) operatörü lehine expect çağrısını kaldırdık. Hata durumunda panic! çağırmak yerine, ? mevcut fonksiyondan gelen hatayı fonksiyonu çağıran kısma ele alması için döndürecek.
Üçüncüsü, calistir fonksiyonu artık başarılı durumlarda bir Ok (Tamam) değeri döndürüyor. İmzada calistir fonksiyonunun başarılı dönüş türünün () olacağını belirttik, bu nedenle birim türü değerini Ok değeri ile sarmalamamız gerekir. Başlangıçta bu Ok(()) (Tamam) sözdizimi kulağa biraz garip gelebilir. Fakat () ifadesini bu şekilde kullanmak, calistir fonksiyonunu yalnızca fonksiyonun yapacağı yan etkileri için çağırdığımızın; dolayısıyla da geri döndürdüğü değere ihtiyaç duymadığımızın idiyomatik karşılığıdır.
Bu kodu çalıştırdığınızda derlenecektir fakat ekranda bir uyarıyla karşılaşacaksınız:
$ cargo run -- mezar siir.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
warning: unused `Result` that must be used
--> src/main.rs:23:5
|
23 | 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
|
23 | 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 bize yazdığımız kodun bir Result değerini yok saydığını, oysaki bu değerin potansiyel bir hataya işaret edebileceğini söylüyor. Ortada bir hata olup olmadığını kontrol etmiyoruz ve derleyici bize büyük olasılıkla burada bir hata yönetimi (error-handling) yazmayı kastettiğimizi hatırlatıyor! Haydi şimdi o sorunu da halledelim.
main Fonksiyonunda, calistir Fonksiyonundan Dönen Hataların Yönetimi (Handling Errors)
Hataları Liste 12-10’da Yapilandirma::olustur fonksiyonunda kullandığımız tekniğe benzer ufak farklılıkları bulunan bir teknikle kontrol edip ele alacağı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 })
}
}
Eğer calistir fonksiyonu bir Err (hata) değeri döndürdüyse bunu tespit edip, programı process::exit(1) komutuyla durdurmak için unwrap_or_else yerine if let komutunu kullandık. calistir fonksiyonu bizim için Yapilandirma::olustur fonksiyonu gibi unwrap ile paketten çıkartıp alabileceğimiz (return) bir değer sunmuyor. Başarılı senaryolarda calistir yalnızca () döndürdüğü için sadece hatayı tespit etmekle ilgileniyoruz; bu sebeple kullanıldığında sadece () döndürecek bir metoda yani unwrap_or_else’e ihtiyacımız yok.
if let ile unwrap_or_else fonksiyonlarının gövdeleri her iki örnekte de tamamen aynıdır: Hatayı yazdırıyoruz ve programdan çıkıyoruz.
Kodu Bir Kütüphane (Library) Crate’ine Ayırmak
Şu ana kadar minigrep projemiz hiç fena durmuyor! Artık src/main.rs dosyasını bölecek ve kodun ufak bir kısmını src/lib.rs dosyasına aktaracağız. Bu sayede kodlarımızı çok daha kolayca test edebilir (test) ve çok daha az sorumluluğa sahip olan bir src/main.rs dosyası yaratabiliriz.
Hadi src/main.rs yerine metni aramaktan sorumlu kodu src/lib.rs’de tanımlayalım, bu bizim (veya minigrep kütüphanemizi kullanan herhangi birinin) arama fonksiyonunu kendi minigrep ikili dosyamızdan daha farklı bağlamlardan çağırmamıza olanak tanır.
İlk olarak, src/lib.rs içerisindeki ara fonksiyonu imzasını Liste 12-13’te gösterildiği üzere bir unimplemented! makrosu ile tanımlayalım. Bir sonraki adımda içini kodlarla doldurduğumuzda imza hakkında daha ayrıntılı açıklama yapıyor olacağız.
pub fn ara<'a>(sorgu: &str, icerik: &'a str) -> Vec<&'a str> {
unimplemented!();
}
ara fonksiyonunu tanımlamakKütüphane crate’imizin açık bir API’sinin bir parçası olduğunu atamak için fonksiyon tanımlamasında pub anahtar sözcüğünü kullandık. Artık ikili crate’imizden de kolaylıkla kullanılabilecek, test edilebilir bir kütüphane crate’imiz var!
Artık Liste 12-14’te gösterildiği gibi src/lib.rs’de tanımlanan kodu src/main.rs içerisindeki ikili crate’in kapsamına sokmamız ve çağırmamız gerekiyor.
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(())
}
minigrep kütüphanesinden ara fonksiyonunu çağırmakKütüphane crate’inin içerisindeki ara fonksiyonunu ikili crate’imizin kapsamına alabilmek için kodumuza use minigrep::ara satırını ekledik. Daha sonra, calistir fonksiyonunun içerisinden metni okuyup yazdırmak yerine, ara fonksiyonunu çağırıp içerisine argüman olarak yapilandirma.sorgu (config.query) değeri ile icerik değerini iletiyoruz. Böylelikle calistir, bir for döngüsü vasıtasıyla ara fonksiyonundan dönen ve aranan metne uygun her bir satırı kolaylıkla yazdırabilecektir. Bu aynı zamanda arama sonucunun ekrana başarıyla yansıtıldığı takdirde sadece aranan kelimeye dair bulguları yazdırabilmesi için main fonksiyonunun içerisinde çalıştırdığımız sorgu ile dosya_yolunu yazdıran println! kodlarını da kaldırmak için en uygun zamandır.
Arama fonksiyonunun, herhangi bir yazdırma gerçekleşmeden önce tüm sonuçları döndürdüğü bir vektöre toplayacağını unutmayın. Bu tür bir yaklaşımda, büyük dosyalar üzerinde arama gerçekleştirilirken, bulunan her sonuç bulunduğu an basılamayacağı için çıktı oldukça yavaş verilebilir. Bunu çözmek adına kullanılabilecek olası çözüm olan yineleyicileri Bölüm 13’te işliyor olacağız.
Vay canına! Epey iş başardık, ama gelecekte kodlarımızın başarısı için kendi ayarlamalarımızı şimdiden yapmış olduk. Artık olası hata durumlarını yönetmek (handle errors) ve koda modüler bir görünüm kazandırmak daha kolay. İşimizin neredeyse tamamı bundan sonra src/lib.rs üzerinden gerçekleşecek.
Eski kodlarla yapılamayıp yeni edindiğimiz modüler özelliklerle kazandığımız yeni avantajların tadını, biraz test yazarak kutlayalım!