본문 바로가기

`match` 제어 흐름 구조

match의 패턴과 값 바인딩으로 열거형을 분기하고 모든 경우를 빠짐없이 처리하는 철저성 검사를 익힙니다.

러스트의 match는 값을 여러 패턴과 차례로 비교해, 맞는 패턴에 따라 코드를 실행하는 제어 흐름 구조입니다. 처음에는 if와 else를 늘어놓은 것과 비슷해 보이지만, 패턴으로 값을 분해할 수 있고, 가능한 경우를 하나라도 빠뜨리면 컴파일러가 알려 준다는 점이 다릅니다. 이 글에서는 match로 열거형을 다루는 방법, 패턴으로 배리언트 안의 값을 꺼내는 방법, Option<T>를 다루는 방법, 그리고 모든 경우를 처리해야 한다는 규칙과 그 밖의 값을 한꺼번에 처리하는 패턴을 살펴봅니다.

match 제어 흐름 구조

match의 패턴은 리터럴 값, 변수 이름, 와일드카드 등 여러 가지로 만들 수 있으며, 모든 종류는 패턴과 매칭에서 다룹니다. match는 패턴으로 여러 경우를 표현할 수 있고, 컴파일러가 모든 경우를 처리했는지 확인한다는 데 힘이 있습니다.

match는 동전 분류기처럼 생각할 수 있습니다. 동전이 크기가 다른 구멍이 뚫린 길을 따라 미끄러지다가 처음으로 맞는 구멍에 떨어지듯이, 값은 match의 패턴을 차례로 지나다가 처음으로 맞는 패턴에서 그 패턴의 코드 블록으로 들어갑니다. 동전 이야기가 나왔으니 동전으로 예를 듭니다. 다음 함수는 미국 동전 하나를 받아 그 종류를 가리고, 센트 단위의 값을 돌려줍니다.

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

match 키워드 뒤에는 식을 쓰며, 여기서는 coin입니다. if의 조건과 비슷해 보이지만 큰 차이가 있습니다. if의 조건은 불리언이어야 하지만, match의 식은 어떤 타입이든 될 수 있습니다. 여기서 coin의 타입은 첫 줄에서 정의한 Coin 열거형입니다.

그다음은 match의 갈래(arm)들입니다. 갈래는 패턴과 코드 두 부분으로 이루어지며, 첫 갈래는 Coin::Penny라는 패턴과, 패턴과 코드를 나누는 => 연산자, 그리고 코드인 값 1로 되어 있습니다. 갈래와 갈래는 쉼표로 나눕니다. match가 실행되면 값을 갈래의 패턴과 위에서부터 차례로 비교해, 맞는 패턴이 있으면 그 갈래의 코드를 실행하고, 맞지 않으면 동전 분류기처럼 다음 갈래로 넘어갑니다.

각 갈래의 코드는 식이며, 맞은 갈래의 식이 계산한 값이 match 식 전체의 값이 됩니다. 이 예처럼 갈래의 코드가 값을 돌려주기만 할 만큼 짧으면 보통 중괄호를 쓰지 않습니다. 갈래에서 여러 줄을 실행하려면 중괄호를 쓰며, 그때는 갈래 뒤의 쉼표를 생략해도 됩니다. 예를 들어 다음 코드는 Coin::Penny가 들어올 때마다 Lucky penny!를 출력하고, 블록의 마지막 값인 1을 돌려줍니다.

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

값을 바인딩하는 패턴

match 갈래의 또 다른 쓸모는, 패턴에 맞는 값의 일부를 변수에 묶을 수 있다는 것입니다. 이 방법으로 열거형의 배리언트 안에 든 값을 꺼냅니다. 예를 들어 배리언트 하나가 값을 담도록 바꿔 봅니다. 미국은 1999년부터 2008년까지 50개 주마다 한 면의 그림이 다른 쿼터 동전을 만들었고, 다른 동전에는 주의 그림이 없습니다. 이 정보를 Quarter 배리언트에 UsState 값을 담아 나타냅니다.

#[derive(Debug)] // 주를 출력해 보려고 붙임
enum UsState {
    Alabama,
    Alaska,
    // --생략--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

친구가 50개 주의 쿼터를 모두 모으려 한다고 합시다. 동전을 종류별로 가르면서 쿼터가 나오면 그 주의 이름을 외쳐 주면, 친구가 아직 없는 주의 쿼터를 모을 수 있습니다. 이를 위해 match에서 Coin::Quarter 배리언트에 맞는 패턴에 state라는 변수를 넣습니다. Coin::Quarter가 맞으면 state에 그 쿼터의 주가 묶이므로, 갈래의 코드에서 state를 쓸 수 있습니다.

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {state:?}!");
            25
        }
    }
}

아래 그림은 value_in_cents(Coin::Quarter(UsState::Alaska))를 부를 때 일어나는 일입니다.

match는 첫 번째로 성립한 갈래를 실행한다

값을 한 번 평가한 뒤 소스 순서로 pattern·guard를 검사해 처음 성공한 갈래만 실행하는 제어 흐름이다. 실패 후보와 false guard의 다음 후보, 성공 뒤 이동/Copy를 추적하되 철저성 컴파일 조건은 별도로 둔다.

