Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ปรับปรุงโปรเจกต์ I/O ของเรา

ด้วยความรู้ใหม่นี้เกี่ยวกับ iterator เราปรับปรุงโปรเจกต์ I/O ในบทที่ 12 ได้โดยใช้ iterator เพื่อทำให้โค้ดชัดเจนและกระชับมากขึ้น มาดู ว่า iterator ปรับปรุง implementation ของเราของฟังก์ชัน Config::build และฟังก์ชัน search ได้อย่างไร

ลบ clone โดยใช้ Iterator

ใน Listing 12-6 เราเพิ่มโค้ดที่รับ slice ของค่า String และสร้าง instance ของ struct Config โดย index เข้า slice และ clone ค่า ทำให้ struct Config own ค่าเหล่านั้นได้ ใน Listing 13-17 เราได้ reproduce implementation ของฟังก์ชัน Config::build ในแบบที่มันเป็น ใน Listing 12-23

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

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

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

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

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        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,
        })
    }
}

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(())
}
Listing 13-17: Reproduce ของฟังก์ชัน Config::build จาก Listing 12-23

ในเวลานั้น เราบอกไม่ให้กังวลเกี่ยวกับการเรียก clone ที่ไม่มี ประสิทธิภาพเพราะเราจะลบพวกมันในอนาคต ดี เวลานั้นคือตอนนี้!

เราต้องการ clone ที่นี่เพราะเรามี slice ที่มี element String ใน parameter args แต่ฟังก์ชัน build ไม่ own args เพื่อ return ownership ของ instance Config เราต้อง clone ค่าจาก field query และ file_path ของ Config เพื่อให้ instance Config own ค่าของ มันได้

ด้วยความรู้ใหม่ของเราเกี่ยวกับ iterator เราเปลี่ยนฟังก์ชัน build ให้รับ ownership ของ iterator เป็นอาร์กิวเมนต์แทน borrow slice ได้ เราจะใช้ functionality iterator แทนโค้ดที่ตรวจสอบความยาวของ slice และ index เข้าที่เฉพาะ นี่จะทำให้ชัดเจนว่าฟังก์ชัน Config::build กำลังทำอะไรเพราะ iterator จะเข้าถึงค่า

เมื่อ Config::build รับ ownership ของ iterator และหยุดใช้ operation indexing ที่ borrow เราย้ายค่า String จาก iterator เข้า Config แทนการเรียก clone และทำ allocation ใหม่ได้

ใช้ Iterator ที่ Return โดยตรง

เปิดไฟล์ src/main.rs ของโปรเจกต์ I/O ของคุณ ซึ่งควรดูแบบนี้:

Filename: src/main.rs

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

use minigrep::{search, search_case_insensitive};

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

    let config = Config::build(&args).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    // --snip--

    if let Err(e) = run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

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

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        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,
        })
    }
}

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

เราจะเปลี่ยนจุดเริ่มต้นของฟังก์ชัน main ที่เรามีใน Listing 12-24 ก่อน ให้เป็นโค้ดใน Listing 13-18 ซึ่งคราวนี้ใช้ iterator นี่จะไม่ คอมไพล์จนกว่าเราจะอัพเดท Config::build ด้วย

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    // --snip--

    if let Err(e) = run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

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

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        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,
        })
    }
}

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(())
}
Listing 13-18: ส่งค่า return ของ env::args ให้ Config::build

ฟังก์ชัน env::args return iterator! ไม่ใช่ collect ค่า iterator เข้า vector แล้วส่ง slice ให้ Config::build ตอนนี้เรากำลังส่ง ownership ของ iterator ที่ return จาก env::args ให้ Config::build โดยตรง

ถัดไป เราต้องอัพเดทนิยามของ Config::build มาเปลี่ยน signature ของ Config::build ให้ดูเหมือน Listing 13-19 นี่ยังจะไม่คอมไพล์ เพราะ เราต้องอัพเดท body ของฟังก์ชัน

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

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

impl Config {
    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,
        })
    }
}

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(())
}
Listing 13-19: อัพเดท signature ของ Config::build ให้คาดหวัง iterator

documentation ของ standard library สำหรับฟังก์ชัน env::args แสดง ว่า type ของ iterator ที่มัน return คือ std::env::Args และ type นั้น implement trait Iterator และ return ค่า String

เราได้อัพเดท signature ของฟังก์ชัน Config::build เพื่อให้ parameter args มี generic type ที่มี trait bound impl Iterator<Item = String> แทน &[String] การใช้ syntax impl Trait ที่เราพูดถึงในส่วน “ใช้ Trait เป็น Parameter” ของบทที่ 10 แปลว่า args เป็น type ใดก็ได้ที่ implement trait Iterator และ return item String

เพราะเรากำลังรับ ownership ของ args และเราจะ mutate args โดย iterate ผ่านมัน เราเพิ่ม keyword mut เข้า specification ของ parameter args เพื่อทำให้มัน mutable ได้

ใช้เมธอด Trait Iterator

ถัดไป เราจะแก้ body ของ Config::build เพราะ args implement trait Iterator เรารู้ว่าเราเรียกเมธอด next บนมันได้! Listing 13-20 อัพเดทโค้ดจาก Listing 12-23 เพื่อใช้เมธอด next

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

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

