본문 바로가기

반박 가능성: 패턴이 매칭에 실패할지의 여부

모든 값에 성공하는 반박 불가능 패턴과 실패할 수 있는 반박 가능 패턴을 구분해 각 구문에 허용되는 형태를 판단합니다.

패턴에는 두 형태가 있습니다. 반박 가능한(refutable) 패턴과 반박 불가능한(irrefutable) 패턴입니다. 넘길 수 있는 어떤 값에든 맞는 패턴은 반박 불가능합니다. 예를 들어 let x = 5; 구문의 x는 무엇이든 맞으므로 맞추기에 실패할 수 없습니다. 가능한 어떤 값에는 맞추기에 실패할 수 있는 패턴은 반박 가능합니다. 예를 들어 if let Some(x) = a_value 표현식의 Some(x)는, a_value 변수의 값이 Some이 아니라 None이면 Some(x) 패턴이 맞지 않으므로 반박 가능합니다.

구문마다 허용되는 패턴

함수 매개변수, let 구문, for 루프는 반박 불가능한 패턴만 받습니다. 값이 맞지 않을 때 프로그램이 할 수 있는 의미 있는 일이 없기 때문입니다. if let과 while let 표현식, 그리고 let...else 구문은 반박 가능한 패턴과 반박 불가능한 패턴을 모두 받지만, 반박 불가능한 패턴을 쓰면 컴파일러가 경고합니다. 이 구문들은 정의상 실패할 수 있는 경우를 다루려는 것이기 때문입니다. 조건문의 기능은 성공인지 실패인지에 따라 다르게 동작하는 데 있습니다.

구문받는 패턴
함수 매개변수, let, for반박 불가능한 패턴만
if let, while let, let...else둘 다 받지만, 반박 불가능한 패턴에는 경고
match의 갈래반박 가능한 패턴. 남은 값을 모두 잡는 마지막 갈래는 반박 불가능한 패턴

대개 반박 가능한 패턴과 반박 불가능한 패턴의 구분을 신경 쓸 필요는 없습니다. 하지만 오류 메시지에서 보게 되었을 때 대응할 수 있도록 반박 가능성이라는 개념에는 익숙해져야 합니다. 그런 경우에는 코드의 의도에 따라 패턴을 바꾸거나, 패턴을 쓰는 구문을 바꿔야 합니다. 아래 그림은 같은 반박 가능한 패턴을 서로 다른 세 구문에 놓았을 때 어떻게 되는지를 비교한 것입니다.

RUST · 패턴과 코드

반박 가능성은 현재 타입의 모든 값에 맞는지로 판단한다

일부 값에서 실패하는 패턴은 그 불일치를 처리할 수 있는 구문에 놓습니다. 단순 let과 let-else는 계약이 다릅니다.

같은 Option 패턴을 세 구문에 놓을 때의 판정 Some(x)는 Option의 일부 값에만 일치합니다. 화살표는 이 패턴과 단순 let, if let, let-else의 비교 관계 및 각 결과를 연결합니다. 단순 let은 E0005, if let은 Some에서 블록 실행과 None에서 건너뜀, let-else는 Some 뒤의 바인딩과 None에서 발산하는 else를 보여 줍니다. 세 갈래는 같은 패턴을 서로 다른 구문에 대입한 비교입니다. Some(x) Option<T>의 패턴 일부 값에 불일치 단순 let else 없는 구문 불일치 경로 없음 if let 일치 시 블록 블록 안의 x let-else 불일치 시 else else는 반드시 발산 E0005 Option에 None 가능 현재 값이 Some여도 단순 let은 오류 Some / None Some: 블록 실행 None: 블록 건너뜀 x는 블록 안의 값 Some / None Some: 이후 x 사용 None: else로 이동 else: 반드시 발산 return: 함수 종료
같은 Option 패턴을 세 구문에 놓을 때의 판정 Some(x)는 Option의 일부 값에만 일치합니다. 화살표는 이 패턴과 단순 let, if let, let-else의 비교 관계 및 각 결과를 연결합니다. 단순 let은 E0005, if let은 Some에서 블록 실행과 None에서 건너뜀, let-else는 Some 뒤의 바인딩과 None에서 발산하는 else를 보여 줍니다. 세 갈래는 같은 패턴을 서로 다른 구문에 대입한 비교입니다. Some(x) Option<T>의 패턴 일부 값에 불일치 단순 let else 없는 구문 불일치 경로 없음 if let 일치 시 블록 블록 안의 x let-else 불일치 시 else else는 반드시 발산 E0005 Option에 None 가능 현재 값이 Some여도 단순 let은 오류 Some / None Some: 블록 실행 None: 블록 건너뜀 x는 블록 안의 값 Some / None Some: 이후 x 사용 None: else로 이동 else: 반드시 발산 return: 함수 종료

화살표는 같은 패턴과 세 구문, 각 구문에서의 판정·실행 결과를 연결합니다. 한 갈래를 따라 해당 구문의 실패 처리와 x의 사용 범위를 함께 확인하세요.

반박 불가능 · 모든 값에 일치