Rust match가 값을 한 번 평가한 뒤 후보 패턴을 소스 순서대로 검사하고, 패턴과 선택적 가드가 모두 성립한 첫 갈래의 표현식을 실행하며, 가드 중 바인딩의 공유 참조와 성공 뒤 이동 또는 복사 및 컴파일 단계의 철저성을 구분하는 흐름도

FIRST MATCH · GUARD · EXHAUSTIVENESS

패턴과 가드를 처음 통과한 갈래 하나가 실행된다

match는 갈래를 위에서 아래로 검사합니다. 패턴이 맞고 선택적 가드까지 통과한 첫 갈래만 실행되므로, 좁은 갈래에서 넓은 갈래 순으로 배치합니다.

match의 현재 후보·가드·다음 후보와 하나의 실행 경로원6노드7간선과동일하다. 값평가→현재패턴조건→일치시바인딩/가드검사→가드없음또는true조건→하나의갈래표현식. 패턴불일치와거짓가드는같은다음후보노드로오른쪽바깥레일을통해합류한다. 다음후보는왼쪽바깥레일로현재후보조건에돌아가며남은or대안또는다음갈래를검사한다. 실행노드에서다음후보로향하는간선은없고철저성은컴파일조건으로별도다룬다. 모바일도같은반복/성공관계다. match 값 평가비교 대상은 하나현재 패턴 일치?후보를 소스 순서로바인딩·가드 검사가드 중 공유 참조성공 뒤 이동·복사가드 통과?없음 또는 true갈래 표현식 실행이 갈래만 실행전체 match의 결과다음 후보남은 | 대안또는 다음 갈래예예아니요아니요다음 후보실행되는 갈래는 처음 성립한 하나철저성은 별도 컴파일 검사
match의 현재 후보·가드·다음 후보와 하나의 실행 경로원6노드7간선과동일하다. 값평가→현재패턴조건→일치시바인딩/가드검사→가드없음또는true조건→하나의갈래표현식. 패턴불일치와거짓가드는같은다음후보노드로오른쪽바깥레일을통해합류한다. 다음후보는왼쪽바깥레일로현재후보조건에돌아가며남은or대안또는다음갈래를검사한다. 실행노드에서다음후보로향하는간선은없고철저성은컴파일조건으로별도다룬다. 모바일도같은반복/성공관계다.match 값 평가비교 대상은 하나현재 패턴 일치?후보를 소스 순서로바인딩·가드 검사가드 중 공유 참조성공 뒤 이동·복사가드 통과?없음 또는 true갈래 표현식 실행이 갈래만 실행전체 match의 결과다음 후보남은 | 대안또는 다음 갈래예예아니요아니요다음 후보실행되는 갈래는 처음 성립한 하나철저성은 별도 컴파일 검사
  1. match 뒤의 값을 평가하고 첫 갈래부터 시작합니다.

  2. 현재 후보 패턴이 맞지 않으면 남은 | 대안 또는 다음 갈래의 패턴을 검사합니다.

  3. 바인딩 방식은 패턴에 따라 이동, 복사, 참조가 됩니다. 가드 중에는 대응 부분을 공유 참조로 읽습니다.

  4. 가드가 없거나 true이면 갈래 표현식을 실행하며, 값의 이동·복사는 가드 성공 뒤 일어납니다. false이면 다음 후보 패턴으로 이동합니다.

첫 번째로 성립한 갈래

그 갈래의 표현식만 실행되며 그 값이 전체 match의 결과가 됩니다.

더 넓은 패턴은 뒤에

가드 없는 이름 바인딩이나 _ 같은 포괄 패턴 앞에 구체 패턴과 가드 갈래를 둡니다.

컴파일 단계 · 철저성

가드 식은 철저성 판단에 쓰이지 않습니다. 가드 없이 가능한 모든 값을 덮지 못하면 E0004로 컴파일되지 않습니다.

실행 단계 · 순서

먼저 성립한 갈래가 이후 갈래를 가립니다. 가드 없는 포괄 패턴 뒤의 구체 패턴은 unreachable_patterns 경고 대상입니다.

패턴 바인딩의 방식은 이동·복사·참조 중 패턴에 따라 정해집니다. 갈래 표현식들의 타입은 전체 match 표현식의 한 결과 타입으로 결정되며, 값이 필요 없는 포괄 갈래는 _ => ()처럼 명시할 수 있습니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명원래의 6노드와 7간선과 동일하다. 값 평가→현재 패턴 조건→일치 시 바인딩/가드 검사→가드 없음 또는 true 조건→하나의 갈래 표현식의 흐름이다. 패턴 불일치와 거짓 가드는 같은 다음 후보 노드로 오른쪽 바깥 레일을 통해 합류한다. 다음 후보는 왼쪽 바깥 레일로 현재 후보 조건에 돌아가며 남은 or 대안 또는 다음 갈래를 검사한다. 실행 노드에서 다음 후보로 향하는 간선은 없고 철저성은 컴파일 조건으로 별도 다룬다. 세로 도표도 같은 반복과 성공 관계다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
값·후보

본문의 Quarter(Alaska)를 한 번 평가한다.

  • 값: Coin::Quarter(Alaska)
  • 먼저 확인: Penny/Nickel/Dime
  • 현재 패턴이 실패하면 다음 후보를 확인한다.
