Skip to content
Learn Rust
Theme

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

Rustsrc/main.rsRun ↗
fn main() {
let day = 6;
let kind = match day {
1..=5 => "weekday",
6 | 7 => "weekend",
_ => "not a day", // _ means "anything else"
};
println!("{kind}");
}
$ cargo run
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)
}
$ go run main.go
weekend
fun main() {
val day = 6
val kind = when (day) {
in 1..5 -> "weekday"
6, 7 -> "weekend"
else -> "not a day"
}
println(kind)
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
weekend
day = 6
match day:
case 1 | 2 | 3 | 4 | 5:
kind = "weekday"
case 6 | 7:
kind = "weekend"
case _: # _ means "anything else"
kind = "not a day"
print(kind)
$ python3 main.py
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));
$ tsc --strict --module nodenext --types node main.ts && node main.js
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);
}
}
$ javac Main.java && java Main
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);
$ dotnet run
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;
}
$ gcc -Wall -Wextra main.c -o main && ./main
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";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
weekend

What's different

  • Cases never fall through to the next one. There is no break to forget.
  • | means "or". 1..=5 is a range that includes 5; 1..5 stops 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.
  • match gets much more useful with enums, Option and Result (lessons below).
Gotcha

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.
Rustsrc/main.rs✗ won't compile
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)
}
$ go run main.go✓ runs
weekend
fun main() {
val day = 6
var kind = ""
when (day) {
in 1..5 -> kind = "weekday"
6, 7 -> kind = "weekend"
}
println(kind)
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar✓ runs
weekend
day = 6
match day:
case 1 | 2 | 3 | 4 | 5:
kind = "weekday"
case 6 | 7:
kind = "weekend"
print(kind)
$ python3 main.py✓ runs
weekend
TypeScript✗ refuses too

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 too

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 error
var day = 6;
var kind = day switch
{
>= 1 and <= 5 => "weekday",
6 or 7 => "weekend",
};
Console.WriteLine(kind);
$ dotnet run✓ runs
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;
}
$ gcc -Wall -Wextra main.c -o main && ./main✓ runs
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";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main✓ runs
weekend
What does the compiler say? Guess first, then open.
$ cargo buildWhat is E0004?
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!(),
What happens in Go

Go runs this and prints weekend. With day = 99 no case would match, and kind would quietly stay an empty string.

What happens in Kotlin

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.

What happens in Python

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.

What happens in TypeScript

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.

What happens in Java

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.

What happens in C#

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.

What happens in C

C does not check that a switch covers every value. Day 6 prints weekend, but day 9 would quietly print an empty line.

What happens in C++

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.

What Rust sees

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.

Why Rust says no

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.

Rust, fixedsrc/main.rsRun ↗
fn main() {
let day = 6;
let kind = match day {
1..=5 => "weekday",
6 | 7 => "weekend",
_ => "not a day",
};
println!("{kind}");
}
$ cargo run
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.

Rustsrc/main.rsRun ↗
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}");
}
}
$ cargo run
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.

Rustsrc/main.rsRun ↗
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}");
}
}
$ cargo run
45 s
2 min 30 s
2 h

Every case must be covered

Leave out a case and the program does not build:

Rustsrc/main.rs✗ won't compile
fn main() {
let n = 7;
let size = match n {
0 => "none",
1..=5 => "few",
};
println!("{size}");
}
$ cargo buildWhat is E0004?
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:

Rustsrc/main.rs✗ won't compile
fn main() {
let (raining, windy) = (true, false);
let advice = match (raining, windy) {
(true, true) => "stay in",
(true, false) => "umbrella",
(false, true) => "jacket",
};
println!("{advice}");
}
$ cargo buildWhat is E0004?
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:

Rustsrc/main.rsRun ↗
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}");
}
}
$ cargo run
'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):

Rustsrc/main.rsRun ↗
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"),
}
}
$ cargo run
go: moving
run: moving
stop: halting
quit: leaving
fly: unknown command
typed: halting

Without as_str() the compiler says so:

Rustsrc/main.rs✗ won't compile
fn main() {
let cmd = String::from("go");
match cmd {
"go" => println!("moving"),
_ => println!("unknown command"),
}
}
$ cargo buildWhat is E0308?
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.

Rustsrc/main.rsRun ↗
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),
}
}
}
$ cargo run
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:

Rustsrc/main.rsRun ↗
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}");
}
}
$ cargo run
-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:

Rustsrc/main.rsRun ↗
// 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"),
}
}
}
$ cargo run
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:

Rustsrc/main.rs✗ won't compile
#![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"),
}
}
}
$ cargo build
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:

Rustsrc/main.rsRun ↗
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"),
}
}
}
$ cargo run
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.

Rustsrc/main.rsRun ↗
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}"),
}
}
}
$ cargo run
3: Fizz
5: Buzz
7: 7
15: FizzBuzz
Rustsrc/main.rsRun ↗
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}");
}
}
$ cargo run
(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.

Rustsrc/main.rsRun ↗
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]);
}
$ cargo run
[]: 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:

Rustsrc/main.rsRun ↗
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(&[]);
}
$ cargo 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().

Rustsrc/main.rs✗ won't compile
fn main() {
let v = vec![1, 2, 3];
match v {
[] => println!("empty"),
[first, ..] => println!("starts with {first}"),
}
}
$ cargo buildWhat is E0529?
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:

Rustsrc/main.rs✗ won't compile
fn main() {
let n = 3;
let label = match n {
0 => "zero",
_ => n,
};
println!("{label}");
}
$ cargo buildWhat is E0308?
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.)

Rustsrc/main.rsRun ↗
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}");
}
$ cargo run
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.)

Rustsrc/main.rsRun ↗
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));
}
$ cargo run
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:

Rustsrc/main.rsRun ↗
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}");
}
}
$ cargo run
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:

Rustsrc/main.rs✗ won't compile
fn main() {
let n = 5;
let sign = match n {
x if x < 0 => "negative",
x if x >= 0 => "not negative",
};
println!("{sign}");
}
$ cargo buildWhat is E0004?
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:

Rustsrc/main.rs✗ won't compile
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}");
}
}
$ cargo buildWhat is E0004?
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:

Rustsrc/main.rsRun ↗
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}");
}
}
$ cargo run
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.

Starterexercises/door.rs
// 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
One solutionexercises/door.solution.rsRun ↗
// 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");
}
$ cargo run
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.

Starterexercises/palindrome.rs
// 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
One solutionexercises/palindrome.solution.rsRun ↗
// 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");
}
$ cargo run
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.

Starterexercises/budget.rs
// 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
One solutionexercises/budget.solution.rsRun ↗
// 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");
}
$ cargo run
all checks passed

Read more