G/Ç (I/O) Projemizi İyileştirme
İteratörler hakkındaki bu yeni bilgilerimizle, kodun çeşitli bölümlerini daha net ve özlü hâle getirmek üzere iteratörleri kullanarak Bölüm 12’deki G/Ç projesini iyileştirebiliriz. İteratörlerin Yapilandirma::olustur ve ara fonksiyonlarının uygulamalarını nasıl iyileştirebileceğine bakalım.
Bir İteratör Kullanarak clone Çağrısını Kaldırma
Liste 12-6’da, String değerlerinden oluşan bir dilim alıp dilime indeksle erişerek ve Yapilandirma struct’ının kendi değerlerine sahip olabilmesi için bu değerleri klonlayarak bir Yapilandirma örneği oluşturan kodu eklemiştik. Liste 13-17’de, Yapilandirma::olustur fonksiyonunun Liste 12-23’teki uygulamasını yeniden gösteriyoruz.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{ara, buyuk_kucuk_harf_duyarsiz_ara};
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);
});
if let Err(e) = calistir(yapilandirma) {
println!("Uygulama hatası: {e}");
process::exit(1);
}
}
pub struct Yapilandirma {
pub sorgu: String,
pub dosya_yolu: String,
pub buyuk_kucuk_harf_yoksay: bool,
}
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();
let buyuk_kucuk_harf_yoksay = env::var("IGNORE_CASE").is_ok();
Ok(Yapilandirma {
sorgu,
dosya_yolu,
buyuk_kucuk_harf_yoksay,
})
}
}
fn calistir(yapilandirma: Yapilandirma) -> Result<(), Box<dyn Error>> {
let icerik = fs::read_to_string(yapilandirma.dosya_yolu)?;
let sonuclar = if yapilandirma.buyuk_kucuk_harf_yoksay {
buyuk_kucuk_harf_duyarsiz_ara(&yapilandirma.sorgu, &icerik)
} else {
ara(&yapilandirma.sorgu, &icerik)
};
for satir in sonuclar {
println!("{satir}");
}
Ok(())
}
Yapilandirma::olustur fonksiyonunun yeniden gösterimiO sırada, verimsiz clone çağrıları için endişelenmememiz gerektiğini, çünkü bunları ileride kaldıracağımızı söylemiştik. İşte o an geldi!
Burada clone çağrısına ihtiyaç duymamızın nedeni, argumanlar parametresinde String öğelerinden oluşan bir dilim bulunması, ancak olustur fonksiyonunun argumanlar değerinin sahibi olmamasıydı. Bir Yapilandirma örneğinin sahipliğini döndürebilmek ve bu örneğin kendi verilerine sahip olmasını sağlamak için Yapilandirma’nın sorgu ve dosya_yolu alanlarına atanacak değerleri klonlamak zorunda kalmıştık.
İteratörler hakkındaki yeni bilgilerimizle, olustur fonksiyonunu bir dilimi ödünç almak yerine argüman olarak bir iteratörün sahipliğini alacak şekilde değiştirebiliriz. Dilimin uzunluğunu denetleyen ve belirli konumlara indeksle erişen kod yerine iteratör işlevselliğini kullanacağız. Değerlere erişim işini iteratör üstleneceği için bu değişiklik Yapilandirma::olustur fonksiyonunun ne yaptığını çok daha net hâle getirecektir.
Yapilandirma::olustur iteratörün sahipliğini aldığında ve ödünç alma yoluyla çalışan indeksleme işlemlerini bıraktığında, clone çağırıp öbekte yeni bir bellek tahsisi yapmak yerine String değerlerini doğrudan iteratörden Yapilandirma içine taşıyabiliriz.
Döndürülen İteratörü Doğrudan Kullanma
G/Ç projenizin src/main.rs dosyasını açın; dosya şu şekilde görünmelidir:
Dosya adı: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{ara, buyuk_kucuk_harf_duyarsiz_ara};
fn main() {
let argumanlar: Vec<String> = env::args().collect();
let yapilandirma =
Yapilandirma::olustur(&argumanlar).unwrap_or_else(|hata| {
eprintln!("Argümanları ayrıştırırken problem oluştu: {hata}");
process::exit(1);
});
// --snip--
if let Err(e) = calistir(yapilandirma) {
eprintln!("Uygulama hatası: {e}");
process::exit(1);
}
}
pub struct Yapilandirma {
pub sorgu: String,
pub dosya_yolu: String,
pub buyuk_kucuk_harf_yoksay: bool,
}
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();
let buyuk_kucuk_harf_yoksay = env::var("IGNORE_CASE").is_ok();
Ok(Yapilandirma {
sorgu,
dosya_yolu,
buyuk_kucuk_harf_yoksay,
})
}
}
fn calistir(yapilandirma: Yapilandirma) -> Result<(), Box<dyn Error>> {
let icerik = fs::read_to_string(yapilandirma.dosya_yolu)?;
let sonuclar = if yapilandirma.buyuk_kucuk_harf_yoksay {
buyuk_kucuk_harf_duyarsiz_ara(&yapilandirma.sorgu, &icerik)
} else {
ara(&yapilandirma.sorgu, &icerik)
};
for satir in sonuclar {
println!("{satir}");
}
Ok(())
}
Öncelikle Liste 12-24’teki main fonksiyonunun başlangıcını, bu kez iteratör kullanan Liste 13-18’deki kodla değiştireceğiz. Yapilandirma::olustur fonksiyonunu da güncelleyene kadar bu kod derlenmeyecektir.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{ara, buyuk_kucuk_harf_duyarsiz_ara};
fn main() {
let yapilandirma =
Yapilandirma::olustur(env::args()).unwrap_or_else(|hata| {
eprintln!("Argümanları ayrıştırırken problem oluştu: {hata}");
process::exit(1);
});
// --snip--
if let Err(e) = calistir(yapilandirma) {
eprintln!("Uygulama hatası: {e}");
process::exit(1);
}
}
pub struct Yapilandirma {
pub sorgu: String,
pub dosya_yolu: String,
pub buyuk_kucuk_harf_yoksay: bool,
}
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();
let buyuk_kucuk_harf_yoksay = env::var("IGNORE_CASE").is_ok();
Ok(Yapilandirma {
sorgu,
dosya_yolu,
buyuk_kucuk_harf_yoksay,
})
}
}
fn calistir(yapilandirma: Yapilandirma) -> Result<(), Box<dyn Error>> {
let icerik = fs::read_to_string(yapilandirma.dosya_yolu)?;
let sonuclar = if yapilandirma.buyuk_kucuk_harf_yoksay {
buyuk_kucuk_harf_duyarsiz_ara(&yapilandirma.sorgu, &icerik)
} else {
ara(&yapilandirma.sorgu, &icerik)
};
for satir in sonuclar {
println!("{satir}");
}
Ok(())
}
env::args dönüş değerini Yapilandirma::olustur fonksiyonuna aktarmaenv::args fonksiyonu zaten bir iteratör döndürür! İteratör değerlerini bir vektörde toplayıp ardından Yapilandirma::olustur fonksiyonuna bir dilim geçirmek yerine, artık env::args tarafından döndürülen iteratörün sahipliğini doğrudan Yapilandirma::olustur fonksiyonuna aktarıyoruz.
Sırada Yapilandirma::olustur tanımını güncellemek var. Yapilandirma::olustur fonksiyonunun imzasını Liste 13-19’daki gibi değiştirelim. Fonksiyon gövdesini henüz güncellemediğimiz için bu kod şimdilik derlenmeyecektir.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{ara, buyuk_kucuk_harf_duyarsiz_ara};
fn main() {
let yapilandirma =
Yapilandirma::olustur(env::args()).unwrap_or_else(|hata| {
eprintln!("Argümanları ayrıştırırken problem oluştu: {hata}");
process::exit(1);
});
if let Err(e) = calistir(yapilandirma) {
eprintln!("Uygulama hatası: {e}");
process::exit(1);
}
}
pub struct Yapilandirma {
pub sorgu: String,
pub dosya_yolu: String,
pub buyuk_kucuk_harf_yoksay: bool,
}
impl Yapilandirma {
fn olustur(
mut argumanlar: impl Iterator<Item = String>,
) -> Result<Yapilandirma, &'static str> {
// --snip--
if argumanlar.len() < 3 {
return Err("yeterli argüman yok");
}
let sorgu = argumanlar[1].clone();
let dosya_yolu = argumanlar[2].clone();
let buyuk_kucuk_harf_yoksay = env::var("IGNORE_CASE").is_ok();
Ok(Yapilandirma {
sorgu,
dosya_yolu,
buyuk_kucuk_harf_yoksay,
})
}
}
fn calistir(yapilandirma: Yapilandirma) -> Result<(), Box<dyn Error>> {
let icerik = fs::read_to_string(yapilandirma.dosya_yolu)?;
let sonuclar = if yapilandirma.buyuk_kucuk_harf_yoksay {
buyuk_kucuk_harf_duyarsiz_ara(&yapilandirma.sorgu, &icerik)
} else {
ara(&yapilandirma.sorgu, &icerik)
};
for satir in sonuclar {
println!("{satir}");
}
Ok(())
}
Yapilandirma::olustur imzasını bir iteratör bekleyecek şekilde güncellemeenv::args fonksiyonunun standart kütüphane belgeleri, döndürdüğü iteratörün türünün std::env::Args olduğunu ve bu türün String değerleri üretecek şekilde Iterator trait’ini uyguladığını gösterir.
Yapilandirma::olustur fonksiyonunun imzasını, argumanlar parametresi &[String] yerine impl Iterator<Item = String> trait sınırına sahip jenerik bir tür olacak şekilde güncelledik. Bölüm 10’un “Trait’leri Parametre Olarak Kullanma” kısmında ele aldığımız impl Trait sözdiziminin bu kullanımı, argumanlar parametresinin Iterator trait’ini uygulayan ve String öğeleri döndüren herhangi bir tür olabileceği anlamına gelir.
argumanlar değerinin sahipliğini aldığımız ve üzerinde ilerleyerek iç durumunu değiştireceğimiz için, argumanlar parametresinin tanımına mut anahtar kelimesini ekleyerek onu değiştirilebilir hâle getirebiliriz.
Iterator Trait Metotlarını Kullanma
Şimdi Yapilandirma::olustur fonksiyonunun gövdesini düzelteceğiz. argumanlar parametresi Iterator trait’ini uyguladığı için üzerinde next metodunu çağırabileceğimizi biliyoruz! Liste 13-20, Liste 12-23’teki kodu next metodunu kullanacak şekilde günceller.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{ara, buyuk_kucuk_harf_duyarsiz_ara};
fn main() {
let yapilandirma =
Yapilandirma::olustur(env::args()).unwrap_or_else(|hata| {
eprintln!("Argümanları ayrıştırırken problem oluştu: {hata}");
process::exit(1);
});
if let Err(e) = calistir(yapilandirma) {
eprintln!("Uygulama hatası: {e}");
process::exit(1);
}
}
pub struct Yapilandirma {
pub sorgu: String,
pub dosya_yolu: String,
pub buyuk_kucuk_harf_yoksay: bool,
}
impl Yapilandirma {
fn olustur(
mut argumanlar: impl Iterator<Item = String>,
) -> Result<Yapilandirma, &'static str> {
argumanlar.next();
let sorgu = match argumanlar.next() {
Some(arg) => arg,
None => return Err("Sorgu metni alınamadı"),
};
let dosya_yolu = match argumanlar.next() {
Some(arg) => arg,
None => return Err("Dosya yolu alınamadı"),
};
let buyuk_kucuk_harf_yoksay = env::var("IGNORE_CASE").is_ok();
Ok(Yapilandirma {
sorgu,
dosya_yolu,
buyuk_kucuk_harf_yoksay,
})
}
}
fn calistir(yapilandirma: Yapilandirma) -> Result<(), Box<dyn Error>> {
let icerik = fs::read_to_string(yapilandirma.dosya_yolu)?;
let sonuclar = if yapilandirma.buyuk_kucuk_harf_yoksay {
buyuk_kucuk_harf_duyarsiz_ara(&yapilandirma.sorgu, &icerik)
} else {
ara(&yapilandirma.sorgu, &icerik)
};
for satir in sonuclar {
println!("{satir}");
}
Ok(())
}
Yapilandirma::olustur gövdesini değiştirmeenv::args dönüş değerindeki ilk öğenin programın adı olduğunu hatırlayın. Bu değeri yok sayıp bir sonrakine geçmek istediğimiz için önce next metodunu çağırıyor ve dönüş değeriyle hiçbir şey yapmıyoruz. Ardından Yapilandirma’nın sorgu alanına yerleştirmek istediğimiz değeri almak için next metodunu tekrar çağırıyoruz. next çağrısı Some döndürürse değeri çıkarmak için match ifadesini kullanıyoruz; None döndürürse yeterli argüman verilmemiş demektir ve bir Err değeriyle fonksiyondan erken dönüyoruz. Aynı işlemi dosya_yolu değeri için de uyguluyoruz.
İteratör Adaptörleriyle Kodu Netleştirme
G/Ç projemizdeki ara fonksiyonunda da iteratörlerden yararlanabiliriz. Fonksiyonun Liste 12-19’daki hâli, Liste 13-21’de yeniden gösterilmiştir.
pub fn ara<'a>(sorgu: &str, icerik: &'a str) -> Vec<&'a str> {
let mut sonuclar = Vec::new();
for satir in icerik.lines() {
if satir.contains(sorgu) {
sonuclar.push(satir);
}
}
sonuclar
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tek_sonuc() {
let sorgu = "güven";
let icerik = "\
Güven:
güvenli, hızlı, üretken.
Üçünü de seç.";
assert_eq!(vec!["güvenli, hızlı, üretken."], ara(sorgu, icerik));
}
}
ara fonksiyonunun uygulamasıİteratör adaptörü metotlarını kullanarak bu kodu çok daha özlü bir şekilde yazabiliriz. Üstelik bu yaklaşım, değiştirilebilir bir ara sonuclar vektörü tanımlama ihtiyacını da ortadan kaldırır. Fonksiyonel programlama stili, kodu daha anlaşılır kılmak adına değiştirilebilir durum (mutable state) miktarını en aza indirmeyi tercih eder. Değiştirilebilir durumu ortadan kaldırmak, sonuclar vektörüne eşzamanlı erişimi yönetmek zorunda kalmayacağımız için ileride arama işlemini paralel hâle getirmemizi de kolaylaştırabilir. Liste 13-22 bu değişikliği göstermektedir.
pub fn ara<'a>(sorgu: &str, icerik: &'a str) -> Vec<&'a str> {
icerik
.lines()
.filter(|satir| satir.contains(sorgu))
.collect()
}
pub fn buyuk_kucuk_harf_duyarsiz_ara<'a>(
sorgu: &str,
icerik: &'a str,
) -> Vec<&'a str> {
let sorgu = sorgu.to_lowercase();
let mut sonuclar = Vec::new();
for satir in icerik.lines() {
if satir.to_lowercase().contains(&sorgu) {
sonuclar.push(satir);
}
}
sonuclar
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn buyuk_kucuk_harf_duyarli() {
let sorgu = "güven";
let icerik = "\
Güven:
güvenli, hızlı, üretken.
Üçünü de seç.
Güven kolay kazanılmaz.";
assert_eq!(vec!["güvenli, hızlı, üretken."], ara(sorgu, icerik));
}
#[test]
fn buyuk_kucuk_harf_duyarsiz() {
let sorgu = "gÜvEn";
let icerik = "\
Güven:
önce sağlamlık gelir.
Planlı çalış, sakin ol.
güven kazanılır.";
assert_eq!(
vec!["Güven:", "güven kazanılır."],
buyuk_kucuk_harf_duyarsiz_ara(sorgu, icerik)
);
}
}
ara fonksiyonunun uygulamasında iteratör adaptörü metotlarını kullanmaara fonksiyonunun amacının icerik içindeki sorgu metnini barındıran tüm satırları döndürmek olduğunu hatırlayın. Liste 13-16’daki filter örneğine benzer şekilde bu kod, yalnızca satir.contains(sorgu) ifadesinin true döndürdüğü satırları tutmak için filter adaptörünü kullanır. Ardından eşleşen satırları collect metoduyla yeni bir vektörde toplarız. Çok daha sade! Aynı değişikliği buyuk_kucuk_harf_duyarsiz_ara fonksiyonunda da iteratör metotlarını kullanacak şekilde yapabilirsiniz.
Daha ileri bir iyileştirme olarak, collect çağrısını kaldırıp dönüş türünü impl Iterator<Item = &'a str> olarak değiştirerek ara fonksiyonundan doğrudan bir iteratör döndürebilir ve böylece fonksiyonun kendisini bir iteratör adaptörüne dönüştürebilirsiniz. Bu durumda testleri de güncellemeniz gerekeceğini unutmayın! Davranış farkını gözlemlemek için bu değişikliği yapmadan önce ve yaptıktan sonra minigrep aracınızla büyük bir dosyada arama yapmayı deneyin. Değişiklikten önce program tüm eşleşmeleri bir vektörde toplayana dek ekrana hiçbir sonuç yazdırmazken, değişiklikten sonra calistir fonksiyonundaki for döngüsü iteratörün tembelliğinden (laziness) yararlanabildiği için eşleşen her satır bulunur bulunmaz anında yazdırılacaktır.
Not: Bu bölümde yaptığımız iki yeniden düzenleme, Rust’ta sahiplik sistemi ile fonksiyonel iteratör zincirlerinin nasıl uyum içinde çalıştığını somut biçimde gösterir:
- Öbek Tahsisinden (
clone) Kaçınma: ÖncekiYapilandirma::olustur(&[String])imzasında argüman dilimi yalnızca ödünç alındığı için her.clone()çağrısı öbekte (heap) yeni bellek tahsis edip karakter verisini bayt bayt kopyalıyordu.impl Iterator<Item = String>ile iteratörün sahipliğini doğrudan tükettiğimizde iseargumanlar.next()çağrısı mevcutStringdeğerinin sahipliğini (yalnızca yığındaki işaretçi, uzunluk ve kapasite bilgisini) ek öbek tahsisi yapmadanYapilandirmaalanlarına taşır. Üstelik parametre somutstd::env::Argstürü yerineimpl Iterator<Item = String>kabul ettiği için, birim testlerinde gerçek komut satırı argümanlarına ihtiyaç duymadan doğrudan bellek içi bir iteratör geçirerekYapilandirma::olusturfonksiyonunu kolayca test edebilirsiniz. - Değiştirilebilir Ara Durumu Kaldırma ve Paralelleştirme:
arafonksiyonunda döngü içinde adım adım büyütülenlet mut sonuclarvektörü yerineicerik.lines().filter(...).collect()yazmak, değiştirilebilir ara durumu (mutable intermediate state) tamamen ortadan kaldırır. İleride çok büyük dosyalarda çok çekirdekli arama yapmak istediğinizde, paylaşılan birsonuclarvektörünü kilitlerle (Mutex) senkronize etmekle uğraşmak yerine — örneğin ekosistemde yaygın kullanılanrayoncrate’indekipar_lines()adaptörüne geçerek — neredeyse aynı iteratör zinciriyle aramayı güvenli biçimde paralelleştirebilirsiniz.
Döngüler ve İteratörler Arasında Seçim Yapma
Sıradaki mantıklı soru, kendi kodunuzda hangi stili ve neden seçmeniz gerektiğidir: Liste 13-21’deki orijinal uygulama mı, yoksa Liste 13-22’deki iteratör kullanan sürüm mü (doğrudan iteratör döndürmek yerine tüm sonuçları toplayıp döndürdüğümüzü varsayarsak)? Çoğu Rust programcısı iteratör stilini kullanmayı tercih eder. İlk başta alışması biraz daha zor olsa da çeşitli iteratör adaptörlerine ve ne işe yaradıklarına aşina olduğunuzda iteratörleri okumak çok daha kolaylaşır. Döngü ayrıntılarıyla ve yeni vektörler oluşturmakla uğraşmak yerine kod, döngünün üst düzey amacına odaklanır. Bu yaklaşım sıradan, tekrar eden kodu soyutlayarak, iteratördeki her öğenin sağlaması gereken filtreleme koşulu gibi bu koda özgü asıl mantığın çok daha net görülmesini sağlar.
Peki bu iki uygulama gerçekten eşdeğer midir? Sezgisel olarak daha alt düzey olan döngünün daha hızlı çalışacağı düşünülebilir. Şimdi performans konusunu ele alalım.