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)
        );
    }
}

Liste 13-17: Liste 12-23'ten Config::new işlevinin çoğaltılması

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);
    }
}

Liste 13-18: env::args dönüş değerinin Config::new öğesine iletilmesi

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)
        );
    }
}

Liste 13-19: Bir yineleyici beklemek için Config::new imzasını güncelleme

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)
        );
    }
}

Liste 13-20: Yineleyici yöntemlerini kullanmak için Config::new gövdesini değiştirme

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));
    }
}

Liste 13-21: Liste 12-19'dan search fonksiyonunun yazılması

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)
        );
    }
}

Liste 13-22: "arama" işlevinin uygulanmasında yineleyici bağdaştırıcı yöntemlerini kullanma

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.