성공 바인딩

Quarter 패턴이 state를 바인딩한다.

  • state: UsState::Alaska
  • 현재: 성공한 갈래
  • 가드가 있으면 성공 전에 확인하지만 이 Coin 사례에는 가드가 없다.
한 갈래 결과

Quarter의 본문만 실행한다.

  • 선택: Quarter 갈래
  • 다른 갈래: 실행하지 않음
  • 철저성은 별도 컴파일 계약이며 런타임에 모든 갈래를 수행하는 뜻이 아니다.

match는 첫 번째로 성립한 갈래를 실행한다

값을 한 번 평가한 뒤 소스 순서로 pattern·guard를 검사해 처음 성공한 갈래만 실행하는 제어 흐름이다. 실패 후보와 false guard의 다음 후보, 성공 뒤 이동/Copy를 추적하되 철저성 컴파일 조건은 별도로 둔다.

Rust match가 값을 한 번 평가한 뒤 후보 패턴을 소스 순서대로 검사하고, 패턴과 선택적 가드가 모두 성립한 첫 갈래의 표현식을 실행하며, 가드 중 바인딩의 공유 참조와 성공 뒤 이동 또는 복사 및 컴파일 단계의 철저성을 구분하는 흐름도

FIRST MATCH · GUARD · EXHAUSTIVENESS

패턴과 가드를 처음 통과한 갈래 하나가 실행된다

match는 갈래를 위에서 아래로 검사합니다. 패턴이 맞고 선택적 가드까지 통과한 첫 갈래만 실행되므로, 좁은 갈래에서 넓은 갈래 순으로 배치합니다.

match의 현재 후보·가드·다음 후보와 하나의 실행 경로원6노드7간선과동일하다. 값평가→현재패턴조건→일치시바인딩/가드검사→가드없음또는true조건→하나의갈래표현식. 패턴불일치와거짓가드는같은다음후보노드로오른쪽바깥레일을통해합류한다. 다음후보는왼쪽바깥레일로현재후보조건에돌아가며남은or대안또는다음갈래를검사한다. 실행노드에서다음후보로향하는간선은없고철저성은컴파일조건으로별도다룬다. 모바일도같은반복/성공관계다. match 값 평가비교 대상은 하나현재 패턴 일치?후보를 소스 순서로바인딩·가드 검사가드 중 공유 참조성공 뒤 이동·복사가드 통과?없음 또는 true갈래 표현식 실행이 갈래만 실행전체 match의 결과다음 후보남은 | 대안또는 다음 갈래예예아니요아니요다음 후보실행되는 갈래는 처음 성립한 하나철저성은 별도 컴파일 검사
match의 현재 후보·가드·다음 후보와 하나의 실행 경로원6노드7간선과동일하다. 값평가→현재패턴조건→일치시바인딩/가드검사→가드없음또는true조건→하나의갈래표현식. 패턴불일치와거짓가드는같은다음후보노드로오른쪽바깥레일을통해합류한다. 다음후보는왼쪽바깥레일로현재후보조건에돌아가며남은or대안또는다음갈래를검사한다. 실행노드에서다음후보로향하는간선은없고철저성은컴파일조건으로별도다룬다. 모바일도같은반복/성공관계다.match 값 평가비교 대상은 하나현재 패턴 일치?후보를 소스 순서로바인딩·가드 검사가드 중 공유 참조성공 뒤 이동·복사가드 통과?없음 또는 true갈래 표현식 실행이 갈래만 실행전체 match의 결과다음 후보남은 | 대안또는 다음 갈래예예아니요아니요다음 후보실행되는 갈래는 처음 성립한 하나철저성은 별도 컴파일 검사
  1. match 뒤의 값을 평가하고 첫 갈래부터 시작합니다.

  2. 현재 후보 패턴이 맞지 않으면 남은 | 대안 또는 다음 갈래의 패턴을 검사합니다.

  3. 바인딩 방식은 패턴에 따라 이동, 복사, 참조가 됩니다. 가드 중에는 대응 부분을 공유 참조로 읽습니다.

  4. 가드가 없거나 true이면 갈래 표현식을 실행하며, 값의 이동·복사는 가드 성공 뒤 일어납니다. false이면 다음 후보 패턴으로 이동합니다.

첫 번째로 성립한 갈래

그 갈래의 표현식만 실행되며 그 값이 전체 match의 결과가 됩니다.

더 넓은 패턴은 뒤에

가드 없는 이름 바인딩이나 _ 같은 포괄 패턴 앞에 구체 패턴과 가드 갈래를 둡니다.

컴파일 단계 · 철저성

가드 식은 철저성 판단에 쓰이지 않습니다. 가드 없이 가능한 모든 값을 덮지 못하면 E0004로 컴파일되지 않습니다.

실행 단계 · 순서

먼저 성립한 갈래가 이후 갈래를 가립니다. 가드 없는 포괄 패턴 뒤의 구체 패턴은 unreachable_patterns 경고 대상입니다.