let x = 5;
let (a, b) = (1, 2);

이름 x는 어떤 i32에도, (a,b)는 두 항목 튜플의 모든 값에 맞습니다. 단순 let·for 항목·매개변수에 사용할 수 있습니다.

반박 가능 · 일부 값에 불일치

Some(x) // Option<T>의 패턴

Some에는 맞지만 None에는 맞지 않습니다. 현재 Option<T> 타입에 None이라는 다른 가능한 값이 있기 때문입니다.

단순 let · 불일치가 처리되지 않음

let Some(x) = value; // E0005

value: Option<T>이면 None에서 실패할 수 있으므로 컴파일 오류입니다. 런타임에 현재 값이 Some이어도 이 단순 let은 허용되지 않습니다.

if let / let-else · 실패 경로가 있음

if let Some(x) = value {
    // 일치할 때만 x 사용
}
let Some(x) = value else { return };

if let은 건너뛰고, let-else는 발산하는 else로 벗어납니다. while let은 불일치 시 반복을 끝냅니다.

경고와 match의 완전성은 별도로 확인

단독 if let x = 5처럼 항상 맞는 조건은 기본 설정에서 경고합니다. match는 Some(x)와 None 두 반박 가능 갈래만으로도 모든 Option 값을 처리할 수 있습니다. 마지막 갈래가 반드시 와일드카드일 필요는 없습니다.

반박 가능성은 패턴과 타입의 관계이고, 허용 여부는 구문의 실패 처리 계약까지 함께 판단합니다.

반박 가능성은 지금 들어 있는 값이 아니라, 그 타입의 모든 값에 맞는지로 판단합니다. Some(x)는 Option<T>의 일부 값에만 맞습니다. 단순한 let에는 맞지 않을 때 갈 경로가 없으므로, 지금 값이 Some이더라도 Option에는 None이 있을 수 있어 E0005 오류가 납니다. if let에서는 Some이면 블록을 실행하고 None이면 건너뛰며, x는 블록 안에서만 씁니다. let...else에서는 Some이면 그 뒤로 x를 계속 쓰고, None이면 else로 가며, else는 return처럼 반드시 현재 흐름을 벗어나야 합니다.

더 깊이: 값이 없는 배리언트는 빼도 됩니다

반박 가능성을 그 타입의 모든 값으로 판단한다면, 결코 만들어질 수 없는 값은 따질 필요가 없습니다. 예를 들어 std::convert::Infallible은 배리언트가 하나도 없는 열거형이어서 값을 만들 수 없으므로, Result<T, Infallible>은 언제나 Ok입니다. Rust 1.82 발표에 따르면 이 버전부터는 이렇게 값이 없는 타입을 담은 패턴을 생략할 수 있어서, Result<T, Infallible> 값에는 Err 갈래 없이 let Ok(x) = value;라고 쓸 수 있습니다. Ok(x)가 이 타입의 가능한 모든 값에 맞는 반박 불가능한 패턴이 되는 것입니다.

다만 값이 없는 타입에 참조나 포인터, 유니온의 필드를 거쳐 닿는 경우에는 생략할 수 없습니다. 발표는 또 여러 러스트 버전을 함께 지원하는 크레이트가 깨지지 않도록, 값이 없는 타입을 매칭하는 갈래를 아직은 도달할 수 없는 갈래로 경고하지 않는다고 설명합니다. 그래서 예전 버전도 지원해야 하는 코드는 Err(e) => match e {} 같은 갈래를 그대로 두어도 됩니다.

반박 가능한 패턴을 let에 쓰기

러스트가 반박 불가능한 패턴을 요구하는 곳에 반박 가능한 패턴을 쓰려 하면 어떻게 되는지, 그리고 그 반대의 경우를 예로 봅니다. 다음 코드는 let 구문을 보여 주지만, 패턴 자리에 반박 가능한 패턴인 Some(x)를 지정했습니다. 짐작하듯 이 코드는 컴파일되지 않습니다.

src/main.rs (컴파일되지 않음)
fn main() {
    let some_option_value: Option<i32> = None;
    let Some(x) = some_option_value;
}

some_option_value가 None 값이라면 Some(x) 패턴에 맞지 않으므로, 이 패턴은 반박 가능합니다. 하지만 let 구문은 None 값으로는 코드가 할 수 있는 올바른 일이 없으므로, 반박 불가능한 패턴만 받을 수 있습니다. 컴파일 시점에 러스트는 반박 불가능한 패턴이 필요한 곳에 반박 가능한 패턴을 쓰려 했다고 불평합니다.

error[E0005]: refutable pattern in local binding
 --> src/main.rs:3:9
  |
3 |     let Some(x) = some_option_value;
  |         ^^^^^^^ pattern `None` not covered
  |
  = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
  = note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
  = note: the matched value is of type `Option<i32>`
help: you might want to use `let...else` to handle the variant that isn't matched
  |
3 |     let Some(x) = some_option_value else { todo!() };
  |                                     ++++++++++++++++

For more information about this error, try `rustc --explain E0005`.

