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

Cải thiện Dự án I/O của chúng ta

Với kiến thức mới về iterators, chúng ta có thể cải thiện dự án I/O trong Chương 12 bằng cách sử dụng iterators để làm cho những phần trong mã nguồn trở nên rõ ràng và ngắn gọn hơn. Hãy xem cách iterators có thể cải thiện cách triển khai hàm Config::build và hàm search của chúng ta.

Loại bỏ clone bằng cách sử dụng Iterator

Trong Listing 12-6, chúng ta đã thêm mã lấy một slice của các giá trị String và tạo một thể hiện của struct Config bằng cách lập chỉ mục vào slice và sao chép các giá trị, cho phép struct Config sở hữu những giá trị đó. Trong Listing 13-17, chúng ta đã tái hiện lại cách triển khai hàm Config::build như trong Listing 12-23.

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

Vào thời điểm đó, chúng ta đã nói đừng lo lắng về việc gọi clone không hiệu quả vì chúng ta sẽ loại bỏ chúng trong tương lai. Vâng, thời điểm đó là bây giờ!

Chúng ta cần clone ở đây vì chúng ta có một slice với các phần tử String trong tham số args, nhưng hàm build không sở hữu args. Để trả về quyền sở hữu của một thể hiện Config, chúng ta đã phải sao chép giá trị từ trường queryfile_path của Config để thể hiện Config có thể sở hữu các giá trị của nó.

Với kiến thức mới của chúng ta về iterators, chúng ta có thể thay đổi hàm build để nhận quyền sở hữu một iterator làm đối số thay vì mượn một slice. Chúng ta sẽ sử dụng chức năng của iterator thay vì mã kiểm tra độ dài của slice và lập chỉ mục vào các vị trí cụ thể. Điều này sẽ làm rõ những gì hàm Config::build đang làm vì iterator sẽ truy cập các giá trị.

Một khi Config::build nắm quyền sở hữu iterator và ngừng sử dụng các thao tác lập chỉ mục đang mượn, chúng ta có thể di chuyển các giá trị String từ iterator vào Config thay vì gọi clone và tạo một phân bổ mới.

Sử dụng Iterator trả về trực tiếp

Mở file src/main.rs của dự án I/O, nó sẽ trông như thế này:

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

Đầu tiên chúng ta sẽ thay đổi phần đầu của hàm main mà chúng ta đã có trong Listing 12-24 thành mã trong Listing 13-18, lần này sử dụng một iterator. Điều này sẽ không biên dịch cho đến khi chúng ta cũng cập nhật Config::build.

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

Hàm env::args trả về một iterator! Thay vì thu thập các giá trị iterator vào một vector và sau đó truyền một slice cho Config::build, bây giờ chúng ta đang truyền quyền sở hữu của iterator được trả về từ env::args trực tiếp cho Config::build.

Tiếp theo, chúng ta cần cập nhật định nghĩa của Config::build. Hãy thay đổi chữ ký của Config::build để trông giống như Listing 13-19. Điều này vẫn chưa biên dịch được, vì chúng ta cần cập nhật phần thân hàm.

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

Tài liệu thư viện tiêu chuẩn cho hàm env::args cho thấy rằng kiểu của iterator nó trả về là std::env::Args, và kiểu đó triển khai trait Iterator và trả về các giá trị String.

Chúng ta đã cập nhật chữ ký của hàm Config::build để tham số args có một kiểu tổng quát với ràng buộc trait impl Iterator<Item = String> thay vì &[String]. Cách sử dụng cú pháp impl Trait mà chúng ta đã thảo luận trong phần "Traits as Parameters" của Chương 10 có nghĩa là args có thể là bất kỳ kiểu nào triển khai trait Iterator và trả về các mục String.

Bởi vì chúng ta đang lấy quyền sở hữu của args và chúng ta sẽ thay đổi args bằng cách lặp qua nó, chúng ta có thể thêm từ khóa mut vào chỉ định của tham số args để làm cho nó có thể thay đổi.

Sử dụng các phương thức Trait Iterator thay vì lập chỉ mục

Tiếp theo, chúng ta sẽ sửa phần thân của Config::build. Bởi vì args triển khai trait Iterator, chúng ta biết rằng chúng ta có thể gọi phương thức next trên nó! Listing 13-20 cập nhật mã từ Listing 12-23 để sử dụng phương thức next.

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

