改进 I/O 项目

ch13-03-improving-our-io-project.md

commit cc958ca579816ea6ac7e9067d628b0423a1ed3e4

有了这些关于迭代器的新知识,我们可以使用迭代器来改进第十二章中 I/O 项目的实现来使得代码更简洁明了。让我们看看迭代器如何能够改进 Config::new 函数和 search 函数的实现。

使用迭代器并去掉 clone

在示例 12-6 中,我们增加了一些代码获取一个 String slice 并创建一个 Config 结构体的实例,他们索引 slice 中的值并克隆这些值以便 Config 结构体可以拥有这些值。在示例 13-24 中重现了第十二章结尾示例 12-23 中 Config::new 函数的实现:

文件名: src/lib.rs

1. use std::env;
2. use std::error::Error;
3. use std::fs;

5. pub struct Config {
6. pub query: String,
7. pub filename: String,
8. pub case_sensitive: bool,
9. }

11. impl Config {
12. pub fn new(args: &[String]) -> Result<Config, &'static str> {
13. if args.len() < 3 {
14. return Err("not enough arguments");
15. }

17. let query = args[1].clone();
18. let filename = args[2].clone();

20. let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

22. Ok(Config {
23. query,
24. filename,
25. case_sensitive,
26. })
27. }
28. }

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

33. let results = if config.case_sensitive {
34. search(&config.query, &contents)
35. } else {
36. search_case_insensitive(&config.query, &contents)
37. };

39. for line in results {
40. println!("{}", line);
41. }

43. Ok(())
44. }

46. pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
47. let mut results = Vec::new();

49. for line in contents.lines() {
50. if line.contains(query) {
51. results.push(line);
52. }
53. }

55. results
56. }

58. pub fn search_case_insensitive<'a>(
59. query: &str,
60. contents: &'a str,
61. ) -> Vec<&'a str> {
62. let query = query.to_lowercase();
63. let mut results = Vec::new();

65. for line in contents.lines() {
66. if line.to_lowercase().contains(&query) {
67. results.push(line);
68. }
69. }

71. results
72. }

74. #[cfg(test)]
75. mod tests {
76. use super::*;

78. #[test]
79. fn case_sensitive() {
80. let query = "duct";
81. let contents = "\
82. Rust:
83. safe, fast, productive.
84. Pick three.
85. Duct tape.";

87. assert_eq!(vec!["safe, fast, productive."], search(query, contents));
88. }

90. #[test]
91. fn case_insensitive() {
92. let query = "rUsT";
93. let contents = "\
94. Rust:
95. safe, fast, productive.
96. Pick three.
97. Trust me.";

99. assert_eq!(
100. vec!["Rust:", "Trust me."],
101. search_case_insensitive(query, contents)
102. );
103. }
104. }

示例 13-24:重现第十二章结尾的 Config::new 函数

那时我们说过不必担心低效的 clone 调用了,因为将来可以对他们进行改进。好吧,就是现在!

起初这里需要 clone 的原因是参数 args 中有一个 String 元素的 slice,而 new 函数并不拥有 args。为了能够返回 Config 实例的所有权,我们需要克隆 Config 中字段 query 和 filename 的值,这样 Config 实例就能拥有这些值。

在学习了迭代器之后,我们可以将 new 函数改为获取一个有所有权的迭代器作为参数而不是借用 slice。我们将使用迭代器功能之前检查 slice 长度和索引特定位置的代码。这会明确 Config::new 的工作因为迭代器会负责访问这些值。

一旦 Config::new 获取了迭代器的所有权并不再使用借用的索引操作,就可以将迭代器中的 String 值移动到 Config 中,而不是调用 clone 分配新的空间。

直接使用 env::args 返回的迭代器

打开 I/O 项目的 src/main.rs 文件,它看起来应该像这样:

文件名: src/main.rs

1. use std::env;
2. use std::process;

4. use minigrep::Config;

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