Some(x) 패턴으로 가능한 모든 값을 덮지 않았으므로(덮을 수도 없으므로), 러스트는 당연히 컴파일러 오류를 냅니다. 오류의 note가 가리키는 주소는 공식 Book에서 이 내용을 다루는 곳입니다.

반박 불가능한 패턴이 필요한 곳에 반박 가능한 패턴이 있다면, 패턴을 쓰는 코드를 바꿔 고칠 수 있습니다. let을 쓰는 대신 let...else를 씁니다. 그러면 패턴이 맞지 않을 때 중괄호 안의 코드가 실행되어, 코드가 올바르게 계속될 수 있게 합니다. 다음 코드는 앞의 코드를 고친 것입니다.

src/main.rs
fn main() {
    let some_option_value: Option<i32> = None;
    let Some(x) = some_option_value else {
        return;
    };
}

코드에 출구를 주었습니다. 이 코드는 완벽하게 유효하지만, 반박 불가능한 패턴은 경고 없이 쓸 수 없다는 뜻이기도 합니다. 다음 코드처럼 x 같은 언제나 맞는 패턴을 let...else에 주면, 컴파일러가 경고를 냅니다.

src/main.rs
fn main() {
    let x = 5 else {
        return;
    };
}
warning: unreachable `else` clause
 --> src/main.rs:2:15
  |
2 |     let x = 5 else {
  |     --------- ^^^^
  |     |
  |     assigning to binding pattern will always succeed
  |
  = note: this pattern always matches, so the else clause is unreachable
  = note: `#[warn(irrefutable_let_patterns)]` on by default

러스트는 반박 불가능한 패턴과 함께 let...else를 쓰는 것은 말이 되지 않는다고 불평합니다. 이 패턴은 언제나 맞으므로 else 절에 결코 이르지 않기 때문입니다.

이런 까닭으로 match의 갈래는 반박 가능한 패턴을 써야 하며, 남은 모든 값을 반박 불가능한 패턴으로 맞추는 마지막 갈래만 예외입니다. 러스트는 갈래가 하나뿐인 match에 반박 불가능한 패턴을 쓰는 것을 허용하지만, 이 문법은 그리 쓸모가 없으며 더 단순한 let 구문으로 바꿀 수 있습니다.

패턴을 어디에 쓰는지, 반박 가능한 패턴과 반박 불가능한 패턴의 차이를 알았으니, 패턴을 만드는 데 쓸 수 있는 모든 문법을 봅니다.

정리

  • 모든 값에 맞는 패턴은 반박 불가능하고, 일부 값에서 맞지 않을 수 있는 패턴은 반박 가능합니다. 판단의 기준은 지금 값이 아니라 그 타입의 모든 값입니다.
  • 함수 매개변수, let, for는 반박 불가능한 패턴만 받으며, 반박 가능한 패턴을 쓰면 E0005 오류가 납니다.
  • if let, while let, let...else는 둘 다 받지만, 반박 불가능한 패턴을 쓰면 irrefutable_let_patterns 경고가 납니다.
  • 반박 가능한 패턴을 let에 쓰고 싶다면 let...else로 바꾸고, else에서 return 같은 방법으로 흐름을 벗어납니다.
  • match의 갈래는 반박 가능한 패턴을 쓰고, 남은 값을 모두 잡는 마지막 갈래만 반박 불가능한 패턴을 씁니다.

연습 문제

1. 반박 가능할까

다음 패턴은 각각 반박 가능한가요? (가) let (a, b) = pair;의 (a, b), pair의 타입은 (i32, i32). (나) if let [first, ..] = slice의 [first, ..], slice의 타입은 &[i32]. (다) let Point { x, y } = p;의 Point { x, y }.

해설 보기

(가)는 원소가 둘인 튜플의 모든 값에 맞으므로 반박 불가능합니다. (나)는 슬라이스가 비어 있으면 첫 원소가 없어 맞지 않으므로 반박 가능하며, 그래서 if let에 썼습니다. (다)는 구조체의 모든 값에 맞으므로 반박 불가능하고 let에 쓸 수 있습니다.

2. 경고와 오류 고치기

(가) while let x = next_value() { ... }에 경고가 납니다. (나) for Some(v) in items { ... }가 컴파일되지 않습니다. items의 타입은 Vec<Option<i32>>입니다. 각각 어떻게 고치나요?

해설 보기

(가)는 x가 언제나 맞는 반박 불가능한 패턴이라 반복이 끝나지 않으므로 irrefutable_let_patterns 경고가 납니다. next_value()가 Option을 돌려준다면 while let Some(x) = next_value()로 끝나는 조건을 패턴에 담습니다. (나)는 for가 반박 불가능한 패턴만 받으므로, for item in items { if let Some(v) = item { ... } }처럼 안에서 다루거나, for v in items.into_iter().flatten()처럼 Some만 남긴 반복자를 씁니다.

다음 글 패턴 문법에서는 리터럴, 범위, 해체, 값 무시, 매치 가드, @ 바인딩 같은 패턴 문법을 모두 살펴봅니다.