패턴 바인딩의 방식은 이동·복사·참조 중 패턴에 따라 정해집니다. 갈래 표현식들의 타입은 전체 match 표현식의 한 결과 타입으로 결정되며, 값이 필요 없는 포괄 갈래는 _ => ()처럼 명시할 수 있습니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명원래의 6노드와 7간선과 동일하다. 값 평가→현재 패턴 조건→일치 시 바인딩/가드 검사→가드 없음 또는 true 조건→하나의 갈래 표현식의 흐름이다. 패턴 불일치와 거짓 가드는 같은 다음 후보 노드로 오른쪽 바깥 레일을 통해 합류한다. 다음 후보는 왼쪽 바깥 레일로 현재 후보 조건에 돌아가며 남은 or 대안 또는 다음 갈래를 검사한다. 실행 노드에서 다음 후보로 향하는 간선은 없고 철저성은 컴파일 조건으로 별도 다룬다. 세로 도표도 같은 반복과 성공 관계다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
값·후보

본문의 Quarter(Alaska)를 한 번 평가한다.

  • 값: Coin::Quarter(Alaska)
  • 먼저 확인: Penny/Nickel/Dime
  • 현재 패턴이 실패하면 다음 후보를 확인한다.
성공 바인딩

Quarter 패턴이 state를 바인딩한다.

  • state: UsState::Alaska
  • 현재: 성공한 갈래
  • 가드가 있으면 성공 전에 확인하지만 이 Coin 사례에는 가드가 없다.
한 갈래 결과

Quarter의 본문만 실행한다.

  • 선택: Quarter 갈래
  • 다른 갈래: 실행하지 않음
  • 철저성은 별도 컴파일 계약이며 런타임에 모든 갈래를 수행하는 뜻이 아니다.

coin은 Coin::Quarter(UsState::Alaska)가 되고, 이 값을 갈래와 차례로 비교하면 Coin::Quarter(state)에 이를 때까지 맞는 것이 없습니다. 그 갈래에서 state가 UsState::Alaska에 묶이고, 그 갈래의 코드만 실행되어 State quarter from Alaska!를 출력한 뒤 25를 돌려줍니다. 이렇게 Coin 열거형의 Quarter 배리언트에서 안의 값을 꺼냈습니다. 패턴 뒤에 가드가 붙어 있다면 패턴이 맞은 뒤 가드까지 통과해야 그 갈래가 실행되는데, 가드는 이 글의 끝에서 다룹니다.

Option<T>를 이용하는 매칭

열거형 정의에서 Option<T>의 Some 안에 든 T를 쓰려면 배리언트마다 처리하는 코드가 필요하다고 했습니다. Coin처럼 Option<T>도 match로 다룰 수 있습니다. 비교하는 배리언트만 다를 뿐 match의 동작은 같습니다. Option<i32>를 받아 값이 있으면 1을 더하고, 값이 없으면 아무 계산도 하지 않고 None을 돌려주는 함수를 만들어 봅니다.

src/main.rs
fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

fn main() {
    println!("{:?}", plus_one(Some(5)));
    println!("{:?}", plus_one(None));
    println!("{:?}", plus_one(Some(-1)));
}
Some(6)
None
Some(0)

plus_one(Some(5))를 부르면 x는 Some(5)입니다. 첫 갈래의 None과는 맞지 않으므로 다음 갈래로 가고, Some(5)는 같은 배리언트인 Some(i)와 맞으므로 안의 5가 i에 묶입니다. 그 갈래의 코드가 i에 1을 더해 6을 담은 새 Some을 만듭니다. plus_one(None)은 첫 갈래의 None과 바로 맞으므로, 더할 값 없이 => 오른쪽의 None을 돌려주고 나머지 갈래는 보지 않습니다. Some(-1)의 결과 Some(0)도 0이라는 숫자가 있는 상태이므로 None과 다릅니다.

이 match는 Some과 None 두 경우를 모두 처리했지만, i + 1이 i32의 범위를 넘는 경우까지 정한 것은 아닙니다. i가 i32::MAX이면 디버그 빌드에서는 오버플로로 패닉이 납니다. 범위를 넘는 경우도 결과로 정하려면 checked_add를 씁니다. 아래 그림은 이렇게 고친 함수의 흐름입니다.

Option match는 Some과 None을 모두 처리한다

별도 plus_one_checked가 입력 None을 반환하거나 Some에 checked_add(1)을 적용해 범위 안/초과를 가르는 실제 분기다. 세 입력을 별도 사례로 보여 주고 같은 None의 두 원인과 반환 타입 자체는 원인을 담지 않는다는 점을 유지한다.

Rust plus_one 함수가 Option i32의 Some 5를 Some i 패턴으로 바인딩해 Some 6으로 만들고, None은 None 갈래에서 그대로 반환하며 두 배리언트를 모두 다뤄 철저성을 만족하는 비교표

OPTION<I32> · TWO COMPLETE PATHS

plus_one은 Some과 None의 결과를 각각 정한다

Option<i32>에는 Some(i)와 None 두 경우가 있습니다. match는 값이 있는 경로와 없는 경로의 결과를 모두 코드로 남깁니다.

