I/O loyihamizni takomillashtirish

Iteratorlar haqidagi yangi bilimlar bilan biz koddagi joylarni aniqroq va ixchamroq qilish uchun iteratorlardan foydalangan holda 12-bobdagi I/O(input/output) loyihasini yaxshilashimiz mumkin. Keling, iteratorlar Config::build va qidiruv funksiyalarini amalga implement qilishni qanday yaxshilashi mumkinligini ko'rib chiqaylik.

Iterator yordamida cloneni olib tashlash

12-6 roʻyxatda biz String qiymatlari boʻlagini olgan kodni qoʻshdik va boʻlimga indekslash va qiymatlarni klonlash orqali Config strukturasining namunasini yaratdik, Config strukturasiga ushbu qiymatlarga ownershiplik(egalik) qilish imkonini berdi. 13-17 ro'yxatda biz 12-23 ro'yxatdagi kabi Config::build funksiyasining bajarilishini takrorladik:

Fayl nomi: src/lib.rs

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

pub struct Config {
    pub sorov: String,
    pub fayl_yoli: String,
    pub ignore_case: bool,
}

impl Config {
    pub fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("argumentlar yetarli emas");
        }

        let sorov = args[1].clone();
        let fayl_yoli = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            sorov,
            fayl_yoli,
            ignore_case,
        })
    }
}

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

    let natijalar = if config.ignore_case {
        harflarga_etiborsiz_qidirish(&config.sorov, &tarkib)
    } else {
        qidiruv(&config.sorov, &tarkib)
    };

    for line in natijalar {
        println!("{line}");
    }

    Ok(())
}

pub fn qidiruv<'a>(sorov: &str, tarkib: &'a str) -> Vec<&'a str> {
    let mut natijalar = Vec::new();

    for line in tarkib.lines() {
        if line.contains(sorov) {
            natijalar.push(line);
        }
    }

    natijalar
}

pub fn harflarga_etiborsiz_qidirish<'a>(
    sorov: &str,
    tarkib: &'a str,
) -> Vec<&'a str> {
    let sorov = sorov.to_lowercase();
    let mut natijalar = Vec::new();

    for line in tarkib.lines() {
        if line.to_lowercase().contains(&sorov) {
            natijalar.push(line);
        }
    }

    natijalar
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn harflarga_etiborli() {
        let sorov = "duct";
        let tarkib = "\
Rust:
xavfsiz, tez, samarali.
Uchtasini tanlang.
Duct tape.";

        assert_eq!(vec!["xavfsiz, tez, samarali."], qidiruv(sorov, tarkib));
    }

    #[test]
    fn harflarga_etiborsiz() {
        let sorov = "rUsT";
        let tarkib = "\
Rust:
xavfsiz, tez, samarali.
Uchtasini tanlang.
Menga ishoning.";

        assert_eq!(
            vec!["Rust:", "Menga ishoning."],
            harflarga_etiborsiz_qidirish(sorov, tarkib)
        );
    }
}

Ro'yxat 13-17: Config::build funksiyasining 12-23-Ro'yxatdan takrorlanishi

O'shanda biz samarasiz clone chaqiruvlari(call) haqida qayg'urmaslikni aytdik, chunki kelajakda ularni olib tashlaymiz. Xo'sh, bu vaqt hozir!

Bizga bu yerda clone kerak edi, chunki bizda args parametrida String elementlari bo‘lgan slice bor, lekin build funksiyasi argsga ega emas. Config namunasiga ownershiplikni(egalik) qaytarish uchun Configning sorov va fayl_yoli maydonlaridagi qiymatlarni klonlashimiz kerak edi, shunda Config namunasi o‘z qiymatlariga ega bo‘lishi mumkin.

