返回首页返回书籍《The Rust Programming Language》
目录

目录 · CONTENTS

  1. 01The Rust Programming Language
  2. 02Foreword
  3. 03Introduction
  4. 04Getting Started
  5. 05Installation
  6. 06Hello, World!
  7. 07Hello, Cargo!
  8. 08Programming a Guessing Game
  9. 09Common Programming Concepts
  10. 10Variables and Mutability
  11. 11Data Types
  12. 12Functions
  13. 13Comments
  14. 14Control Flow
  15. 15Understanding Ownership
  16. 16What is Ownership?
  17. 17References and Borrowing
  18. 18The Slice Type
  19. 19Using Structs to Structure Related Data
  20. 20Defining and Instantiating Structs
  21. 21An Example Program Using Structs
  22. 22Methods
  23. 23Enums and Pattern Matching
  24. 24Defining an Enum
  25. 25The `match` Control Flow Construct
  26. 26Concise Control Flow with `if let` and `let...else`
  27. 27Packages, Crates, and Modules
  28. 28Packages and Crates
  29. 29Control Scope and Privacy with Modules
  30. 30Paths for Referring to an Item in the Module Tree
  31. 31Bringing Paths Into Scope with the `use` Keyword
  32. 32Separating Modules into Different Files
  33. 33Common Collections
  34. 34Storing Lists of Values with Vectors
  35. 35Storing UTF-8 Encoded Text with Strings
  36. 36Storing Keys with Associated Values in Hash Maps
  37. 37Error Handling
  38. 38Unrecoverable Errors with `panic!`
  39. 39Recoverable Errors with `Result`
  40. 40To `panic!` or Not to `panic!`
  41. 41Generic Types, Traits, and Lifetimes
  42. 42Generic Data Types
  43. 43Defining Shared Behavior with Traits
  44. 44Validating References with Lifetimes
  45. 45Writing Automated Tests
  46. 46How to Write Tests
  47. 47Controlling How Tests Are Run
  48. 48Test Organization
  49. 49An I/O Project: Building a Command Line Program
  50. 50Accepting Command Line Arguments
  51. 51Reading a File
  52. 52Refactoring to Improve Modularity and Error Handling
  53. 53Adding Functionality with Test Driven Development
  54. 54Working with Environment Variables
  55. 55Redirecting Errors to Standard Error
  56. 56Functional Language Features: Iterators and Closures
  57. 57Closures
  58. 58Processing a Series of Items with Iterators
  59. 59Improving Our I/O Project
  60. 60Performance in Loops vs. Iterators
  61. 61More About Cargo and Crates.io
  62. 62Customizing Builds with Release Profiles
  63. 63Publishing a Crate to Crates.io
  64. 64Cargo Workspaces
  65. 65Installing Binaries with `cargo install`
  66. 66Extending Cargo with Custom Commands
  67. 67Smart Pointers
  68. 68Using `Box<T>` to Point to Data on the Heap
  69. 69Treating Smart Pointers Like Regular References
  70. 70Running Code on Cleanup with the `Drop` Trait
  71. 71`Rc<T>`, the Reference Counted Smart Pointer
  72. 72`RefCell<T>` and the Interior Mutability Pattern
  73. 73Reference Cycles Can Leak Memory
  74. 74Fearless Concurrency
  75. 75Using Threads to Run Code Simultaneously
  76. 76Transfer Data Between Threads with Message Passing
  77. 77Shared-State Concurrency
  78. 78Extensible Concurrency with `Send` and `Sync`
  79. 79Fundamentals of Asynchronous Programming: Async, Await, Futures, and Streams
  80. 80extern crate trpl; // required for mdbook test
  81. 81Applying Concurrency with Async
  82. 82Working With Any Number of Futures
  83. 83Streams: Futures in Sequence
  84. 84A Closer Look at the Traits for Async
  85. 85Futures, Tasks, and Threads
  86. 86Object-Oriented Programming Features
  87. 87Characteristics of Object-Oriented Languages
  88. 88Using Trait Objects to Abstract over Shared Behavior
  89. 89Implementing an Object-Oriented Design Pattern
  90. 90Patterns and Matching
  91. 91All the Places Patterns Can Be Used
  92. 92Refutability: Whether a Pattern Might Fail to Match
  93. 93Pattern Syntax
  94. 94Advanced Features
  95. 95Unsafe Rust
  96. 96Advanced Traits
  97. 97Advanced Types
  98. 98Advanced Functions and Closures
  99. 99Macros
  100. 100Final Project: Building a Multithreaded Web Server
  101. 101Building a Single-Threaded Web Server
  102. 102From Single-Threaded to Multithreaded Server
  103. 103Graceful Shutdown and Cleanup
  104. 104Appendix
  105. 105A - Keywords
  106. 106B - Operators and Symbols
  107. 107C - Derivable Traits
  108. 108D - Useful Development Tools
  109. 109E - Editions
  110. 110F - Translations of the Book
  111. 111G - How Rust is Made and “Nightly Rust”