별도 checked함수는 입력 부재와 산술 범위를 서로 다른 조건으로 검사한다새별도plus_one_checked본문7노드6간선이다. 입력→Some조건. 아니요는부재None결과,예는이노드를우회해checked_add1→i32결과범위조건. 범위예는Some(next),아니요는범위초과None결과다. 같은None결과라도두원인을구분한경로이며함수반환타입자체는원인을구분하지않는다. 세terminal결과끼리간선이없고모바일의예/아니요/우회경로가같다. 아래원표의plus_oneSome5→Some6/None→None은작은입력전제로유지한다.plus_one_checked입력 Option<i32>Some 입력?숫자가 있는가?None 반환원인: 입력 부재checked_add(1)있는 숫자에 1 더하기범위 안의 결과?i32의 덧셈 결과Some(next) 반환계산한 숫자 포함None 반환원인: 산술 범위 초과아니요예예아니요별도 checked 함수의 계약아래 원 표는 작은 입력의 plus_one
별도 checked함수는 입력 부재와 산술 범위를 서로 다른 조건으로 검사한다새별도plus_one_checked본문7노드6간선이다. 입력→Some조건. 아니요는부재None결과,예는이노드를우회해checked_add1→i32결과범위조건. 범위예는Some(next),아니요는범위초과None결과다. 같은None결과라도두원인을구분한경로이며함수반환타입자체는원인을구분하지않는다. 세terminal결과끼리간선이없고모바일의예/아니요/우회경로가같다. 아래원표의plus_oneSome5→Some6/None→None은작은입력전제로유지한다.plus_one_checked입력 Option<i32>Some 입력?숫자가 있는가?None 반환원인: 입력 부재checked_add(1)있는 숫자에 1 더하기범위 안의 결과?i32의 덧셈 결과Some(next) 반환계산한 숫자 포함None 반환원인: 산술 범위 초과아니요예예아니요별도 checked 함수의 계약아래 원 표는 작은 입력의 plus_one
plus_one에서 입력이 처음 맞는 갈래와 결과
입력 소스 순서의 검사 바인딩 반환값
Some(5) None 불일치 → Some(i) 일치 i = 5 Some(i + 1) → Some(6)
None 첫 갈래 None 일치 내부 값 없음 None
Some(5)
None은 맞지 않고 Some(i)가 맞습니다.
i = 5를 바인딩해 Some(6)을 반환합니다.
None
첫 None 갈래가 맞으므로 뒤의 갈래는 검사하지 않습니다.
더할 값이 없으며 결과도 None입니다.
Some(i)만 쓰고 None을 빼면 컴파일러는 E0004로 누락을 알립니다. 두 갈래는 모두 Option<i32> 값을 만들어 함수의 반환 타입과 일치합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
도표 제목별도 checked 함수는 입력 부재와 산술 범위를 서로 다른 조건으로 검사한다
관계·조건 설명새로 추가한 별도 plus_one_checked 본문의 7노드와 6간선이다. 입력→Some 조건. 아니요는 부재 None 결과로, 예는 이 노드를 우회해 checked_add 1→i32 결과 범위 조건으로 간다. 범위가 예이면 Some(next), 아니요이면 범위 초과 None 결과다. 같은 None 결과라도 두 원인을 구분한 경로이며 함수 반환 타입 자체는 원인을 구분하지 않는다. 세 최종 결과끼리 간선이 없고 세로 도표의 예/아니요/우회 경로가 같다. 아래 원래 표의 plus_one Some 5→Some 6/None→None은 작은 입력을 전제로 유지한다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
존재 확인

plus_one_checked의 입력 Some(5)를 받는다.

  • 입력: Some(5)
  • None 입력: 즉시 None
  • 숫자 부재와 산술 실패를 다른 중단 지점으로 둔다.
안전 덧셈

존재하는 숫자에 checked_add(1)을 적용한다.

  • 계산: 5+1
  • 산출물: Some(6)
  • 범위 초과면 여기서 None이 되며 정상 덧셈값을 만들지 않는다.
반환

성공 숫자를 Option으로 반환한다.

  • 현재 결과: Some(6)
  • None 원인: 입력 부재 또는 범위 초과
  • 반환 Option만으로 두 None 원인이 구분되지는 않는다.

Option match는 Some과 None을 모두 처리한다

별도 plus_one_checked가 입력 None을 반환하거나 Some에 checked_add(1)을 적용해 범위 안/초과를 가르는 실제 분기다. 세 입력을 별도 사례로 보여 주고 같은 None의 두 원인과 반환 타입 자체는 원인을 담지 않는다는 점을 유지한다.

Rust plus_one 함수가 Option i32의 Some 5를 Some i 패턴으로 바인딩해 Some 6으로 만들고, None은 None 갈래에서 그대로 반환하며 두 배리언트를 모두 다뤄 철저성을 만족하는 비교표

OPTION<I32> · TWO COMPLETE PATHS

plus_one은 Some과 None의 결과를 각각 정한다

Option<i32>에는 Some(i)와 None 두 경우가 있습니다. match는 값이 있는 경로와 없는 경로의 결과를 모두 코드로 남깁니다.

