G/Ç Projemizi Geliştirmek
Yineleyiciler hakkındaki bu yeni bilgiyle, koddaki yerleri daha açık ve öz hale getirmek için yineleyicileri kullanarak Bölüm 12'deki
G/Ç projesini geliştirebiliriz. Şimdi yineleyicilerin Config::new
fonksiyonu ve search
fonksiyonu uygulamamızı nasıl
geliştirebileceğine bakalım.
Yineleyici Kullanarak bir clone
'u Kaldırma
Liste 12-6'da, String
değerlerinin bir dilimini alan ve dilime indeksleme yapıp değerleri klonlayarak Config
yapısının bir
örneğini oluşturan ve Config
yapısının bu değerlere sahip olmasını sağlayan kodu ekledik. Liste 13-17'de, Config::new
fonksiyonunun
uygulamasını Liste 12-23'te olduğu gibi yeniden ürettik:
Dosya adı: src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub filename: String,
pub ignore_case: bool,
}
impl Config {
pub 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();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
filename,
ignore_case,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{}", line);
}
Ok(())
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
O zaman, verimsiz clone
çağrıları konusunda endişelenmememizi çünkü gelecekte bunları kaldıracağımızı söylemiştik.
İşte o zaman şimdi!
Burada clone
'a ihtiyacımız vardı çünkü parametre args
'ta String
öğeleri olan bir dilimimiz var,
ancak yeni fonksiyon args
'a sahip değil. Bir Config
örneğinin sahipliğini döndürmek için Config
'in query
ve filename
alanlarındaki değerleri klonlamamız gerekiyordu, böylece Config
örneği kendi değerlerine sahip olabilirdi.
Yineleyiciler hakkındaki yeni bilgilerimizle, yeni fonksiyonu bir dilimi ödünç almak yerine argümanı olarak bir
yineleyicinin sahipliğini alacak şekilde değiştirebiliriz. Dilimin uzunluğunu kontrol eden ve belirli konumlara indeksleyen
kod yerine yineleyici fonksiyonu kullanacağız. Bu, Config::new
fonksiyonunun ne yaptığını netleştirecektir çünkü yineleyici
değerlere erişecektir.
Config::new
yineleyicinin sahipliğini aldığında ve ödünç alan indeksleme işlemlerini kullanmayı bıraktığında,
String
değerlerini clone
'u çağırmak ve yeni bir tahsisat yapmak yerine yineleyiciden Config
'e taşıyabiliriz.
Döndürülen Yineleyiciyi Doğrudan Kullanma
G/Ç projenizin aşağıdaki gibi görünmesi gereken src/main.rs dosyasını açın:
Dosya adı: src/main.rs
use std::env;
use std::process;
use minigrep::Config;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1);
});
// --snip--
if let Err(e) = minigrep::run(config) {
eprintln!("Application error: {}", e);
process::exit(1);
}
}
Liste 12-24'te sahip olduğumuz main
fonksiyonun başlangıcını Liste 13-18'deki kodla değiştireceğiz.
Bu, biz Config::new
'i de güncelleyene kadar derlenmeyecektir.
Dosya adı: src/main.rs
use std::env;
use std::process;
use minigrep::Config;
fn main() {
let config = Config::new(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1);
});
// --snip--
if let Err(e) = minigrep::run(config) {
eprintln!("Application error: {}", e);
process::exit(1);
}
}
env::args
fonksiyonu bir yineleyici döndürür! Yineleyici değerlerini bir vektörde toplamak ve ardından bir
dilimi Config::new
'e aktarmak yerine, şimdi env::args
'dan dönen yineleyicinin sahipliğini doğrudan Config::new
'e aktarıyoruz.
Daha sonra, Config::new
'in tanımını güncellememiz gerekiyor. G/Ç projenizin src/lib.rs dosyasında,
Config::new
'in imzasını Liste 13-19'daki gibi görünecek şekilde değiştirelim. Bu yine de derlenmeyecektir çünkü fonksiyon
gövdesini güncellememiz gerekir.
Dosya adı: src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub filename: String,
pub ignore_case: bool,
}
impl Config {
pub fn new(
mut args: impl Iterator<Item = 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();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
filename,
ignore_case,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{}", line);
}
Ok(())
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
env::args
fonksiyonunun standart kütüphane belgeleri, döndürdüğü yineleyicinin türünün std::env::Args
olduğunu ve bu türün
Iterator
özelliğini uyguladığını ve String
değerleri döndürdüğünü gösterir.
Config::new
fonksiyonunun imzasını güncelledik, böylece args
parametresi &[String]
yerine tanım sınırları olan
impl Iterator<Item = String>
ile yaygın bir türe sahip olacak. Bölüm 10'un “Parametreler Olarak Tanımlar” bölümünde
tartıştığımız impl Trait
söz diziminin bu kullanımı, args
'nin Iterator
türünü uygulayan ve String
öğeleri döndüren herhangi bir tür
olabileceği anlamına gelir.
args
'nin sahipliğini aldığımız ve üzerinde yineleme yaparak args
'yi mutasyona uğratacağımız için, mut
anahtar sözcüğünü args
parametresinin belirtimine ekleyerek onu mutasyona uğratılabilir hale getirebiliriz.
İndeksleme Yerine Iterator
Tanım Yöntemlerini Kullanma
Sonra, Config::new
'in gövdesini düzelteceğiz. args
, Iterator
tanımını uyguladığı için, bir sonraki
yöntemi çağırabileceğimizi biliyoruz! Liste 13-20, next
yöntemini kullanmak için Liste 12-23'teki kodu günceller:
Dosya adı: src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub filename: String,
pub ignore_case: bool,
}
impl Config {
pub fn new(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a query string"),
};
let filename = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file name"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
filename,
ignore_case,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{}", line);
}
Ok(())
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
env::args
'ın dönüş değerindeki ilk değerin programın adı olduğunu unutmayın.
Bunu yok saymak ve bir sonraki değere ulaşmak istiyoruz, bu yüzden önce next
'i çağırıyoruz ve geri dönüş değeriyle hiçbir şey yapmıyoruz.
İkinci olarak, Config'
in query
alanına koymak istediğimiz değeri almak için next
'i çağırıyoruz. next
, Some
döndürürse,
değeri çıkarmak için match
kullanırız. None
döndürürse, yeterli argüman verilmediği anlamına gelir ve bir Err
değeriyle erken döneriz.
Aynı şeyi filename
değeri için de yaparız.
Yineleyici Bağdaştırıcılar ile Kodu Daha Anlaşılır Hale Getirme
G/Ç projemizdeki search
fonksiyonunda da yineleyicilerden yararlanabiliriz; bu fonksiyon burada Liste 12-19'da olduğu gibi
Liste 13-21'de yeniden üretilmiştir:
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> {
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>> {
let contents = fs::read_to_string(config.filename)?;
Ok(())
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
Bu kodu yineleyici bağdaştırıcı metodlarını kullanarak daha kısa bir şekilde yazabiliriz.
Bunu yapmak aynı zamanda değiştirilebilir bir ara results
vektörüne sahip olmaktan kaçınmamızı sağlar.
Fonksiyonel programlama stili, kodu daha anlaşılır hale getirmek için değiştirilebilir durum miktarını en aza indirmeyi tercih eder.
Değişken durumu kaldırmak, results
vektörüne eşzamanlı erişimi yönetmek zorunda kalmayacağımız için gelecekte yapılacak bir
geliştirmeyle aramanın paralel olarak yapılmasını sağlayabilir. Liste 13-22 bu değişikliği göstermektedir:
Dosya adı: src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub filename: String,
pub ignore_case: bool,
}
impl Config {
pub fn new(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a query string"),
};
let filename = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file name"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
filename,
ignore_case,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{}", line);
}
Ok(())
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
search
fonksiyonunun amacının, query
'i içeren içerikteki 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 line.contains(query)
öğesinin true
döndürdüğü satırları tutmak için filter
kullanır. Daha sonra eşleşen satırları collect
ile başka bir vektörde topluyoruz. Çok daha basit!
Aynı değişikliği search_case_insensitive
fonksiyonunda yineleyici yöntemlerini kullanmak için de yapmaktan çekinmeyin.
Bir sonraki mantıksal soru, kendi kodunuzda hangi stili ve neden seçmeniz gerektiğidir: Liste 13-21'deki orijinal uygulama mı yoksa Liste 13-22'deki yineleyicileri kullanan sürüm mü? Çoğu Rust programcısı yineleyici stilini kullanmayı tercih eder. İlk başta alışmak biraz daha zordur, ancak çeşitli yineleyici uyarlayıcılarını ve ne yaptıklarını bir kez hissettiğinizde, yineleyicileri anlamak daha kolay olabilir. Döngünün çeşitli kısımlarıyla uğraşmak ve yeni vektörler oluşturmak yerine, kod döngünün üst düzey hedefine odaklanır. Bu, sıradan kodların bazılarını soyutlaştırır, böylece yineleyicideki her bir öğenin geçmesi gereken filtreleme koşulu gibi bu koda özgü kavramları görmek daha kolaydır.
Ancak iki uygulama gerçekten eş değer midir? Sezgisel varsayım, daha düşük seviyeli döngünün daha hızlı olacağı yönünde olabilir. Şimdi performans hakkında konuşalım.