Patterns
Data · Lesson 28
A pattern is a shape that a value is taken apart by (destructuring), with a name wherever you want to keep a part. match is where you meet them, but they also stand in let, for, function parameters and if let. The compiler checks that a pattern which must always fit does, and what a pattern does to ownership is worth knowing.
Coming from GoGo has no way to take a struct or an array apart: you read p.x and a[0], and the nearest things are a, b := f(), for i, v := range and v, ok := m[k]. A Rust pattern does those jobs and also takes structs, tuples and enums apart, in let, for, parameters and if let.
Coming from KotlinThis is the idea behind val (a, b) = pair and for ((k, v) in map), with three differences: a struct pattern matches fields by name where Kotlin goes by position (by default), patterns nest, and a pattern can fail (if let), where a destructuring declaration never tests the shape (on a List it calls component2() and can throw).
Coming from PythonThis is tuple unpacking (a, b = pair, first, *rest = xs, for k, v in d.items()), extended to structs and enums. A pattern can also fail on purpose (if let), where unpacking the wrong number of items raises ValueError at run time.
Coming from TypeScriptThis is destructuring (const { x, y } = p, const [a, b] = pair), but a Rust pattern also tests the shape (Some(n)), and the compiler checks the length of an array and the variants instead of leaving a missing item undefined. A pattern has no default values and no object rest, and matching moves where destructuring only adds names.
Coming from JavaJava 21's record patterns (p instanceof Point(int x, int y)) are the nearest thing, and they only appear in instanceof and switch. Rust's work on structs, tuples, arrays and enums, and also stand in let, for and function parameters.
Coming from C#C# deconstruction (var (x, y) = p) and its positional, property and list patterns are the nearest thing. Rust patterns also stand in function and closure parameters and in let ... else, and have | and ranges but no and or not.
Coming from CC has no destructuring: you read p.x and a[0] one at a time (int x = p.x, y = p.y;). A Rust pattern takes a value apart in one step, and the compiler checks that the places where it cannot fail really cannot.
Coming from C++C++17 structured bindings (auto [x, y] = p;) are the nearest thing: by position, no nesting, and no way to test a shape. Rust's patterns nest, bind struct fields by name, and if let tests the shape and takes it apart in one go.
The quick comparison
struct Point { x: i32, y: i32,}
fn main() { // let takes a value apart, by field name let Point { x, y } = Point { x: 3, y: -2 }; println!("x = {x}, y = {y}");
// so does a for loop, one item at a time for (i, c) in "abc".chars().enumerate() { println!("{i}: {c}"); }
// if let takes a value apart only when it fits if let Some((key, value)) = "mode=fast".split_once('=') { println!("{key} -> {value}"); }
// [first, .., last] names the ends of an array let [first, .., last] = [10, 20, 30, 40]; println!("first = {first}, last = {last}");}x = 3, y = -2 0: a 1: b 2: c mode -> fast first = 10, last = 40
package main
import ( "fmt" "strings")
type Point struct{ x, y int }
func main() { // no destructuring of a struct: read the fields p := Point{3, -2} x, y := p.x, p.y fmt.Printf("x = %d, y = %d\n", x, y)
// range gives (index, rune) pairs for i, c := range "abc" { fmt.Printf("%d: %c\n", i, c) }
// strings.Cut returns a found flag if key, value, found := strings.Cut("mode=fast", "="); found { fmt.Printf("%s -> %s\n", key, value) }
// an array is read by index a := [4]int{10, 20, 30, 40} first, last := a[0], a[len(a)-1] fmt.Printf("first = %d, last = %d\n", first, last)}x = 3, y = -2 0: a 1: b 2: c mode -> fast first = 10, last = 40
data class Point(val x: Int, val y: Int)
fun main() { // destructuring declaration: by position, through component1() and component2() val (x, y) = Point(3, -2) println("x = $x, y = $y")
for ((i, c) in "abc".withIndex()) { println("$i: $c") }
val kv = "mode=fast".split("=", limit = 2) if (kv.size == 2) { val (key, value) = kv println("$key -> $value") }
// a List destructures by position too; _ skips an item val (first, _, _, last) = listOf(10, 20, 30, 40) println("first = $first, last = $last")}x = 3, y = -2 0: a 1: b 2: c mode -> fast first = 10, last = 40
from typing import NamedTuple
class Point(NamedTuple): x: int y: int
# unpacking is by positionx, y = Point(3, -2)print(f"x = {x}, y = {y}")
for i, c in enumerate("abc"): print(f"{i}: {c}")
key, sep, value = "mode=fast".partition("=")if sep: print(f"{key} -> {value}")
# * collects the items in the middlefirst, *_, last = [10, 20, 30, 40]print(f"first = {first}, last = {last}")x = 3, y = -2 0: a 1: b 2: c mode -> fast first = 10, last = 40
const { x, y } = { x: 3, y: -2 };console.log(`x = ${x}, y = ${y}`);
for (const [i, c] of [..."abc"].entries()) { console.log(`${i}: ${c}`);}
const kv = "mode=fast".split("=");if (kv.length === 2) { const [key, value] = kv; console.log(`${key} -> ${value}`);}
// an empty slot skips an itemconst [first, , , last] = [10, 20, 30, 40];console.log(`first = ${first}, last = ${last}`);x = 3, y = -2 0: a 1: b 2: c mode -> fast first = 10, last = 40
public class Main { record Point(int x, int y) {}
public static void main(String[] args) { // a record pattern takes the record apart Point p = new Point(3, -2); if (p instanceof Point(int x, int y)) { System.out.println("x = " + x + ", y = " + y); }
String abc = "abc"; for (int i = 0; i < abc.length(); i++) { System.out.println(i + ": " + abc.charAt(i)); }
String[] kv = "mode=fast".split("=", 2); if (kv.length == 2) { System.out.println(kv[0] + " -> " + kv[1]); }
// an array is read by index int[] a = {10, 20, 30, 40}; System.out.println("first = " + a[0] + ", last = " + a[a.length - 1]); }}x = 3, y = -2 0: a 1: b 2: c mode -> fast first = 10, last = 40
using System;using System.Linq;
var (x, y) = new Point(3, -2);Console.WriteLine($"x = {x}, y = {y}");
foreach (var (c, i) in "abc".Select((c, i) => (c, i))){ Console.WriteLine($"{i}: {c}");}
if ("mode=fast".Split('=', 2) is [var key, var value]){ Console.WriteLine($"{key} -> {value}");}
// a list pattern: .. stands for the items in the middleint[] a = { 10, 20, 30, 40 };if (a is [var first, .., var last]){ Console.WriteLine($"first = {first}, last = {last}");}
record Point(int X, int Y);x = 3, y = -2 0: a 1: b 2: c mode -> fast first = 10, last = 40
#include <stdio.h>#include <string.h>
struct Point { int x, y;};
int main(void) { // no destructuring: read the fields struct Point p = {3, -2}; int x = p.x, y = p.y; printf("x = %d, y = %d\n", x, y);
const char *abc = "abc"; for (int i = 0; abc[i] != '\0'; i++) { printf("%d: %c\n", i, abc[i]); }
// strchr returns a pointer to the '=' or NULL const char *line = "mode=fast"; const char *eq = strchr(line, '='); if (eq != NULL) { printf("%.*s -> %s\n", (int)(eq - line), line, eq + 1); }
int a[] = {10, 20, 30, 40}; printf("first = %d, last = %d\n", a[0], a[3]); return 0;}x = 3, y = -2 0: a 1: b 2: c mode -> fast first = 10, last = 40
#include <array>#include <iostream>#include <string>
struct Point { int x, y;};
int main() { // structured binding: by position auto [x, y] = Point{3, -2}; std::cout << "x = " << x << ", y = " << y << "\n";
std::string abc = "abc"; for (size_t i = 0; i < abc.size(); i++) { std::cout << i << ": " << abc[i] << "\n"; }
std::string line = "mode=fast"; if (auto eq = line.find('='); eq != std::string::npos) { std::cout << line.substr(0, eq) << " -> " << line.substr(eq + 1) << "\n"; }
// an array is read by index std::array<int, 4> a = {10, 20, 30, 40}; std::cout << "first = " << a.front() << ", last = " << a.back() << "\n";}x = 3, y = -2 0: a 1: b 2: c mode -> fast first = 10, last = 40
What's different
- A pattern is a shape with names in it. When the value fits, each name holds its part.
- Patterns stand wherever Rust introduces a variable:
let, parameters,for,if let,while let,let ... else,matches!andmatch. let, parameters andforneed a pattern that always fits. A pattern that can fail belongs inif let,let ... elseormatch.- Matching a value moves a part that is not
Copy, where destructuring in Python or Kotlin only adds names. Match a reference (&opt,opt.as_ref()) to borrow instead. _ignores a part and does not move it._nameis a variable that does.
Taking something apart that might not be there
Looks fine: in Go, TypeScript and C++ it runs but gives a wrong answer; in Kotlin, Python, Java, C# and C it crashes at runtime. Rust won't compile it.
Broken in Go. Rust catches it.Broken in Kotlin. Rust catches it.Broken in Python. Rust catches it.Broken in TypeScript. Rust catches it.Broken in Java. Rust catches it.Broken in C#. Rust catches it.Broken in C. Rust catches it.Broken in C++. Rust catches it.fn main() { let line = "mode"; let Some((key, value)) = line.split_once('='); println!("{key} -> {value}");}package main
import ( "fmt" "strings")
func main() { line := "mode" key, value, _ := strings.Cut(line, "=") fmt.Printf("%s -> %s\n", key, value)}mode ->
fun main() { val line = "mode" val (key, value) = line.split("=") println("$key -> $value")}Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 1, Size: 1 at java.base/java.util.Collections$SingletonList.get(Collections.java:5290) at MainKt.main(Main.kt:3) at MainKt.main(Main.kt)
line = "mode"key, value = line.split("=")print(f"{key} -> {value}")Traceback (most recent call last):
File "main.py", line 2, in <module>
key, value = line.split("=")
^^^^^^^^^^
ValueError: not enough values to unpack (expected 2, got 1)const line = "mode";const [key, value] = line.split("=");console.log(`${key} -> ${value}`);mode -> undefined
public class Main { public static void main(String[] args) { String line = "mode"; String[] parts = line.split("="); System.out.println(parts[0] + " -> " + parts[1]); }}Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1 at Main.main(Main.java:5)
using System;
var line = "mode";var parts = line.Split('=');Console.WriteLine($"{parts[0]} -> {parts[1]}");Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array. at Program.<Main>$(String[] args) in Program.cs:line 5
#include <stdio.h>#include <string.h>
int main(void) { const char *line = "mode"; const char *eq = strchr(line, '='); printf("%.*s -> %s\n", (int)(eq - line), line, eq + 1); return 0;}#include <iostream>#include <string>
int main() { std::string line = "mode"; auto eq = line.find('='); std::cout << line.substr(0, eq) << " -> " << line.substr(eq + 1) << "\n";}mode -> mode
What does the compiler say? Guess first, then open.
error[E0005]: refutable pattern in local binding --> src/main.rs:3:9 | 3 | let Some((key, value)) = line.split_once('='); | ^^^^^^^^^^^^^^^^^^ pattern `None` not covered | = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant = note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html = note: the matched value is of type `Option<(&str, &str)>` help: you might want to use `let...else` to handle the variant that isn't matched | 3 | let Some((key, value)) = line.split_once('=') else { todo!() }; | ++++++++++++++++
strings.Cut returns a found flag, but key, value, _ := throws it away; the layer-1 program keeps it with if ...; found. go vet accepts this version. For mode it prints mode -> with an empty value and no error.
val (key, value) = line.split("=") calls component2() on a list with one item, and throws IndexOutOfBoundsException.
key, value = line.split("=") raises ValueError: not enough values to unpack. Unpacking checks the count, but at run time.
TypeScript accepts this under --strict: value has type string, and at run time it is undefined, so the program prints mode -> undefined. The option --noUncheckedIndexedAccess, which --strict does not include, types value as string | undefined and then refuses value.toUpperCase(): the question Rust asks.
Java has no array pattern, so this indexes: parts[1] throws ArrayIndexOutOfBoundsException for the one-item array that split gives back.
This indexes, it is not a pattern: parts[1] throws IndexOutOfRangeException. C# 11's list pattern, line.Split('=') is [var key, var value], has the shape of Rust's and is a test with an else, so it cannot crash.
strchr returns NULL when there is no =; the layer-1 program checks that, and this version leaves the check out, so eq - line and eq + 1 are arithmetic on a null pointer. Undefined behaviour: on this machine the program died with a segmentation fault.
find returns npos when there is no = (the layer-1 program checks it; this version does not), and npos + 1 wraps to 0, so the value half is the whole line. The program prints mode -> mode with no error.
split_once returns an Option: None when the line has no =. A let pattern has to fit every value it can be given, and Some((key, value)) does not fit None, so Rust refuses (E0005) and suggests let ... else.
The line mode has no =. In the other languages the check for that is a step you can leave out, and nothing complains; the programs here leave it out and were run with that line: a wrong value, undefined, an exception, or a crash. Careful code checks (the layer-1 programs do), and C# 11's list pattern makes you. Rust makes the check part of the pattern, so leaving it out does not build.
The fix
Compiles and runs.
fn main() { let line = "mode"; let Some((key, value)) = line.split_once('=') else { println!("no '=' in {line:?}"); return; }; println!("{key} -> {value}");}no '=' in "mode"
The toolkit
A pattern is a shape a value can have, with names where you want to keep a part. Matching a value against it either fails or gives each name its part, and taking a value apart this way is called destructuring. Match is where you met patterns (literals, ranges, guards, slices) and this page does not repeat it. It covers the other places a pattern can stand, what a pattern does to ownership, and the small rules (_ against _name, &) that trip people up. Outputs come from Linux on 64-bit ARM with Rust 1.99.1.
Patterns stand in many places
Wherever Rust introduces a variable it accepts a pattern. Each line of this program takes something apart in a different place:
struct Point { x: i32, y: i32,}
// a parameter can be a pattern: this takes the point apartfn manhattan(Point { x, y }: &Point) -> i32 { x.abs() + y.abs()}
fn main() { let p = Point { x: 3, y: -4 };
// let let Point { x, y } = p; println!("let: x = {x}, y = {y}");
// function parameter println!("parameter: {}", manhattan(&p));
// for: each item is split into a pair let scores = [("ana", 90), ("bo", 72)]; for (name, score) in scores { println!("for: {name} scored {score}"); }
// if let: only when the shape fits if let Some((key, value)) = "mode=fast".split_once('=') { println!("if let: {key} -> {value}"); }
// while let: until the shape stops fitting let mut stack = vec![1, 2, 3]; while let Some(top) = stack.pop() { println!("while let: popped {top}"); }
// let ... else: the shape must fit, or leave let Some((head, _)) = "a,b".split_once(',') else { return; }; println!("let else: head = {head}");
// a closure parameter let add = |(a, b): (i32, i32)| a + b; println!("closure: {}", add((2, 5)));
// matches! println!("matches!: {}", matches!(p, Point { x: 3, .. }));}let: x = 3, y = -4 parameter: 7 for: ana scored 90 for: bo scored 72 if let: mode -> fast while let: popped 3 while let: popped 2 while let: popped 1 let else: head = a closure: 7 matches!: true
| Place | Example | Must the pattern always fit? |
|---|---|---|
let |
let (a, b) = pair; |
yes |
| function parameter | fn f((a, b): (i32, i32)) |
yes |
| closure parameter | |(a, b)| a + b |
yes |
for |
for (i, c) in … |
yes |
match arm |
Some(n) => … |
no, but together the arms must cover every value |
if let |
if let Some(n) = o { … } |
no |
while let |
while let Some(n) = it.next() { … } |
no |
let … else |
let Some(n) = o else { return }; |
no, and the else must leave |
matches! |
matches!(o, Some(1 | 2)) |
no |
A closure is a small unnamed function (Closures). Some(..) and Ok(..) come from Option and Errors.
Patterns that must always fit
A pattern that fits every value of its type is irrefutable: (a, b) for a pair, Point { x, y }, _. One that can fail is refutable: Some(n), Ok(n), 1..=5. The last column of the table is the rule. Declarations and types showed the error for let and its fix, let ... else. The other places that need a pattern which always fits say the same, and none of them has an else to add:
fn first_of(Some(x): Option<i32>) -> i32 { x}
fn main() { let items = [Some(1), None, Some(3)]; for Some(n) in items { println!("{n}"); } let double = |Some(n): Option<i32>| n * 2; println!("{} {}", first_of(Some(1)), double(Some(2)));}error[E0005]: refutable pattern in function argument --> src/main.rs:1:13 | 1 | fn first_of(Some(x): Option<i32>) -> i32 { | ^^^^^^^ pattern `None` not covered | = note: the matched value is of type `Option<i32>` error[E0005]: refutable pattern in `for` loop binding --> src/main.rs:7:9 | 7 | for Some(n) in items { | ^^^^^^^ pattern `None` not covered | = note: the matched value is of type `Option<i32>` error[E0005]: refutable pattern in closure argument --> src/main.rs:10:19 | 10 | let double = |Some(n): Option<i32>| n * 2; | ^^^^^^^ pattern `None` not covered | = note: the matched value is of type `Option<i32>`
Take the whole value and look inside it with match or if let. For arrays the compiler also checks the length, which is part of the type:
fn main() { let [x, y] = [1, 2, 3]; println!("{x} {y}");}error[E0527]: pattern requires 2 elements but array has 3 --> src/main.rs:2:9 | 2 | let [x, y] = [1, 2, 3]; | ^^^^^^ expected 3 elements
Whether a pattern can fail depends on the type, not on how it looks. Ok(x) cannot fail against a Result whose error type has no values (std::convert::Infallible), so let Ok(x) = r; compiles there.
Taking things apart
A struct pattern names fields, in any order. x: px renames, y: 0 tests, and .. skips what you do not name. A tuple or tuple struct pattern goes by position, and .. skips items in the middle or at the end. Patterns nest as deep as the data does:
struct Point { x: i32, y: i32,}
struct Rgb(u8, u8, u8);
enum Shape { Circle { center: Point, radius: u32 }, Rect(Point, Point),}
fn describe(shape: &Shape) -> String { match shape { Shape::Circle { center: Point { x: 0, y: 0 }, radius, } => format!("circle at the origin, radius {radius}"), Shape::Circle { center, radius } => { format!("circle at ({}, {}), radius {radius}", center.x, center.y) } Shape::Rect(Point { x: x1, y: y1 }, Point { x: x2, y: y2 }) => { format!("rect {} wide, {} high", x2 - x1, y2 - y1) } }}
fn main() { // by field name, in any order; a new name after the colon let Point { y, x: px } = Point { x: 3, y: -2 }; println!("px = {px}, y = {y}");
// `..` skips the fields (or items) you do not name let color = Rgb(200, 100, 50); let Rgb(red, ..) = color; let Rgb(_, green, blue) = color; let (first, .., last) = (1, 2, 3, 4); println!("red = {red}, green = {green}, blue = {blue}"); println!("first = {first}, last = {last}");
// patterns nest as deep as the data does let shapes = [ Shape::Circle { center: Point { x: 0, y: 0 }, radius: 2, }, Shape::Circle { center: Point { x: 5, y: 1 }, radius: 3, }, Shape::Rect(Point { x: 1, y: 1 }, Point { x: 4, y: 3 }), ]; for shape in &shapes { println!("{}", describe(shape)); }
// an Option of a pair, and a pair of Options let found = Some(("ana", 30)); if let Some((name, age)) = found { println!("{name} is {age}"); } match (Some(1), None::<i32>) { (Some(a), Some(b)) => println!("both: {a} {b}"), (Some(a), None) | (None, Some(a)) => println!("exactly one: {a}"), (None, None) => println!("neither"), }}px = 3, y = -2 red = 200, green = 100, blue = 50 first = 1, last = 4 circle at the origin, radius 2 circle at (5, 1), radius 3 rect 3 wide, 2 high ana is 30 exactly one: 1
A struct pattern has to mention every field or end in ... Leaving one out is an error:
struct Point { x: i32, y: i32, z: i32,}
fn main() { let p = Point { x: 1, y: 2, z: 3 }; // a struct pattern must mention every field, or end in `..` let Point { x } = p; println!("x = {x}");}error[E0027]: pattern does not mention fields `y`, `z` --> src/main.rs:10:9 | 10 | let Point { x } = p; | ^^^^^^^^^^^ missing fields `y`, `z` | help: include the missing fields in the pattern | 10 | let Point { x, y, z } = p; | ++++++ help: if you don't care about these missing fields, you can explicitly ignore them | 10 | let Point { x, y: _, z: _ } = p; | ++++++++++++ help: or always ignore missing fields here | 10 | let Point { x, .. } = p; | ++++
A slice pattern fits an array or a slice, not a Vec (Match has the shapes). Slice the Vec first, with as_slice() or [..]; the rest you get is a borrowed slice, not a new list. The RPN exercise below reads its stack with stack[..]:
fn main() { // a slice pattern needs an array or a slice, so slice the Vec first let words = vec!["a", "b", "c"]; if let [first, rest @ ..] = words.as_slice() { println!("first = {first}, rest = {rest:?}"); }
let mut stack = vec![7]; for _ in 0..2 { match stack[..] { [] => println!("empty"), [only] => println!("one value: {only}"), [.., top] => println!("top of {}: {top}", stack.len()), } stack.push(8); }}first = a, rest = ["b", "c"] one value: 7 top of 2: 8
Alternatives and @
Match showed | and @ at the top of a pattern. Both also work inside another pattern, so Some(1 | 2) is shorter than two arms. Every alternative must bind the same names with the same types, or the compiler says which one is missing:
fn size(n: Option<u32>) -> String { match n { // an or-pattern can sit inside another pattern Some(1 | 2) => String::from("one or two"), // `@` names the whole value and still tests the shape Some(small @ 3..=9) => format!("small: {small}"), Some(other) => format!("big: {other}"), None => String::from("missing"), }}
fn main() { for n in [Some(2), Some(5), Some(50), None] { println!("{n:?} -> {}", size(n)); }
// both sides of | must bind the same names, with the same types let results = [Ok(3), Err(4)]; for r in results { let (Ok(n) | Err(n)) = r; println!("{r:?} -> n = {n}"); }}Some(2) -> one or two Some(5) -> small: 5 Some(50) -> big: 50 None -> missing Ok(3) -> n = 3 Err(4) -> n = 4
fn main() { let pair = (Some(1), Some(2)); match pair { // `a` is bound on the left of the `|` but not on the right (Some(a), _) | (_, None) => println!("matched"), _ => println!("other"), }}error[E0408]: variable `a` is not bound in all patterns --> src/main.rs:5:24 | 5 | (Some(a), _) | (_, None) => println!("matched"), | - ^^^^^^^^^ pattern doesn't bind `a` | | | variable not in all patterns
A name always binds
A name in a pattern is a new variable, not a comparison; the exception is a const, which is compared (Match). That includes the name of an enum variant written without its path, which the compiler catches (E0170, shown in Enums). A static is not compared either. A name that matches a static would be read as a new binding, and the compiler refuses it (E0530):
static LIMIT: u32 = 10;
fn main() { let spent = 12; match spent { LIMIT => println!("at the limit"), _ => println!("elsewhere"), }}error[E0530]: match bindings cannot shadow statics --> src/main.rs:6:9 | 1 | static LIMIT: u32 = 10; | ----------------------- the static `LIMIT` is defined here ... 6 | LIMIT => println!("at the limit"), | ^^^^^ cannot be named the same as a static
_ ignores, _name binds
_ is a pattern that fits anything and binds nothing. A name that starts with an underscore is a normal variable that only tells the compiler not to warn when it is unused. The difference shows when the value is not Copy: _ leaves it where it is, and _name moves it. Inside a pattern, let (_, right) = pair; moves only the right half.
fn main() { let s = String::from("hello"); let pair = (String::from("left"), String::from("right"));
let _ = s; // `_` does not bind, so nothing moves println!("after `let _ = s`, s is still {s}");
let (_, right) = pair; // only the right half moves println!("after `let (_, right) = pair`, pair.0 is still {}", pair.0); println!("right is {right}");}after `let _ = s`, s is still hello after `let (_, right) = pair`, pair.0 is still left right is right
fn main() { let s = String::from("hello"); let _unused = s; // a name starting with `_` is still a name: it binds println!("{s}");}error[E0382]: borrow of moved value: `s` --> src/main.rs:4:16 | 2 | let s = String::from("hello"); | - move occurs because `s` has type `String`, which does not implement the `Copy` trait 3 | let _unused = s; // a name starting with `_` is still a name: it binds | - value moved here 4 | println!("{s}"); | ^ value borrowed here after move | help: consider cloning the value if the performance cost is acceptable | 3 | let _unused = s.clone(); // a name starting with `_` is still a name: it binds | ++++++++
References in patterns
When you match a reference against a pattern that is not a reference, Rust looks through the reference and makes each name a reference too. The name’s type follows the value: a copy for a value, &T for &value, &mut T for &mut value. These are called the default binding modes, also known as match ergonomics. This program prints the type of each name:
fn show<T>(label: &str, _value: &T) { println!("{label}: {}", std::any::type_name::<T>());}
fn main() { let mut n = Some(5); let pair = (1, 2);
// the value itself: the name gets a copy if let Some(x) = n { show("if let Some(x) = n", &x); } // a shared reference to the value: the name gets a shared reference if let Some(x) = &n { show("if let Some(x) = &n", &x); } // a mutable reference: the name gets a mutable reference if let Some(x) = &mut n { *x += 1; show("if let Some(x) = &mut n", &x); } println!("n is now {n:?}");
// the same for tuples let (a, _) = &pair; show("let (a, _) = &pair", &a); // a & in the pattern strips the reference by hand let &(b, _) = &pair; show("let &(b, _) = &pair", &b);}if let Some(x) = n: i32 if let Some(x) = &n: &i32 if let Some(x) = &mut n: &mut i32 n is now Some(6) let (a, _) = &pair: &i32 let &(b, _) = &pair: i32
| You match | The names are | Use it to |
|---|---|---|
value |
copies, or moved parts | take the value apart and use it up |
&value |
&T into the value |
read parts and keep the value |
&mut value |
&mut T into the value |
change parts in place |
A & in a pattern goes the other way: it removes one reference by hand, so the name is the value behind it. That is handy for a Copy part such as a number. for x in &v and for &x in &v differ in just this way (Looping Over a Collection explains the loop forms):
fn show<T>(label: &str, _value: &T) { println!("{label}: {}", std::any::type_name::<T>());}
fn main() { let pairs = vec![(1, 10), (2, 20)];
// `first` gives an Option holding a reference to the first pair // no &: the pattern passes through the reference, so a and b are references if let Some((a, b)) = pairs.first() { show("Some((a, b)), a", &a); show("Some((a, b)), b", &b); } // with &: the pattern strips the reference, so a and b are plain numbers if let Some(&(a, b)) = pairs.first() { show("Some(&(a, b)), a", &a); show("Some(&(a, b)), b", &b); }
// in a loop it is the same choice let mut total = 0; for &(a, b) in &pairs { total += a * b; } println!("total: {total}");}Some((a, b)), a: &i32 Some((a, b)), b: &i32 Some(&(a, b)), a: i32 Some(&(a, b)), b: i32 total: 50
A related surprise: a &i32 cannot be compared with 60.
fn main() { let scores = vec![40, 75, 90]; for score in &scores { if score > 60 { println!("{score} passes"); } }}error[E0308]: mismatched types --> src/main.rs:4:20 | 4 | if score > 60 { | ^^ expected `&_`, found integer | = note: expected reference `&_` found type `{integer}` help: consider dereferencing the borrow | 4 | if *score > 60 { | +
Write *score > 60, or for &score in &scores. Closure parameters are patterns too, which is why |&x| and |&&x| appear in iterator chains (Iterators explains the references being taken off). For a part that is not Copy, a & pattern cannot work, because it would move the part out of a borrowed value:
fn main() { let names = vec![String::from("ana"), String::from("bo")]; for &name in &names { println!("{name}"); }}error[E0507]: cannot move out of a shared reference --> src/main.rs:3:18 | 3 | for &name in &names { | ---- ^^^^^^ | | | data moved here because `name` has type `String`, which does not implement the `Copy` trait | help: consider removing the borrow | 3 - for &name in &names { 3 + for name in &names {
Does a pattern move, copy or borrow?
Matching a value by value moves each part that is not Copy into its name, and the value can no longer be used whole. A struct can be partly moved. (The compiler suggests ref name, the older spelling of “borrow this part”, explained below.)
struct Person { name: String, age: u32,}
fn main() { let p = Person { name: String::from("Ana"), age: 30, }; let Person { name, age } = p; println!("{name} is {age}"); println!("p.name is still {}", p.name);}error[E0382]: borrow of moved value: `p.name` --> src/main.rs:13:36 | 11 | let Person { name, age } = p; | ---- value moved here 12 | println!("{name} is {age}"); 13 | println!("p.name is still {}", p.name); | ^^^^^^ value borrowed here after move | = note: move occurs because `p.name` has type `String`, which does not implement the `Copy` trait help: borrow this binding in the pattern to avoid moving the value | 11 | let Person { ref name, age } = p; | +++
A frequent case is an Option<String> in an if let:
fn main() { let nick: Option<String> = Some(String::from("ana")); if let Some(n) = nick { println!("hello, {n}"); } println!("nick is {nick:?}");}error[E0382]: borrow of partially moved value: `nick` --> src/main.rs:6:24 | 3 | if let Some(n) = nick { | - value partially moved here ... 6 | println!("nick is {nick:?}"); | ^^^^ value borrowed here after partial move | = note: partial move occurs because value has type `String`, which does not implement the `Copy` trait help: borrow this binding in the pattern to avoid moving the value | 3 | if let Some(ref n) = nick { | +++
Match a reference instead: &nick, or nick.as_ref(), which makes an Option<&String> (Option::as_ref). Either way n is a &String, and &mut nick lets the arm change the text in place:
fn main() { let mut nick: Option<String> = Some(String::from("ana"));
// match on a reference: `n` is a &String and nick stays whole if let Some(n) = &nick { println!("hello, {n}"); } // as_ref() makes the same Option<&String> as a method call if let Some(n) = nick.as_ref() { println!("hello again, {n}"); } // a mutable reference lets the arm change the String in place if let Some(n) = &mut nick { n.push_str("!!"); } println!("nick is {nick:?}");}hello, ana
hello again, ana
nick is Some("ana!!")To compare text inside an Option<String> with string literals, as_deref() makes an Option<&str> without moving anything (Option::as_deref):
fn greeting(nick: &Option<String>) -> String { // as_deref turns &Option<String> into Option<&str>, which string patterns can test match nick.as_deref() { Some("ana") => String::from("hello, ana"), Some(other) => format!("hi, {other}"), None => String::from("hello, stranger"), }}
fn main() { for nick in [Some(String::from("ana")), Some(String::from("bo")), None] { println!("{nick:?} -> {}", greeting(&nick)); }}Some("ana") -> hello, ana
Some("bo") -> hi, bo
None -> hello, strangerBefore default binding modes, the way to borrow a part was ref (and ref mut) in front of the name, as the compiler’s hint in the first error shows. It still works on a value that is not behind a reference:
struct Person { name: String, age: u32,}
fn main() { let p = Person { name: String::from("Ana"), age: 30, };
// `ref` borrows the field instead of moving it out let Person { ref name, age } = p; println!("ref: {name} is {age}");
// matching on a reference borrows too, without `ref`; here `age` is a &u32 as well let Person { name, age } = &p; println!("&p: {name} is {age}");
// p is still whole, so it can be used or moved afterwards let moved = p; println!("moved: {}", moved.name);}ref: Ana is 30 &p: Ana is 30 moved: Ana
&p borrows the same way without ref, but every name, age too, is then a reference.
When the compiler complains
| Error | What happened | Fix |
|---|---|---|
| E0005 | a pattern that can fail stands where it must always fit | if let, let ... else or match |
| E0027 | a struct pattern left a field out | name it, or end with .. |
| E0527 | an array pattern has the wrong number of items | match the length, or use .. |
| E0408 | the sides of a | bind different names |
bind the same names on every side |
| E0170 | a bare name that looks like a variant binds instead | write the path, or use the variants |
| E0530 | a static used as a pattern |
use a const, or a guard |
| E0382 | a pattern moved a part out, then the whole value was used | match a reference, or ref |
| E0507 | a & pattern tried to move out of a borrow |
drop the &, or clone |
| E0308 | &i32 met i32 |
*x, or a & pattern |
Going deeper
What a binding does in memory
A pattern by value and a pattern through a reference build different things. The program prints what the two diagrams show:
struct Person { name: String, age: u32,}
fn main() { // the pattern takes `p` apart by value let p = Person { name: String::from("Ana"), age: 30, }; let heap_text = p.name.as_ptr(); let Person { name, age } = p; println!( "by value: name is the same heap text: {}", name.as_ptr() == heap_text ); println!("by value: name len {}, cap {}", name.len(), name.capacity()); println!("by value: age is a copy, p.age is still {}", p.age); println!("by value: age is {age}");
// the pattern looks at `q` through a reference let q = Person { name: String::from("Bo"), age: 25, }; println!( "by reference: q.name len {}, cap {}", q.name.len(), q.name.capacity() ); let Person { name, age } = &q; println!( "by reference: name points at q.name: {}", std::ptr::eq(name, &q.name) ); println!( "by reference: age points at q.age: {}", std::ptr::eq(age, &q.age) );}by value: name is the same heap text: true by value: name len 3, cap 3 by value: age is a copy, p.age is still 30 by value: age is 30 by reference: q.name len 2, cap 2 by reference: name points at q.name: true by reference: age points at q.age: true
Two rules that live elsewhere
When a value that nothing holds is dropped, and why let _ = lock.lock() is refused, are in Ownership. Some pattern rules depend on the edition set in Cargo.toml: a let chain (if let Some(n) = v && n > 5) needs edition 2024 (If / else runs it under both), and the Edition Guide lists where edition 2024 reserves ref, mut and & under a default binding mode.
Practice
Try it
Each exercise has checks in main. Open it in the Playground, replace the todo!(), and run it until it prints all checks passed.
Describe a shape
Take a Shape apart: a circle, a rectangle given by two corners, and a tag that owns a String. describe takes &Shape, so the names your patterns bind are references; grow takes &mut Shape, so they need a *. The checks include corners in either order, empty shapes, and the shapes afterwards.
struct Point { x: i32, y: i32,}
enum Shape { Circle { center: Point, radius: u32 }, Rect(Point, Point), Tag { name: String, at: Point },}
/// Circle: "circle r=2 at (0, 0)". Rect (the corners can come in either order): "rect 3x2",/// width then height. Tag: "tag ana at (5, -1)". A circle with radius 0, or a rect with/// no width or no height, is "empty".fn describe(shape: &Shape) -> String { todo!("match on the shape; each arm names the fields it needs")}
/// Make a shape bigger in place: a circle's radius goes up by 1, and a rect's second corner/// moves 1 right and 1 down (x + 1, y + 1). A tag does not change.fn grow(shape: &mut Shape) { todo!("names bound through &mut are &mut references: write *radius += 1")}
fn main() { let at = |x, y| Point { x, y }; let mut shapes = vec![ Shape::Circle { center: at(0, 0), radius: 2, }, Shape::Rect(at(1, 1), at(4, 3)), Shape::Rect(at(4, 3), at(1, 1)), Shape::Tag { name: String::from("ana"), at: at(5, -1), }, Shape::Circle { center: at(0, 0), radius: 0, }, Shape::Rect(at(2, 2), at(2, 9)), ]; let before: Vec<String> = shapes.iter().map(describe).collect(); assert_eq!( before, [ "circle r=2 at (0, 0)", "rect 3x2", "rect 3x2", "tag ana at (5, -1)", "empty", "empty", ] ); // describing only borrows: the tag still owns its name afterwards assert_eq!(describe(&shapes[3]), "tag ana at (5, -1)");
for shape in &mut shapes { grow(shape); } let after: Vec<String> = shapes.iter().map(describe).collect(); assert_eq!( after, [ "circle r=3 at (0, 0)", "rect 4x3", "rect 2x1", "tag ana at (5, -1)", "circle r=1 at (0, 0)", "rect 1x8", ] ); 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
struct Point { x: i32, y: i32,}
enum Shape { Circle { center: Point, radius: u32 }, Rect(Point, Point), Tag { name: String, at: Point },}
/// Circle: "circle r=2 at (0, 0)". Rect (the corners can come in either order): "rect 3x2",/// width then height. Tag: "tag ana at (5, -1)". A circle with radius 0, or a rect with/// no width or no height, is "empty".fn describe(shape: &Shape) -> String { match shape { Shape::Circle { radius: 0, .. } => String::from("empty"), Shape::Circle { center: Point { x, y }, radius, } => format!("circle r={radius} at ({x}, {y})"), Shape::Rect(Point { x: x1, y: y1 }, Point { x: x2, y: y2 }) => { let (w, h) = (x1.abs_diff(*x2), y1.abs_diff(*y2)); if w == 0 || h == 0 { String::from("empty") } else { format!("rect {w}x{h}") } } Shape::Tag { name, at: Point { x, y }, } => format!("tag {name} at ({x}, {y})"), }}
/// Make a shape bigger in place: a circle's radius goes up by 1, and a rect's second corner/// moves 1 right and 1 down (x + 1, y + 1). A tag does not change.fn grow(shape: &mut Shape) { match shape { Shape::Circle { radius, .. } => *radius += 1, Shape::Rect(_, Point { x, y }) => { *x += 1; *y += 1; } Shape::Tag { .. } => {} }}
fn main() { let at = |x, y| Point { x, y }; let mut shapes = vec![ Shape::Circle { center: at(0, 0), radius: 2, }, Shape::Rect(at(1, 1), at(4, 3)), Shape::Rect(at(4, 3), at(1, 1)), Shape::Tag { name: String::from("ana"), at: at(5, -1), }, Shape::Circle { center: at(0, 0), radius: 0, }, Shape::Rect(at(2, 2), at(2, 9)), ]; let before: Vec<String> = shapes.iter().map(describe).collect(); assert_eq!( before, [ "circle r=2 at (0, 0)", "rect 3x2", "rect 3x2", "tag ana at (5, -1)", "empty", "empty", ] ); // describing only borrows: the tag still owns its name afterwards assert_eq!(describe(&shapes[3]), "tag ana at (5, -1)");
for shape in &mut shapes { grow(shape); } let after: Vec<String> = shapes.iter().map(describe).collect(); assert_eq!( after, [ "circle r=3 at (0, 0)", "rect 4x3", "rect 2x1", "tag ana at (5, -1)", "circle r=1 at (0, 0)", "rect 1x8", ] ); println!("all checks passed");}all checks passed
Borrow the nickname
Person has an Option<String> nickname. Return the nickname, or the name, borrowed from the person. match person.nick would move the String out of a borrow, so match a reference (as_deref also works). The checks include non-ASCII text, a tie, and an answer that must point into the person, not at a copy.
struct Person { name: String, nick: Option<String>,}
/// The nickname if there is one, otherwise the name. Borrow it from the person: do not clone.fn display_name(person: &Person) -> &str { todo!("match &person.nick: matching person.nick itself would try to move the String out")}
/// The longest nickname (counting characters, not bytes), borrowed from the people./// The first one wins a tie. None if nobody has a nickname.fn longest_nick(people: &[Person]) -> Option<&str> { todo!("loop over the people and bind each nickname through a reference")}
fn main() { let person = |name: &str, nick: Option<&str>| Person { name: String::from(name), nick: nick.map(String::from), }; let people = vec![ person("Ana", Some("annie")), person("Bo", None), person("Cy", Some("ñññ")), person("Di", Some("abcd")), person("Ed", Some("wxyz")), ]; assert_eq!(display_name(&people[0]), "annie"); assert_eq!(display_name(&people[1]), "Bo");
// the answer points into the person: nothing was copied or moved out let shown = display_name(&people[0]); assert!(std::ptr::eq( shown.as_ptr(), people[0].nick.as_ref().unwrap().as_ptr() ));
assert_eq!(longest_nick(&people), Some("annie")); // "ñññ" is 3 characters but 6 bytes, and "abcd" and "wxyz" tie at 4: the first wins assert_eq!(longest_nick(&people[2..]), Some("abcd")); assert_eq!(longest_nick(&people[1..2]), None); assert_eq!(longest_nick(&[]), None);
// the people are still whole afterwards assert_eq!(people[0].nick.as_deref(), Some("annie")); assert_eq!(people[3].name, "Di"); 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
struct Person { name: String, nick: Option<String>,}
/// The nickname if there is one, otherwise the name. Borrow it from the person: do not clone.fn display_name(person: &Person) -> &str { match &person.nick { Some(nick) => nick, None => &person.name, }}
/// The longest nickname (counting characters, not bytes), borrowed from the people./// The first one wins a tie. None if nobody has a nickname.fn longest_nick(people: &[Person]) -> Option<&str> { let mut best: Option<&str> = None; for person in people { if let Some(nick) = &person.nick { let longer = match best { Some(current) => nick.chars().count() > current.chars().count(), None => true, }; if longer { best = Some(nick); } } } best}
fn main() { let person = |name: &str, nick: Option<&str>| Person { name: String::from(name), nick: nick.map(String::from), }; let people = vec![ person("Ana", Some("annie")), person("Bo", None), person("Cy", Some("ñññ")), person("Di", Some("abcd")), person("Ed", Some("wxyz")), ]; assert_eq!(display_name(&people[0]), "annie"); assert_eq!(display_name(&people[1]), "Bo");
// the answer points into the person: nothing was copied or moved out let shown = display_name(&people[0]); assert!(std::ptr::eq( shown.as_ptr(), people[0].nick.as_ref().unwrap().as_ptr() ));
assert_eq!(longest_nick(&people), Some("annie")); // "ñññ" is 3 characters but 6 bytes, and "abcd" and "wxyz" tie at 4: the first wins assert_eq!(longest_nick(&people[2..]), Some("abcd")); assert_eq!(longest_nick(&people[1..2]), None); assert_eq!(longest_nick(&[]), None);
// the people are still whole afterwards assert_eq!(people[0].nick.as_deref(), Some("annie")); assert_eq!(people[3].name, "Di"); println!("all checks passed");}all checks passed
Reverse Polish
Evaluate [Num(3), Num(4), Op('+')]: numbers go on a stack, and an operator pops two. The token slice is borrowed, so the names your pattern binds are references (*n gets the number). Use let (Some(right), Some(left)) = (stack.pop(), stack.pop()) else { … } (two ? also work), and match stack[..] at the end, as in the slice example above. The checks include the order of the operands, too few operands, a division by zero and leftover values; ignore overflow.
enum Token { Num(i64), Op(char),}
/// Evaluate a reverse Polish expression: numbers go on a stack, and an operator/// takes the top two (the older one is the left operand) and pushes the result./// None for: a missing operand, an operator other than + - * /, a division by/// zero, or anything but exactly one value left at the end.fn eval(tokens: &[Token]) -> Option<i64> { todo!("match each token; let ... else for the two operands; match stack[..] at the end")}
fn main() { use Token::{Num, Op}; assert_eq!(eval(&[Num(3), Num(4), Op('+')]), Some(7)); assert_eq!(eval(&[Num(2), Num(3), Op('-')]), Some(-1)); assert_eq!(eval(&[Num(8), Num(2), Op('/')]), Some(4)); // (5 + ((1 + 2) * 4)) - 3 let long = [ Num(5), Num(1), Num(2), Op('+'), Num(4), Op('*'), Op('+'), Num(3), Op('-'), ]; assert_eq!(eval(&long), Some(14)); assert_eq!(eval(&[Num(7)]), Some(7)); assert_eq!(eval(&[]), None); assert_eq!(eval(&[Num(1), Num(2)]), None); assert_eq!(eval(&[Op('+')]), None); assert_eq!(eval(&[Num(1), Op('+')]), None); assert_eq!(eval(&[Num(1), Num(0), Op('/')]), None); assert_eq!(eval(&[Num(1), Num(2), Op('^')]), None); 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
enum Token { Num(i64), Op(char),}
/// Evaluate a reverse Polish expression: numbers go on a stack, and an operator/// takes the top two (the older one is the left operand) and pushes the result./// None for: a missing operand, an operator other than + - * /, a division by/// zero, or anything but exactly one value left at the end.fn eval(tokens: &[Token]) -> Option<i64> { let mut stack: Vec<i64> = Vec::new(); for token in tokens { match token { Token::Num(n) => stack.push(*n), Token::Op(op) => { let (Some(right), Some(left)) = (stack.pop(), stack.pop()) else { return None; }; let value = match op { '+' => left + right, '-' => left - right, '*' => left * right, '/' if right != 0 => left / right, _ => return None, }; stack.push(value); } } } match stack[..] { [result] => Some(result), _ => None, }}
fn main() { use Token::{Num, Op}; assert_eq!(eval(&[Num(3), Num(4), Op('+')]), Some(7)); assert_eq!(eval(&[Num(2), Num(3), Op('-')]), Some(-1)); assert_eq!(eval(&[Num(8), Num(2), Op('/')]), Some(4)); // (5 + ((1 + 2) * 4)) - 3 let long = [ Num(5), Num(1), Num(2), Op('+'), Num(4), Op('*'), Op('+'), Num(3), Op('-'), ]; assert_eq!(eval(&long), Some(14)); assert_eq!(eval(&[Num(7)]), Some(7)); assert_eq!(eval(&[]), None); assert_eq!(eval(&[Num(1), Num(2)]), None); assert_eq!(eval(&[Op('+')]), None); assert_eq!(eval(&[Num(1), Op('+')]), None); assert_eq!(eval(&[Num(1), Num(0), Op('/')]), None); assert_eq!(eval(&[Num(1), Num(2), Op('^')]), None); println!("all checks passed");}all checks passed
Read more
- The Rust Book, chapter 19: Patterns and Matching: places, refutability and syntax.
- The Rust Reference: Patterns, with the exact rules for binding modes and or-patterns.
- Rust by Example: destructuring,
if let,let ... elseandwhile let. - The Edition Guide: match ergonomics reservations and let chains.
matches!in the standard library docs.- Reading compiler errors: E0005, E0027, E0382, E0507, E0527 and E0408 from this page.
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.