Iteratorlar haqidagi yangi bilimlarimiz bilan biz build funksiyasini oʻzgartirib, bir sliceni olish oʻrniga iteratorga argument sifatida ownershiplik qilishimiz mumkin. Biz slice uzunligini tekshiradigan kod o'rniga iterator funksiyasidan foydalanamiz va ma'lum joylarga ko'rsatamiz. Bu Config::build funksiyasi nima qilayotganini aniqlaydi, chunki iterator qiymatlarga kira oladi.

Config::build iteratorga ownershiplik qilib, borrow qilingan indekslash operatsiyalaridan foydalanishni to'xtatgandan so'ng, biz clone deb chaqirish va yangi ajratish(allocation) o'rniga String qiymatlarini iteratordan Configga ko'chirishimiz mumkin.

Qaytarilgan(return) iteratordan to'g'ridan-to'g'ri foydalanish

I/O loyihangizning src/main.rs faylini oching, u quyidagicha ko'rinishi kerak:

Fayl nomi: src/main.rs

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

use minigrep::Config;

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

    let config = Config::build(&args).unwrap_or_else(|err| {
        eprintln!("Argumentlarni tahlil qilish muammosi: {err}");
        process::exit(1);
    });

    // --snip--

    if let Err(e) = minigrep::run(config) {
        eprintln!("Dastur xatosi: {e}");
        process::exit(1);
    }
}

Biz birinchi navbatda 12-24-Ro'yhatdagi main funksiyaning boshlanishini 13-18-Ro'yxatdagi kodga almashtiramiz, bu safar iteratordan foydalanadi. Biz Config::buildni ham yangilamagunimizcha, bu kompilyatsiya qilinmaydi.

Fayl nomi: src/main.rs

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

use minigrep::Config;

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Argumentlarni tahlil qilish muammosi: {err}");
        process::exit(1);
    });

    // --snip--

    if let Err(e) = minigrep::run(config) {
        eprintln!("Dastur xatosi: {e}");
        process::exit(1);
    }
}

Ro'yxat 13-18: env::args ning return(qaytish) qiymatini `Config::build`` ga o'tkazish

env::args funksiyasi iteratorni qaytaradi! Iterator qiymatlarini(value) vectorga yig'ib, keyin sliceni(bo'lak) Config::build ga o'tkazish o'rniga, endi biz env::args dan qaytarilgan(return) iteratorga ownershiplik(egalik) huquqini to'g'ridan-to'g'ri Config::build ga o'tkazmoqdamiz.

Keyinchalik, Config::build definitioni yangilashimiz kerak. I/O loyihangizning src/lib.rs faylida keling, Config::build signaturesni 13-19-raqamli roʻyxatga oʻxshatib oʻzgartiraylik. Bu hali ham kompilyatsiya qilinmaydi, chunki biz funksiya bodysini(tanasi) yangilashimiz kerak.

Fayl nomi: src/lib.rs

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

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    pub fn build(
        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 file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

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

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

Listing 13-19: Updating the signature of Config::build to expect an iterator

The standard library documentation for the env::args function shows that the type of the iterator it returns is std::env::Args, and that type implements the Iterator trait and returns String values.

We’ve updated the signature of the Config::build function so the parameter args has a generic type with the trait bounds impl Iterator<Item = String> instead of &[String]. This usage of the impl Trait syntax we discussed in the “Traits as Parameters” section of Chapter 10 means that args can be any type that implements the Iterator type and returns String items.

Because we’re taking ownership of args and we’ll be mutating args by iterating over it, we can add the mut keyword into the specification of the args parameter to make it mutable.

Using Iterator Trait Methods Instead of Indexing

Next, we’ll fix the body of Config::build. Because args implements the Iterator trait, we know we can call the next method on it! Listing 13-20 updates the code from Listing 12-23 to use the next method:

Fayl nomi: src/lib.rs

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

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    pub fn build(
        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 file_path = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a file path"),
        };

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

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

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

Listing 13-20: Changing the body of Config::build to use iterator methods

Remember that the first value in the return value of env::args is the name of the program. We want to ignore that and get to the next value, so first we call next and do nothing with the return value. Second, we call next to get the value we want to put in the query field of Config. If next returns a Some, we use a match to extract the value. If it returns None, it means not enough arguments were given and we return early with an Err value. We do the same thing for the file_path value.

Making Code Clearer with Iterator Adaptors

We can also take advantage of iterators in the search function in our I/O project, which is reproduced here in Listing 13-21 as it was in Listing 12-19:

Fayl nomi: src/lib.rs

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

pub struct Config {
    pub sorov: String,
    pub fayl_yoli: String,
}

impl Config {
    pub fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("argumentlar yetarli emas");
        }

        let sorov = args[1].clone();
        let fayl_yoli = args[2].clone();

        Ok(Config { sorov, fayl_yoli })
    }
}

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

    Ok(())
}