9. let config = Config::new(&args).unwrap_or_else(|err| {
10. eprintln!("Problem parsing arguments: {}", err);
11. process::exit(1);
12. });

14. // --snip--

16. if let Err(e) = minigrep::run(config) {
17. eprintln!("Application error: {}", e);

19. process::exit(1);
20. }
21. }

修改第十二章结尾示例 12-24 中的 main 函数的开头为示例 13-25 中的代码。在更新 Config::new 之前这些代码还不能编译:

文件名: src/main.rs

1. use std::env;
2. use std::process;

4. use minigrep::Config;

6. fn main() {
7. let config = Config::new(env::args()).unwrap_or_else(|err| {
8. eprintln!("Problem parsing arguments: {}", err);
9. process::exit(1);
10. });

12. // --snip--

14. if let Err(e) = minigrep::run(config) {
15. eprintln!("Application error: {}", e);

17. process::exit(1);
18. }
19. }

示例 13-25:将 env::args 的返回值传递给 Config::new

env::args 函数返回一个迭代器!不同于将迭代器的值收集到一个 vector 中接着传递一个 slice 给 Config::new,现在我们直接将 env::args 返回的迭代器的所有权传递给 Config::new。

接下来需要更新 Config::new 的定义。在 I/O 项目的 src/lib.rs 中,将 Config::new 的签名改为如示例 13-26 所示。这仍然不能编译因为我们还需更新函数体:

文件名: src/lib.rs

1. use std::env;
2. use std::error::Error;
3. use std::fs;

5. pub struct Config {
6. pub query: String,
7. pub filename: String,
8. pub case_sensitive: bool,
9. }

11. impl Config {
12. pub fn new(mut args: env::Args) -> Result<Config, &'static str> {
13. // --snip--
14. if args.len() < 3 {
15. return Err("not enough arguments");
16. }

18. let query = args[1].clone();
19. let filename = args[2].clone();

21. let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

23. Ok(Config {
24. query,
25. filename,
26. case_sensitive,
27. })
28. }
29. }

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

34. let results = if config.case_sensitive {
35. search(&config.query, &contents)
36. } else {
37. search_case_insensitive(&config.query, &contents)
38. };

40. for line in results {
41. println!("{}", line);
42. }

44. Ok(())
45. }

47. pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
48. let mut results = Vec::new();

50. for line in contents.lines() {
51. if line.contains(query) {
52. results.push(line);
53. }
54. }

56. results
57. }

59. pub fn search_case_insensitive<'a>(
60. query: &str,
61. contents: &'a str,
62. ) -> Vec<&'a str> {
63. let query = query.to_lowercase();
64. let mut results = Vec::new();

66. for line in contents.lines() {
67. if line.to_lowercase().contains(&query) {
68. results.push(line);
69. }
70. }

72. results
73. }

75. #[cfg(test)]
76. mod tests {
77. use super::*;

79. #[test]
80. fn case_sensitive() {
81. let query = "duct";
82. let contents = "\
83. Rust:
84. safe, fast, productive.
85. Pick three.
86. Duct tape.";

88. assert_eq!(vec!["safe, fast, productive."], search(query, contents));
89. }

91. #[test]
92. fn case_insensitive() {
93. let query = "rUsT";
94. let contents = "\
95. Rust:
96. safe, fast, productive.
97. Pick three.
98. Trust me.";

100. assert_eq!(
101. vec!["Rust:", "Trust me."],
102. search_case_insensitive(query, contents)
103. );
104. }
105. }

示例 13-26:以迭代器作为参数更新 Config::new 的签名

env::args 函数的标准库文档显示,它返回的迭代器的类型为 std::env::Args。我们已经更新了 Config :: new 函数的签名,因此参数 args 的类型为 std::env::Args 而不是 &[String]。因为我们拥有 args 的所有权,并且将通过对其进行迭代来改变 args ,所以我们可以将 mut 关键字添加到 args 参数的规范中以使其可变。

