Rust 패턴 매칭
match 페이지에서 기본을 배웠어요. 갈래(arm), _, |로 값 묶기, 그리고 범위였죠. 그건 시작일 뿐이에요. Rust의 패턴은 값의 안쪽을 들여다보고, 필요한 부분을 꺼내고, 추가 조건까지 확인할 수 있어요. 그것도 한 줄로요.
패턴을 쿠키 틀이라고 생각해 보세요. 틀을 값에 꾹 눌러요. 모양이 맞으면 Rust가 안에 든 조각들을 건네줘요. 모양이 맞지 않으면 Rust는 다음 갈래를 시도해요. 이게 한번 이해되면 패턴을 여기저기서 쓰게 될 거예요. match에서, let에서, 함수 매개변수에서, 그리고 if let에서도요.
매치 가드: if 덧붙이기
섹션 제목: “매치 가드: if 덧붙이기”모양만으로는 부족해서 추가로 확인해야 할 때가 있어요. 매치 가드(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, 마지막 = 5Mira: 레벨 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);}실행 결과:
1920x1080x: 4, y: -2[3, 7, 11]클로저 |&(a, b)| a + b도 같은 원리예요. 각 항목은 &(i32, i32)이고, 패턴이 그것을 바로 a와 b로 풀어 줘요. 반복자를 쓸 때 이런 모습을 자주 보게 될 거예요.
matches!와 let-else
섹션 제목: “matches!와 let-else”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)'서른': 숫자가 아니에요Nonelet-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 value6 | _ => println!("오답"), | ^ no value can reach this경고는 “5번째 줄의 패턴이 모든 값과 매칭되니, 6번째 줄에는 어떤 값도 도달할 수 없다”라는 뜻이에요. 고치려면 가드를 쓰세요: g if g == expected => println!("정답!").
연습 문제
섹션 제목: “연습 문제”1. 튜플로 FizzBuzz 하기
섹션 제목: “1. 튜플로 FizzBuzz 하기”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}"), } }}실행 결과:
FizzBuzz11Fizz1314FizzBuzz(0, 0)이 맨 앞에 와야 해요. (0, _)가 먼저 오면 15는 Fizz로 출력돼요.
2. 은행 거래
섹션 제목: “2. 은행 거래”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
더 알아보기
섹션 제목: “더 알아보기”- 패턴과 매칭 - The Rust Programming Language 한국어판: 패턴이 쓰일 수 있는 모든 곳과, 패턴이 매칭에 실패할 수 있는 경우를 다루는 공식 문서의 장이에요.
- 패턴 문법 - The Rust Programming Language 한국어판: 범위와 구조 분해부터 가드와
@바인딩까지 모든 패턴 형태를 둘러봐요. - Staying on the Happy Path with let…else - The Rust Book (영어): let-else를 차근차근 예제로 설명해요. 한국어판에는 아직 이 절이 없어요.
- Destructuring - Rust by Example (영어): 튜플, 배열, 열거형, 포인터, 구조체를 구조 분해하는 실행 가능한 예제예요.
matches!매크로 - 표준 라이브러리 문서 (영어): 가드를 붙이는 방법까지 포함한 매크로의 정확한 문법이에요.