패턴 문법
리터럴·변수·다중·범위 패턴과 구조체·열거형 해체, 값 무시, 매치 가드·바인딩을 조합해 복잡한 데이터를 분기합니다.
이 글에서는 패턴에서 쓸 수 있는 모든 문법을 모으고, 각각을 언제, 왜 쓰고 싶어 할지 이야기합니다.
리터럴 매칭
match 제어 흐름 구조에서 보았듯, 패턴을 리터럴에 직접 맞출 수 있습니다. 다음 코드가 그 예입니다.
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}x의 값이 1이므로 이 코드는 one을 출력합니다. 이 문법은 코드가 특정한 구체적 값을 받았을 때 어떤 동작을 하게 하고 싶을 때 쓸모가 있습니다.
명명된 변수 매칭
이름 붙은 변수는 어떤 값에든 맞는 반박 불가능한 패턴이며, 이 교재에서 여러 번 써 왔습니다. 하지만 match, if let, while let 표현식에서 이름 붙은 변수를 쓰면 복잡해지는 점이 있습니다. 이런 표현식은 각각 새 스코프를 시작하므로, 표현식 안에서 패턴의 일부로 선언한 변수는 바깥에 있는 같은 이름의 변수를 가립니다. 모든 변수가 그렇습니다. 다음 코드에서는 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 값이었다면, 첫 두 갈래의 패턴은 맞지 않았을 것이므로 값은 밑줄에 맞았을 것입니다. 밑줄 갈래의 패턴에는 x 변수를 도입하지 않았으므로, 표현식의 x는 여전히 가려지지 않은 바깥의 x입니다. 이 가정의 경우에는 match가 Default case, x = None을 출력했을 것입니다.
match 표현식이 끝나면 그 스코프가 끝나고, 안쪽 y의 스코프도 끝납니다. 마지막 println!은 at the end: x = Some(5), y = 10을 출력합니다.
Matched, y = 5
at the end: x = Some(5), y = 10바깥의 x와 y의 값을 비교하는 match 표현식을 만들려면, 가리는 변수를 도입하는 대신 매치 가드(match guard)라는 조건을 써야 합니다. 매치 가드는 이 글의 매치 가드를 사용한 추가 조건에서 이야기합니다. 아래 그림은 이 글의 예제들에서 패턴의 이름이 어떤 값을 가리키는지를 나란히 비교한 것입니다.
RUST · 패턴과 코드
새 바인딩, 바깥 값 비교, @의 검사와 바인딩을 구분한다
본문의 Some(5), 바깥 y=10, id=5를 사용해 각 이름이 어떤 값을 뜻하는지 비교합니다.
Some(y) · 새 y를 바인딩
let x = Some(5);
let y = 10;
// match x의 갈래
Some(y) => ...갈래 안의 y=5가 바깥 y를 가립니다. match 이후 바깥 y는 여전히 10입니다.
guard · 바깥 y와 비교
// 같은 x=Some(5), y=10
Some(n) if n == y => ...n=5는 새 바인딩이고 y=10은 바깥 변수입니다. 5==10이 거짓이므로 다음 갈래로 넘어갑니다.
@ · 검사하면서 값을 저장
// Message::Hello의 필드
id: v @ 3..=7id=5는 범위 검사에 성공하며 v=5를 함께 바인딩합니다. 본문의 id_variable을 v로 줄여 표시했습니다.
or-pattern의 guard는 전체 패턴에 적용
4 | 5 | 6 if y => ...의미는 (4 | 5 | 6) if y입니다. 본문의 x=4, y=false는 패턴에 맞아도 guard가 거짓이므로 다음 갈래에서 no를 출력합니다.
코드는 비교에 필요한 갈래 발췌입니다. @와 guard를 새 이름 바인딩과 함께 조합할 수도 있습니다.
Some(y)는 새 y를 바인딩하므로, 갈래 안의 y=5가 바깥의 y를 가리고 match가 끝난 뒤에도 바깥의 y는 10입니다. 매치 가드 Some(n) if n == y에서는 n=5가 새 바인딩이고 y=10은 바깥 변수이므로, 5 == 10이 거짓이라 다음 갈래로 넘어갑니다. @는 id: v @ 3..=7처럼 범위를 검사하면서 그 값을 저장하며, id=5이면 검사에 성공하고 v=5를 함께 바인딩합니다. 그림의 v는 이 글의 id_variable을 줄인 것입니다. 여러 패턴에 붙인 가드 4 | 5 | 6 if y는 (4 | 5 | 6) if y라는 뜻이므로, x=4, y=false이면 패턴에는 맞아도 가드가 거짓이라 다음 갈래에서 no를 출력합니다. 각 문법은 이 글에서 차례로 봅니다.
다중 패턴
match 표현식에서 패턴 또는(or) 연산자인 | 문법으로 여러 패턴을 맞출 수 있습니다. 예를 들어 다음 코드에서는 x 값을 match 갈래에 맞추는데, 첫 갈래에 또는 선택지가 있어, x의 값이 그 갈래의 값 가운데 어느 하나와 맞으면 그 갈래의 코드가 실행됩니다.
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}이 코드는 one or two를 출력합니다.
..=를 이용한 값의 범위 매칭
..= 문법으로 값의 끝을 포함하는 범위에 맞출 수 있습니다. 다음 코드에서는 패턴이 주어진 범위 안의 어떤 값과 맞으면 그 갈래가 실행됩니다.
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 값의 범위를 쓰는 예입니다.
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}러스트는 'c'가 첫 패턴의 범위 안에 있다는 것을 알 수 있으므로 early ASCII letter를 출력합니다.
더 깊이: 끝을 빼는 범위와 한쪽이 열린 범위
패턴에는 ..= 말고도 범위를 쓰는 형태가 더 있습니다. Reference의 범위 패턴은 아래쪽 끝값만 적어 그 이상을 모두 맞추는 1.., 위쪽 끝값까지 포함하는 ..=10을 정의합니다. Rust 1.80 발표에 따르면 끝을 빼는 a..b와 ..b는 이 버전부터 패턴에 쓸 수 있으며, 그 전까지는 끝을 포함하는 범위와 a.. 형태만 허용되었습니다. 끝을 빼는 범위는 패턴에서 하나 차이의 실수를 늘릴 수 있다는 우려로 오래 불안정한 기능으로 남아 있었고, 이 버전은 범위의 끝이 서로 이어지지 않거나 겹치는 것을 알려 주는 non_contiguous_range_endpoints와 overlapping_range_endpoints 린트를 함께 더했습니다.
범위 패턴은 적어도 값 하나를 포함해야 하므로, 10..=0이나 10..10은 컴파일 오류입니다. 반대로 0u8..=255u8처럼 타입의 모든 값을 덮는 정수나 char의 범위 패턴은 반박 불가능한 패턴이 됩니다. 2018 에디션까지는 ..= 대신 ...라고도 쓸 수 있었지만, 2021 에디션부터는 이 형태를 쓸 수 없습니다.
값을 해체하여 분리하기
패턴으로 구조체, 열거형, 튜플을 해체해 이 값들의 서로 다른 부분을 쓸 수도 있습니다. 각 값을 차례로 봅니다. 아래 그림은 이 절의 예제들에서 해체 패턴의 각 자리가 실제 값의 어디에 대응하는지 보여 줍니다.
RUST · 패턴과 코드
해체 패턴은 구조의 각 위치를 검사하거나 바인딩한다
구조체 필드, 중첩 enum의 variant, 튜플의 위치를 본문의 실제 값과 대응시킵니다.
구조체 · 필드별 바인딩
let p = Point { x: 0, y: 7 };
let Point { x: a, y: b } = p;필드 x→a=0, 필드 y→b=7. 필드 이름과 바인딩 이름은 달라도 됩니다.
구조체 · 리터럴 검사
// match p의 갈래
Point { x, y: 0 } => ...
Point { x: 0, y } => ...p=(0,7)은 첫 갈래의 y=0 검사에 실패하고 두 갈래의 x=0 검사에 성공하여 y=7을 바인딩합니다.
중첩 enum · 계층을 따라 해체
// 값
Message::ChangeColor(
Color::Hsv(0, 160, 255)
)
// 대응 패턴
Message::ChangeColor(
Color::Hsv(h, s, v)
)ChangeColor→Hsv 두 variant를 검사하고 h=0, s=160, v=255를 바인딩합니다.
튜플 + 구조체 · 각 위치 해체
let ((feet, inches), Point { x, y })
= ((3, 10),
Point { x: 3, y: -10 });feet=3, inches=10, x=3, y=-10. 바깥 튜플과 안쪽 튜플·구조체가 같은 구조로 대응됩니다.
전체 enum·Point 정의와 match의 나머지 갈래는 본문에 있습니다. 각 패턴은 단순한 변수 이름보다 구조·검사를 더 많이 표현합니다.
구조체 Point { x: 0, y: 7 }을 Point { x: a, y: b }로 해체하면 필드 x가 a=0에, 필드 y가 b=7에 바인딩되며, 필드 이름과 바인딩 이름은 달라도 됩니다. match의 갈래에서는 Point { x, y: 0 }이 y=0 검사에 실패하고 Point { x: 0, y }가 x=0 검사에 성공해 y=7을 바인딩합니다. 중첩된 열거형에서는 Message::ChangeColor(Color::Hsv(h, s, v))가 ChangeColor와 Hsv 두 배리언트를 차례로 검사하고 h=0, s=160, v=255를 바인딩합니다. 튜플과 구조체를 섞어 해체하면 feet=3, inches=10, x=3, y=-10처럼 바깥 튜플과 안쪽 튜플, 구조체가 같은 구조로 대응됩니다.
구조체 해체하기
다음 코드는 x와 y 두 필드를 가진 Point 구조체를, let 구문의 패턴으로 분리하는 예입니다. 패턴 Point { x: a, y: b }에서 콜론 왼쪽은 구조체의 필드 이름이고, 오른쪽은 그 필드의 값을 받을 새 변수의 이름입니다.
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;라고 쓰면 중복이 많으므로, 러스트에는 구조체 필드에 맞추는 패턴의 줄임 문법이 있습니다. 구조체 필드의 이름만 나열하면, 패턴에서 만들어지는 변수의 이름이 필드와 같아집니다. 다음 코드는 앞의 코드와 같은 방식으로 동작하지만, 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 구조체의 값이 들어 있습니다.
모든 필드에 변수를 만드는 대신, 구조체 패턴의 일부로 리터럴 값을 넣어 해체할 수도 있습니다. 그러면 일부 필드가 특정 값인지 확인하면서, 다른 필드는 해체한 변수로 만들 수 있습니다.
다음 코드에는 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만 출력할 것입니다.
열거형 해체하기
이 교재에서 열거형을 해체해 왔지만, 열거형을 해체하는 패턴이 열거형 안에 저장된 데이터가 정의된 방식에 대응한다는 것을 아직 명시적으로 이야기하지 않았습니다. 예로 열거형 정의의 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 같은 구조체 형태의 열거형 배리언트에는, 구조체에 맞추려고 지정하는 패턴과 비슷한 패턴을 쓸 수 있습니다. 배리언트 이름 뒤에 중괄호를 두고 필드와 변수를 나열해, 이 갈래의 코드에서 쓸 조각들로 나눕니다. 여기서는 앞의 예에서처럼 줄임 형태를 썼습니다.
하나의 원소를 가진 튜플을 담는 Message::Write나 원소 셋을 가진 튜플을 담는 Message::ChangeColor 같은 튜플 형태의 열거형 배리언트에는, 튜플에 맞추려고 지정하는 패턴과 비슷한 패턴을 씁니다. 패턴의 변수의 수는 맞추려는 배리언트의 원소 수와 같아야 합니다.
중첩된 구조체와 열거형 해체하기
지금까지의 예는 모두 한 단계 깊이의 구조체나 열거형에 맞췄지만, 중첩된 항목에도 매칭할 수 있습니다. 예를 들어 앞의 코드를 고쳐, 다음처럼 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 표현식에서 이런 복잡한 조건을 지정할 수 있습니다. 이 코드는 Change color to hue 0, saturation 160, value 255를 출력합니다.
구조체와 튜플 해체하기
해체 패턴은 더 복잡한 방식으로 섞고, 맞추고, 중첩할 수 있습니다. 다음 예는 튜플 안에 구조체와 튜플을 중첩하고, 기본 값을 모두 해체하는 복잡한 해체를 보여 줍니다.
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });이 코드로 복잡한 타입을 구성 요소로 나눠, 관심 있는 값을 따로따로 쓸 수 있습니다.
패턴으로 해체하는 것은 구조체의 각 필드의 값처럼 값의 조각을 서로 따로 쓰는 편리한 방법입니다.
패턴에서 값 무시하기
패턴에서 값을 무시하는 것이 쓸모 있을 때가 있습니다. match의 마지막 갈래에서, 실제로는 아무것도 하지 않지만 남은 모든 가능한 값을 다루는 모든 것을 잡는 패턴을 얻을 때가 그렇습니다. 패턴에서 값 전체나 값의 일부를 무시하는 방법이 몇 가지 있습니다. _ 패턴을 쓰는 것(이미 보았습니다), 다른 패턴 안에서 _ 패턴을 쓰는 것, 밑줄로 시작하는 이름을 쓰는 것, ..로 값의 남은 부분을 무시하는 것입니다. 아래 그림은 이 가운데 두 가지 비교를 보여 줍니다.
패턴 생략 문법 선택
독립 Some(String) 실험에서 _s는 내부 String 이동/나중 s 사용 오류, Some(_)는 바인딩 없음/원 s 유지라는 소유 가능 상태 변화이다. 잘못된 print를 런타임 단계로 실행하지 않는다.
RUST · 패턴과 코드
_name은 바인딩하고, _와 ..는 값을 바인딩하지 않는다
밑줄로 시작하는 이름과 와일드카드의 소유권 차이는 String이 들어 있는 Option 예제에서 드러납니다.
_name은 참조도 바인딩할 수 있다. _·..는 배타적 기능이 아니며 중첩 패턴은 각각 판단한다.
코드·조건·비교표와 보충 설명
_name · 미사용 경고를 피하는 이름
let s = Some(
String::from("Hello!")
);
if let Some(_s) = s {
// 내부 String을 _s로 이동
}
// 아래는 이동 후 사용 오류
println!("{s:?}");_s는 실제 바인딩입니다. 값이나 참조를 바인딩할 수 있으며 이 owned String 예제에서는 내부 값을 이동합니다.
_ · 한 위치를 바인딩하지 않음
let s = Some(
String::from("Hello!")
);
if let Some(_) = s {
// 내부 값을 바인딩하지 않음
}
println!("{s:?}"); // 사용 가능_ 자체는 복사·이동·대여를 만들지 않습니다. 다른 부분에서 값을 이동하는 패턴과 함께 쓸 때는 그 다른 부분을 따로 봅니다.
.. · 나머지 구조를 생략
Point { x, .. }
(first, .., last)구조체의 나머지 필드 또는 튜플·슬라이스의 나머지 위치를 생략합니다. numbers=(2,4,8,16,32)에서 first=2, last=32입니다.
중복 rest · 같은 튜플 안에서는 오류
(.., second, ..) // 허용되지 않음같은 튜플 패턴의 두 ..는 second가 어느 위치인지 모호하게 만듭니다. 각 중첩 하위 패턴의 ..는 별도로 판단합니다.
코드는 본문 예제의 핵심 발췌입니다. _name·_·..는 배타적인 기능이 아니므로 한 구조 패턴 안에서 조합할 수 있습니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 두 독립 패턴 Some(String)을 _s 또는 _로 처리하는 독립 사본이다. |
|
|
| 이동 여부 _s는 내부 값을 바인딩하고, _는 바인딩하지 않는다. |
|
|
| 후속 사용 판정 원래 s의 사용 가능성을 컴파일 경계에서 확인한다. |
|
|
패턴 생략 문법 선택
독립 Some(String) 실험에서 _s는 내부 String 이동/나중 s 사용 오류, Some(_)는 바인딩 없음/원 s 유지라는 소유 가능 상태 변화이다. 잘못된 print를 런타임 단계로 실행하지 않는다.
RUST · 패턴과 코드
_name은 바인딩하고, _와 ..는 값을 바인딩하지 않는다
밑줄로 시작하는 이름과 와일드카드의 소유권 차이는 String이 들어 있는 Option 예제에서 드러납니다.
_name은 참조도 바인딩할 수 있다. _·..는 배타적 기능이 아니며 중첩 패턴은 각각 판단한다.
코드·조건·비교표와 보충 설명
_name · 미사용 경고를 피하는 이름
let s = Some(
String::from("Hello!")
);
if let Some(_s) = s {
// 내부 String을 _s로 이동
}
// 아래는 이동 후 사용 오류
println!("{s:?}");_s는 실제 바인딩입니다. 값이나 참조를 바인딩할 수 있으며 이 owned String 예제에서는 내부 값을 이동합니다.
_ · 한 위치를 바인딩하지 않음
let s = Some(
String::from("Hello!")
);
if let Some(_) = s {
// 내부 값을 바인딩하지 않음
}
println!("{s:?}"); // 사용 가능_ 자체는 복사·이동·대여를 만들지 않습니다. 다른 부분에서 값을 이동하는 패턴과 함께 쓸 때는 그 다른 부분을 따로 봅니다.
.. · 나머지 구조를 생략
Point { x, .. }
(first, .., last)구조체의 나머지 필드 또는 튜플·슬라이스의 나머지 위치를 생략합니다. numbers=(2,4,8,16,32)에서 first=2, last=32입니다.
중복 rest · 같은 튜플 안에서는 오류
(.., second, ..) // 허용되지 않음같은 튜플 패턴의 두 ..는 second가 어느 위치인지 모호하게 만듭니다. 각 중첩 하위 패턴의 ..는 별도로 판단합니다.
코드는 본문 예제의 핵심 발췌입니다. _name·_·..는 배타적인 기능이 아니므로 한 구조 패턴 안에서 조합할 수 있습니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 두 독립 패턴 Some(String)을 _s 또는 _로 처리하는 독립 사본이다. |
|
|
| 이동 여부 _s는 내부 값을 바인딩하고, _는 바인딩하지 않는다. |
|
|
| 후속 사용 판정 원래 s의 사용 가능성을 컴파일 경계에서 확인한다. |
|
|
Some(String)에 Some(_s)를 쓰면 _s는 실제로 바인딩하는 이름이므로 String이 옮겨 가, 그 뒤로 원래의 s를 쓰면 이동한 값을 썼다는 오류가 납니다. Some(_)는 아무것도 바인딩하지 않으므로 값이 옮겨 가지 않고 s를 계속 쓸 수 있습니다. 이것과 별개로, 튜플 (2, 4, 8, 16, 32)에서 ..를 한 번 쓰면 나머지 자리가 하나로 정해져 first=2, last=32가 되지만, 같은 튜플에 ..를 두 번 쓰면 어느 자리를 가리키는지 모호해 컴파일 오류가 납니다. 두 비교는 서로 다른 축이며 함께 쓸 수도 있고, _name은 참조를 바인딩할 수도 있습니다.
_로 전체 값 무시하기
밑줄을 어떤 값에든 맞지만 값에 바인딩하지 않는 와일드카드 패턴으로 써 왔습니다. 이것은 match 표현식의 마지막 갈래에 특히 쓸모 있지만, 다음처럼 함수 매개변수를 포함해 어떤 패턴에서든 쓸 수 있습니다.
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를 출력합니다.
대부분의 경우 특정 함수 매개변수가 더 이상 필요 없다면, 쓰지 않는 매개변수가 없도록 시그니처를 바꿀 것입니다. 함수 매개변수를 무시하는 것은, 예를 들어 트레이트를 구현하는데 특정 타입 시그니처가 필요하지만 구현의 함수 본문에는 매개변수 하나가 필요 없는 경우에 특히 쓸모 있습니다. 그러면 이름을 썼을 때처럼 쓰지 않는 함수 매개변수에 대한 컴파일러 경고를 받지 않게 됩니다.
중첩된 _로 값의 일부 무시하기
값의 일부만 검사하고 대응하는 코드에서 다른 부분은 쓸 필요가 없을 때, 다른 패턴 안에서 _를 써 값의 일부만 무시할 수도 있습니다. 다음 코드는 설정 값을 관리하는 코드입니다. 업무 요구 사항은 사용자가 이미 설정한 값은 덮어쓰지 못하지만, 설정을 해제하고 현재 해제된 설정에는 값을 줄 수 있다는 것입니다.
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가 되게 합니다.
한 패턴 안의 여러 곳에서 밑줄을 써 특정 값을 무시할 수도 있습니다. 다음 코드는 원소가 다섯인 튜플에서 두 번째와 네 번째 값을 무시하는 예입니다.
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은 무시됩니다.
_로 시작하는 이름으로 사용하지 않는 변수 무시하기
변수를 만들었는데 어디서도 쓰지 않으면, 쓰지 않는 변수가 버그일 수 있으므로 러스트는 보통 경고를 냅니다. 하지만 프로토타입을 만들거나 프로젝트를 막 시작할 때처럼, 아직 쓰지 않을 변수를 만들 수 있는 것이 쓸모 있을 때가 있습니다. 이런 상황에서는 변수 이름을 밑줄로 시작해, 쓰지 않는 변수에 대해 경고하지 말라고 러스트에게 알릴 수 있습니다. 다음 코드에서는 쓰지 않는 변수를 둘 만들지만, 이 코드를 컴파일하면 그 가운데 하나에 대해서만 경고를 받습니다.
fn main() {
let _x = 5;
let y = 10;
}여기서는 변수 y를 쓰지 않는다는 경고는 받지만, _x를 쓰지 않는다는 경고는 받지 않습니다.
_만 쓰는 것과 밑줄로 시작하는 이름을 쓰는 것 사이에는 미묘한 차이가 있다는 점을 알아 두세요. _x 문법은 여전히 값을 변수에 바인딩하지만, _는 전혀 바인딩하지 않습니다. 이 구분이 중요한 경우를 보여 주기 위해, 다음 코드는 오류가 납니다.
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
}s 값이 여전히 _s 안으로 옮겨지므로, 그 뒤에 s를 다시 쓸 수 없어 오류가 납니다. 컴파일러는 s의 일부가 옮겨진 뒤에 빌렸다는 E0382 오류를 냅니다. 하지만 밑줄만 쓰면 결코 값에 바인딩하지 않습니다. 다음 코드는 s가 _ 안으로 옮겨지지 않으므로 아무 오류 없이 컴파일됩니다.
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{s:?}");
}이 코드는 s를 어떤 것에도 바인딩하지 않으므로 문제없이 동작하며, s는 옮겨지지 않습니다.
..로 값의 나머지 부분 무시하기
여러 부분을 가진 값에서는 .. 문법으로 특정 부분만 쓰고 나머지는 무시할 수 있으며, 무시하는 값마다 밑줄을 나열하지 않아도 됩니다. .. 패턴은 패턴의 나머지 부분에서 명시적으로 맞추지 않은 값의 부분을 모두 무시합니다. 다음 코드에는 3차원 공간의 좌표를 담는 Point 구조체가 있습니다. match 표현식에서 x 좌표에만 작업하고 y와 z 필드의 값은 무시하려 합니다.
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: _를 나열하는 것보다 빠르며, 특히 필드가 많은 구조체에서 관련된 필드가 한두 개뿐일 때 그렇습니다.
.. 문법은 필요한 만큼의 값으로 늘어납니다. 다음 코드는 튜플에 ..를 쓰는 방법을 보여 줍니다.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}이 코드에서 첫 값과 마지막 값은 first와 last에 맞춰집니다. ..는 가운데의 모든 것에 맞고 그것을 무시합니다. 이 코드는 Some numbers: 2, 32를 출력합니다.
하지만 ..는 모호하지 않아야 합니다. 어느 값을 맞추고 어느 값을 무시할지 분명하지 않으면, 러스트는 오류를 냅니다. 다음 코드는 ..를 모호하게 쓰는 예이므로 컴파일되지 않습니다.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}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러스트는 second에 값을 맞추기 전에 튜플의 값 몇 개를 무시해야 하는지, 그 뒤에 몇 개를 더 무시해야 하는지 알아낼 수 없습니다. 이 코드는 2를 무시하고 second를 4에 바인딩한 뒤 8, 16, 32를 무시하라는 뜻일 수도 있고, 2와 4를 무시하고 second를 8에 바인딩한 뒤 16과 32를 무시하라는 뜻일 수도 있습니다. 변수 이름 second는 러스트에게 특별한 의미가 없으므로, 이렇게 두 곳에 ..를 쓰는 것은 모호해서 컴파일러 오류가 납니다.
매치 가드를 사용한 추가 조건
매치 가드(match guard)는 match 갈래의 패턴 뒤에 지정하는 추가 if 조건으로, 그 갈래가 선택되려면 이 조건도 맞아야 합니다. 매치 가드는 패턴만으로 표현할 수 있는 것보다 복잡한 생각을 나타내는 데 쓸모가 있습니다. 다만 매치 가드는 match 표현식에서만 쓸 수 있고, if let이나 while let 표현식에서는 쓸 수 없다는 점을 알아 두세요.
조건에서는 패턴에서 만든 변수를 쓸 수 있습니다. 다음 코드에서 첫 갈래의 패턴은 Some(x)이고, 매치 가드는 if x % 2 == 0입니다. 이 조건은 숫자가 짝수이면 true가 됩니다.
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)였다면, 5를 2로 나눈 나머지는 1이라 0이 아니므로 첫 갈래의 매치 가드가 false였을 것입니다. 그러면 러스트는 두 번째 갈래로 가고, 두 번째 갈래에는 매치 가드가 없어 어떤 Some 배리언트에든 맞으므로 그 갈래가 맞았을 것입니다.
패턴 안에서는 if x % 2 == 0 조건을 나타낼 방법이 없으므로, 매치 가드가 이 로직을 나타낼 수 있게 해 줍니다. 이 추가 표현력의 단점은, 매치 가드가 들어 있으면 컴파일러가 철저한지 확인하지 않는다는 것입니다.
앞의 명명된 변수 매칭에서 이야기할 때, 매치 가드로 패턴이 가리는 문제를 풀 수 있다고 했습니다. 그 코드에서는 match 바깥의 변수를 쓰는 대신 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)
at the end: x = Some(5), y = 10이 코드는 이제 Default case, x = Some(5)를 출력합니다. 두 번째 match 갈래의 패턴은 바깥의 y를 가릴 새 변수 y를 도입하지 않으므로, 매치 가드에서 바깥의 y를 쓸 수 있습니다. 바깥의 y를 가렸을 Some(y)로 패턴을 지정하는 대신 Some(n)을 지정합니다. 그러면 match 바깥에 n 변수가 없으므로 아무것도 가리지 않는 새 변수 n이 만들어집니다.
매치 가드 if n == y는 패턴이 아니므로 새 변수를 도입하지 않습니다. 이 y는 새로 가려진 y가 아니라 바깥의 y이며, n과 y를 비교해 바깥의 y와 같은 값을 찾을 수 있습니다.
매치 가드에서 또는 연산자 |로 여러 패턴을 지정할 수도 있습니다. 매치 가드의 조건은 모든 패턴에 적용됩니다. 다음 코드는 |를 쓰는 패턴과 매치 가드를 결합할 때의 우선순위를 보여 줍니다. 이 예에서 중요한 부분은 매치 가드 if y가 6에만 적용되는 것처럼 보일 수 있지만, 실제로는 4, 5, 6에 모두 적용된다는 것입니다.
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 조건이 마지막 값 6에만이 아니라 4 | 5 | 6 패턴 전체에 적용되기 때문입니다. 다시 말해 패턴에 대한 매치 가드의 우선순위는 다음과 같이 동작합니다.
(4 | 5 | 6) if y => ...다음과 같이 동작하지 않습니다. 아래의 (6 if y)처럼 가드를 패턴 안에 넣는 문법은 실제로 없으며, 잘못 읽은 우선순위를 나타내려고 적은 모양입니다.
4 | 5 | (6 if y) => ...코드를 실행한 뒤 우선순위의 동작은 분명합니다. 매치 가드가 | 연산자로 지정한 값 목록의 마지막 값에만 적용되었다면, 갈래가 맞아 프로그램이 yes를 출력했을 것입니다.
@ 바인딩
at 연산자 @를 쓰면, 값이 패턴에 맞는지 검사하면서 동시에 그 값을 담는 변수를 만들 수 있습니다. 다음 코드에서는 Message::Hello의 id 필드가 3..=7 범위 안에 있는지 검사하고 싶습니다. 그 값을 id_variable 변수에 바인딩해 그 갈래의 코드에서도 쓰고 싶습니다.
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 필드의 값에 어떤 검사도 하지 않았습니다. 어떤 값이든 이 패턴에 맞을 것입니다.
@를 쓰면 하나의 패턴 안에서 값을 검사하고 그것을 변수에 저장할 수 있습니다.
더 깊이: 참조를 매칭하면 바인딩은 어떻게 정해질까
이 글의 패턴은 대부분 값 자체에 맞췄지만, 실제 코드에서는 &Option<i32>처럼 참조를 매칭하는 일이 많습니다. Reference의 바인딩 모드 설명에 따르면, 참조 값을 참조가 아닌 패턴으로 매칭하면 컴파일러가 참조를 자동으로 따라가며 기본 바인딩 모드(default binding mode)를 바꿉니다. 처음 모드는 값을 옮기는 move이고, 공유 참조를 지나면 ref, 가변 참조를 지나면 ref mut이 됩니다. 그래서 x: &Option<i32>에 if let Some(y) = x라고 쓰면, y는 i32가 아니라 &i32로 묶입니다.
2024 에디션은 이 규칙에 제한을 더했습니다. 에디션 가이드에 따르면 기본 바인딩 모드가 move가 아닌 곳에서는 바인딩에 mut, ref, ref mut을 쓸 수 없고, &나 &mut 참조 패턴도 쓸 수 없습니다. 예전 에디션에서는 이런 곳의 mut이 바인딩 모드를 슬그머니 move로 되돌려 변수의 타입까지 바꾸었는데, 이렇게 예상하기 어려운 동작을 앞으로 고칠 여지를 남기려는 것입니다. cargo fix --edition은 rust_2024_incompatible_pat 린트로 그런 패턴을 let &[ref x, mut y] = ...처럼 모든 단계를 명시한 형태로 바꿔 줍니다.
정리
러스트의 패턴은 서로 다른 종류의 데이터를 구분하는 데 아주 쓸모가 있습니다. match 표현식에서 쓰면 러스트는 패턴이 모든 가능한 값을 덮도록 보장하며, 그렇지 않으면 프로그램이 컴파일되지 않습니다. let 구문과 함수 매개변수의 패턴은 이런 구조를 더 쓸모 있게 만들어, 값을 더 작은 부분으로 해체하는 동시에 그 부분들을 변수에 대입할 수 있게 해 줍니다. 필요에 맞게 단순하거나 복잡한 패턴을 만들 수 있습니다.
- 리터럴,
|로 묶은 여러 패턴,..=로 나타낸 숫자나char의 범위에 값을 맞출 수 있습니다. match,if let,while let의 패턴에서 선언한 이름은 새 스코프의 새 변수라 바깥의 같은 이름을 가립니다.- 구조체, 열거형, 튜플을 해체하고 중첩할 수 있으며, 일부 필드를 리터럴로 검사하면서 나머지를 바인딩할 수 있습니다.
_는 값을 바인딩하지 않고,_x는 바인딩하되 쓰지 않아도 경고하지 않으며,..는 남은 부분을 한 번에 무시하되 모호하게 두 번 쓸 수 없습니다.- 매치 가드
if 조건은|로 묶은 패턴 전체에 적용되며,@는 패턴을 검사하면서 그 값을 변수에 바인딩합니다.
연습 문제
1. 무엇이 출력될까
다음 match는 n이 각각 0, 7, 15, 42일 때 무엇을 출력하나요? 갈래는 위에서부터 차례로 비교되며, 셋째 갈래의 패턴 x는 모든 값에 맞으므로 그 갈래가 선택될지는 가드의 조건이 정합니다.
match n {
0 => println!("zero"),
x @ 1..=9 => println!("digit {x}"),
x if x % 5 == 0 => println!("multiple of five {x}"),
_ => println!("other"),
}해설 보기
0은 첫 갈래에 맞아 zero를 출력합니다. 7은 1..=9 범위에 맞아 x에 바인딩되므로 digit 7을 출력합니다. 15는 범위 밖이지만 세 번째 갈래의 가드 15 % 5 == 0이 참이라 multiple of five 15를 출력합니다. 42는 어느 조건에도 맞지 않아 other를 출력합니다.
2. 패턴으로 다시 쓰기
Point { x, y }를 받아, 원점이면 origin, x와 y가 같으면 diagonal, 그 밖에는 x의 부호에 따라 left나 right를 출력하는 match를 쓰세요. x가 0이고 y가 0이 아닐 때는 right로 칩니다.
해설 보기
다음처럼 쓸 수 있습니다.
match p {
Point { x: 0, y: 0 } => println!("origin"),
Point { x, y } if x == y => println!("diagonal"),
Point { x, .. } if x < 0 => println!("left"),
_ => println!("right"),
}원점을 리터럴 패턴으로 먼저 잡아야, 원점이 x == y 가드에도 맞아 diagonal로 출력되는 일을 막을 수 있습니다. x의 부호만 볼 때는 ..로 y를 무시했고, 가드가 있는 갈래는 철저한지 따질 때 세지 않으므로 마지막에 _ 갈래를 둡니다.
이것으로 17장을 마칩니다. 다음 장의 안전하지 않은 러스트부터는 러스트의 여러 기능 가운데 고급 측면을 살펴봅니다.