使用 Iterator trait 代替索引

接下来,我们将修改 Config::new 的内容。标准库文档还提到 std::env::Args 实现了 Iterator trait,因此我们知道可以对其调用 next 方法!示例 13-27 更新了示例 12-23 中的代码,以使用 next 方法:

文件名: src/lib.rs

1. use std::env;
2. use std::error::Error;
3. use std::fs;

5. pub struct Config {
6. pub query: String,
7. pub filename: String,
8. pub case_sensitive: bool,
9. }

11. impl Config {
12. pub fn new(mut args: env::Args) -> Result<Config, &'static str> {
13. args.next();

15. let query = match args.next() {
16. Some(arg) => arg,
17. None => return Err("Didn't get a query string"),
18. };

20. let filename = match args.next() {
21. Some(arg) => arg,
22. None => return Err("Didn't get a file name"),
23. };

25. let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

27. Ok(Config {
28. query,
29. filename,
30. case_sensitive,
31. })
32. }
33. }

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

38. let results = if config.case_sensitive {
39. search(&config.query, &contents)
40. } else {
41. search_case_insensitive(&config.query, &contents)
42. };

44. for line in results {
45. println!("{}", line);
46. }

48. Ok(())
49. }

51. pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
52. let mut results = Vec::new();

54. for line in contents.lines() {
55. if line.contains(query) {
56. results.push(line);
57. }
58. }

60. results
61. }

63. pub fn search_case_insensitive<'a>(
64. query: &str,
65. contents: &'a str,
66. ) -> Vec<&'a str> {
67. let query = query.to_lowercase();
68. let mut results = Vec::new();

70. for line in contents.lines() {
71. if line.to_lowercase().contains(&query) {
72. results.push(line);
73. }
74. }

76. results
77. }

79. #[cfg(test)]
80. mod tests {
81. use super::*;

83. #[test]
84. fn case_sensitive() {
85. let query = "duct";
86. let contents = "\
87. Rust:
88. safe, fast, productive.
89. Pick three.
90. Duct tape.";

92. assert_eq!(vec!["safe, fast, productive."], search(query, contents));
93. }

95. #[test]
96. fn case_insensitive() {
97. let query = "rUsT";
98. let contents = "\
99. Rust:
100. safe, fast, productive.
101. Pick three.
102. Trust me.";

104. assert_eq!(
105. vec!["Rust:", "Trust me."],
106. search_case_insensitive(query, contents)
107. );
108. }
109. }

示例 13-27:修改 Config::new 的函数体来使用迭代器方法

请记住 env::args 返回值的第一个值是程序的名称。我们希望忽略它并获取下一个值,所以首先调用 next 并不对返回值做任何操作。之后对希望放入 Config 中字段 query 调用 next。如果 next 返回 Some,使用 match 来提取其值。如果它返回 None,则意味着没有提供足够的参数并通过 Err 值提早返回。对 filename 值进行同样的操作。

使用迭代器适配器来使代码更简明

I/O 项目中其他可以利用迭代器的地方是 search 函数,示例 13-28 中重现了第十二章结尾示例 12-19 中此函数的定义:

文件名: src/lib.rs

1. use std::error::Error;
2. use std::fs;

4. pub struct Config {
5. pub query: String,
6. pub filename: String,
7. }

9. impl Config {
10. pub fn new(args: &[String]) -> Result<Config, &'static str> {
11. if args.len() < 3 {
12. return Err("not enough arguments");
13. }

15. let query = args[1].clone();
16. let filename = args[2].clone();

18. Ok(Config { query, filename })
19. }
20. }

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

25. Ok(())
26. }

28. pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
29. let mut results = Vec::new();

31. for line in contents.lines() {
32. if line.contains(query) {
33. results.push(line);
34. }
35. }

37. results
38. }