impl Config {
    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,
        })
    }
}

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(())
}
Listing 13-20: เปลี่ยน body ของ Config::build ให้ใช้เมธอด iterator

จำได้ว่าค่าแรกในค่า return ของ env::args คือชื่อของโปรแกรม เรา ต้องการ ignore นั้นและไปยังค่าถัดไป ดังนั้นก่อนอื่นเราเรียก next และไม่ทำอะไรกับค่า return จากนั้น เราเรียก next เพื่อรับค่าที่เรา ต้องการใส่ใน field query ของ Config ถ้า next return Some เราใช้ match เพื่อดึงค่า ถ้ามัน return None หมายความว่า อาร์กิวเมนต์ไม่พอที่ถูกให้ และเรา return เร็วด้วยค่า Err เราทำสิ่ง เดียวกันสำหรับค่า file_path

ทำให้โค้ดชัดเจนด้วย Iterator Adapter

เรายังใช้ประโยชน์ของ iterator ในฟังก์ชัน search ในโปรเจกต์ I/O ของเราได้ ซึ่ง reproduce ที่นี่ใน Listing 13-21 ในแบบที่มันเป็นใน Listing 12-19

Filename: src/lib.rs
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));
    }
}
Listing 13-21: implementation ของฟังก์ชัน search จาก Listing 12-19

เราเขียนโค้ดนี้ในแบบที่กระชับมากขึ้นโดยใช้เมธอด iterator adapter ได้ การทำเช่นนั้นยังให้เราหลีกเลี่ยงการมี vector results ระหว่างกลาง ที่ mutable สไตล์ functional programming ชอบลดจำนวน mutable state เพื่อทำให้โค้ดชัดเจน การลบ mutable state อาจช่วยให้การปรับปรุงใน อนาคตทำให้การค้นหาเกิดขึ้นแบบขนานได้ เพราะเราไม่ต้องจัดการการเข้าถึง แบบ concurrent ของ vector results Listing 13-22 แสดงการเปลี่ยนนี้

Filename: src/lib.rs
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)
        );
    }
}
Listing 13-22: ใช้เมธอด iterator adapter ใน implementation ของฟังก์ชัน search

จำได้ว่าจุดประสงค์ของฟังก์ชัน search คือ return บรรทัดทั้งหมดใน contents ที่มี query คล้ายกับตัวอย่าง filter ใน Listing 13-16 โค้ดนี้ใช้ adapter filter เพื่อเก็บเฉพาะบรรทัดที่ line.contains(query) return true เราจากนั้น collect บรรทัดที่ ตรงกันเข้า vector อื่นด้วย collect ง่ายขึ้นมาก! รู้สึกอิสระที่จะ ทำการเปลี่ยนแปลงเดียวกันเพื่อใช้เมธอด iterator ในฟังก์ชัน search_case_insensitive ด้วย

สำหรับการปรับปรุงเพิ่มเติม return iterator จากฟังก์ชัน search โดย ลบการเรียก collect และเปลี่ยน return type เป็น impl Iterator<Item = &'a str> เพื่อให้ฟังก์ชันกลายเป็น iterator adapter สังเกตว่าคุณจะต้องอัพเดทเทสด้วย! ค้นหาผ่านไฟล์ขนาดใหญ่โดยใช้ เครื่องมือ minigrep ของคุณก่อนและหลังการเปลี่ยนแปลงนี้ เพื่อสังเกต ความแตกต่างในพฤติกรรม ก่อนการเปลี่ยนแปลงนี้ โปรแกรมจะไม่ print ผลใด จนกว่ามันจะ collect ผลทั้งหมด แต่หลังการเปลี่ยนแปลง ผลจะถูก print เมื่อแต่ละบรรทัดที่ตรงกันถูกพบ เพราะ loop for ในฟังก์ชัน run สามารถใช้ประโยชน์จากความเป็น lazy ของ iterator

เลือกระหว่าง Loop และ Iterator

คำถามที่มีตรรกะถัดไปคือสไตล์ไหนคุณควรเลือกในโค้ดของคุณเองและทำไม — implementation เดิมใน Listing 13-21 หรือเวอร์ชันที่ใช้ iterator ใน Listing 13-22 (สมมติเรากำลัง collect ผลทั้งหมดก่อน return พวกมัน ไม่ใช่ return iterator) programmer Rust ส่วนใหญ่ชอบใช้สไตล์ iterator มันยากขึ้นหน่อยที่จะเข้าใจในตอนแรก แต่เมื่อคุณคุ้นกับ iterator adapter ต่าง ๆ และสิ่งที่พวกมันทำ iterator เข้าใจง่ายขึ้นได้ แทนที่ จะ fiddle กับชิ้นส่วนต่าง ๆ ของการ loop และสร้าง vector ใหม่ โค้ด โฟกัสที่วัตถุประสงค์ระดับสูงของ loop นี่ทำให้โค้ดที่พบทั่วไปเป็นนามธรรม เพื่อให้ง่ายขึ้นในการเห็นแนวคิดที่เป็นเอกลักษณ์ของโค้ดนี้ เช่น เงื่อนไข filter ที่แต่ละ element ใน iterator ต้องผ่าน

แต่ implementation สองแบบเทียบเท่ากันจริงไหม? สมมุติฐานโดยสัญชาตญาณ อาจเป็นว่า loop ระดับต่ำกว่าจะเร็วกว่า มาพูดถึง performance