第 59 / 111 节

第 59 节 · SECTION 59

Improving Our I/O Project

Improving Our I/O Project

With this new knowledge about iterators, we can improve the I/O project in Chapter 12 by using iterators to make places in the code clearer and more concise. Let’s look at how iterators can improve our implementation of the Config::build function and the search function.

Removing a clone Using an Iterator

In Listing 12-6, we added code that took a slice of String values and created an instance of the Config struct by indexing into the slice and cloning the values, allowing the Config struct to own those values. In Listing 13-17, we’ve reproduced the implementation of the Config::build function as it was in Listing 12-23.

Listing 13-17: Reproduction of the Config::build function from Listing 12-23 Filename: src/main.rs

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

At the time, we said not to worry about the inefficient clone calls because we would remove them in the future. Well, that time is now!

We needed clone here because we have a slice with String elements in the parameter args, but the build function doesn’t own args. To return ownership of a Config instance, we had to clone the values from the query and file_path fields of Config so that the Config instance can own its values.

With our new knowledge about iterators, we can change the build function to take ownership of an iterator as its argument instead of borrowing a slice. We’ll use the iterator functionality instead of the code that checks the length of the slice and indexes into specific locations. This will clarify what the Config::build function is doing because the iterator will access the values.

Once Config::build takes ownership of the iterator and stops using indexing operations that borrow, we can move the String values from the iterator into Config rather than calling clone and making a new allocation.

Using the Returned Iterator Directly

Open your I/O project’s src/main.rs file, which should look like this:

Filename: src/main.rs

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

We’ll first change the start of the main function that we had in Listing 12-24 to the code in Listing 13-18, which this time uses an iterator. This won’t compile until we update Config::build as well.

Listing 13-18: Passing the return value of env::args to Config::build Filename: src/main.rs

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

    // --snip--
}

The env::args function returns an iterator! Rather than collecting the iterator values into a vector and then passing a slice to Config::build, now we’re passing ownership of the iterator returned from env::args to Config::build directly.

Next, we need to update the definition of Config::build. Let’s change the signature of Config::build to look like Listing 13-19. This still won’t compile, because we need to update the function body.

Listing 13-19: Updating the signature of Config::build to expect an iterator Filename: src/main.rs

impl Config {
    fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        // --snip--

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 that 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 “Using Traits as Parameters” section of Chapter 10 means that args can be any type that implements the Iterator trait 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

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.

Listing 13-20: Changing the body of Config::build to use iterator methods Filename: src/main.rs

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

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. Then, we call next to get the value we want to put in the query field of Config. If next returns 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.

Clarifying Code with Iterator Adapters

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.

Listing 13-21: The implementation of the search function from 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
}

We can write this code in a more concise way using iterator adapter 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.

Listing 13-22: Using iterator adapter methods in the implementation of the search function Filename: src/lib.rs

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

Recall that the purpose of the search function is to return all lines in contents that contain the query. Similar to the filter example in Listing 13-16, this code uses the filter adapter to keep only the lines for which line.contains(query) returns true. We then collect the matching lines into another vector with collect. Much simpler! Feel free to make the same change to use iterator methods in the search_case_insensitive function as well.

For a further improvement, return an iterator from the search function by removing the call to collect and changing the return type to impl Iterator<Item = &'a str> so that the function becomes an iterator adapter. Note that you’ll also need to update the tests! Search through a large file using your minigrep tool before and after making this change to observe the difference in behavior. Before this change, the program won’t print any results until it has collected all of the results, but after the change, the results will be printed as each matching line is found because the for loop in the run function is able to take advantage of the laziness of the iterator.

Choosing Between Loops and Iterators

The next logical question is which style you should choose in your own code and why: the original implementation in Listing 13-21 or the version using iterators in Listing 13-22 (assuming we’re collecting all the results before returning them rather than returning the iterator). Most Rust programmers prefer to use the iterator style. It’s a bit tougher to get the hang of at first, but once you get a feel for the various iterator adapters and what they do, iterators can be easier to understand. Instead of fiddling with the various bits of looping and building new vectors, the code focuses on the high-level objective of the loop. This abstracts away some of the commonplace code so that it’s easier to see the concepts that are unique to this code, such as the filtering condition each element in the iterator must pass.

But are the two implementations truly equivalent? The intuitive assumption might be that the lower-level loop will be faster. Let’s talk about performance.