返回首页返回书籍《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”
第 91 / 111 节

第 91 节 · SECTION 91

All the Places Patterns Can Be Used

All the Places Patterns Can Be Used

Patterns pop up in a number of places in Rust, and you’ve been using them a lot without realizing it! This section discusses all the places where patterns are valid.

match Arms

As discussed in Chapter 6, we use patterns in the arms of match expressions. Formally, match expressions are defined as the keyword match, a value to match on, and one or more match arms that consist of a pattern and an expression to run if the value matches that arm’s pattern, like this:

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

For example, here’s the match expression from Listing 6-5 that matches on an Option<i32> value in the variable x:

match x {
    None => None,
    Some(i) => Some(i + 1),
}

The patterns in this match expression are the None and Some(i) to the left of each arrow.

One requirement for match expressions is that they need to be exhaustive in the sense that all possibilities for the value in the match expression must be accounted for. One way to ensure that you’ve covered every possibility is to have a catch-all pattern for the last arm: For example, a variable name matching any value can never fail and thus covers every remaining case.

The particular pattern _ will match anything, but it never binds to a variable, so it’s often used in the last match arm. The _ pattern can be useful when you want to ignore any value not specified, for example. We’ll cover the _ pattern in more detail in “Ignoring Values in a Pattern” later in this chapter.

let Statements

Prior to this chapter, we had only explicitly discussed using patterns with match and if let, but in fact, we’ve used patterns in other places as well, including in let statements. For example, consider this straightforward variable assignment with let:

let x = 5;

Every time you’ve used a let statement like this you’ve been using patterns, although you might not have realized it! More formally, a let statement looks like this:

let PATTERN = EXPRESSION;

In statements like let x = 5; with a variable name in the PATTERN slot, the variable name is just a particularly simple form of a pattern. Rust compares the expression against the pattern and assigns any names it finds. So, in the let x = 5; example, x is a pattern that means “bind what matches here to the variable x.” Because the name x is the whole pattern, this pattern effectively means “bind everything to the variable x, whatever the value is.”

To see the pattern-matching aspect of let more clearly, consider Listing 19-1, which uses a pattern with let to destructure a tuple.

Listing 19-1: Using a pattern to destructure a tuple and create three variables at once

    let (x, y, z) = (1, 2, 3);

Here, we match a tuple against a pattern. Rust compares the value (1, 2, 3) to the pattern (x, y, z) and sees that the value matches the pattern—that is, it sees that the number of elements is the same in both—so Rust binds 1 to x, 2 to y, and 3 to z. You can think of this tuple pattern as nesting three individual variable patterns inside it.

If the number of elements in the pattern doesn’t match the number of elements in the tuple, the overall type won’t match and we’ll get a compiler error. For example, Listing 19-2 shows an attempt to destructure a tuple with three elements into two variables, which won’t work.

Listing 19-2: Incorrectly constructing a pattern whose variables don’t match the number of elements in the tuple

    let (x, y) = (1, 2, 3);

Attempting to compile this code results in this type error:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
 --> src/main.rs:2:9
  |
2 |     let (x, y) = (1, 2, 3);
  |         ^^^^^^   --------- this expression has type `({integer}, {integer}, {integer})`
  |         |
  |         expected a tuple with 3 elements, found one with 2 elements
  |
  = note: expected tuple `({integer}, {integer}, {integer})`
             found tuple `(_, _)`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error

To fix the error, we could ignore one or more of the values in the tuple using _ or .., as you’ll see in the “Ignoring Values in a Pattern” section. If the problem is that we have too many variables in the pattern, the solution is to make the types match by removing variables so that the number of variables equals the number of elements in the tuple.

Conditional if let Expressions

In Chapter 6, we discussed how to use if let expressions mainly as a shorter way to write the equivalent of a match that only matches one case. Optionally, if let can have a corresponding else containing code to run if the pattern in the if let doesn’t match.

Listing 19-3 shows that it’s also possible to mix and match if let, else if, and else if let expressions. Doing so gives us more flexibility than a match expression in which we can express only one value to compare with the patterns. Also, Rust doesn’t require that the conditions in a series of if let, else if, and else if let arms relate to each other.

The code in Listing 19-3 determines what color to make your background based on a series of checks for several conditions. For this example, we’ve created variables with hardcoded values that a real program might receive from user input.