별도 checked함수는 입력 부재와 산술 범위를 서로 다른 조건으로 검사한다새별도plus_one_checked본문7노드6간선이다. 입력→Some조건. 아니요는부재None결과,예는이노드를우회해checked_add1→i32결과범위조건. 범위예는Some(next),아니요는범위초과None결과다. 같은None결과라도두원인을구분한경로이며함수반환타입자체는원인을구분하지않는다. 세terminal결과끼리간선이없고모바일의예/아니요/우회경로가같다. 아래원표의plus_oneSome5→Some6/None→None은작은입력전제로유지한다.plus_one_checked입력 Option<i32>Some 입력?숫자가 있는가?None 반환원인: 입력 부재checked_add(1)있는 숫자에 1 더하기범위 안의 결과?i32의 덧셈 결과Some(next) 반환계산한 숫자 포함None 반환원인: 산술 범위 초과아니요예예아니요별도 checked 함수의 계약아래 원 표는 작은 입력의 plus_one
별도 checked함수는 입력 부재와 산술 범위를 서로 다른 조건으로 검사한다새별도plus_one_checked본문7노드6간선이다. 입력→Some조건. 아니요는부재None결과,예는이노드를우회해checked_add1→i32결과범위조건. 범위예는Some(next),아니요는범위초과None결과다. 같은None결과라도두원인을구분한경로이며함수반환타입자체는원인을구분하지않는다. 세terminal결과끼리간선이없고모바일의예/아니요/우회경로가같다. 아래원표의plus_oneSome5→Some6/None→None은작은입력전제로유지한다.plus_one_checked입력 Option<i32>Some 입력?숫자가 있는가?None 반환원인: 입력 부재checked_add(1)있는 숫자에 1 더하기범위 안의 결과?i32의 덧셈 결과Some(next) 반환계산한 숫자 포함None 반환원인: 산술 범위 초과아니요예예아니요별도 checked 함수의 계약아래 원 표는 작은 입력의 plus_one
plus_one에서 입력이 처음 맞는 갈래와 결과
입력 소스 순서의 검사 바인딩 반환값
Some(5) None 불일치 → Some(i) 일치 i = 5 Some(i + 1) → Some(6)
None 첫 갈래 None 일치 내부 값 없음 None
Some(5)
None은 맞지 않고 Some(i)가 맞습니다.
i = 5를 바인딩해 Some(6)을 반환합니다.
None
첫 None 갈래가 맞으므로 뒤의 갈래는 검사하지 않습니다.
더할 값이 없으며 결과도 None입니다.
Some(i)만 쓰고 None을 빼면 컴파일러는 E0004로 누락을 알립니다. 두 갈래는 모두 Option<i32> 값을 만들어 함수의 반환 타입과 일치합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
도표 제목별도 checked 함수는 입력 부재와 산술 범위를 서로 다른 조건으로 검사한다
관계·조건 설명새로 추가한 별도 plus_one_checked 본문의 7노드와 6간선이다. 입력→Some 조건. 아니요는 부재 None 결과로, 예는 이 노드를 우회해 checked_add 1→i32 결과 범위 조건으로 간다. 범위가 예이면 Some(next), 아니요이면 범위 초과 None 결과다. 같은 None 결과라도 두 원인을 구분한 경로이며 함수 반환 타입 자체는 원인을 구분하지 않는다. 세 최종 결과끼리 간선이 없고 세로 도표의 예/아니요/우회 경로가 같다. 아래 원래 표의 plus_one Some 5→Some 6/None→None은 작은 입력을 전제로 유지한다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
존재 확인

plus_one_checked의 입력 Some(5)를 받는다.

  • 입력: Some(5)
  • None 입력: 즉시 None
  • 숫자 부재와 산술 실패를 다른 중단 지점으로 둔다.
안전 덧셈

존재하는 숫자에 checked_add(1)을 적용한다.

  • 계산: 5+1
  • 산출물: Some(6)
  • 범위 초과면 여기서 None이 되며 정상 덧셈값을 만들지 않는다.
반환

성공 숫자를 Option으로 반환한다.

  • 현재 결과: Some(6)
  • None 원인: 입력 부재 또는 범위 초과
  • 반환 Option만으로 두 None 원인이 구분되지는 않는다.
src/main.rs
fn plus_one_checked(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => i.checked_add(1),
    }
}

fn main() {
    println!("{:?}", plus_one_checked(Some(i32::MAX - 1)));
    println!("{:?}", plus_one_checked(Some(i32::MAX)));
    println!("{:?}", plus_one_checked(None));
}
Some(2147483647)
None
None

입력이 None이면 바로 None을 돌려주고, 숫자가 있으면 checked_add(1)을 부릅니다. checked_add는 결과가 범위 안이면 Some으로, 범위를 넘으면 None으로 돌려줍니다. i32::MAX는 2147483647이므로 첫 줄은 Some(2147483647)이고, i32::MAX에 1을 더하는 둘째 줄은 범위를 넘어 None입니다. 둘째와 셋째 줄은 같은 None이지만 원인이 다르며, 반환 타입 Option<i32>만으로는 두 원인을 구분할 수 없습니다.

열거형에 match를 쓰고, 안의 데이터를 변수에 묶고, 그 변수로 코드를 실행하는 방식은 러스트 코드에서 아주 자주 보게 됩니다. 처음에는 조금 까다롭지만, 익숙해지면 다른 언어에서도 쓰고 싶어질 만큼 편리합니다.

매치는 철저합니다

match에서 짚어 둘 것이 하나 더 있습니다. 갈래의 패턴은 가능한 경우를 모두 다뤄야 합니다. 다음 plus_one에는 오류가 있어 컴파일되지 않습니다.

