콘텐츠로 이동

Rust 패턴 매칭

match 페이지에서 기본을 배웠어요. 갈래(arm), _, |로 값 묶기, 그리고 범위였죠. 그건 시작일 뿐이에요. Rust의 패턴은 값의 안쪽을 들여다보고, 필요한 부분을 꺼내고, 추가 조건까지 확인할 수 있어요. 그것도 한 줄로요.

패턴을 쿠키 틀이라고 생각해 보세요. 틀을 값에 꾹 눌러요. 모양이 맞으면 Rust가 안에 든 조각들을 건네줘요. 모양이 맞지 않으면 Rust는 다음 갈래를 시도해요. 이게 한번 이해되면 패턴을 여기저기서 쓰게 될 거예요. match에서, let에서, 함수 매개변수에서, 그리고 if let에서도요.

모양만으로는 부족해서 추가로 확인해야 할 때가 있어요. 매치 가드(match guard)는 패턴 뒤에 쓰는 if 조건이에요. 패턴이 맞고 그리고 조건도 참일 때만 그 갈래가 실행돼요.

fn main() {
let temperatures = [-5, 0, 18, 35];
for t in temperatures {
let feeling = match t {
t if t < 0 => "꽁꽁 얼어요",
0 => "딱 0도예요",
t if t < 25 => "쾌적해요",
_ => "더워요",
};
println!("{t}°C는 {feeling}");
}
}

실행 결과:

-5°C는 꽁꽁 얼어요
0°C는 딱 0도예요
18°C는 쾌적해요
35°C는 더워요
▶ 직접 실행해 보기

Rust는 갈래를 위에서부터 아래로 확인하기 때문에 순서가 중요해요. 18은 첫 번째 가드를 통과하지 못하고, 0도 아니지만, t < 25는 통과해요. 그래서 “쾌적해요”가 돼요.

구조 분해(destructuring)는 값을 여러 부분으로 쪼개는 거예요. 튜플이라면 튜플과 같은 모양으로 패턴을 쓰면 돼요. 구조체라면 원하는 필드 이름을 적어요.

struct Point {
x: i32,
y: i32,
}
fn main() {
let pair = (3, -7);
match pair {
(0, 0) => println!("원점"),
(x, 0) => println!("x축 위의 {x}"),
(0, y) => println!("y축 위의 {y}"),
(x, y) => println!("다른 곳: ({x}, {y})"),
}
let p = Point { x: 0, y: 5 };
match p {
Point { x: 0, y } => println!("y축 위, y = {y}"),
Point { x, y: 0 } => println!("x축 위, x = {x}"),
Point { x, y } => println!("위치: ({x}, {y})"),
}
}

실행 결과:

다른 곳: (3, -7)
y축 위, y = 5
▶ 직접 실행해 보기

두 가지가 섞여 있는 걸 눈여겨보세요. 0은 정확히 일치해야 하는 고정된 값이에요. x와 y는 그 자리에 무엇이 있든 받아 내는 새 변수예요. Point { x, y }는 Point { x: x, y: y }를 줄여 쓴 거예요.

데이터를 가진 열거형 구조 분해하기

섹션 제목: “데이터를 가진 열거형 구조 분해하기”

패턴이 진짜 빛을 발하는 곳이 여기예요. 열거형의 배리언트(variant)는 데이터를 가질 수 있고, 패턴은 어떤 배리언트인지 확인하면서 그 데이터를 꺼낼 수 있어요.

enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Square(f64),
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Square(side) => side * side,
}
}
fn main() {
let shapes = [
Shape::Circle { radius: 2.0 },
Shape::Rectangle { width: 3.0, height: 4.5 },
Shape::Square(1.5),
];
for shape in &shapes {
println!("넓이 = {:.2}", area(shape));
}
}

실행 결과:

넓이 = 12.57
넓이 = 13.50
넓이 = 2.25
▶ 직접 실행해 보기

값이 Circle이라는 게 먼저 확인되지 않으면 radius에 접근할 수 없어요. 이게 바로 안전성의 이점이에요. 컴파일러는 엉뚱한 배리언트에서 데이터를 읽도록 내버려 두지 않아요.

패턴 안에 다른 패턴을 원하는 만큼 깊이 넣을 수 있어요. 아래 예제에서 Command 열거형은 Direction 열거형을 담고 있고, 갈래 하나가 두 단계를 한 번에 매칭해요.

@ 바인딩을 쓰면 값을 패턴과 비교하면서 동시에 변수에 담아 둘 수 있어요. n @ 1..=9는 “1에서 9 사이인지 확인하고, 그 값을 n이라고 부르자”라는 뜻이에요.

enum Direction {
Up,
Down,
}
enum Command {
Move(Direction, u32),
Say(String),
Quit,
}
fn run(cmd: Command) {
match cmd {
Command::Move(Direction::Up, steps @ 1..=9) => println!("위로 {steps}칸 (작은 걸음)"),
Command::Move(Direction::Up, steps) => println!("위로 {steps}칸 (크게 점프!)"),
Command::Move(Direction::Down, steps) => println!("아래로 {steps}칸"),
Command::Say(text) => println!("말하기: {text}"),
Command::Quit => println!("잘 가요"),
}
}
fn main() {
run(Command::Move(Direction::Up, 3));
run(Command::Move(Direction::Up, 50));
run(Command::Move(Direction::Down, 2));
run(Command::Say(String::from("안녕하세요")));
run(Command::Quit);
}