Listing 19-3: Mixing if let, else if, else if let, and else Filename: src/main.rs

fn main() {
    let favorite_color: Option<&str> = None;
    let is_tuesday = false;
    let age: Result<u8, _> = "34".parse();

    if let Some(color) = favorite_color {
        println!("Using your favorite color, {color}, as the background");
    } else if is_tuesday {
        println!("Tuesday is green day!");
    } else if let Ok(age) = age {
        if age > 30 {
            println!("Using purple as the background color");
        } else {
            println!("Using orange as the background color");
        }
    } else {
        println!("Using blue as the background color");
    }
}

If the user specifies a favorite color, that color is used as the background. If no favorite color is specified and today is Tuesday, the background color is green. Otherwise, if the user specifies their age as a string and we can parse it as a number successfully, the color is either purple or orange depending on the value of the number. If none of these conditions apply, the background color is blue.

This conditional structure lets us support complex requirements. With the hardcoded values we have here, this example will print Using purple as the background color.

You can see that if let can also introduce new variables that shadow existing variables in the same way that match arms can: The line if let Ok(age) = age introduces a new age variable that contains the value inside the Ok variant, shadowing the existing age variable. This means we need to place the if age > 30 condition within that block: We can’t combine these two conditions into if let Ok(age) = age && age > 30. The new age we want to compare to 30 isn’t valid until the new scope starts with the curly bracket.

The downside of using if let expressions is that the compiler doesn’t check for exhaustiveness, whereas with match expressions it does. If we omitted the last else block and therefore missed handling some cases, the compiler would not alert us to the possible logic bug.

while let Conditional Loops

Similar in construction to if let, the while let conditional loop allows a while loop to run for as long as a pattern continues to match. In Listing 19-4, we show a while let loop that waits on messages sent between threads, but in this case checking a Result instead of an Option.

Listing 19-4: Using a while let loop to print values for as long as rx.recv() returns Ok

    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        for val in [1, 2, 3] {
            tx.send(val).unwrap();
        }
    });

    while let Ok(value) = rx.recv() {
        println!("{value}");
    }

This example prints 1, 2, and then 3. The recv method takes the first message out of the receiver side of the channel and returns an Ok(value). When we first saw recv back in Chapter 16, we unwrapped the error directly, or we interacted with it as an iterator using a for loop. As Listing 19-4 shows, though, we can also use while let, because the recv method returns an Ok each time a message arrives, as long as the sender exists, and then produces an Err once the sender side disconnects.

for Loops

In a for loop, the value that directly follows the keyword for is a pattern. For example, in for x in y, the x is the pattern. Listing 19-5 demonstrates how to use a pattern in a for loop to destructure, or break apart, a tuple as part of the for loop.

Listing 19-5: Using a pattern in a for loop to destructure a tuple

    let v = vec!['a', 'b', 'c'];

    for (index, value) in v.iter().enumerate() {
        println!("{value} is at index {index}");
    }

The code in Listing 19-5 will print the following:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2

We adapt an iterator using the enumerate method so that it produces a value and the index for that value, placed into a tuple. The first value produced is the tuple (0, 'a'). When this value is matched to the pattern (index, value), index will be 0 and value will be 'a', printing the first line of the output.

Function Parameters

Function parameters can also be patterns. The code in Listing 19-6, which declares a function named foo that takes one parameter named x of type i32, should by now look familiar.

Listing 19-6: A function signature using patterns in the parameters

fn foo(x: i32) {
    // code goes here
}

The x part is a pattern! As we did with let, we could match a tuple in a function’s arguments to the pattern. Listing 19-7 splits the values in a tuple as we pass it to a function.

Listing 19-7: A function with parameters that destructure a tuple Filename: src/main.rs

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({x}, {y})");
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}

This code prints Current location: (3, 5). The values &(3, 5) match the pattern &(x, y), so x is the value 3 and y is the value 5.

We can also use patterns in closure parameter lists in the same way as in function parameter lists because closures are similar to functions, as discussed in Chapter 13.

At this point, you’ve seen several ways to use patterns, but patterns don’t work the same in every place we can use them. In some places, the patterns must be irrefutable; in other circumstances, they can be refutable. We’ll discuss these two concepts next.