패턴을 쓸 수 있는 모든 곳
패턴을 쓸 수 있는 모든 곳
Rust에서 패턴이 등장하는 곳은 꽤 많아요. 사실 여러분은 이미 그걸 아주 많이 써 왔는데, 알아차리지 못했을 뿐이에요! 이 절에서는 패턴이 유효한 모든 위치를 하나씩 짚어볼게요.
출처: Rust 공식문서
본문
match Arms (match 갈래)
6장에서 살펴봤듯이, 우리는 match 표현식의 갈래(arm)에서 패턴을 써요. 형식적으로 match 표현식은 이렇게 정의돼요. match 키워드, 매치할 값, 그리고 값이 그 갈래의 패턴에 맞으면 실행할 표현식으로 이루어진 하나 이상의 match 갈래. 모양은 이렇게 생겼죠:
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
예를 들어, 변수 x에 담긴 Option<i32> 값에 매치하는 6-5 목록의 match 표현식을 보죠:
match x {
None => None,
Some(i) => Some(i + 1),
}
이 match 표현식의 패턴은 각 화살표 왼쪽에 있는 None과 Some(i)예요.
match 표현식에는 한 가지 요구사항이 있어요. 바로 철저함(exhaustive)이어야 한다는 건데요, match 표현식의 값에 대한 모든 가능성을 반드시 처리해야 해요. 모든 가능성을 빠짐없이 처리했는지 보장하는 방법 중 하나는 마지막 갈래에 포괄(catch-all) 패턴을 두는 거예요. 예를 들어 어떤 값이든 매치되는 변수 이름은 절대 실패할 수 없으므로 남은 모든 경우를 덮어주죠.
특별한 패턴 _는 무엇이든 매치하지만 변수에 바인딩하지는 않아요. 그래서 보통 마지막 match 갈래에서 자주 쓰여요. _ 패턴은 예를 들어 지정되지 않은 값을 무시하고 싶을 때 유용하죠. _ 패턴은 이 장의 뒷부분인 "Ignoring Values in a Pattern"에서 더 자세히 다룰게요.
let 문 (let Statements)
이 장 이전까지 우리는 패턴을 쓰는 곳을 match와 if let에서만 명시적으로 다뤘는데요, 사실 패턴은 다른 곳에서도 써 왔어요. let 문도 그중 하나죠. 예를 들어 let으로 변수에 값을 할당하는 이런 단순한 코드를 보면요:
#![allow(unused)]
fn main() {
let x = 5;
}
이런 let 문을 쓸 때마다 여러분은 사실 패턴을 쓰고 있었던 거예요. 알아차리지 못했을지라도요! 더 형식적으로 말하면, let 문은 이렇게 생겼어요:
let PATTERN = EXPRESSION;
PATTERN 자리에 변수 이름이 들어가는 let x = 5; 같은 문에서, 그 변수 이름은 특히 단순한 형태의 패턴일 뿐이에요. Rust는 표현식을 패턴과 비교해서 거기서 찾은 모든 이름에 값을 할당하죠. let x = 5; 예시에서 x는 "여기 매치되는 것을 변수 x에 바인딩한다"는 뜻의 패턴이에요. x라는 이름이 패턴 전체이므로, 이 패턴은 사실상 "값이 무엇이든 간에 모든 것을 변수 x에 바인딩한다"는 뜻이 돼요.
let의 패턴 매칭 측면을 더 또렷하게 보려면 19-1 목록을 보면 돼요. 튜플을 분해(destructure)하기 위해 let에 패턴을 쓴 코드죠:
fn main() {
let (x, y, z) = (1, 2, 3);
}
여기서 튜플을 패턴에 매치해요. Rust는 값 (1, 2, 3)을 패턴 (x, y, z)와 비교하면서 값이 패턴에 맞는지 보는데요, 즉 양쪽의 요소 개수가 같은지 확인해요. 맞다면 1을 x에, 2를 y에, 3을 z에 바인딩하죠. 이 튜플 패턴은 세 개의 개별 변수 패턴이 안에 중첩돼 있다고 생각하면 돼요.
패턴의 요소 수와 튜플의 요소 수가 맞지 않으면 전체 타입이 안 맞아서 컴파일러 오류가 나요. 예를 들어 19-2 목록은 세 개 요소를 가진 튜플을 두 개 변수로 분해하려는 시도인데, 이건 안 되죠.
fn main() {
let (x, y) = (1, 2, 3);
}
이 코드를 컴파일하려 시도하면 이런 타입 오류가 나요:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
--> src/main.rs:2:9
|
2 | let (x, y) = (1, 2, 3);
| ^^^^^^ --------- this expression has type `({integer}, {integer}, {integer})`
| |
| expected a tuple with 3 elements, found one with 2 elements
|
= note: expected tuple `({integer}, {integer}, {integer})`
found tuple `(_, _)`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error
오류를 고치려면 튜플의 값 중 하나 이상을 _나 ..로 무시하면 돼요. 이건 "Ignoring Values in a Pattern" 섹션에서 볼게요. 문제가 패턴에 변수가 너무 많기 때문이라면, 변수를 줄여서 변수 개수가 튜플의 요소 개수와 같게 만들어 타입을 맞추면 돼요.
조건부 if let 표현식 (Conditional if let Expressions)
6장에서 if let 표현식을 주로 한 가지 경우만 매치하는 match를 더 짧게 쓰는 방법이라고 배웠죠. 선택적으로 if let은 if let의 패턴이 매치되지 않을 때 실행할 코드가 담긴 else도 가질 수 있어요.
19-3 목록은 if let, else if, else if let 표현식을 섞어 쓸 수도 있다는 걸 보여줘요. 이렇게 하면 패턴과 비교할 값을 하나만 표현할 수 있는 match 표현식보다 훨씬 유연해져요. 게다가 Rust는 if let, else if, else if let 갈래들의 조건이 서로 연관되어 있을 것을 요구하지 않아요.
19-3 목록의 코드는 여러 조건을 순서대로 확인해서 배경색을 정해요. 이 예시에서는 실제 프로그램이 사용자 입력으로 받을 수도 있는 값을 하드코딩한 변수를 만들었어요.
fn main() {
let favorite_color: Option<&str> = None;
let is_tuesday = false;
let age: Result<u8, _> = "34".parse();
if let Some(color) = favorite_color {
println!("Using your favorite color, {color}, as the background");
} else if is_tuesday {
println!("Tuesday is green day!");
} else if let Ok(age) = age {
if age > 30 {
println!("Using purple as the background color");
} else {
println!("Using orange as the background color");
}
} else {
println!("Using blue as the background color");
}
}
사용자가 좋아하는 색을 지정하면 그 색이 배경으로 쓰여요. 좋아하는 색이 없고 오늘이 화요일이면 배경색은 초록색이죠. 또 사용자가 나이를 문자열로 제공했고 그걸 숫자로 성공적으로 파싱할 수 있다면, 그 숫자 값에 따라 보라색이나 주황색이 돼요. 이 조건 중 어느 것도 해당하지 않으면 배경색은 파란색이에요.
이 조건부 구조 덕분에 복잡한 요구사항을 지원할 수 있어요. 여기 하드코딩된 값들로는 이 예시가 Using purple as the background color를 출력할 거예요.
if let은 match 갈래처럼 기존 변수를 가리는(shadow) 새 변수를 도입할 수도 있다는 걸 알 수 있어요. if let Ok(age) = age 줄은 Ok 배리언트 안의 값을 담은 새 age 변수를 도입해서 기존 age 변수를 가려요. 그래서 if age > 30 조건을 그 블록 안에 둬야 해요. 이 두 조건을 if let Ok(age) = age && age > 30으로 합칠 수는 없어요. 30과 비교하려는 새 age는 중괄호로 시작하는 새 스코프가 시작되기 전에는 아직 유효하지 않거든요.
if let 표현식의 단점은 match 표현식과 달리 컴파일러가 철저함(exhaustiveness)을 검사하지 않는다는 거예요. 마지막 else 블록을 빼먹어서 일부 경우를 처리하지 못해도 컴파일러는 그 잠재적 논리 버그를 알려주지 않아요.
while let 조건부 반복문 (Conditional while let Loops)
while let 조건부 반복문은 if let과 구조가 비슷한데요, 패턴이 계속 매치되는 동안 while 반복문이 실행되게 해줘요. 19-4 목록에서는 스레드 사이에 전송되는 메시지를 기다리는 while let 반복문을 보여주는데, 이 경우 Option 대신 Result를 확인해요.
fn main() {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
for val in [1, 2, 3] {
tx.send(val).unwrap();
}
});
while let Ok(value) = rx.recv() {
println!("{value}");
}
}
이 예시는 1, 2, 그리고 3을 출력해요. recv 메서드는 채널의 수신 쪽에서 첫 번째 메시지를 꺼내 Ok(value)를 반환하죠. 16장에서 recv를 처음 봤을 때는 오류를 바로 unwrap했거나 for 반복문으로 반복자처럼 다뤘어요. 그런데 19-4 목록처럼 while let을 쓸 수도 있어요. recv 메서드는 송신자가 존재하는 한 메시지가 도착할 때마다 Ok를 반환하고, 송신 쪽이 끊기면 Err를 반환하기 때문이에요.
for 반복문 (for Loops)
for 반복문에서는 키워드 for 바로 뒤에 오는 값이 패턴이에요. 예를 들어 for x in y에서 x가 패턴이죠. 19-5 목록은 for 반복문 안에서 패턴으로 튜플을 분해(break apart)하는 방법을 보여줘요.
fn main() {
let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
println!("{value} is at index {index}");
}
}
19-5 목록의 코드는 다음을 출력할 거예요:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2
enumerate 메서드로 반복자를 변형해서 값과 그 값의 인덱스를 튜플로 묶어서 만들어내게 해요. 처음 만들어내는 값은 튜플 (0, 'a')예요. 이 값이 패턴 (index, value)에 매치되면 index는 0, value는 'a'가 되어 첫 번째 출력 줄을 찍어요.
함수 매개변수 (Function Parameters)
함수 매개변수도 패턴이 될 수 있어요. 이제 익숙할 19-6 목록의 코드는 타입 i32인 매개변수 x를 하나 받는 foo라는 함수를 선언해요.
fn foo(x: i32) {
// code goes here
}
fn main() {}
x 부분이 바로 패턴이에요! let에서 했던 것처럼 함수의 인자에서도 튜플을 패턴에 매치할 수 있죠. 19-7 목록은 튜플 값을 함수에 전달하면서 값들을 쪼개 줘요.
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({x}, {y})");
}
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
이 코드는 Current location: (3, 5)를 출력해요. 값 &(3, 5)가 패턴 &(x, y)에 매치되므로 x는 값 3, y는 값 5가 돼요.
클로저는 함수와 비슷하다고 13장에서 다뤘는데요, 그래서 클로저의 매개변수 목록에서도 함수 매개변수 목록과 같은 방식으로 패턴을 쓸 수 있어요.
지금까지 패턴을 쓰는 여러 방법을 봤는데요, 패턴이 우리가 쓸 수 있는 모든 곳에서 똑같이 작동하는 건 아니에요. 어떤 곳에서는 패턴이 반드시 반박 불가(irrefutable)해야 하고, 어떤 경우에는 반박 가능(refutable)할 수 있어요. 이 두 개념은 다음 절에서 다룰게요.
더 알아보기
이 절에서 "나중에 다룬다"고 넘긴 내용들의 원문을 직접 보고 싶다면 공식문서를 이어서 읽으면 돼요.
- 패턴이 매치에 실패할 수 있는지, 즉 반박 가능성(refutability)을 정리한 Refutability: Whether a Pattern Might Fail to Match
_,..,@등 패턴 문법을 자세히 다루는 Pattern Syntax