src/main.rs (컴파일되지 않음)
fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            Some(i) => Some(i + 1),
        }
    }
}
error[E0004]: non-exhaustive patterns: `None` not covered
 --> src/main.rs:3:15
  |
3 |         match x {
  |               ^ pattern `None` not covered
  |
  = note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
  |
4 ~             Some(i) => Some(i + 1),
5 ~             None => todo!(),
  |

None인 경우를 처리하지 않았으므로 이 코드는 오류를 낼 것인데, 러스트가 이를 잡아냅니다. E0004는 패턴이 모든 경우를 다루지 않는다는 오류로, 빠뜨린 경우가 있다는 것뿐 아니라 그것이 None이라는 것까지 알려 주고, None 갈래를 더하라는 도움말을 보여 줍니다. 이처럼 러스트의 match는 철저(exhaustive)하므로, 코드가 컴파일되려면 모든 가능성을 다뤄야 합니다. 특히 Option<T>에서 None을 처리하는 것을 잊으면 러스트가 알려 주므로, 값이 없는 경우를 빠뜨린 코드는 컴파일되지 않습니다.

포괄 패턴과 _ 자리표시자

열거형에서 몇몇 값에만 특별한 일을 하고 나머지 값에는 모두 같은 기본 동작을 하고 싶을 때가 있습니다. 주사위를 굴려 3이 나오면 플레이어가 움직이는 대신 멋진 모자를 얻고, 7이 나오면 모자를 잃으며, 그 밖의 숫자가 나오면 그 수만큼 게임판에서 움직이는 게임을 생각해 봅니다. 실제 게임을 구현하는 것은 이 글의 범위를 벗어나므로, 주사위 값은 정해진 값으로 두고 함수의 본문은 비워 둡니다.

    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        other => move_player(other),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn move_player(num_spaces: u8) {}

처음 두 갈래의 패턴은 리터럴 값 3과 7이고, 나머지 모든 값을 다루는 마지막 갈래의 패턴은 other라는 변수입니다. other 갈래는 그 값을 move_player에 넘깁니다. u8이 가질 수 있는 값을 모두 나열하지 않았는데도 컴파일되는 것은, 마지막 패턴이 따로 나열하지 않은 모든 값에 맞기 때문입니다. 이런 포괄(catch-all) 패턴은 match의 철저함을 만족시킵니다. 패턴은 순서대로 평가되므로 포괄 갈래는 맨 끝에 두어야 하며, 앞에 두면 그 뒤의 갈래는 결코 실행되지 않으므로 러스트가 경고합니다.

모든 값을 받아야 하지만 그 값을 쓰지는 않는다면 _를 씁니다. _는 어떤 값에도 맞지만 그 값을 변수에 묶지 않는 특별한 패턴이어서, 쓰지 않는 변수라는 경고가 나지 않습니다. 이번에는 3이나 7이 아닌 숫자가 나오면 주사위를 다시 굴리도록 규칙을 바꿉니다. 그 값을 쓸 필요가 없으므로 other 대신 _를 씁니다.

    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => reroll(),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn reroll() {}

마지막 갈래에서 나머지 값을 모두 받아 버리므로 이 코드도 철저함을 만족합니다. 마지막으로 3이나 7이 아니면 아무 일도 일어나지 않게 규칙을 바꾸려면, _ 갈래의 코드를 튜플 타입에서 본 유닛 값 ()으로 둡니다.

    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => (),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}

앞의 갈래에 맞지 않은 값은 쓰지 않으며, 그때는 아무 코드도 실행하지 않겠다고 분명히 적은 것입니다.

더 깊이: 배리언트를 더해도 다른 크레이트가 깨지지 않게 하려면

match가 철저하다는 규칙은 라이브러리를 만드는 쪽에 부담이 됩니다. 공개 열거형에 배리언트를 하나 더하면, 그 열거형의 배리언트를 모두 나열해 둔 다른 크레이트의 match가 E0004로 컴파일되지 않기 때문입니다. 그래서 앞으로 배리언트가 늘어날 열거형에는 #[non_exhaustive] 속성을 붙일 수 있습니다. Reference에 따르면 이 속성은 정의한 크레이트 안에서는 아무 효과가 없지만, 바깥 크레이트에서는 배리언트를 모두 나열해도 철저함을 채우지 못하게 하므로 _ 같은 포괄 갈래를 반드시 두게 합니다.

표준 라이브러리의 std::io::ErrorKind가 이런 열거형이며, 문서는 배리언트 목록이 앞으로 늘어날 것이므로 모든 배리언트를 나열해 매칭하지 말고 기대하는 값만 매칭한 뒤 나머지는 _로 받으라고 안내합니다. 이 속성은 구조체에도 붙일 수 있는데, 그러면 바깥 크레이트는 구조체 문법으로 그 값을 만들 수 없고 패턴으로 분해할 때 ..를 넣어야 합니다. 대신 바깥 크레이트는 새 배리언트를 빠뜨렸다는 컴파일 오류를 받을 수 없게 되므로, _ 갈래가 새 배리언트까지 말없이 받아 버린다는 단점을 받아들이는 셈입니다.

매치 가드로 패턴을 더 좁히기

