패턴 문법
이번 절에서는 패턴이 유효한 모든 문법을 모아보고 각 문법의 사용이 필요한 이유와 시기에 대해 설명합니다.
리터럴 매칭
6장에서 살펴본 것처럼 패턴과 리터럴을 직접 매칭시킬 수 있습니다. 다음 코드는 몇 가지 예를 제공합니다:
fn main() { let x = 1; match x { 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), _ => println!("anything"), } }
x
의 값이 1이므로 이 코드는 one
을 출력합니다. 이 문법은
코드에서 특정한 구체적인 값을 가질 때 어떤 동작을 수행하려는 경우에
유용합니다.
명명된 변수 매칭
명명된 변수는 어떤 값과도 매칭되는 반박 불가능한 패턴으로, 이 책에서
여러 번 사용했습니다. 하지만 match
표현식에서 명명된 변수를 사용할
때는 복잡한 문제가 있습니다. match
는 새로운 스코프를 시작하기
때문에, match
표현식 내부에서 패턴의 일부로써 선언된 변수는 모든
변수의 경우와 마찬가지로 match
구조 외부에 있는 같은 이름의 변수를
가리게 될 것입니다. 예제 18-11에서는 값 Some(5)
을 가진 변수
x
와 값 10
을 가진 변수 y
를 선언합니다. 그다음 값 x
에
match
표현식을 만듭니다. 이 코드를 실행하거나 뒷부분을 더 읽기
전에 매치 갈래의 패턴과 마지막에 있는 println!
을 보고 코드가
무엇을 출력할지 알아보세요.
파일명: src/main.rs
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 = {:?}, y = {y}", x); }
match
표현식이 실행되면 어떤 일이 일어나는지 살펴봅시다.
첫 번째 매치 갈래의 패턴이 x
에 정의된 값과 매칭되지 않으므로
코드는 계속 실행됩니다.
두 번째 매치 갈래의 패턴은 Some
값 안에 있는 모든 값과 매칭되는
y
라는 새 변수를 도입합니다. match
표현식 내부의 새로운 스코프에
있기 때문에, 이것은 처음에 10이라는 값으로 선언한 y
가 아니라
새로운 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
표현식을 만들려면 대신 매치 가드 조건문 (match guard conditional) 을
사용해야 합니다. 매치 가드에 대해서는 나중에
‘매치 가드를 사용한 추가 조건’절에서
설명하겠습니다.
다중 패턴
match
표현식에서는 패턴에 대한 또는 (or) 연산자인 |
문법을 사용하여
여러 패턴을 매칭시킬 수 있습니다. 예를 들어, 다음 코드에서는 매치
갈래에 대해 x
값을 매칭시키는데, 첫 번째 갈래에는 또는 옵션이
있으므로 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 사이의 숫자 같은 것과 매칭시키려는 경우 범위를 지정하는
것이 훨씬 더 짧습니다!
컴파일러는 컴파일 타임에 범위가 비어 있지 않은지 확인하며, 러스트가 범위가
비어 있는지를 알 수 있는 유일한 타입은 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"), } }
러스트는 'c'
가 첫 번째 패턴의 범위 내에 있음을 인식하고
early ASCII letter
를 출력합니다.
값을 해체하여 분리하기
구조체, 열거형, 튜플을 분해하여 이 값의 부분들을 쓰기 위해 패턴을 사용할 수도 있습니다. 각각에 대해 알아봅시다.
구조체 해체하기
예제 18-12는 x
와 y
두 개의 필드를 가진 Point
구조체를
보여주며, let
구문에 패턴을 사용해 분해할 수 있습니다.
파일명: src/main.rs
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;
라고 작성하는 것이 많은
중복을 발생시키는 이유로, 러스트에는 구조체 필드와 일치하는 패턴에
대한 축약법이 있습니다: 구조체 필드 이름만 나열하면 패턴에서
생성된 변수는 동일한 이름을 갖습니다. 예제 18-13은 예제 18-12의
코드와 동일한 방식으로 동작하지만, let
패턴에서 생성된
변수는 a
와 b
대신 x
와 y
입니다.
파일명: src/main.rs
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
구조체로부터
나온 값을 갖습니다.
또한 모든 필드에 대해 변수를 생성하는 대신 구조체 패턴의 일부에 리터럴 값을 사용하여 해체할 수도 있습니다. 이렇게 하면 일부 필드에서 특정 값을 테스트하는 동시에 다른 필드를 해체하여 변수를 생성할 수 있습니다.
예제 18-14에는 Point
값을 세 가지 경우로 나눈 match
표현식이 있습니다: x
축 위의 점 (y = 0
이 참인 경우),
y
축 위의 점 (x = 0
), 그 외의 경우입니다.
파일명: src/main.rs
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
는 0을 가지고 있는 x
에 의해 두 번째 갈래에 매칭되므로,
이 코드는 On the y axis at 7
를 출력합니다.
match
표현식은 첫 번째 매칭되는 패턴을 찾으면 갈래 검사를
중지하므로, Point { x: 0, y: 0}
가 x
축과 y
축에 있더라도
이 코드는 On the x axis at 0
만 출력한다는 점을 기억하세요.
열거형 해체하기
이 책에서 열거형을 분해해 봤지만 (예를 들면 6장의 예제 6-5),
열거형을 분해하는 패턴이 열거형 내에 저장되는 데이터가 정의되는
방식과 일치한다는 것을 아직 명시적으로 논의하지는 않았습니다.
예를 들어, 예제 18-15에서는 예제 6-2의 Message
열거형을
사용하여 각 내부 값을 해체하는 패턴으로 match
를 작성합니다.
파일명: src/main.rs
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 the color to red {r}, green {g}, and blue {b}",) } } }
이 코드는 Change the color to red 0, green 160, and blue 255
를 출력할
것입니다. 다른 갈래가 실행되는 것을 보려면 msg
의 값을 변경해 보세요.
Message::Quit
처럼 데이터가 없는 열거형 배리언트의 경우 값을
더 이상 해체할 수는 없습니다. 리터럴 Message::Quit
값만 매칭시킬
수 있으며 해당 패턴에 변수는 없습니다.
Message::Move
처럼 구조체형 열거형 배리언트의 경우, 구조체와
매칭되도록 지정한 패턴과 유사한 패턴을 사용할 수 있습니다. 배리언트 이름
뒤에 중괄호를 넣은 다음 변수가 있는 필드를 나열하여 이 갈래에 대한
코드에서 사용할 부분을 분해합니다. 여기서는 예제 18-13에서 했던 것처럼
축약형을 사용합니다.
하나의 요소로 이루어진 튜플을 갖는 Message::Write
와 세 개의 요소로
되어있는 튜플을 갖는 Message::ChangeColor
같은 튜플형 열거형
배리언트의 경우, 패턴은 튜플을 매칭시키기 위해 지정하는 패턴과
비슷합니다. 패턴에 포함된 변수의 개수는 매칭시키려는 배리언트의 요소
개수와 일치해야 합니다.
중첩된 구조체와 열거형 해체하기
지금까지의 예제는 모두 한 단계 깊이의 구조체나 열거형을 매칭시켰지만,
중첩된 아이템에 대해서도 매칭시킬 수 있습니다! 예를 들어, 예제 18-15의
코드를 리팩터링하여 예제 18-16처럼 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 }); }
이 코드를 사용하면 복잡한 타입을 구성 요소로 분해하여 필요한 값을 개별적으로 사용할 수 있습니다.
패턴으로 구조체를 해체하는 것은 구조체의 각 필드에 있는 값처럼 값들의 조각을 서로 분리하여 사용하는 편리한 방식입니다.
패턴에서 값 무시하기
match
의 마지막 갈래와 같이 실제로는 아무 일도 하지 않지만, 나머지
가능한 모든 값을 매칭하는 캐치올을 얻기 위해 패턴의 값을 무시하는
것이 종종 유용할 때가 있다는 것을 보았습니다. 패턴에서 전체 값 또는
값의 일부를 무시하는 방법은 몇 가지가 있습니다: _
패턴 사용하기 (여러분이
이미 보셨던 것이죠), 다른 패턴 내에서 _
패턴 사용하기, 밑줄로 시작하는
이름 사용하기, 또는 ..
을 사용하여 값의 나머지 부분을 무시하는 방법이
있습니다. 이러한 각 패턴을 사용하는 방법과 사용하는 이유를 알아봅시다.
_
로 전체 값 무시하기
밑줄은 어떤 값과도 일치하지만 값에 바인딩되지는 않는 와일드카드
패턴으로 사용했습니다. 이는 match
표현식의 마지막 갈래로 특히
유용하지만, 예제 18-17처럼 함수 매개변수를 포함한 모든 패턴에
사용할 수도 있습니다.
파일명: src/main.rs
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
를 출력합니다.
특정 함수 매개변수가 더 이상 필요 없는 경우에는 대부분 함수 시그니처를 변경하여 사용하지 않는 매개변수가 포함되지 않도록 합니다. 함수 매개변수를 무시하는 것은, 예를 들면 특정 타입의 시그니처가 필요한 트레이트를 구현하는 중인데 구현체의 함수 본문에는 매개변수 중 하나가 필요하지 않은 경우 특히 유용할 수 있습니다. 만일 사용하지 않는 매개변수에 이름을 사용하면 컴파일러 경고를 받게 되는데 이를 방지할 수 있습니다.
중첩된 _
로 값의 일부 무시하기
다른 패턴 내에서 _
을 사용하여 값의 일부만 무시할 수도 있습니다.
예를 들어, 값의 일부만 테스트하고 싶지만 다른 부분은 실행하고자 하는
해당 코드에서 사용되는 곳이 없는 경우입니다. 예제 18-18은 설정값
관리를 담당하는 코드를 보여줍니다. 비즈니스 요구사항은 사용자가
기존의 사용자 설정값을 덮어쓸 수는 없지만 설정값을 해제할 수는 있으며
해제된 상태라면 값을 지정할 수 있어야 한다는 것입니다.
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)
를 출력합니다. 첫 번째 매치 갈래에서는 Some
배리언트
내부의 값을 매칭시키거나 사용할 필요는 없지만 setting_value
와
new_setting_value
가 Some
배리언트인 경우를 테스트할 필요는 있습니다.
그런 경우 setting_value
를 변경하지 않는 이유를 출력하고 변경하지
않습니다.
두 번째 갈래에서 _
패턴으로 표현된 다른 모든 경우에는 (즉 setting_value
또는 new_setting_value
가 None
인 경우에는) new_setting_value
가
setting_value
가 될 수 있도록 허용하고자 합니다.
또한 하나의 패턴 내에서 여러 위치에 밑줄을 사용하여 특정 값을 무시할 수도 있습니다. 예제 18-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은 무시될
것입니다.
_
로 시작하는 이름으로 사용하지 않는 변수 무시하기
변수가 만들어졌지만 어디에도 사용되지 않는 경우, 사용하지 않는 변수는 버그가 될 수 있으므로 러스트는 보통 경고를 표시합니다. 그러나 프로토타이핑 중이거나 프로젝트를 막 시작할 때와 같이, 아직 사용하지 않을 변수를 생성하는 것이 유용할 때도 있습니다. 이런 상황에서는 변수 이름을 밑줄로 시작하는 것으로 사용하지 않는 변수에 대해 경고하지 않도록 러스트에게 지시할 수 있습니다. 예제 18-20에서는 사용하지 않는 변수를 두 개 생성했지만, 이 코드를 컴파일할 때는 이 중 하나에 대해서만 경고를 받게 됩니다.
파일명: src/main.rs
fn main() { let _x = 5; let y = 10; }
여기서는 변수 y
가 사용되지 않는 것에 대한 경고를 받지만, _x
가
사용되지 않는 것에 대한 경고는 받지 않습니다.
_
만 사용하는 것과 밑줄로 시작하는 이름을 사용하는 것 사이에는
미묘한 차이가 있다는 점을 유의하세요. 문법 _x
는 여전히 변수에 값을
바인딩하는 반면, _
는 전혀 바인딩하지 않습니다. 이 구분이 중요한 경우를
보여드리기 위해서, 예제 18-21은 에러가 발생할 것입니다.
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{:?}", s);
}
s
값이 여전히 _s
로 이동되는데, 이는 s
를 다시 사용할 수 없도록
하기 때문에 에러가 발생합니다. 그러나 밑줄만 단독으로 사용하면
값이 바인딩되지 않습니다. 예제 18-22는 s
가 _
로 이동되지
않기 때문에 에러 없이 컴파일됩니다.
fn main() { let s = Some(String::from("Hello!")); if let Some(_) = s { println!("found a string"); } println!("{:?}", s); }
이 코드는 s
를 어디에도 바인딩하지 않았기 때문에, 즉 이동되지 않았으므로 잘 작동합니다.
..
로 값의 나머지 부분 무시하기
여러 부분으로 구성된 값의 경우, ..
문법을 사용하여 특정
부분만 사용하고 나머지는 무시할 수 있으므로 무시된 각 값에
밑줄을 나열할 필요가 없습니다. ..
패턴은 나머지 패턴에서
명시적으로 매칭시키지 않은 값의 모든 부분을 무시합니다.
예제 18-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: _
를 나열해야 하는 것보다 빠르며, 특히 많은 필드가
있는 구조체로 작업하는 데 한두 개의 필드만 관련 있는 상황에서
유용합니다.
..
문법은 필요한 만큼의 값으로 확장됩니다. 예제 18-24는
튜플에 ..
를 사용하는 방법을 보여줍니다.
파일명: src/main.rs
fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (first, .., last) => { println!("Some numbers: {first}, {last}"); } } }
이 코드에서는 첫 번째와 마지막 값이 first
와 last
로 매칭됩니다.
..
는 중간의 모든 것들과 매칭되고 무시될 것입니다.
그러나, ..
을 사용하는 것은 모호하지 않아야 합니다. 어떤 값을
매칭시키고 어떤 값을 무시해야 하는지 불분명하다면, 러스트는 에러를
발생시킵니다. 예제 18-25는 ..
을 모호하게 사용하는 예제를 보여주며,
컴파일되지 않습니다.
파일명: src/main.rs
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` due to previous error
러스트로서는 second
에 값을 매칭시키기 전에 튜플에서 몇 개의 값을 무시하고,
그 이후에 몇 개의 값을 더 무시할지 결정하는 것이 불가능합니다. 이 코드는
2
를 무시하고 second
를 4
에 바인딩한 다음 8
, 16
, 32
를 무시하려 함을
의미하는 것일 수 있습니다; 혹은 2
와 4
를 무시하고 second
를 8
에 바인딩한
다음 16
과 32
를 무시하는 등등을 의미하는 것일 수도 있습니다. 변수 이름
second
는 러스트에겐 특별한 의미가 없으므로, 이와 같이 두 곳에 ..
를
사용하는 것은 모호하기 때문에 컴파일러 에러가 발생합니다.
매치 가드를 사용한 추가 조건
매치 가드 (match guard) 는 match
갈래의 패턴 뒤에 지정되는 추가 if
조건으로, 해당 갈래가 선택되려면 이 조건도 매칭되어야 합니다. 매치 가드는
패턴만 가지고는 할 수 없는 더 복잡한 아이디어를 표현할 때 유용합니다.
조건은 패턴에서 생성된 변수를 사용할 수 있습니다. 예제 18-26은
첫 번째 갈래에 Some(x)
패턴이 있고 if x % 2 == 0
의 매치
가드(숫자가 짝수면 참)가 있는 match
를 보여줍니다.
fn main() { let num = Some(4); match num { Some(x) if x % 2 == 0 => println!("The number {} is even", x), Some(x) => println!("The number {} is odd", x), None => (), } }
이 예제는 The number 4 is even
을 출력합니다. Some(4)
가 Some(x)
에
매칭되기 때문에, num
이 첫 번째 갈래의 패턴과 비교될 때 매칭됩니다.
그다음 매치 가드가 x
를 2로 나눈 나머지가 0과 같은지 검사하고,
같으므로 첫 번째 갈래가 선택됩니다.
대신 num
이 Some(5)
였다면, 5를 2로 나눈 나머지는 1이고,
0과 같지 않으므로 첫 번째 갈래의 매치 가드는 거짓이 되었을 것입니다.
그러면 러스트는 두 번째 갈래로 이동하는데, 두 번째 갈래에는 매치
가드가 없어 모든 Some
배리언트와 매칭되기 때문에 매칭되었을 것입니다.
패턴 내에서 if x % 2 == 0
조건을 표현할 방법이 없으며, 매치
가드는 이런 로직을 표현하는 기능을 제공합니다. 이 추가적인 표현
능력의 단점은 매치 가드 표현식이 포함되면 컴파일러가 완전성을
확인하려고 하지 않는다는 것입니다.
예제 18-11에서 패턴 섀도잉 문제를 해결하기 위해 매치 가드를
사용할 수 있다고 언급했습니다. match
외부의 변수를 사용하는 대신
match
표현식의 패턴 내부에 새 변수가 만들어졌던 것을 상기합시다.
이 새로운 변수는 외부 변수의 값에 대해 테스트할 수 없다는 것을
의미했습니다. 예제 18-27은 이 문제를 해결하기 위해 매치 가드를
사용하는 방법을 보여줍니다.
파일명: src/main.rs
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 = {:?}, y = {y}", x); }
이제 이 코드는 Default case, x = Some(5)
를 출력합니다. 두 번째 매치
갈래의 패턴은 외부 y
를 가리는 새로운 변수 y
를 도입하지 않으며,
이는 매치 가드에서 외부 y
를 사용할 수 있음을 뜻합니다. 패턴을
Some(y)
로 지정하여 외부 y
를 가리는 대신 Some(n)
을 지정합니다.
이렇게 하면 match
외부에 n
변수가 없으므로 아무것도 가리지 않는
새 변수 n
이 생성됩니다.
매치 가드 if n == y
는 패턴이 아니므로 새로운 변수를 도입하지
않습니다. 이 y
는 새로운 섀도잉 y
가 아니라 외부의 y
이며,
n
과 y
를 비교하여 외부 y
와 같은 값을 가진 값을 찾을 수
있습니다.
매치 가드에 또는 연산자 |
를 사용하여 여러 패턴을 지정할 수도
있습니다; 매치 가드 조건은 모든 패턴에 적용될 것입니다. 예제 18-28은
|
를 사용하는 패턴과 매치 가드를 조합할 때의 우선순위를
보여줍니다. 이 예제의 중요한 부분은 if y
매치 가드가 6
에만
적용되는 것처럼 보일지라도 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
는 거짓이므로
첫 번째 갈래가 선택되지 않습니다. 이 코드는 두 번째 암으로 이동하고,
여기에 매칭되고, 이 프로그램은 no
를 출력합니다. 그 이유는 if
조건이 마지막 값 6
뿐만 아니라 전체 패턴 4 | 5 | 6
에 적용되기
때문입니다. 바꿔 말하면, 패턴과 관련된 매치 가드의 우선순위는 다음과
같이 동작합니다:
(4 | 5 | 6) if y => ...
이런 식이 아니고요:
4 | 5 | (6 if y) => ...
코드가 실행하고 나면 우선순위 동작은 명확해집니다: |
연산자를 사용하여 지정한 값 목록의 마지막 값에만 매치 가드가
적용되었다면, 해당 갈래에 매칭되어 프로그램은 yes
를 출력했을
것입니다.
@
바인딩
at 연산자 @
을 사용하면 값에 대한 패턴 매칭 여부를 테스트하는
동시에 해당 값을 갖는 변수를 만들 수 있습니다. 예제 18-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
필드의 값에 어떤 테스트도 적용하지 않았습니다: 즉, 어떤
값이라도 이 패턴과 매칭될 것입니다.
@
를 사용하면 하나의 패턴 내에서 값을 테스트하면서 그 값을 변수에 저장하게 해 줍니다.
정리
러스트의 패턴은 다양한 종류의 데이터를 구분하는 데 매우 유용합니다.
match
표현식에 패턴을 사용하면, 러스트는 패턴이 가능한 모든 값을
포함하도록 보장하며, 그렇지 않으면 프로그램이 컴파일되지 않습니다.
let
구문과 함수 매개변수에서의 패턴은 이러한 구성을 더 유용하게
만들어 값을 더 작은 부분으로 분해하는 동시에 변수에 할당할 수 있게
해 줍니다. 필요에 따라 단순하거나 복잡한 패턴을 만들 수 있습니다.
다음으로 이 책의 끝에서 두 번째 장에서는 러스트의 다양한 기능 중 고급 기능 몇 가지를 살펴보겠습니다.