패턴 문법
패턴 문법 (Pattern Syntax)
이 섹션에서는 패턴에서 유효한 모든 문법을 모아서, 각각을 왜 그리고 언제 쓰고 싶을지 논의해 볼게요.
리터럴 매칭하기 (Matching Literals)
6장에서 봤듯, 패턴을 리터럴과 직접 매칭할 수 있어요. 다음 코드가 몇 가지 예시를 보여줘요.
fn main() {
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
}
x의 값이 1이므로 이 코드는 one을 출력해요. 이 문법은 코드가 특정한 구체 값 하나를 받았을 때 어떤 작업을 하길 원할 때 유용하죠.
이름 있는 변수 매칭하기 (Matching Named Variables)
이름 있는 변수는 어떤 값과도 일치하는 반증 불가능한 패턴이고, 이 책에서 여러 번 써 왔어요. 하지만 match, if let, while let 표현식에서 이름 있는 변수를 쓸 때는 함정이 있어요. 이런 표현식들 각각은 새 스코프를 시작하기 때문에, 그 표현식 안의 패턴 일부로 선언된 변수는 다른 모든 변수와 마찬가지로 구성 요소 바깥의 같은 이름을 가진 변수들을 섀도잉하죠. Listing 19-11에서 값 Some(5)를 가진 x라는 변수와 값 10을 가진 y라는 변수를 선언해요. 그러고 나서 x 값에 대한 match 표현식을 만드는데, match 갈래의 패턴과 마지막 println!을 보고, 이 코드를 실행하거나 더 읽기 전에 무엇이 출력될지 스스로 맞혀 보세요.
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched, y = {y}"),
_ => println!("Default case, x = {x:?}"),
}
println!("at the end: x = {x:?}, y = {y}");
}
match 표현식이 실행될 때 무슨 일이 일어나는지 함께 따라가 볼게요. 첫 번째 match 갈래의 패턴은 정의된 x 값과 일치하지 않으니 코드는 계속돼요.
두 번째 match 갈래의 패턴은 Some 값 안의 어떤 값과도 일치하는 y라는 새 변수를 도입해요. match 표현식 안의 새 스코프에 있으므로, 이것은 처음에 값 10으로 선언한 y가 아니라 새 y 변수예요. 이 새 y 바인딩은 Some 안의 어떤 값과도 일치하는데, 우리 x에 있는 게 바로 그것이죠. 따라서 이 새 y는 x의 Some 안의 내부 값에 바인딩돼요. 그 값이 5이므로 그 갈래의 표현식이 실행되어 Matched, y = 5를 출력합니다.
만약 x가 Some(5) 대신 None 값이었다면, 처음 두 갈래의 패턴은 일치하지 않았을 테니 값은 밑줄(underscore)에 매칭됐을 거예요. 밑줄 갈래의 패턴에서는 x 변수를 도입하지 않으므로, 그 표현식의 x는 여전히 섀도잉되지 않은 바깥쪽 x예요. 이 가상의 경우라면 match는 Default case, x = None을 출력했을 거예요.
match 표현식이 끝나면 그 스코프가 끝나고, 안쪽 y의 스코프도 끝나요. 마지막 println!은 at the end: x = Some(5), y = 10을 만들어 냅니다.
기존 y 변수를 섀도잉하는 새 변수를 도입하는 대신 바깥쪽 x와 y의 값을 비교하는 match 표현식을 만들려면, 대신 match 가드(match guard) 조건부를 써야 해요. match 가드에 대해서는 이 장 뒷부분의 "match 가드로 조건부 추가하기(Adding Conditionals with Match Guards)"에서 이야기할 거예요.
여러 패턴 매칭하기 (Matching Multiple Patterns)
match 표현식에서 | 문법으로 여러 패턴을 매칭할 수 있어요. |는 패턴 or 연산자이죠. 예를 들어 다음 코드에서 x의 값을 match 갈래에 매칭하는데, 첫 번째 갈래는 or 옵션이 있어요. 즉 x의 값이 그 갈래의 두 값 중 하나와 일치하면 그 갈래의 코드가 실행되죠.
fn main() {
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
}
이 코드는 one or two를 출력해요.
..= 로 값의 범위 매칭하기 (Matching Ranges of Values with ..=)
..= 문법은 값을 포함(inclusive)하는 범위에 매칭하게 해 줘요. 다음 코드에서 패턴이 주어진 범위 안의 어떤 값과 일치하면 그 갈래가 실행되죠.
fn main() {
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
}
x가 1, 2, 3, 4, 5 중 하나이면 첫 번째 갈래가 일치해요. 이 문법은 같은 아이디어를 | 연산자로 표현하는 것보다 여러 매칭 값에 더 편리해요. |를 쓴다면 1 | 2 | 3 | 4 | 5라고 지정해야 하잖아요. 범위를 지정하는 게 훨씬 짧죠. 특히 1에서 1,000 사이의 어떤 숫자에 매칭하고 싶다면요!
컴파일러는 컴파일 시점에 범위가 비어 있지 않은지 검사해요. Rust가 범위가 비어 있는지 아닌지 말할 수 있는 타입은 char와 숫자 값뿐이라서, 범위는 숫자 값이나 char 값에만 허용돼요.
char 값의 범위를 쓰는 예시를 볼게요.
fn main() {
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
}
Rust는 'c'가 첫 번째 패턴의 범위 안에 있다는 걸 알아내고 early ASCII letter를 출력해요.
값을 분해하기 위한 구조 분해 (Destructuring to Break Apart Values)
패턴으로 구조체, 열거형, 튜플을 구조 분해해서 이 값들의 서로 다른 부분을 사용할 수도 있어요. 각 값들을 하나씩 살펴볼게요.
구조체
Listing 19-12는 x와 y 두 필드를 가진 Point 구조체를 보여주는데, let 문의 패턴으로 분해할 수 있어요.
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x: a, y: b } = p;
assert_eq!(0, a);
assert_eq!(7, b);
}
이 코드는 p 구조체의 x와 y 필드의 값과 일치하는 a와 b 변수를 만들어요. 이 예시는 패턴 안의 변수 이름이 구조체의 필드 이름과 일치할 필요가 없다는 걸 보여주죠. 하지만 어느 변수가 어느 필드에서 왔는지 기억하기 쉽도록 변수 이름을 필드 이름과 일치시키는 게 일반적이에요. 이런 일반적인 용법 때문에, 그리고 let Point { x: x, y: y } = p;라고 쓰는 건 중복이 많기 때문에, Rust는 구조체 필드에 매칭하는 패턴의 축약형(shorthand)을 제공해요. 구조체 필드의 이름만 나열하면 되고 패턴에서 만들어진 변수는 같은 이름을 갖게 되죠. Listing 19-13은 Listing 19-12의 코드와 같은 방식으로 동작하지만, let 패턴에서 만들어지는 변수는 a와 b가 아니라 x와 y예요.
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x, y } = p;
assert_eq!(0, x);
assert_eq!(7, y);
}
이 코드는 p 변수의 x와 y 필드와 일치하는 x와 y 변수를 만들어요. 결과는 x와 y 변수가 p 구조체의 값들을 담는다는 거죠.
모든 필드에 변수를 만드는 대신 구조체 패턴의 일부로 리터럴 값으로 구조 분해할 수도 있어요. 그렇게 하면 일부 필드는 특정 값인지 테스트하면서, 나머지 필드는 구조 분해할 변수를 만들 수 있어요.
Listing 19-14에는 Point 값을 세 가지 경우로 나누는 match 표현식이 있어요. x 축에 정확히 놓인 점(y = 0일 때 참), y 축에 정확히 놓인 점(x = 0), 그리고 어느 축에도 놓이지 않은 점이죠.
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
match p {
Point { x, y: 0 } => println!("On the x axis at {x}"),
Point { x: 0, y } => println!("On the y axis at {y}"),
Point { x, y } => {
println!("On neither axis: ({x}, {y})");
}
}
}
첫 번째 갈래는 y 필드가 값이 리터럴 0과 일치할 때 매칭되도록 지정해서, x 축에 놓인 어떤 점과도 일치해요. 그 패턴은 여전히 이 갈래의 코드에서 쓸 수 있는 x 변수를 만들죠.
비슷하게 두 번째 갈래는 x 필드가 그 값이 0일 때 매칭되도록 지정해서 y 축에 놓인 어떤 점과도 일치하고, y 필드의 값을 위한 y 변수를 만들어요. 세 번째 갈래는 리터럴을 지정하지 않으므로 다른 어떤 Point와도 일치하고 x와 y 필드 양쪽에 변수를 만들어요.
이 예시에서 p 값은 x가 0을 담고 있다는 이유로 두 번째 갈래에 일치하므로, 이 코드는 On the y axis at 7을 출력할 거예요.
match 표현식은 첫 번째로 일치하는 패턴을 찾으면 갈래 검사를 멈춘다는 걸 기억하세요. 그래서 Point { x: 0, y: 0 }은 x 축에도 y 축에도 있지만, 이 코드는 On the x axis at 0만 출력할 거예요.
열거형
이 책에서 열거형을 구조 분해해 왔지만(예: 6장의 Listing 6-5), 열거형을 분해하는 패턴이 열거형 안에 저장된 데이터가 정의된 방식에 대응한다는 걸 명시적으로 논의하진 않았어요. Listing 19-15에서는 Listing 6-2의 Message 열거형을 사용하고, 각 내부 값을 분해할 패턴으로 match를 작성해요.
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => {
println!("The Quit variant has no data to destructure.");
}
Message::Move { x, y } => {
println!("Move in the x direction {x} and in the y direction {y}");
}
Message::Write(text) => {
println!("Text message: {text}");
}
Message::ChangeColor(r, g, b) => {
println!("Change color to red {r}, green {g}, and blue {b}");
}
}
}
이 코드는 Change color to red 0, green 160, and blue 255를 출력해요. 다른 갈래의 코드가 실행되는 걸 보려면 msg 값을 바꿔 보세요.
Message::Quit처럼 데이터가 없는 열거형 변형은 값을 더 이상 분해할 수 없어요. 리터럴 Message::Quit 값에만 매칭할 수 있고 그 패턴에는 변수가 없죠.
Message::Move 같은 구조체형 열거형 변형은 구조체에 매칭할 때 지정하는 패턴과 비슷한 패턴을 쓸 수 있어요. 변형 이름 뒤에 중괄호를 놓고 변수와 함께 필드를 나열해서, 이 갈래의 코드에서 사용할 조각들을 분해하죠. 여기서는 Listing 19-13에서 했던 것처럼 축약형을 사용해요.
Message::Write처럼 한 요소를 가진 튜플을 담는 변형이나 Message::ChangeColor처럼 세 요소를 가진 튜플을 담는 변형 같은 튜플형 열거형 변형은, 튜플에 매칭할 때 지정하는 패턴과 비슷해요. 패턴의 변수 수는 매칭하는 변형의 요소 수와 일치해야 하죠.
중첩된 구조체와 열거형
지금까지 예시들은 전부 한 단계 깊이의 구조체나 열거형을 매칭했지만, 매칭은 중첩된 항목에서도 동작해요! 예를 들어 Listing 19-16처럼 Listing 19-15의 코드를 리팩터링해서 ChangeColor 메시지에서 RGB와 HSV 색상을 지원할 수 있어요.
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main() {
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!("Change color to red {r}, green {g}, and blue {b}");
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("Change color to hue {h}, saturation {s}, value {v}");
}
_ => (),
}
}
match 표현식의 첫 번째 갈래 패턴은 Color::Rgb 변형을 담고 있는 Message::ChangeColor 열거형 변형에 매칭하고, 그다음 패턴이 세 개의 내부 i32 값에 바인딩돼요. 두 번째 갈래의 패턴도 Message::ChangeColor 열거형 변형에 매칭하지만, 내부 열거형은 Color::Hsv에 매칭하죠. 두 개의 열거형이 관련돼 있지만, 이런 복잡한 조건을 하나의 match 표현식으로 지정할 수 있어요.
구조체와 튜플
구조 분해 패턴을 더 복잡한 방식으로 섞고, 매칭하고, 중첩할 수 있어요. 다음 예시는 튜플 안에 구조체와 튜플을 중첩하고 모든 기본 값들을 분해해 내는 복잡한 분해를 보여줘요.
fn main() {
struct Point {
x: i32,
y: i32,
}
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}
이 코드는 복잡한 타입을 구성 요소로 분해해서 관심 있는 값을 개별적으로 사용할 수 있게 해 줘요.
패턴으로 구조 분해하는 것은 값을 조각별로, 예를 들어 구조체의 각 필드의 값을 서로 분리해서 사용하는 편리한 방법이에요.
패턴에서 값 무시하기 (Ignoring Values in a Pattern)
패턴에서 값을 무시하는 게 유용할 때가 있다는 걸 봤어요. 예를 들어 match의 마지막 갈래에서 아무것도 하지 않지만 남은 모든 가능한 값은 처리하는 포괄 갈래(catch-all)를 얻기 위해서죠. 패턴에서 전체 값이나 값의 일부를 무시하는 방법은 몇 가지가 있어요. 이미 본 _ 패턴 쓰기, 다른 패턴 안에서 _ 패턴 쓰기, 밑줄로 시작하는 이름 쓰기, 또는 나머지 값을 무시하는 .. 쓰기. 각 패턴을 어떻게, 왜 쓰는지 살펴볼게요.
_ 로 전체 값
와일드카드 패턴으로 밑줄을 써서 어떤 값과도 일치하되 값에 바인딩하지 않게 했어요. 특히 match 표현식의 마지막 갈래로 유용하지만, Listing 19-17처럼 어떤 패턴에서든, 함수 매개변수에서도 쓸 수 있어요.
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {y}");
}
fn main() {
foo(3, 4);
}
이 코드는 첫 번째 인자로 전달된 값 3을 완전히 무시하고 This code only uses the y parameter: 4를 출력할 거예요.
대부분의 경우 특정 함수 매개변수가 더 이상 필요 없으면, 시그니처를 바꿔 사용하지 않는 매개변수를 포함하지 않게 할 거예요. 함수 매개변수를 무시하는 것은 특히 다음과 같은 경우에 유용해요. 특정 타입 시그니처가 필요한 트레이트를 구현하는데, 구현의 함수 본문에는 매개변수 중 하나가 필요 없는 경우죠. 그러면 이름을 쓰는 경우처럼 사용하지 않는 함수 매개변수에 대한 컴파일러 경고를 피할 수 있어요.
중첩된 _ 로 값의 일부
다른 패턴 안에서도 _를 써서 값의 일부만 무시할 수 있어요. 예를 들어 값의 일부만 테스트하고 싶은데, 실행하고 싶은 해당 코드에서는 다른 부분을 쓸 일이 없을 때죠. Listing 19-18은 설정(setting)의 값을 관리하는 코드를 보여줘요. 비즈니스 요구는 사용자가 기존 설정 커스터마이즈를 덮어쓸 수 없지만, 설정을 해제(unsets)하거나 현재 설정되지 않았다면 값 줄 수 있다는 것이에요.
fn main() {
let mut setting_value = Some(5);
let new_setting_value = Some(10);
match (setting_value, new_setting_value) {
(Some(_), Some(_)) => {
println!("Can't overwrite an existing customized value");
}
_ => {
setting_value = new_setting_value;
}
}
println!("setting is {setting_value:?}");
}
이 코드는 Can't overwrite an existing customized value를 출력하고 이어서 setting is Some(5)를 출력해요. 첫 번째 match 갈래에서 어느 Some 변형 안의 값에 매칭하거나 사용할 필요는 없지만, setting_value와 new_setting_value가 Some 변형인 경우를 테스트할 필요는 있어요. 그 경우에 setting_value를 바꾸지 않는 이유를 출력하고, 그것은 바뀌지 않죠.
그 외의 모든 경우(즉 setting_value나 new_setting_value 중 하나가 None인 경우)는 두 번째 갈래의 _ 패턴으로 표현되는데, new_setting_value가 setting_value가 되는 걸 허용하고 싶어요.
한 패턴 안의 여러 곳에서 밑줄을 써서 특정 값들을 무시할 수도 있어요. Listing 19-19는 다섯 항목의 튜플에서 두 번째와 네 번째 값을 무시하는 예시를 보여줘요.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}");
}
}
}
이 코드는 Some numbers: 2, 8, 32를 출력하고, 값 4와 16은 무시될 거예요.
이름을 _ 로 시작해서 사용하지 않는 변수 만들기
변수를 만들었는데 어디에서도 쓰지 않으면, Rust는 보통 경고를 내요. 사용하지 않는 변수는 버그일 수 있으니까요. 하지만 아직 사용하지 않을 변수를 만들 수 있게 되는 것이 유용할 때가 있어요. 프로토타이핑하거나 프로젝트를 막 시작할 때죠. 이런 상황에서는 변수 이름을 밑줄로 시작해서 사용하지 않는 변수에 대한 경고를 하지 말라고 Rust에 알릴 수 있어요. Listing 19-20에서 사용하지 않는 변수 두 개를 만들지만, 이 코드를 컴파일하면 그중 하나에 대해서만 경고를 받아야 해요.
fn main() {
let _x = 5;
let y = 10;
}
여기서 변수 y를 사용하지 않는다는 경고는 받지만 _x를 사용하지 않는다는 경고는 받지 않아요.
_만 쓰는 것과 밑줄로 시작하는 이름을 쓰는 것 사이에는 미묘한 차이가 있다는 점에 주의하세요. _x 문법은 여전히 그 값을 변수에 바인딩하는 반면, _는 전혀 바인딩하지 않아요. 이 차이가 중요해지는 경우를 보여주기 위해 Listing 19-21이 오류를 만들어 낼 거예요.
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
}
s 값이 여전히 _s로 이동되어 s를 다시 사용할 수 없으므로 오류를 받을 거예요. 하지만 밑줄만 사용하는 것은 절대 값을 바인딩하지 않아요. Listing 19-22는 s가 _로 이동되지 않기 때문에 오류 없이 컴파일될 거예요.
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{s:?}");
}
이 코드는 잘 동작하는데, s를 어떤 것에도 바인딩하지 않아서 이동되지 않기 때문이에요.
.. 로 값의 나머지 부분
부분이 많은 값에서 .. 문법을 써서 특정 부분만 사용하고 나머지는 무시할 수 있어요. 무시할 값을 하나하나 밑줄로 나열할 필요가 없어지죠. .. 패턴은 패턴의 나머지 부분에서 명시적으로 매칭하지 않은 값의 어떤 부분이든 무시해요. Listing 19-23에는 3차원 공간의 좌표를 담는 Point 구조체가 있어요. match 표현식에서 x 좌표에만 동작하고 y와 z 필드의 값은 무시하고 싶어요.
fn main() {
struct Point {
x: i32,
y: i32,
z: i32,
}
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
Point { x, .. } => println!("x is {x}"),
}
}
x 값을 나열하고 나서 그냥 .. 패턴만 포함시켰어요. 이건 y: _와 z: _를 나열하는 것보다 빠르죠. 특히 필드가 많은 구조체에서 한두 개의 필드만 관련된 상황일 때요.
.. 문법은 필요한 만큼 많은 값으로 확장돼요. Listing 19-24는 튜플에서 ..를 쓰는 방법을 보여줘요.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}
이 코드에서 처음과 마지막 값은 first와 last로 매칭돼요. ..는 중간의 모든 것을 매칭하고 무시할 거예요.
하지만 ..를 쓰는 건 모호하지 않아야 해요. 어떤 값이 매칭을 위한 것이고 어떤 값이 무시돼야 하는지 불분명하면 Rust가 오류를 줄 거예요. Listing 19-25는 모호하게 ..를 쓰는 예시라서 컴파일되지 않을 거예요.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
이 예시를 컴파일하면 이런 오류를 받아요.
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
--> src/main.rs:5:22
|
5 | (.., second, ..) => {
| -- ^^ can only be used once per tuple pattern
| |
| previously used here
error: could not compile `patterns` (bin "patterns") due to 1 previous error
Rust가 second로 값을 매칭하기 전에 튜플의 값을 몇 개 무시하고, 그 뒤로 또 몇 개를 무시할지 판단하는 건 불가능해요. 이 코드는 2를 무시하고 second를 4에 바인딩한 다음 8, 16, 32를 무시하길 원할 수도 있고, 2와 4를 무시하고 second를 8에 바인딩한 다음 16과 32를 무시하길 원할 수도 있고, 계속 그런 식이죠. 변수 이름 second는 Rust에게 특별한 의미가 없으므로, 이렇게 두 곳에서 ..를 쓰는 건 모호해서 컴파일러 오류를 받는 거예요.
match 가드로 조건부 추가하기 (Adding Conditionals with Match Guards)
match 가드(match guard)는 match 갈래에서 패턴 뒤에 지정하는 추가 if 조건으로, 그 갈래가 선택되려면 이것도 일치해야 해요. match 가드는 패턴 하나만으로는 허용되지 않는 더 복잡한 아이디어를 표현할 때 유용해요. 다만 if let이나 while let 표현식이 아니라 오직 match 표현식에서만 사용할 수 있다는 점에 주의하세요.
그 조건은 패턴에서 만들어진 변수를 사용할 수 있어요. Listing 19-26은 첫 번째 갈래에 Some(x)라는 패턴과 if x % 2 == 0(숫자가 짝수이면 참)이라는 match 가드를 가진 match를 보여줘요.
fn main() {
let num = Some(4);
match num {
Some(x) if x % 2 == 0 => println!("The number {x} is even"),
Some(x) => println!("The number {x} is odd"),
None => (),
}
}
이 예시는 The number 4 is even을 출력할 거예요. num을 첫 번째 갈래의 패턴과 비교하면, Some(4)가 Some(x)와 일치하므로 일치해요. 그러면 match 가드가 x를 2로 나눈 나머지가 0과 같은지 확인하는데, 그렇기 때문에 첫 번째 갈래가 선택돼요.
만약 num이 Some(5)였다면 첫 번째 갈래의 match 가드는 거짓이 됐을 거예요. 5를 2로 나눈 나머지는 1이라 0과 같지 않으니까요. 그러면 Rust는 두 번째 갈래로 갔을 텐데, 두 번째 갈래는 match 가드가 없어서 어떤 Some 변형과도 일치하므로 매칭됐을 거예요.
if x % 2 == 0 조건을 패턴 안에서 표현할 방법은 없어요. 그래서 match 가드가 이 로직을 표현할 능력을 줘요. 이 추가 표현력의 단점은, match 가드 표현식이 관련될 때 컴파일러가 철저성 검사를 시도하지 않는다는 점이에요.
Listing 19-11을 논의할 때, match 가드를 써서 패턴 섀도잉 문제를 해결할 수 있다고 언급했었죠. match 바깥 변수를 쓰는 대신 match 표현식의 패턴 안에 새 변수를 만들었던 걸 기억하세요. 그 새 변수 때문에 바깥 변수의 값에 대해 테스트할 수 없게 됐어요. Listing 19-27은 match 가드로 이 문제를 고치는 방법을 보여줘요.
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(n) if n == y => println!("Matched, n = {n}"),
_ => println!("Default case, x = {x:?}"),
}
println!("at the end: x = {x:?}, y = {y}");
}
이 코드는 이제 Default case, x = Some(5)를 출력할 거예요. 두 번째 match 갈래의 패턴은 바깥쪽 y를 섀도잉할 새 변수 y를 도입하지 않아서, match 가드에서 바깥쪽 y를 쓸 수 있어요. Some(y)로 패턴을 지정해 바깥쪽 y를 섀도잉하는 대신, Some(n)으로 지정해요. 이건 match 바깥에 n 변수가 없으므로 아무것도 섀도잉하지 않는 새 변수 n을 만들어요.
match 가드 if n == y는 패턴이 아니므로 새 변수를 도입하지 않아요. 이 y는 그것을 섀도잉하는 새 y가 아니라 바깥쪽 y고, n을 y와 비교해서 바깥쪽 y와 같은 값을 찾아볼 수 있죠.
match 가드에서 or 연산자 |를 써서 여러 패턴을 지정할 수도 있어요. match 가드 조건은 모든 패턴에 적용돼요. Listing 19-28은 |를 쓰는 패턴과 match 가드를 결합할 때의 우선순위를 보여줘요. 이 예시에서 중요한 부분은 if y가 4, 5, 6 모두에 적용된다는 거예요. if y가 6에만 적용되는 것처럼 보일 수 있지만 그렇지 않아요.
fn main() {
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
}
match 조건은 x의 값이 4, 5, 6 중 하나와 같고 y가 참일 때만 그 갈래가 일치한다고 말해요. 이 코드가 실행되면 x가 4이므로 첫 번째 갈래의 패턴은 일치하지만, match 가드 if y가 거짓이라 첫 번째 갈래는 선택되지 않아요. 코드는 일치하는 두 번째 갈래로 넘어가서 이 프로그램은 no를 출력하죠. 이유는 if 조건이 전체 패턴 4 | 5 | 6에 적용되기 때문이에요. 마지막 값 6에만 적용되는 게 아니죠. 다시 말해 패턴에 대한 match 가드의 우선순위는 이렇게 동작해요.
(4 | 5 | 6) if y => ...
이렇게가 아니라요.
4 | 5 | (6 if y) => ...
코드를 실행하면 우선순위 동작이 분명해져요. match 가드가 | 연산자로 지정된 값 목록의 마지막 값에만 적용된다면 그 갈래는 일치했을 테고, 프로그램은 yes를 출력했을 거예요.
@ 바인딩 사용하기 (Using @ Bindings)
at 연산자 @는 값을 패턴 매칭으로 테스트하는 동시에 그 값을 담는 변수를 만들게 해 줘요. Listing 19-29에서 Message::Hello의 id 필드가 범위 3..=7 안에 있는지 테스트하고 싶어요. 또 그 값을 변수 id에 바인딩해서 그 갈래와 연관된 코드에서 사용하고 싶고요.
fn main() {
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id @ 3..=7 } => {
println!("Found an id in range: {id}")
}
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
}
Message::Hello { id } => println!("Found some other id: {id}"),
}
}
이 예시는 Found an id in range: 5를 출력할 거예요. 범위 3..=7 앞에 id @를 지정함으로써, 값이 범위 패턴에 일치하는지 테스트하면서 그 범위에 일치하는 값을 id라는 변수로 캡처하고 있어요.
두 번째 갈래는 패턴에 범위만 지정되어 있고, 갈래와 연관된 코드에는 id 필드의 실제 값을 담는 변수가 없어요. id 필드의 값이 10, 11, 12 중 하나일 수 있지만, 그 패턴에 딸린 코드는 어떤 값인지 알지 못해요. id 값을 변수에 저장하지 않았으므로 패턴 코드가 id 필드의 값을 사용할 수 없죠.
마지막 갈래는 범위 없이 변수를 지정했는데, id라는 변수에 값을 담아 갈래 코드에서 사용할 수 있어요. 그 이유는 구조체 필드 축약형 문법을 사용했기 때문이에요. 하지만 이 갈래에서는 처음 두 갈래에서 했던 것처럼 id 필드의 값에 어떤 테스트도 적용하지 않았어요. 어떤 값이든 이 패턴에 일치하니까요.
@를 쓰면 값 하나를 테스트하면서 같은 패턴 안에서 변수로 저장할 수 있어요.
요약 (Summary)
Rust의 패턴은 서로 다른 종류의 데이터를 구별하는 데 아주 유용해요. match 표현식에서 쓰면 Rust가 여러분의 패턴이 모든 가능한 값을 덮도록 보장해요. 그렇지 않으면 프로그램이 컴파일되지 않죠. let 문과 함수 매개변수의 패턴은 그 구성 요소들을 더 유용하게 만들어, 값을 더 작은 조각으로 분해하고 그 조각들을 변수에 할당할 수 있게 해 줘요. 필요에 따라 단순한 패턴이나 복잡한 패턴을 만들 수 있어요.
다음으로, 책의 마지막에서 두 번째 장에서 Rust의 다양한 기능 중 고급 측면 몇 가지를 살펴볼 거예요.
출처: The Rust Book