pub fn qidiruv<'a>(sorov: &str, tarkib: &'a str) -> Vec<&'a str> {
    let mut natijalar = Vec::new();

    for line in tarkib.lines() {
        if line.contains(sorov) {
            natijalar.push(line);
        }
    }

    natijalar
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn birinchi_natija() {
        let sorov = "marali";
        let tarkib = "\
Rust:
xavfsiz, tez, samarali.
Uchtasini tanlang.";

        assert_eq!(vec!["xavfsiz, tez, samarali."], qidiruv(sorov, tarkib));
    }
}

Listing 13-21: The implementation of the search function from Listing 12-19

We can write this code in a more concise way using iterator adaptor methods. Doing so also lets us avoid having a mutable intermediate results vector. The functional programming style prefers to minimize the amount of mutable state to make code clearer. Removing the mutable state might enable a future enhancement to make searching happen in parallel, because we wouldn’t have to manage concurrent access to the results vector. Listing 13-22 shows this change:

Fayl nomi: src/lib.rs

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

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    pub fn build(
        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 file_path = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a file path"),
        };

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

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

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

Ro'yxat 13-22: qidiruv funksiyasini impelement qilishda iterator adapter metodlaridan foydalanish

Eslatib o'tamiz, qidiruv funksiyasining maqsadi tarkib dagi sorov ni o'z ichiga olgan barcha qatorlarni qaytarishdir(return). 13-16 Roʻyxatdagi filter misoliga oʻxshab, bu kod filter adapteridan faqat line.contains(sorov) uchun true qaytaradigan satrlarni saqlash uchun foydalanadi. Keyin mos keladigan qatorlarni collect bilan boshqa vectorga yig'amiz. Juda oddiyroq! harflarga_etiborsiz_qidirish funksiyasida ham iterator metodlaridan foydalanish uchun xuddi shunday o'zgartirish kiriting.

Looplar yoki iteratorlar o'rtasida tanlash

Keyingi mantiqiy savol - o'z kodingizda qaysi uslubni tanlashingiz kerakligi va nima uchun: 13-21-Ro'yxatdagi asl dastur yoki 13-22-Ro'yxatdagi iteratorlardan foydalangan holda versiya. Aksariyat Rust dasturchilari iterator uslubidan foydalanishni afzal ko'rishadi. Avvaliga o'rganish biroz qiyinroq, lekin siz turli xil iterator adapterlari va ular nima qilishini his qilganingizdan so'ng, iteratorlarni tushunish osonroq bo'ladi. Kod aylanishning turli bitlari va yangi vectorlarni yaratish o'rniga, loop siklning yuqori darajadagi(high-level) maqsadiga e'tibor qaratadi. Bu ba'zi oddiy kodlarni abstrakt qiladi, shuning uchun ushbu kodga xos bo'lgan tushunchalarni, masalan, iteratordagi har bir element o'tishi kerak bo'lgan filtrlash shartini ko'rish osonroq bo'ladi.

Ammo ikkita dastur haqiqattan ham ekvivalentmi? Intuitiv taxmin shundan iboratki, low-leveldagi loop tezroq bo'ladi. Keling, performance haqida gapiraylik.