패턴 뒤에 if와 불리언 식을 붙여 매치 가드(match guard)를 만들 수 있습니다. 패턴이 먼저 맞은 뒤 가드를 계산하며, 가드가 false이면 그 갈래는 실행되지 않고 다음 갈래로 넘어갑니다.

src/main.rs
fn main() {
    let number = Some(8);

    match number {
        Some(n) if n % 2 == 0 => println!("even: {n}"),
        Some(n) => println!("odd: {n}"),
        None => println!("none"),
    }
}
even: 8

패턴이 묶은 n은 그 뒤의 가드와 갈래의 식에서 쓸 수 있습니다. Some(8)은 첫 갈래의 패턴에 맞고, 가드 8 % 2 == 0도 참이므로 even: 8을 출력합니다. number가 Some(9)라면 첫 갈래의 패턴은 맞지만 가드가 거짓이므로 둘째 갈래로 가서 odd: 9를 출력합니다. 가드가 붙은 좁은 갈래를 먼저 두고 가드 없는 갈래를 뒤에 두어야 나머지 Some 값을 처리할 수 있으며, 컴파일러는 가드가 참일지 거짓일지 미리 알 수 없으므로 철저함을 따질 때 가드가 붙은 갈래는 빼고 생각합니다. 그래서 이 예에서도 가드 없는 Some(n) 갈래가 있어야 컴파일됩니다.

패턴과 매칭은 17장에서 더 자세히 다룹니다. match를 쓸 때는 갈래의 순서, 값을 묶는 패턴, 가드, Option<T>의 두 경우, 포괄 패턴을 함께 점검하면 컴파일러가 요구하는 철저함을 쉽게 맞출 수 있습니다.

정리

  • match는 값을 갈래의 패턴과 위에서부터 차례로 비교해, 처음 맞는 갈래의 코드를 실행하고 그 값을 결과로 돌려줍니다.
  • if의 조건과 달리 match의 식은 어떤 타입이든 될 수 있습니다.
  • 패턴에 변수를 넣으면 배리언트 안의 값을 그 변수에 묶어 갈래에서 쓸 수 있습니다.
  • Option<T>는 Some(i)와 None 두 갈래로 다루며, 계산이 범위를 넘는 경우까지 정하려면 checked_add 같은 메서드를 함께 씁니다.
  • match는 철저해서 가능한 경우를 하나라도 빠뜨리면 E0004 오류가 납니다.
  • 나머지 값을 모두 받는 포괄 패턴은 변수 이름이나 _로 쓰고 맨 끝에 두며, 아무것도 하지 않으려면 ()을 씁니다.
  • 매치 가드 if는 패턴이 맞은 뒤 조건을 더 검사하며, 철저함을 따질 때는 빼고 생각합니다.

연습 문제

1. 가드와 갈래의 순서

매치 가드 예제에서 number를 (가) Some(0), (나) None으로 바꾸면 각각 무엇이 출력되나요? (다) 가드 없는 Some(n) 갈래를 맨 위로 옮기면 Some(8)은 어느 갈래로 가나요?

해설 보기

(가) 0은 2로 나누어떨어지므로 첫 갈래의 가드를 통과해 even: 0을 출력합니다. (나) None은 앞의 두 Some 패턴에 맞지 않으므로 마지막 갈래에서 none을 출력합니다. (다) 가드 없는 Some(n)이 맨 위에 있으면 Some(8)도 그 갈래에 먼저 맞으므로 odd: 8을 출력합니다. 컴파일러는 그 뒤의 가드 갈래가 실행될 수 없다는 경고를 낼 수 있으며, 좁은 갈래를 먼저 두어야 하는 까닭이 이것입니다.

2. 빠뜨린 배리언트

Coin에 HalfDollar 배리언트를 새로 더하고 value_in_cents는 고치지 않았습니다. 어떻게 되나요? _ => 0 갈래를 미리 넣어 두었다면 어떻게 달라지나요?

해설 보기

value_in_cents의 match가 Coin::HalfDollar를 다루지 않으므로, Coin::HalfDollar가 처리되지 않았다(not covered)는 E0004 오류가 나며 컴파일되지 않습니다. 컴파일러가 고칠 곳을 알려 주므로 50센트를 돌려주는 갈래를 빠뜨리지 않게 됩니다. _ => 0이 있었다면 컴파일은 되지만 HalfDollar가 조용히 0센트로 계산됩니다. 열거형에 배리언트를 더할 때 처리를 강제하고 싶다면, 포괄 패턴 대신 배리언트를 모두 나열하는 편이 안전합니다.

3. 포괄 패턴의 위치

다음 match는 어떻게 동작하나요? match가 갈래를 비교하는 순서와, 이름 하나로만 된 패턴 other에 어떤 값이 맞는지를 생각해 보세요.

match dice_roll {
    other => move_player(other),
    3 => add_fancy_hat(),
}
해설 보기

첫 갈래의 other가 모든 값에 맞으므로, 3이 나와도 언제나 move_player가 실행되고 3 갈래는 결코 실행되지 않습니다. 러스트는 그 갈래에 닿을 수 없다는 경고를 냅니다. 포괄 패턴은 맨 끝에 두어야 합니다.

다음 글 if let을 사용한 간결한 제어 흐름에서는 패턴 하나만 다루고 나머지는 무시하고 싶을 때 match보다 짧게 쓰는 if let을 살펴봅니다.