Hãy nhớ rằng giá trị đầu tiên trong giá trị trả về của env::args là tên của chương trình. Chúng ta muốn bỏ qua nó và chuyển đến giá trị tiếp theo, vì vậy đầu tiên chúng ta gọi next và không làm gì với giá trị trả về. Tiếp theo chúng ta gọi next để lấy giá trị mà chúng ta muốn đặt vào trường query của Config. Nếu next trả về Some, chúng ta sử dụng match để trích xuất giá trị. Nếu nó trả về None, điều đó có nghĩa là không đủ đối số được đưa ra và chúng ta trả về sớm với một giá trị Err. Chúng ta cũng làm tương tự cho giá trị file_path.

Làm cho mã rõ ràng hơn với bộ điều hợp Iterator

Chúng ta cũng có thể tận dụng iterators trong hàm search trong dự án I/O của chúng ta, được tái hiện ở đây trong Listing 13-21 như trong Listing 12-19.

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

Chúng ta có thể viết mã này một cách ngắn gọn hơn bằng cách sử dụng phương thức bộ điều hợp iterator. Làm như vậy cũng cho phép chúng ta tránh phải có một vector results trung gian có thể thay đổi. Phong cách lập trình hàm ưu tiên việc giảm thiểu lượng trạng thái có thể thay đổi để làm cho mã rõ ràng hơn. Loại bỏ trạng thái có thể thay đổi có thể cho phép một cải tiến trong tương lai để làm cho việc tìm kiếm diễn ra song song vì chúng ta sẽ không phải quản lý truy cập đồng thời vào vector results. Listing 13-22 cho thấy sự thay đổi này.

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

Nhớ lại rằng mục đích của hàm search là trả về tất cả các dòng trong contents có chứa query. Tương tự như ví dụ filter trong Listing 13-16, mã này sử dụng bộ điều hợp filter để chỉ giữ lại các dòng mà line.contains(query) trả về true. Sau đó, chúng ta thu thập các dòng phù hợp vào một vector khác với collect. Đơn giản hơn nhiều! Hãy tự nhiên thực hiện thay đổi tương tự để sử dụng phương thức iterator trong hàm search_case_insensitive.

Để cải tiến thêm, hãy trả về một iterator từ hàm search bằng cách loại bỏ lệnh gọi collect và thay đổi kiểu trả về thành impl Iterator<Item = &'a str> để hàm trở thành một bộ điều hợp iterator. Lưu ý rằng bạn cũng sẽ cần cập nhật các test! Tìm kiếm qua một file lớn bằng công cụ minigrep của bạn trước và sau khi thực hiện thay đổi này để quan sát sự khác biệt trong hành vi. Trước thay đổi này, chương trình sẽ không in bất kỳ kết quả nào cho đến khi nó đã thu thập tất cả kết quả, nhưng sau thay đổi, kết quả sẽ được in khi mỗi dòng phù hợp được tìm thấy vì vòng lặp for trong hàm run có thể tận dụng tính lười biếng của iterator.

Lựa chọn giữa Vòng lặp và Iterators

Câu hỏi hợp lý tiếp theo là phong cách nào bạn nên chọn trong mã của riêng bạn và tại sao: triển khai ban đầu trong Listing 13-21 hoặc phiên bản sử dụng iterators trong Listing 13-22 (giả sử chúng ta đang thu thập tất cả kết quả trước khi trả về chúng thay vì trả về iterator). Hầu hết các lập trình viên Rust đều thích sử dụng phong cách iterator. Nó hơi khó nắm bắt lúc đầu, nhưng một khi bạn đã cảm nhận được các bộ điều hợp iterator khác nhau và những gì chúng làm, iterators có thể dễ hiểu hơn. Thay vì phải vật lộn với các chi tiết khác nhau của vòng lặp và xây dựng vector mới, mã tập trung vào mục tiêu cấp cao của vòng lặp. Điều này trừu tượng hóa một số mã thông thường để dễ dàng hơn khi thấy các khái niệm độc đáo cho mã này, chẳng hạn như điều kiện lọc mỗi phần tử trong iterator phải vượt qua.

Nhưng hai cách triển khai có thực sự tương đương không? Giả định trực giác có thể là vòng lặp cấp thấp hơn sẽ nhanh hơn. Hãy nói về hiệu suất.