실행 결과:

위로 3칸 (작은 걸음)
위로 50칸 (크게 점프!)
아래로 2칸
말하기: 안녕하세요
잘 가요
▶ 직접 실행해 보기

@가 없으면 1..=9 같은 범위는 값을 확인만 하고 버려요. @를 쓰면 그 값을 계속 쓸 수 있어요.

필요한 조각이 일부뿐일 때가 많아요. 값 하나를 건너뛰려면 _를, “나머지 전부”를 건너뛰려면 ..를 쓰세요.

struct Player {
name: String,
level: u32,
gold: u32,
}
fn main() {
let rgb = (255, 128, 0);
let (red, _, _) = rgb;
println!("빨강 = {red}");
let numbers = (1, 2, 3, 4, 5);
let (first, .., last) = numbers;
println!("처음 = {first}, 마지막 = {last}");
let hero = Player { name: String::from("Mira"), level: 7, gold: 120 };
let Player { name, level, .. } = hero;
println!("{name}: 레벨 {level}");
}

실행 결과:

빨강 = 255
처음 = 1, 마지막 = 5
Mira: 레벨 7
▶ 직접 실행해 보기

..는 패턴 하나에 한 번만 쓸 수 있어요. (first, .., last)는 괜찮지만 (.., middle, ..)는 안 돼요. Rust가 어느 값을 말하는 건지 알 수 없으니까요.

let과 함수 매개변수에서의 패턴

섹션 제목: “let과 함수 매개변수에서의 패턴”

사실 모든 let은 패턴이에요. 모든 함수 매개변수도 마찬가지예요. 그래서 값이 들어오는 바로 그 자리에서 구조 분해할 수 있어요.

fn print_coords(&(x, y): &(i32, i32)) {
println!("x: {x}, y: {y}");
}
fn main() {
let (width, height) = (1920, 1080);
println!("{width}x{height}");
print_coords(&(4, -2));
let points = vec![(1, 2), (3, 4), (5, 6)];
let sums: Vec<i32> = points.iter().map(|&(a, b)| a + b).collect();
println!("{:?}", sums);
}

실행 결과:

1920x1080
x: 4, y: -2
[3, 7, 11]
▶ 직접 실행해 보기

클로저 |&(a, b)| a + b도 같은 원리예요. 각 항목은 &(i32, i32)이고, 패턴이 그것을 바로 a와 b로 풀어 줘요. 반복자를 쓸 때 이런 모습을 자주 보게 될 거예요.

match를 통째로 쓰지 않아도 되게 해 주는 편리한 도구가 두 가지 있어요.

matches!(value, pattern) 매크로는 true 또는 false를 돌려줘요. 예/아니요 답만 필요할 때 if나 .filter() 안에서 쓰기 딱 좋아요. 여기서도 가드를 쓸 수 있어요.

let-else는 “이걸 풀어 보고, 안 되면 바로 떠나”를 처리해요. let 패턴 = 값 else { ... };처럼 써요. 패턴이 맞으면 그 변수들을 함수의 나머지 부분에서 쓸 수 있어요. 맞지 않으면 else 블록이 실행되는데, 이 블록은 return, break, continue, 또는 패닉으로 반드시 빠져나가야 해요.

fn parse_age(input: &str) -> Option<u32> {
let Ok(age) = input.parse::<u32>() else {
println!("'{input}': 숫자가 아니에요");
return None;
};
Some(age)
}
fn main() {
let grade = 'B';
println!("{}", matches!(grade, 'A' | 'B'));
let scores = [45, 72, 90, 38, 66];
let passed = scores.iter().filter(|s| matches!(s, 50..=100)).count();
println!("합격: {passed}명");
println!("{:?}", parse_age("31"));
println!("{:?}", parse_age("서른"));
}

실행 결과:

true
합격: 3명
Some(31)
'서른': 숫자가 아니에요
None
▶ 직접 실행해 보기

let-else를 쓰면 “잘 풀리는 경로”(happy path)를 평평하게 유지할 수 있어요. 코드를 if let ... { } 안에 중첩하는 대신, 나쁜 경우를 먼저 처리하고 다음으로 넘어가는 거예요. Option과 Result 값과 함께 쓰면 잘 어울려요.

가드는 “모든 경우를 처리했는지” 검사에 포함되지 않아요. 컴파일러는 if 조건의 내용까지 따져 볼 수 없어요. 그래서 아래 코드는 두 가드가 모든 숫자를 다 처리하는데도 컴파일에 실패해요.

fn main() {
let n: i32 = 7;
match n {
x if x < 0 => println!("음수"),
x if x >= 0 => println!("0 또는 양수"),
}
}
error[E0004]: non-exhaustive patterns: `i32::MIN..=i32::MAX` not covered
--> src/main.rs:3:11
|
3 | 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

