패턴 문법
이 섹션에서는 패턴에서 유효한 모든 문법을 모아 각각을 왜, 언제 사용해야 하는지 설명한다.
리터럴 매칭
6장에서 살펴보았듯이, 패턴을 리터럴에 직접 매칭할 수 있다. 다음 코드는 몇 가지 예제를 보여준다:
fn main() { let x = 1; match x { 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), _ => println!("anything"), } }
이 코드는 x
의 값이 1이기 때문에 one
을 출력한다. 이 구문은 코드가 특정한 구체적인 값을 받았을 때 동작을 수행하도록 할 때 유용하다.
명명된 변수 매칭
명명된 변수는 어떤 값이든 매칭할 수 있는 무조건적인 패턴이며, 이 책에서 여러 번 사용했다. 그러나 match
, if let
, 또는 while let
표현식에서 명명된 변수를 사용할 때 주의할 점이 있다. 이러한 표현식은 각각 새로운 스코프를 시작하기 때문에, 표현식 내부의 패턴에 선언된 변수는 외부에 있는 동일한 이름의 변수를 가린다. 이는 모든 변수에 적용되는 일반적인 규칙이다. 리스트 19-11에서 x
라는 변수를 Some(5)
로, y
라는 변수를 10
으로 선언했다. 그리고 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}"); }
y
를 가리는 새로운 변수를 도입하는 match
표현식match
표현식이 실행될 때 어떤 일이 일어나는지 살펴보자. 첫 번째 match
패턴은 x
의 정의된 값과 일치하지 않으므로 코드는 계속 진행된다.
두 번째 match
패턴은 Some
값 내부의 어떤 값이든 매칭할 새로운 변수 y
를 도입한다. match
표현식 내부의 새로운 스코프에 있기 때문에, 이 y
는 처음에 10
으로 선언한 y
와는 다른 새로운 변수다. 이 새로운 y
바인딩은 Some
내부의 어떤 값이든 매칭하며, x
에 있는 값이 바로 그렇다. 따라서 이 새로운 y
는 x
의 Some
내부 값에 바인딩된다. 그 값은 5
이므로, 해당 패턴의 표현식이 실행되어 Matched, y = 5
를 출력한다.
만약 x
가 Some(5)
대신 None
값이었다면, 처음 두 패턴은 일치하지 않았을 것이므로 값은 언더스코어 패턴과 일치하게 된다. 언더스코어 패턴에서는 x
변수를 도입하지 않았으므로, 표현식 내의 x
는 여전히 가려지지 않은 외부의 x
다. 이 가상의 경우, match
는 Default case, x = None
을 출력할 것이다.
match
표현식이 끝나면, 그 스코프도 끝나고 내부의 y
스코프도 함께 끝난다. 마지막 println!
은 at the end: x = Some(5), y = 10
을 출력한다.
외부의 x
와 y
값을 비교하는 match
표현식을 만들려면, 기존의 y
변수를 가리는 새로운 변수를 도입하는 대신 매치 가드 조건을 사용해야 한다. 매치 가드에 대해서는 나중에 “매치 가드를 사용한 추가 조건”에서 다룰 것이다.
여러 패턴 매칭
|
문법을 사용하면 여러 패턴을 매칭할 수 있다. 이는 패턴 or 연산자로 동작한다. 예를 들어, 다음 코드에서 x
값을 매치 암과 비교한다. 첫 번째 매치 암은 or 옵션을 가지고 있어, x
값이 해당 암의 값 중 하나와 일치하면 해당 암의 코드가 실행된다:
fn main() { let x = 1; match x { 1 | 2 => println!("one or two"), 3 => println!("three"), _ => println!("anything"), } }
이 코드는 one or two
를 출력한다.
..=
구문으로 값의 범위 매칭하기
..=
구문은 특정 범위 내의 값을 매칭할 때 사용한다. 다음 코드에서 패턴이 주어진 범위 내의 어떤 값과도 일치하면, 해당 분기가 실행된다:
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
를 출력한다.
값 분해를 위한 구조 분해
구조체, 열거형, 튜플의 값을 분해하여 각 부분을 활용할 수 있다. 각 값 유형에 대해 하나씩 살펴보자.
구조체 분해
리스트 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는 구조체 필드와 일치하는 패턴에 대한 단축 문법을 제공한다. 구조체 필드 이름만 나열하면 패턴에서 생성된 변수가 같은 이름을 갖게 된다. 리스트 19-13은 리스트 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
구조체의 값을 갖게 된다.
또한 모든 필드에 대해 변수를 생성하는 대신, 구조체 패턴의 일부로 리터럴 값을 사용해 분해할 수 있다. 이를 통해 특정 필드의 값을 테스트하면서 다른 필드를 분해하는 변수를 생성할 수 있다.
리스트 19-14에서는 match
표현식을 사용해 Point
값을 세 가지 경우로 나눈다: 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
만 출력한다.
열거형 구조 분해
이 책에서 여러 번 열거형을 구조 분해했지만, 아직 열거형 내부에 저장된 데이터를 정의하는 방식과 구조 분해 패턴이 어떻게 대응하는지 명시적으로 다루지 않았다. 예를 들어, 리스트 19-15에서는 리스트 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
처럼 구조체 형태의 열거형 변형은 구조체를 매치할 때 지정하는 패턴과 유사한 패턴을 사용할 수 있다. 변형 이름 뒤에 중괄호를 넣고, 필드와 변수를 나열하여 코드에서 사용할 수 있도록 조각을 분리한다. 여기서는 리스트 19-13에서 했던 것처럼 단축 형태를 사용한다.
Message::Write
처럼 하나의 요소를 가진 튜플을 보유하거나 Message::ChangeColor
처럼 세 개의 요소를 가진 튜플을 보유하는 튜플 형태의 열거형 변형은 튜플을 매치할 때 지정하는 패턴과 유사하다. 패턴의 변수 수는 매치하려는 변형의 요소 수와 일치해야 한다.
중첩된 구조체와 열거형의 구조 분해
지금까지 다룬 예제들은 모두 한 단계 깊이의 구조체나 열거형을 매칭하는 것이었다. 하지만 매칭은 중첩된 항목에서도 동작한다! 예를 들어, ChangeColor
메시지에서 RGB와 HSV 색상을 지원하도록 Listing 19-15의 코드를 Listing 19-16과 같이 리팩토링할 수 있다.
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 }); }
이 코드를 통해 복잡한 타입을 구성 요소로 분해하고, 필요한 값만 따로 사용할 수 있다.
패턴을 사용한 구조 분해는 구조체의 각 필드 값과 같은 값의 일부를 개별적으로 사용할 수 있는 편리한 방법이다.
패턴에서 값 무시하기
패턴에서 값을 무시하는 것이 유용한 경우가 있다. 예를 들어, match
의 마지막 부분에서 나머지 모든 가능한 값을 처리하기 위해 실제로 아무 작업도 하지 않는 catchall 패턴을 사용할 때가 있다. 패턴에서 전체 값이나 값의 일부를 무시하는 몇 가지 방법이 있다: _
패턴을 사용하거나, 다른 패턴 내에서 _
패턴을 사용하거나, 밑줄로 시작하는 이름을 사용하거나, ..
를 사용해 값의 나머지 부분을 무시하는 방법이 있다. 각 패턴을 어떻게 사용하고 왜 사용하는지 살펴보자.
_
를 사용한 전체 값 무시
언더스코어(_
)는 모든 값과 일치하지만 해당 값에 바인딩하지 않는 와일드카드 패턴으로 사용한다. 이는 특히 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
를 출력한다.
대부분의 경우, 특정 함수 매개변수가 더 이상 필요하지 않다면 시그니처를 수정해 사용하지 않는 매개변수를 제거한다. 하지만 함수 매개변수를 무시하는 방식은 특정 타입 시그니처가 필요하지만 구현체에서 해당 매개변수를 사용하지 않는 경우에 특히 유용하다. 예를 들어, 트레이트를 구현할 때 이런 상황이 발생할 수 있다. 이름을 사용하는 대신 _
를 활용하면 사용하지 않는 함수 매개변수에 대한 컴파일러 경고를 피할 수 있다.
중첩된 _
를 사용하여 값의 일부 무시하기
값의 일부만 테스트하고 나머지 부분은 사용하지 않을 때, 패턴 내부에 _
를 사용하여 특정 부분을 무시할 수 있다. 예를 들어, 특정 설정 값을 관리하는 코드에서 사용자가 기존의 커스텀 설정을 덮어쓰지 못하게 하지만, 설정이 현재 해제된 상태라면 값을 부여할 수 있도록 하는 비즈니스 요구사항이 있다고 가정해 보자. 아래 코드는 이러한 시나리오를 보여준다.
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:?}"); }
Some
변형과 매칭할 때 Some
내부의 값을 사용하지 않으려면 패턴 내부에 _
사용하기이 코드는 Can't overwrite an existing customized value
를 출력한 후 setting is Some(5)
를 출력한다. 첫 번째 매치 암에서는 Some
변형 내부의 값을 매칭하거나 사용할 필요는 없지만, setting_value
와 new_setting_value
가 모두 Some
변형인 경우를 테스트해야 한다. 이 경우, setting_value
를 변경하지 않는 이유를 출력하고, 값은 변경되지 않는다.
두 번째 암에서 _
패턴으로 표현된 다른 모든 경우(즉, setting_value
또는 new_setting_value
가 None
인 경우)에는 new_setting_value
가 setting_value
가 되도록 허용한다.
또한 하나의 패턴 내에서 여러 위치에 _
를 사용하여 특정 값을 무시할 수도 있다. 아래 코드는 다섯 개의 아이템으로 이루어진 튜플에서 두 번째와 네 번째 값을 무시하는 예제를 보여준다.
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가 해당 변수에 대해 경고하지 않도록 할 수 있다. 리스트 19-20에서는 사용하지 않는 변수 두 개를 생성했지만, 코드를 컴파일하면 그중 하나에 대해서만 경고가 발생한다.
fn main() { let _x = 5; let y = 10; }
여기서는 변수 y
를 사용하지 않았다는 경고가 발생하지만, _x
에 대해서는 경고가 발생하지 않는다.
단순히 _
를 사용하는 것과 변수명 앞에 밑줄을 붙이는 것 사이에는 미묘한 차이가 있다. _x
와 같은 구문은 여전히 값을 변수에 바인딩하지만, _
는 전혀 바인딩하지 않는다. 이 차이가 중요한 경우를 보여주기 위해 리스트 19-21에서는 오류가 발생한다.
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
}
s
값이 여전히 _s
로 이동되기 때문에 오류가 발생한다. 이로 인해 s
를 다시 사용할 수 없게 된다. 하지만 단순히 밑줄(_
)을 사용하면 값이 바인딩되지 않는다. 리스트 19-22에서는 s
가 _
로 이동하지 않기 때문에 오류 없이 컴파일된다.
fn main() { let s = Some(String::from("Hello!")); if let Some(_) = s { println!("found a string"); } println!("{s:?}"); }
이 코드는 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}"), } }
..
를 사용해 Point
의 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
구문의 패턴 뒤에 추가로 지정하는 if
조건이다. 이 조건도 만족해야 해당 매치 분기가 선택된다. 매치 가드는 단순한 패턴만으로는 표현하기 어려운 복잡한 조건을 처리할 때 유용하다. 다만, if let
이나 while let
표현식에서는 사용할 수 없다는 점에 주의해야 한다.
조건은 패턴에서 생성된 변수를 사용할 수 있다. 리스트 19-26은 첫 번째 매치 분기에 Some(x)
패턴과 함께 if x % 2 == 0
이라는 매치 가드를 추가한 예제를 보여준다. 이 조건은 숫자가 짝수일 때 true
가 된다.
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)
와 일치하므로 매치가 성립한다. 이후 매치 가드에서 x
를 2로 나눈 나머지가 0인지 확인하고, 조건이 참이므로 첫 번째 분기가 선택된다.
만약 num
이 Some(5)
였다면, 첫 번째 매치 분기의 조건은 false
가 된다. 5를 2로 나눈 나머지는 1이기 때문이다. 이 경우 러스트는 두 번째 매치 분기로 이동하고, 두 번째 분기는 매치 가드가 없으므로 모든 Some
변형과 일치한다.
if x % 2 == 0
과 같은 조건은 패턴 내에서 표현할 수 없으므로, 매치 가드는 이러한 로직을 표현할 수 있는 기능을 제공한다. 다만, 이 추가적인 표현력의 단점은 매치 가드가 포함된 경우 컴파일러가 완전성(exhaustiveness)을 검사하지 않는다는 점이다.
리스트 19-11에서 언급했듯이, 매치 가드를 사용하면 패턴 섀도잉 문제를 해결할 수 있다. match
표현식 내부에서 새로운 변수를 생성하는 대신, 외부 변수를 사용할 수 있게 된다. 리스트 19-27은 매치 가드를 사용해 이 문제를 해결하는 방법을 보여준다.
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)
를 출력한다. 두 번째 매치 분기의 패턴은 외부 변수 y
를 섀도잉하지 않는 새로운 변수 n
을 생성한다. 따라서 매치 가드에서 외부 변수 y
를 사용할 수 있다. Some(y)
로 패턴을 지정하면 외부 변수 y
를 섀도잉하지만, Some(n)
으로 지정하면 새로운 변수 n
이 생성되고 외부 변수 y
를 사용할 수 있다.
매치 가드 if n == y
는 패턴이 아니므로 새로운 변수를 생성하지 않는다. 여기서 y
는 외부 변수 y
를 가리키며, n
과 y
를 비교해 같은 값을 찾는다.
또한, 매치 가드에서 |
연산자를 사용해 여러 패턴을 지정할 수도 있다. 이 경우 매치 가드 조건은 모든 패턴에 적용된다. 리스트 19-28은 |
를 사용한 패턴과 매치 가드를 결합할 때의 우선순위를 보여준다. 이 예제에서 중요한 점은 if y
매치 가드가 4
, 5
, 6
모두에 적용된다는 것이다.
fn main() { let x = 4; let y = false; match x { 4 | 5 | 6 if y => println!("yes"), _ => println!("no"), } }
이 매치 조건은 x
가 4
, 5
, 6
중 하나이고 y
가 true
일 때만 매치 분기가 선택된다. 이 코드를 실행하면 첫 번째 매치 분기의 패턴은 x
가 4
이므로 일치하지만, 매치 가드 if y
가 false
이므로 선택되지 않는다. 코드는 두 번째 매치 분기로 이동하고, 이 분기는 일치하므로 no
를 출력한다. 이는 if
조건이 패턴 4 | 5 | 6
전체에 적용되기 때문이다. 즉, 매치 가드의 우선순위는 다음과 같이 동작한다:
(4 | 5 | 6) if y => ...
다음과 같이 동작하지 않는다:
4 | 5 | (6 if y) => ...
코드를 실행한 후, 매치 가드가 |
연산자로 지정된 값 목록의 마지막 값에만 적용된다면, 매치 분기가 선택되고 yes
가 출력되었을 것이다.
@
바인딩
@
연산자는 패턴 매칭을 테스트하는 동시에 해당 값을 변수에 저장할 수 있게 해준다. Listing 19-29에서는 Message::Hello
의 id
필드가 3..=7
범위에 있는지 테스트하면서, 동시에 그 값을 id_variable
변수에 바인딩한다. 이 변수를 id
필드와 같은 이름으로 지을 수도 있지만, 이 예제에서는 다른 이름을 사용한다.
fn main() { enum Message { Hello { id: i32 }, } let msg = Message::Hello { id: 5 }; match msg { Message::Hello { id: id_variable @ 3..=7, } => println!("Found an id in range: {id_variable}"), 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_variable @
를 지정함으로써, 범위 패턴과 일치하는 값을 캡처하면서 동시에 그 값을 테스트한다.
두 번째 패턴에서는 범위만 지정했기 때문에, 해당 패턴과 연결된 코드에서는 id
필드의 실제 값을 담는 변수가 없다. id
필드의 값이 10, 11, 또는 12일 수 있지만, 이 패턴과 연결된 코드는 그 값이 무엇인지 알 수 없다. id
필드의 값을 변수에 저장하지 않았기 때문에, 패턴 코드는 id
필드의 값을 사용할 수 없다.
마지막 패턴에서는 범위 없이 변수만 지정했기 때문에, id
라는 변수로 값을 사용할 수 있다. 이는 구조체 필드 단축 구문을 사용했기 때문이다. 하지만 이 패턴에서는 id
필드의 값에 대해 어떤 테스트도 적용하지 않는다. 첫 두 패턴과 달리, 모든 값이 이 패턴과 일치한다.
@
를 사용하면 하나의 패턴 내에서 값을 테스트하고 동시에 변수에 저장할 수 있다.
요약
Rust의 패턴은 다양한 종류의 데이터를 구분하는 데 매우 유용하다. match
표현식에서 패턴을 사용할 때, Rust는 모든 가능한 값을 다루는지 확인한다. 만약 그렇지 않으면 프로그램이 컴파일되지 않는다. let
문과 함수 매개변수에서 패턴을 사용하면, 값을 더 작은 부분으로 분해하면서 동시에 그 부분들을 변수에 할당할 수 있어 더 유용하게 활용할 수 있다. 우리는 필요에 따라 단순하거나 복잡한 패턴을 만들 수 있다.
다음 장에서는 Rust의 다양한 기능 중 몇 가지 고급 주제를 살펴본다.