Match
Basics · Lesson 12
match is Rust's switch / when. It also returns a value, it must cover every possible value, and its patterns can take a value apart (a tuple, a list, a range).
Coming from Gomatch is a switch with no fall-through, where _ is default, 6 | 7 lists values, and 1..=5 matches a range. It must cover every possible value, and it gives back a value, so there is no var kind string to fill in.
Coming from Kotlinmatch is your when: _ is else, 6 | 7 is 6, 7, and 1..=5 is in 1..5. It must cover every value even for an Int used as a statement, which Kotlin's when does not check. A pattern can also take a value apart ((0, y), [first, ..]), which a when branch cannot.
Coming from PythonThis is Python's match/case, with | and _ meaning the same thing. The differences are that match gives back a value, and the compiler refuses to build if some value isn't covered.
Coming from TypeScriptmatch replaces switch, with no fall-through and no break. Each arm gives a value, and the compiler checks that every case is covered, which is the never trick built in.
Coming from Javamatch is a Java 21 switch expression with arrows: it returns a value and must cover every case. 6 | 7 is case 6, 7, _ is default, and the range 1..=5 is a pattern of its own, where Java uses a when guard (on an Object or Integer).
Coming from C#match is your switch expression: 1..=5 is >= 1 and <= 5, 6 | 7 is 6 or 7, and _ is the same discard. The real difference is that a missing case is a compile error in Rust, not a CS8509 warning followed by a runtime exception.
Coming from Cmatch is a switch that returns a value and never falls through, so there is no break. The compiler refuses a match that misses a possible value, where C would quietly skip it.
Coming from C++match is a switch with no fall-through that produces a value, plus ranges (1..=5) and alternatives (6 | 7). Leaving out a case is a hard compile error. In C++ a switch over an int with no default says nothing at all.
The quick comparison
fn main() { let day = 6; let kind = match day { 1..=5 => "weekday", 6 | 7 => "weekend", _ => "not a day", // _ means "anything else" }; println!("{kind}");}weekend
package main
import "fmt"
func main() { day := 6 var kind string switch { case day >= 1 && day <= 5: kind = "weekday" case day == 6 || day == 7: kind = "weekend" default: kind = "not a day" } fmt.Println(kind)}weekend
fun main() { val day = 6 val kind = when (day) { in 1..5 -> "weekday" 6, 7 -> "weekend" else -> "not a day" } println(kind)}weekend
day = 6match day: case 1 | 2 | 3 | 4 | 5: kind = "weekday" case 6 | 7: kind = "weekend" case _: # _ means "anything else" kind = "not a day"print(kind)weekend
function kindOf(day: number): string { switch (day) { case 1: case 2: case 3: case 4: case 5: return "weekday"; case 6: case 7: return "weekend"; default: // anything else return "not a day"; }}
console.log(kindOf(6));weekend
public class Main { public static void main(String[] args) { int day = 6; String kind = switch (day) { case 1, 2, 3, 4, 5 -> "weekday"; case 6, 7 -> "weekend"; default -> "not a day"; // default means "anything else" }; System.out.println(kind); }}weekend
var day = 6;var kind = day switch{ >= 1 and <= 5 => "weekday", 6 or 7 => "weekend", _ => "not a day", // _ means "anything else"};Console.WriteLine(kind);weekend
#include <stdio.h>
int main(void) { int day = 6; const char *kind; switch (day) { case 1: case 2: case 3: case 4: case 5: kind = "weekday"; break; case 6: case 7: kind = "weekend"; break; default: // default means "anything else" kind = "not a day"; break; } printf("%s\n", kind); return 0;}weekend
#include <iostream>#include <string_view>
std::string_view kind_of(int day) { switch (day) { case 1: case 2: case 3: case 4: case 5: return "weekday"; case 6: case 7: return "weekend"; default: // anything else return "not a day"; }}
int main() { int day = 6; std::cout << kind_of(day) << "\n";}weekend
What's different
- Cases never fall through to the next one. There is no
breakto forget. |means "or".1..=5is a range that includes 5;1..5stops before it.- A plain name in a pattern is a new variable that fits anything. It does not compare with a variable of the same name.
matchgets much more useful with enums, Option and Result (lessons below).
Leaving out the "anything else" case
Looks fine: in Go, Kotlin, Python, C#, C and C++ it runs fine. Rust won't compile it.
Works in Go. In Rust it's just a rule.Works in Kotlin. In Rust it's just a rule.Works in Python. In Rust it's just a rule.TypeScript refuses this too. Same idea, different words.Java refuses this too. Same idea, different words.Works in C#. In Rust it's just a rule.Works in C. In Rust it's just a rule.Works in C++. In Rust it's just a rule.fn main() { let day = 6; let kind = match day { 1..=5 => "weekday", 6 | 7 => "weekend", }; println!("{kind}");}package main
import "fmt"
func main() { day := 6 kind := "" switch { case day >= 1 && day <= 5: kind = "weekday" case day == 6 || day == 7: kind = "weekend" } fmt.Println(kind)}weekend
fun main() { val day = 6 var kind = "" when (day) { in 1..5 -> kind = "weekday" 6, 7 -> kind = "weekend" } println(kind)}weekend
day = 6match day: case 1 | 2 | 3 | 4 | 5: kind = "weekday" case 6 | 7: kind = "weekend"print(kind)weekend
TypeScript refuses this too (under --strict): the function promises a string, but when no case matches it falls off the end and would return undefined. The error does not say which case is missing.
function kindOf(day: number): string { switch (day) { case 1: case 2: case 3: case 4: case 5: return "weekday"; case 6: case 7: return "weekend"; }}
console.log(kindOf(6));main.ts(1,31): error TS2366: Function lacks ending return statement and return type does not include 'undefined'.
Java refuses this too: a switch expression on an int needs a default branch, or javac says it does not cover all possible input values.
public class Main { public static void main(String[] args) { int day = 6; String kind = switch (day) { case 1, 2, 3, 4, 5 -> "weekday"; case 6, 7 -> "weekend"; }; System.out.println(kind); }}Main.java:4: error: the switch expression does not cover all possible input values
String kind = switch (day) {
^
1 errorvar day = 6;var kind = day switch{ >= 1 and <= 5 => "weekday", 6 or 7 => "weekend",};Console.WriteLine(kind);Program.cs(2,16): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern '0' is not covered.
weekend
#include <stdio.h>
int main(void) { int day = 6; const char *kind = ""; switch (day) { case 1: case 2: case 3: case 4: case 5: kind = "weekday"; break; case 6: case 7: kind = "weekend"; break; } printf("%s\n", kind); return 0;}weekend
#include <iostream>#include <string>
int main() { int day = 6; std::string kind; switch (day) { case 1: case 2: case 3: case 4: case 5: kind = "weekday"; break; case 6: case 7: kind = "weekend"; break; } std::cout << kind << "\n";}weekend
What does the compiler say? Guess first, then open.
error[E0004]: non-exhaustive patterns: `i32::MIN..=0_i32` and `8_i32..=i32::MAX` not covered --> src/main.rs:3:22 | 3 | let kind = match day { | ^^^ patterns `i32::MIN..=0_i32` and `8_i32..=i32::MAX` not covered | = note: the matched value is of type `i32` help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern, a match arm with multiple or-patterns as shown, or multiple match arms | 5 ~ 6 | 7 => "weekend", 6 ~ i32::MIN..=0_i32 | 8_i32..=i32::MAX => todo!(),
Go runs this and prints weekend. With day = 99 no case would match, and kind would quietly stay an empty string.
Kotlin runs this and prints weekend. A when statement over an Int doesn't have to cover every value (over an enum, a sealed type or a Boolean Kotlin does check), so day = 99 would quietly leave kind empty. Written as val kind = when (day) { … }, Kotlin refuses it, as shown further down the page.
Python prints weekend because 6 matches a case. With day = 99 no case would match, kind would never be set, and the print would raise a NameError.
TypeScript refuses this too (under --strict): the function promises a string, but when no case matches it falls off the end and would return undefined. The error does not say which case is missing.
Java refuses this too: a switch expression on an int needs a default branch, or javac says it does not cover all possible input values.
It prints weekend, but the compiler warns that the switch is not exhaustive (CS8509). With day = 9 it would throw a SwitchExpressionException at run time.
C does not check that a switch covers every value. Day 6 prints weekend, but day 9 would quietly print an empty line.
A switch without default is fine in C++, so this prints weekend. A day like 9 would leave kind as an empty string with no complaint.
The compiler does not know what you know. It only sees that day is an i32, and an i32 can be 0, -5 or 99.
This is a rule, not a safety proof. Nothing unsafe happens in the other languages: each picks what a missing case does (an empty string, an exception). For day = 99, which branch runs, and what is kind? Rust asks you to answer now, for every value, even the ones you think can't happen.
The fix
Compiles and runs.
fn main() { let day = 6; let kind = match day { 1..=5 => "weekday", 6 | 7 => "weekend", _ => "not a day", }; println!("{kind}");}weekend
The toolkit
match compares one value with a list of arms, each written pattern => result (the shape), from the top. A pattern is a shape a value can have, and the first pattern that fits runs its arm. Three things set it apart from a classic C-style switch: it produces a value, it must cover every possible value (the compiler calls that exhaustive), and a pattern can take a value apart (a tuple, a list, a range) as well as compare it. This page is about what patterns can do. Outputs come from Linux on 64-bit ARM with Rust 1.99.1.
match is an expression
The arm that fits gives the whole match its value, so it can sit on the right of a let or be the tail of a function (Statements and expressions). _ fits anything, so it goes last: the first arm that fits wins.
fn main() { for n in [0, 3, 7, 12] { let size = match n { 0 => "none", 1..=5 => "few", 6..=10 => "several", _ => "many", }; println!("{n} -> {size}"); }}0 -> none 3 -> few 7 -> several 12 -> many
An arm that needs several steps uses a block, whose tail expression (its last line, with no ;) is the arm’s value. The comma after a block arm is optional.
fn main() { for seconds in [45, 150, 7200] { let text = match seconds { 0..=59 => format!("{seconds} s"), 60..=3599 => { let minutes = seconds / 60; let rest = seconds % 60; format!("{minutes} min {rest} s") } _ => { let hours = seconds / 3600; format!("{hours} h") } }; println!("{text}"); }}45 s 2 min 30 s 2 h
Every case must be covered
Leave out a case and the program does not build:
fn main() { let n = 7; let size = match n { 0 => "none", 1..=5 => "few", }; println!("{size}");}error[E0004]: non-exhaustive patterns: `i32::MIN..=-1_i32` and `6_i32..=i32::MAX` not covered --> src/main.rs:3:22 | 3 | let size = match n { | ^ patterns `i32::MIN..=-1_i32` and `6_i32..=i32::MAX` not covered | = note: the matched value is of type `i32` help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern, a match arm with multiple or-patterns as shown, or multiple match arms | 5 ~ 1..=5 => "few", 6 ~ i32::MIN..=-1_i32 | 6_i32..=i32::MAX => todo!(),
E0004 (all error codes). The message lists the values you forgot, as ranges, and the help: shows an arm to add. It works on any type. A tuple of two bools has four cases, and the compiler finds the one that is missing:
fn main() { let (raining, windy) = (true, false); let advice = match (raining, windy) { (true, true) => "stay in", (true, false) => "umbrella", (false, true) => "jacket", }; println!("{advice}");}error[E0004]: non-exhaustive patterns: `(false, false)` not covered --> src/main.rs:3:24 | 3 | let advice = match (raining, windy) { | ^^^^^^^^^^^^^^^^ pattern `(false, false)` not covered | = note: the matched value is of type `(bool, bool)` help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | 6 ~ (false, true) => "jacket", 7 ~ (false, false) => todo!(),
You choose what happens for every value, even the ones you believe cannot occur. A wildcard _ is the answer when you do not care. Naming the cases is often better (_ or every case? below).
The patterns
| Pattern | Fits | Example |
|---|---|---|
5, 'a', "go", true |
that exact value | 5 => … |
1..=5 |
1 to 5, both ends included; also for char |
'a'..='z' => … |
1..5 |
1 to 4: the end is left out | 0..10 => … |
10.. and ..=0 |
10 or more; 0 or less | 100.. => … |
1 | 2 |
either | 'y' | 'Y' => … |
_ |
anything, and the value is ignored | _ => … |
n |
anything; a new variable n holds the value |
n => n * 2 |
n @ 1..=5 |
a value in the range, and n holds it |
n @ 1..=9 => … |
(0, _), ((a, _), b) |
a tuple, piece by piece, nested as deep as you like | (0, y) => … |
[], [first, .., last], [head, rest @ ..] |
a list, by its shape | [a, b] => … |
Some(n), Point { x: 0, .. } |
an enum variant or a struct, by its shape | Some(0) => … |
&x |
a reference; x is the value behind it |
&5 => … |
pattern if test |
the pattern, and the extra test (a guard) is true | n if n < 0 => … |
The next sections show each family. Every Rust cell in these tables is run.
Numbers, characters and text
Ranges and | work on integers and on char. The first fitting arm wins, which is why 'a' is a vowel here and not just a letter:
fn main() { for c in ['a', 'Z', '7', ' ', 'é'] { let class = match c { 'a' | 'e' | 'i' | 'o' | 'u' => "vowel", 'a'..='z' | 'A'..='Z' => "letter", '0'..='9' => "digit", ' ' | '\t' | '\n' => "space", _ => "other", }; println!("{c:?} -> {class}"); }}'a' -> vowel 'Z' -> letter '7' -> digit ' ' -> space 'é' -> other
A string literal is a pattern too, and | lists alternatives. A String is not a &str, so matching one against literals needs as_str() first (Strings covers the two types):
fn main() { for cmd in ["go", "run", "stop", "quit", "fly"] { let action = match cmd { "go" | "run" => "moving", "stop" => "halting", "quit" | "exit" => "leaving", _ => "unknown command", }; println!("{cmd}: {action}"); }
// A String is not a &str: borrow the text first. let typed = String::from("stop"); match typed.as_str() { "stop" => println!("typed: halting"), _ => println!("typed: something else"), }}go: moving run: moving stop: halting quit: leaving fly: unknown command typed: halting
Without as_str() the compiler says so:
fn main() { let cmd = String::from("go"); match cmd { "go" => println!("moving"), _ => println!("unknown command"), }}error[E0308]: mismatched types --> src/main.rs:4:9 | 3 | match cmd { | --- this expression has type `String` 4 | "go" => println!("moving"), | ^^^^ expected `String`, found `&str` | help: consider converting the `String` to a `&str` using `.as_str()` | 3 | match cmd.as_str() { | +++++++++
Names, @ and guards
A bare name fits anything and binds: it makes a new variable for that arm. That is how the last arm below gets the number of minutes. @ binds a name and tests a pattern in one go: m @ 1..=59 fits only 1 to 59, and m is the value.
fn main() { for secs in [30, 1800, 4000, 90000] { let minutes = secs / 60; match minutes { 0 => println!("{secs} s: under a minute"), m @ 1..=59 => println!("{secs} s: {m} min"), m => println!("{secs} s: {} h {} min", m / 60, m % 60), } }}30 s: under a minute 1800 s: 30 min 4000 s: 1 h 6 min 90000 s: 25 h 0 min
A guard is if and a condition after the pattern. The arm is used only when the pattern fits and the condition is true. The condition can be any bool expression and can use variables from outside the match. Arms are still tried from the top:
fn main() { for n in [-4, 0, 7, 12] { let what = match n { n if n < 0 => "negative", 0 => "zero", n if n % 2 == 0 => "even", _ => "odd", }; println!("{n} is {what}"); }}-4 is negative 0 is zero 7 is odd 12 is even
A name in a pattern binds, it does not compare
Patterns can compare with literals, not with variables. A name in a pattern is a new variable (unless it names a constant, below), and it shadows any outer variable of the same name. This program meant “is n equal to limit?” and compiles, with warnings:
// The warnings are silenced here so the program builds cleanly; the next example shows them.#![allow(unreachable_patterns, unused_variables)]
fn main() { let limit = 10; for n in [3, 10, 42] { match n { limit => println!("{n}: at the limit"), _ => println!("{n}: not at the limit"), } }}3: at the limit 10: at the limit 42: at the limit
Every n takes the first arm, and the _ arm can never run. The compiler that checks coverage also counts the values an arm can still receive, and warns when that is none, so a specific arm belongs before a wide one. The first line below, #![deny(unreachable_patterns)], is an attribute that turns the warning into an error, and the message even names the shadowed variable:
#![deny(unreachable_patterns)]
fn main() { let limit = 10; for n in [3, 10, 42] { match n { limit => println!("{n}: at the limit"), _ => println!("{n}: not at the limit"), } }}error: unreachable pattern --> src/main.rs:8:13 | 7 | limit => println!("{n}: at the limit"), | ----- matches any value 8 | _ => println!("{n}: not at the limit"), | ^ no value can reach this | note: there is a binding of the same name; if you meant to pattern match against the value of that binding, that is a feature of constants that is not available for `let` bindings --> src/main.rs:4:9 | 4 | let limit = 10; | ^^^^^ note: the lint level is defined here --> src/main.rs:1:9 | 1 | #![deny(unreachable_patterns)] | ^^^^^^^^^^^^^^^^^^^^
There are two fixes. A const is compared, because it is a fixed value known when the program is built. For a value that is only known when the program runs, use a guard:
const LIMIT: i32 = 10;
fn main() { let limit = 10; for n in [3, 10, 42] { match n { LIMIT => println!("{n}: at the limit (const)"), n if n > limit => println!("{n}: over the limit (guard)"), _ => println!("{n}: under the limit"), } }}3: under the limit 10: at the limit (const) 42: over the limit (guard)
Several values at once: tuples
To decide on two or three conditions together, put them in a tuple and match the tuple. The compiler checks that every combination is covered, which a chain of if/else cannot do. Patterns nest: this one takes a pair of points apart, using a guard where a pattern cannot compare two variables.
fn main() { for n in [3, 5, 7, 15] { match (n % 3, n % 5) { (0, 0) => println!("{n}: FizzBuzz"), (0, _) => println!("{n}: Fizz"), (_, 0) => println!("{n}: Buzz"), _ => println!("{n}: {n}"), } }}3: Fizz 5: Buzz 7: 7 15: FizzBuzz
fn main() { let moves = [((0, 0), (0, 5)), ((2, 2), (2, 2)), ((1, 1), (4, 5))]; for (from, to) in moves { let kind = match (from, to) { (a, b) if a == b => "stay", ((x1, _), (x2, _)) if x1 == x2 => "same column", ((_, y1), (_, y2)) if y1 == y2 => "same row", _ => "diagonal-ish", }; println!("{from:?} -> {to:?}: {kind}"); }}(0, 0) -> (0, 5): same column (2, 2) -> (2, 2): stay (1, 1) -> (4, 5): diagonal-ish
Lists: slice patterns
A pattern can describe a list by its shape: how many items, and which ones you name. .. stands for any number of items you do not name, and rest @ .. names them. These patterns work on arrays and on slices (&[i32] is a borrowed view of a list; Vectors and Borrowing cover them). The compiler checks the lengths: [], one item, two, and “first and last” together cover every list.
fn describe(items: &[i32]) { match items { [] => println!("{items:?}: empty"), [only] => println!("{items:?}: just {only}"), [a, b] => println!("{items:?}: a pair, {a} and {b}"), [first, .., last] => println!("{items:?}: from {first} to {last}"), }}
fn main() { describe(&[]); describe(&[7]); describe(&[3, 4]); describe(&[1, 2, 3, 4]);}[]: empty [7]: just 7 [3, 4]: a pair, 3 and 4 [1, 2, 3, 4]: from 1 to 4
Literals can sit inside a slice pattern, so ["add", a, b] is a command with two arguments. [head, rest @ ..] is the shape of recursion over a list:
fn sum(items: &[i32]) -> i32 { match items { [] => 0, [head, rest @ ..] => head + sum(rest), }}
fn run(words: &[&str]) { match words { ["add", a, b] => println!("{words:?}: add {a} and {b}"), ["neg", a] => println!("{words:?}: negate {a}"), [cmd, ..] => println!("{words:?}: unknown command {cmd}"), [] => println!("{words:?}: nothing to do"), }}
fn main() { println!("sum: {}", sum(&[1, 2, 3, 4])); run(&["add", "2", "3"]); run(&["neg", "7"]); run(&["add", "2"]); run(&[]);}sum: 10 ["add", "2", "3"]: add 2 and 3 ["neg", "7"]: negate 7 ["add", "2"]: unknown command add []: nothing to do
A Vec is not a slice, so a slice pattern does not fit it directly. The help: shows the fix: match on a slice of it, v[..] or v.as_slice().
fn main() { let v = vec![1, 2, 3]; match v { [] => println!("empty"), [first, ..] => println!("starts with {first}"), }}error[E0529]: expected an array or slice, found `Vec<{integer}>` --> src/main.rs:4:9 | 4 | [] => println!("empty"), | ^^ pattern cannot match with input type `Vec<{integer}>` | help: consider slicing here | 3 | match v[..] { | ++++ error[E0529]: expected an array or slice, found `Vec<{integer}>` --> src/main.rs:5:9 | 5 | [first, ..] => println!("starts with {first}"), | ^^^^^^^^^^^ pattern cannot match with input type `Vec<{integer}>` | help: consider slicing here | 3 | match v[..] { | ++++
Structs, enums, references
The same patterns take apart a struct (Point { x: 0, .. }, where .. skips the other fields) and an enum, such as Option’s Some(n) (Structs, Enums, Option). A & in a pattern looks through a reference, and what a pattern does to a value it takes (copy, move or borrow) is part of Ownership and Borrowing. Every rule above still applies.
Arms must agree, or not return
Every arm gives the match its value, so the arms need the same type:
fn main() { let n = 3; let label = match n { 0 => "zero", _ => n, }; println!("{label}");}error[E0308]: `match` arms have incompatible types --> src/main.rs:5:14 | 3 | let label = match n { | _________________- 4 | | 0 => "zero", | | ------ this is found to be of type `&str` 5 | | _ => n, | | ^ expected `&str`, found integer 6 | | }; | |_____- `match` arms have incompatible types
An arm that leaves with continue, break, return or panic! has the type ! (never) and fits next to any other. This is how a loop skips input it cannot use. (parse returns a Result, a success or an error; Errors.)
fn main() { let inputs = ["4", "x", "9"]; let mut total = 0; for text in inputs { let n: i32 = match text.parse() { Ok(n) => n, Err(_) => { println!("skipping {text:?}"); continue; } }; total += n; } println!("total: {total}");}skipping "x" total: 13
matches!
When you only want a bool, matches!(value, pattern) is a match with true for the pattern and false for the rest. It takes guards. (The ! marks a macro.)
fn main() { let c = 'k'; println!("lowercase: {}", matches!(c, 'a'..='z'));
let age = 15; println!("teen: {}", matches!(age, 13..=19)); println!( "odd and over 10: {}", matches!(age, n if n % 2 == 1 && n > 10) );
let found = Some(4); println!("some even: {}", matches!(found, Some(n) if n % 2 == 0));}lowercase: true teen: true odd and over 10: true some even: true
match, if let or if?
| You want | Use |
|---|---|
| one yes/no question | if (If / else) |
| to act on one shape and ignore the rest | if let, or let ... else to stop otherwise (same lesson) |
| a decision for several shapes, with a check that none is forgotten | match |
only a bool |
matches! |
A pattern is not only for match: it also follows let (let (a, b) = pair;), sits in a function’s parameters, and follows for (Loops).
Your language, in Rust
Going deeper
How exhaustiveness is checked
The compiler reasons about the values a type can have, not about the text of your arms. Ranges are added up, so this match on a u8 has no _ and builds, because the four ranges cover 0 to 255 exactly. A constant such as u8::MAX can be the end of a range:
fn main() { for byte in [10u8, 65, 127, 200] { let class = match byte { 0..=31 => "control", 32..=126 => "printable", 127 => "delete", 128..=u8::MAX => "high", }; println!("{byte} -> {class}"); }}10 -> control 65 -> printable 127 -> delete 200 -> high
A guard is the exception, because the compiler does not run your condition. A guarded arm does not count towards coverage, even when the guards together cover every value:
fn main() { let n = 5; let sign = match n { x if x < 0 => "negative", x if x >= 0 => "not negative", }; println!("{sign}");}error[E0004]: non-exhaustive patterns: `i32::MIN..=i32::MAX` not covered --> src/main.rs:3:22 | 3 | let sign = match n { | ^ pattern `i32::MIN..=i32::MAX` not covered | = note: the matched value is of type `i32` = note: match arms with guards don't count towards exhaustivity help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | 5 ~ x if x >= 0 => "not negative", 6 ~ i32::MIN..=i32::MAX => todo!(),
The error says it: “match arms with guards don’t count towards exhaustivity”. End with an unguarded arm.
_ or every case?
Both are legal; they differ when the type changes. Take a small enum (a type with a fixed list of named values; Enums defines them) of traffic lights. The match below was written when it had Red and Green, and Yellow was added later. Naming the cases turns the new variant into a compile error at every match that needs a decision:
enum Light { Red, Yellow, Green,}
fn main() { for light in [Light::Red, Light::Yellow, Light::Green] { let action = match light { Light::Red => "stop", Light::Green => "go", }; println!("{action}"); }}error[E0004]: non-exhaustive patterns: `Light::Yellow` not covered --> src/main.rs:9:28 | 9 | let action = match light { | ^^^^^ pattern `Light::Yellow` not covered | note: `Light` defined here --> src/main.rs:1:6 | 1 | enum Light { | ^^^^^ 2 | Red, 3 | Yellow, | ------ not covered = note: the matched value is of type `Light` help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | 11 ~ Light::Green => "go", 12 ~ Light::Yellow => todo!(),
With a _ arm the same change builds, and yellow quietly takes the _ arm’s answer:
enum Light { Red, Yellow, Green,}
fn main() { for light in [Light::Red, Light::Yellow, Light::Green] { let action = match light { Light::Red => "stop", _ => "go", }; println!("{action}"); }}stop go go
Use _ when “everything else” is the real rule (a default, an unknown command). Name the cases when each variant deserves its own thought.
Practice
Try it
Open each exercise in the Playground, replace the todo!(), and run it until it prints all checks passed.
A door, two switches
Match the tuple (open, locked) and give each of the four combinations its own arm, with no _. The starter has a _ arm: delete it first, read the E0004 the compiler gives, and write the arm it names.
// A door has two switches: is it open, is it locked?// closed and unlocked -> "closed"// closed and locked -> "locked"// open and unlocked -> "open"// open and locked -> "jammed" (it is open, yet the bolt is out)fn door(open: bool, locked: bool) -> &'static str { match (open, locked) { _ => todo!("name all four; delete this line to read the E0004"), }}
fn main() { assert_eq!(door(false, false), "closed"); assert_eq!(door(false, true), "locked"); assert_eq!(door(true, false), "open"); assert_eq!(door(true, true), "jammed"); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
// A door has two switches: is it open, is it locked?// closed and unlocked -> "closed"// closed and locked -> "locked"// open and unlocked -> "open"// open and locked -> "jammed" (it is open, yet the bolt is out)fn door(open: bool, locked: bool) -> &'static str { match (open, locked) { (false, false) => "closed", (false, true) => "locked", (true, false) => "open", (true, true) => "jammed", }}
fn main() { assert_eq!(door(false, false), "closed"); assert_eq!(door(false, true), "locked"); assert_eq!(door(true, false), "open"); assert_eq!(door(true, true), "jammed"); println!("all checks passed");}all checks passed
Palindrome by slice pattern
Is a list the same backwards? Use the pattern [first, middle @ .., last] to compare the two ends, then ask the same about middle. The checks include the empty list, one item, and a list whose ends match but whose middle does not.
// Is the list the same read forwards and backwards?// Use a slice pattern with `middle @ ..` and check the outer two items,// then ask the same question about the middle.fn is_palindrome(items: &[i32]) -> bool { todo!("one arm for [first, middle @ .., last], one for everything shorter")}
fn main() { assert!(is_palindrome(&[])); assert!(is_palindrome(&[7])); assert!(is_palindrome(&[1, 2, 1])); assert!(is_palindrome(&[1, 2, 2, 1])); assert!(!is_palindrome(&[1, 2])); assert!(!is_palindrome(&[1, 2, 3])); // The outer pair matches but the inside does not. assert!(!is_palindrome(&[1, 2, 3, 1])); assert!(!is_palindrome(&[4, 5, 6, 5, 9])); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
// Is the list the same read forwards and backwards?// Use a slice pattern with `middle @ ..` and check the outer two items,// then ask the same question about the middle.fn is_palindrome(items: &[i32]) -> bool { match items { [first, middle @ .., last] => first == last && is_palindrome(middle), // Zero or one item: nothing left to compare. _ => true, }}
fn main() { assert!(is_palindrome(&[])); assert!(is_palindrome(&[7])); assert!(is_palindrome(&[1, 2, 1])); assert!(is_palindrome(&[1, 2, 2, 1])); assert!(!is_palindrome(&[1, 2])); assert!(!is_palindrome(&[1, 2, 3])); // The outer pair matches but the inside does not. assert!(!is_palindrome(&[1, 2, 3, 1])); assert!(!is_palindrome(&[4, 5, 6, 5, 9])); println!("all checks passed");}all checks passed
Guards and @
Describe how much of a limit is spent. A pattern cannot compare with the variable limit, so the over-the-limit and at-the-limit arms are guards; a small amount uses n @ 1..=9. Order matters: the checks include 5 spent against a limit of 3, and 0 against a limit of 0.
// Describe how much of a limit has been spent.// nothing spent -> "unused"// more than the limit -> "over by N"// exactly the limit -> "exactly at the limit"// 1 to 9 (under limit) -> "a little (N)"// anything else -> "N of LIMIT used"// A pattern cannot compare with the variable `limit`, so the limit checks are guards.fn budget(spent: u32, limit: u32) -> String { todo!( "a match on spent: a literal arm, guards that use limit, an @ range, then a name for the rest" )}
fn main() { assert_eq!(budget(0, 100), "unused"); // The first arm wins, even when the limit is also 0. assert_eq!(budget(0, 0), "unused"); assert_eq!(budget(150, 100), "over by 50"); assert_eq!(budget(100, 100), "exactly at the limit"); assert_eq!(budget(5, 100), "a little (5)"); assert_eq!(budget(50, 100), "50 of 100 used"); // Over the limit beats "a little". assert_eq!(budget(5, 3), "over by 2"); assert_eq!(budget(3, 3), "exactly at the limit"); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
// Describe how much of a limit has been spent.// nothing spent -> "unused"// more than the limit -> "over by N"// exactly the limit -> "exactly at the limit"// 1 to 9 (under limit) -> "a little (N)"// anything else -> "N of LIMIT used"// A pattern cannot compare with the variable `limit`, so the limit checks are guards.fn budget(spent: u32, limit: u32) -> String { match spent { 0 => String::from("unused"), n if n > limit => format!("over by {}", n - limit), n if n == limit => String::from("exactly at the limit"), n @ 1..=9 => format!("a little ({n})"), n => format!("{n} of {limit} used"), }}
fn main() { assert_eq!(budget(0, 100), "unused"); // The first arm wins, even when the limit is also 0. assert_eq!(budget(0, 0), "unused"); assert_eq!(budget(150, 100), "over by 50"); assert_eq!(budget(100, 100), "exactly at the limit"); assert_eq!(budget(5, 100), "a little (5)"); assert_eq!(budget(50, 100), "50 of 100 used"); // Over the limit beats "a little". assert_eq!(budget(5, 3), "over by 2"); assert_eq!(budget(3, 3), "exactly at the limit"); println!("all checks passed");}all checks passed
Read more
- The Rust Book, chapter 6.2: The
matchControl Flow Construct, and chapter 19: Patterns and Matching, especially Pattern Syntax (every form on this page) and Refutability. - Rust by Example: match, with guards, binding and destructuring.
- The Rust Reference: Patterns and match expressions: the exact rules, including how guards and
|interact. matches!in the standard library docs.- Reading compiler errors: E0004, E0308 and E0529 from this page, with what each means.
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.