40. #[cfg(test)]
41. mod tests {
42. use super::*;

44. #[test]
45. fn one_result() {
46. let query = "duct";
47. let contents = "\
48. Rust:
49. safe, fast, productive.
50. Pick three.";

52. assert_eq!(vec!["safe, fast, productive."], search(query, contents));
53. }
54. }

示例 13-28:示例 12-19 中 search 函数的定义

可以通过使用迭代器适配器方法来编写更简明的代码。这也避免了一个可变的中间 results vector 的使用。函数式编程风格倾向于最小化可变状态的数量来使代码更简洁。去掉可变状态可能会使得将来进行并行搜索的增强变得更容易,因为我们不必管理 results vector 的并发访问。示例 13-29 展示了该变化:

文件名: src/lib.rs

1. use std::env;
2. use std::error::Error;
3. use std::fs;

5. pub struct Config {
6. pub query: String,
7. pub filename: String,
8. pub case_sensitive: bool,
9. }

11. impl Config {
12. pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> {
13. args.next();

15. let query = match args.next() {
16. Some(arg) => arg,
17. None => return Err("Didn't get a query string"),
18. };

20. let filename = match args.next() {
21. Some(arg) => arg,
22. None => return Err("Didn't get a file name"),
23. };

25. let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

27. Ok(Config {
28. query,
29. filename,
30. case_sensitive,
31. })
32. }
33. }

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

38. let results = if config.case_sensitive {
39. search(&config.query, &contents)
40. } else {
41. search_case_insensitive(&config.query, &contents)
42. };

44. for line in results {
45. println!("{}", line);
46. }

48. Ok(())
49. }

51. pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
52. contents
53. .lines()
54. .filter(|line| line.contains(query))
55. .collect()
56. }

58. pub fn search_case_insensitive<'a>(
59. query: &str,
60. contents: &'a str,
61. ) -> Vec<&'a str> {
62. let query = query.to_lowercase();
63. let mut results = Vec::new();

65. for line in contents.lines() {
66. if line.to_lowercase().contains(&query) {
67. results.push(line);
68. }
69. }

71. results
72. }

74. #[cfg(test)]
75. mod tests {
76. use super::*;

78. #[test]
79. fn case_sensitive() {
80. let query = "duct";
81. let contents = "\
82. Rust:
83. safe, fast, productive.
84. Pick three.
85. Duct tape.";

87. assert_eq!(vec!["safe, fast, productive."], search(query, contents));
88. }

90. #[test]
91. fn case_insensitive() {
92. let query = "rUsT";
93. let contents = "\
94. Rust:
95. safe, fast, productive.
96. Pick three.
97. Trust me.";

99. assert_eq!(
100. vec!["Rust:", "Trust me."],
101. search_case_insensitive(query, contents)
102. );
103. }
104. }

示例 13-29:在 search 函数实现中使用迭代器适配器

回忆 search 函数的目的是返回所有 contents 中包含 query 的行。类似于示例 13-19 中的 filter 例子,可以使用 filter 适配器只保留 line.contains(query) 返回 true 的那些行。接着使用 collect 将匹配行收集到另一个 vector 中。这样就容易多了!尝试对 search_case_insensitive 函数做出同样的使用迭代器方法的修改吧。

接下来的逻辑问题就是在代码中应该选择哪种风格:是使用示例 13-28 中的原始实现还是使用示例 13-29 中使用迭代器的版本?大部分 Rust 程序员倾向于使用迭代器风格。开始这有点难以理解,不过一旦你对不同迭代器的工作方式有了感觉之后,迭代器可能会更容易理解。相比摆弄不同的循环并创建新 vector,(迭代器)代码则更关注循环的目的。这抽象掉那些老生常谈的代码,这样就更容易看清代码所特有的概念,比如迭代器中每个元素必须面对的过滤条件。

不过这两种实现真的完全等同吗?直觉上的假设是更底层的循环会更快一些。让我们聊聊性能吧。