에러 메시지는 “i32::MIN..=i32::MAX 범위가 처리되지 않았다”, 그리고 “가드가 있는 갈래는 모든 경우를 처리했는지 따질 때 포함되지 않는다”라는 뜻이에요. 고치려면 마지막 갈래를 _ => println!("0 또는 양수")처럼 가드 없는 나머지 처리 갈래로 바꾸면 돼요.

패턴 안의 변수 이름은 비교가 아니라 새 변수예요. 아래 코드는 guess == expected를 확인하는 것처럼 보이지만, 그렇지 않아요.

fn main() {
let expected = 3;
let guess = 5;
match guess {
expected => println!("정답!"),
_ => println!("오답"),
}
}

이 코드는 컴파일되고 정답!을 출력해요. 갈래에 쓴 expected는 어떤 값이든 받아 내는 새 변수를 만들 뿐이기 때문이에요. Rust는 이렇게 경고해 줘요.

warning: unreachable pattern
--> src/main.rs:6:9
|
5 | expected => println!("정답!"),
| -------- matches any value
6 | _ => println!("오답"),
| ^ no value can reach this

경고는 “5번째 줄의 패턴이 모든 값과 매칭되니, 6번째 줄에는 어떤 값도 도달할 수 없다”라는 뜻이에요. 고치려면 가드를 쓰세요: g if g == expected => println!("정답!").

9부터 15까지 숫자를 출력하되, 3의 배수는 Fizz, 5의 배수는 Buzz, 둘 다의 배수는 FizzBuzz로 출력하세요. if를 줄줄이 잇는 대신 튜플에 대한 match 하나로 해결해 보세요. 힌트: (n % 3, n % 5)를 매칭하세요. 나머지가 0이면 “나누어떨어진다”는 뜻이고, _는 “상관없다”는 뜻이에요.

정답 보기
fn main() {
for n in 9..=15 {
match (n % 3, n % 5) {
(0, 0) => println!("FizzBuzz"),
(0, _) => println!("Fizz"),
(_, 0) => println!("Buzz"),
_ => println!("{n}"),
}
}
}

실행 결과:

Fizz
Buzz
11
Fizz
13
14
FizzBuzz
▶ 직접 실행해 보기

(0, 0)이 맨 앞에 와야 해요. (0, _)가 먼저 오면 15는 Fizz로 출력돼요.

enum Transaction { Deposit(u32), Withdraw(u32), Fee }를 정의하세요. balance를 100에서 시작해서, 다음 목록을 순서대로 적용하세요: 50 입금, 30 출금, 500 출금, 수수료. 수수료는 5예요. 잔액보다 큰 출금은 거절하고 대신 메시지를 출력해야 해요. 마지막에 최종 잔액을 출력하세요. 힌트: 매치 가드 Transaction::Withdraw(amount) if amount <= balance를 쓰고, 그 뒤에 거절하는 경우를 위한 두 번째 Withdraw 갈래를 두세요.

정답 보기
enum Transaction {
Deposit(u32),
Withdraw(u32),
Fee,
}
fn main() {
let mut balance: u32 = 100;
let history = [
Transaction::Deposit(50),
Transaction::Withdraw(30),
Transaction::Withdraw(500),
Transaction::Fee,
];
for t in history {
match t {
Transaction::Deposit(amount) => balance += amount,
Transaction::Withdraw(amount) if amount <= balance => balance -= amount,
Transaction::Withdraw(amount) => println!("거절: {amount}만큼 출금할 수 없어요"),
Transaction::Fee => balance -= 5,
}
}
println!("잔액 = {balance}");
}

실행 결과:

거절: 500만큼 출금할 수 없어요
잔액 = 115
▶ 직접 실행해 보기

두 번째 Withdraw 갈래에는 가드가 없어서, 첫 번째 갈래가 거절한 출금을 모두 받아 내요. 이 갈래가 없으면 match는 컴파일되지 않아요. Rust가 모든 경우를 처리했는지 확인할 때 가드는 포함되지 않기 때문이에요.

  • 매치 가드(패턴 if 조건)는 갈래에 추가 확인을 덧붙여요.
  • 패턴은 튜플, 구조체, 열거형 배리언트를 구조 분해할 수 있고, 중첩할 수도 있어요.
  • 이름 @ 패턴은 값을 확인하면서 동시에 변수에 담아 둬요.
  • _는 값 하나를, ..는 나머지 전부를 건너뛰어요. 패턴은 let, 함수 매개변수, 클로저에서도 쓸 수 있어요.
  • matches!는 빠르게 true/false를 알려 주고, let-else는 값을 풀어 내거나 일찍 빠져나가요.

이해도 점검

1. match 갈래에서 n @ 1..=9는 무슨 일을 하나요?

2. let-else에서 else 블록은 무엇을 해야 하나요?

3. let (first, .., last) = (1, 2, 3, 4, 5);의 결과는 무엇인가요?

4. 가드가 있는 갈래만으로 된 match가 컴파일에 실패하는 경우가 많은 이유는 무엇인가요?

점수: 0 / 4