본문 바로가기

`match` 제어 흐름 구조

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

앞 절의 열거형 배리언트와 Option을 읽고, 이번에는 어느 갈래가 선택되고 어떤 값이 돌아오는지 따라갑니다. Coin과 value_in_cents가 바뀌는 블록들은 별도 버전입니다. 정의는 필요한 버전 하나만 함께 두고 let·호출문은 main 안에 넣습니다. 빈 주사위 함수는 동작 위치를 보여 주는 발췌이며 실제 게임판을 바꾸지는 않습니다.

처음에는 if/else와 비슷해 보여도, 패턴 매칭과 빠짐없는 점검(모든 경우 검사) 덕분에 안정성이 크게 달라집니다.

특히 분기 조건이 늘어나는 코드에서는 빠뜨린 케이스가 런타임 버그로 이어지기 쉬운데, match는 이 위험을 컴파일 단계에서 줄여줍니다.

이 절에서는 match를 단순 문법으로 소비하지 않고, 왜 러스트가 이 구조를 핵심 제어 흐름으로 두는지까지 함께 이해해보겠습니다.

또한 갈래 순서가 코드 의미에 어떤 영향을 주는지 확인해, 예상치 못한 매칭 누락을 예방하도록 하겠습니다.

값을 분해하면서 동시에 바인딩하는 패턴의 장점도 함께 익혀 분기 로직의 가독성을 높일 수 있습니다.

결국 이 단원을 마치면 match를 짧은 문법이 아니라, 안전한 도메인 분기 설계 도구로 다루게 됩니다.

이 관점은 이후 패턴 매칭 고급 주제로 넘어갈 때도 그대로 재사용됩니다.


match 제어 흐름 구조

러스트는 match라고 불리는 매우 강력한 제어 흐름 연산자를 가지고 있는데 이는 일련의 패턴에 대해 어떤 값을 비교한 뒤 어떤 패턴에 매칭되었는지를 바탕으로 코드를 수행하도록 해줍니다.

패턴은 리터럴 값, 변수명, 와일드카드 등 다양한 것으로 구성될 수 있으며, 전체 종류 및 각각의 역할은 17장에서 배울 예정입니다.

match의 힘은 패턴의 표현성으로부터 오며 컴파일러는 모든 가능한 경우가 처리되는지 검사합니다.

match 표현식을 동전 분류기와 비슷한 종류로 생각해보세요.

동전들은 다양한 크기의 구멍들이 있는 트랙으로 미끄러져 내려가고, 각 동전은 그것에 맞는 첫 번째 구멍을 만났을 때 떨어집니다.

동일한 방식으로, 값들은 match 내의 각 패턴을 통과하고, 해당 값에 ‘맞는’ 첫 번째 패턴에서, 그 값은 실행 중에 사용될 연관된 코드 블록 안으로 떨어질 것입니다.

동전 이야기가 나왔으니, match를 이용한 예제로 동전들을 이용해봅시다!

예제 5-3에서 보는 바와 같이, 어떤 모르는 미국 동전을 입력받아서, 동전 계수기와 동일한 방식으로 그 동전의 종류를 판별하고 센트 단위로 값을 반환하는 함수를 작성할 수 있습니다.

예제 5-3: 열거형과 열거형의 배리언트를 패턴으로 사용하는 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,
    }
}

value_in_cents 함수 내의 match를 쪼개 봅시다.

먼저 match 키워드 뒤에 표현식을 써줬는데, 위의 경우에는 coin 값입니다.

이는 if에서 사용하는 조건식과 매우 유사하지만, 큰 차이점이 있습니다.

if를 사용할 경우에는 조건식이 부울린 값으로 평가되어야 하지만, 여기서는 어떤 타입이든 가능합니다.

위 예제에서 coin의 타입은 첫째 줄에서 정의했던 Coin 열거형입니다.

그다음은 match 갈래(arm)들입니다.

하나의 갈래는 패턴과 코드 두 부분으로 이루어져 있습니다.

여기서의 첫 번째 갈래에는 값 Coin::Penny로 되어있는 패턴이 있고 그 뒤에 패턴과 실행되는 코드를 구분해 주는 => 연산자가 있습니다.

위의 경우에서 코드는 그냥 값 1입니다.

각 갈래는 그다음 갈래와 쉼표로 구분됩니다.

match 표현식이 실행될 때, 결괏값을 각 갈래의 패턴에 대해서 순차적으로 비교합니다.

만일 어떤 패턴이 그 값과 매칭되면, 그 패턴과 연관된 코드가 실행됩니다.

만일 그 패턴이 값과 매칭되지 않는다면, 동전 분류기와 비슷하게 다음 갈래로 실행을 계속합니다.

각 갈래와 연관된 코드는 표현식이고, 이 매칭 갈래에서의 표현식의 결과로서 생기는 값은 전체 match 표현식에 대해 반환되는 값입니다.

각 갈래가 그냥 값을 반환하는 예제 5-3에서처럼 매치 갈래의 코드가 짧다면, 중괄호는 보통 사용하지 않습니다.

만일 매치 갈래 내에서 여러 줄의 코드를 실행시키고 싶다면 중괄호를 사용하고, 그렇게 되면 갈래 뒤에 붙이는 쉼표는 옵션이 됩니다.

예를 들어, 아래의 코드는 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,
    }
}

값을 바인딩하는 패턴

매치 갈래의 또 다른 유용한 기능은 패턴과 매칭된 값들의 일부분을 바인딩할 수 있다는 것입니다.

이것이 열거형의 배리언트로부터 어떤 값들을 추출할 수 있는 방법입니다.

한 가지 예로, 열거형 배리언트 중 하나가 내부에 값을 들고 있도록 바꿔봅시다.

1999년부터 2008년까지, 미국은 각 50개 주마다 한쪽 면의 디자인이 다른 쿼터 동전을 주조했습니다.

다른 동전들은 주의 디자인을 갖지 않고, 따라서 오직 쿼터 동전들만 이 특별 값을 갖습니다.

이 정보를 Quarter 배리언트 내에 UsState 값을 담도록 enum을 변경하여 추가할 수 있는데, 이는 예제 5-4와 같습니다.

예제 5-4: Quarter 배리언트가 UsState 값도 담고 있는 Coin 열거형
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --생략--
}

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

한 친구가 모든 50개 주 쿼터 동전을 모으기를 시도하는 중이라고 상상해봅시다.

동전의 종류에 따라 동전을 분류하는 동안 각 쿼터 동전에 연관된 주의 이름을 외치기도 해서, 만일 그것이 친구가 가지고 있지 않은 것이라면, 그 친구는 자기 컬렉션에 그 동전을 추가할 수 있겠지요.

이 코드를 위한 매치 표현식 내에서는 배리언트 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))를 호출했다면, coin은 Coin::Quarter(UsState::Alaska)가 되겠지요.

각각의 매치 갈래와 이 값을 비교하면, Coin::Quarter(state)에 도달할 때까지 아무것에도 매칭되지 않습니다.

이 시점에서, state에 대한 바인딩은 값 UsState::Alaska가 될 것입니다.

그러면 이 바인딩을 println! 표현식에서 사용할 수 있고, 따라서 Quarter에 대한 Coin 열거형 배리언트로부터 주에 대한 내부 값을 얻었습니다.

Option<T>를 이용하는 매칭

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 원인이 구분되지는 않는다.

이전 절에서 Option<T> 값을 사용하려면 Some일 때 실행돼서, Some 내의 T 값을 얻을 수 있는 코드가 필요하다고 했었죠.

이제 Coin 열거형을 다뤘던 것처럼 Option<T>도 match로 다뤄보도록 하겠습니다.

동전들을 비교하는 대신 Option<T>의 배리언트를 비교하겠지만, match 표현식이 동작하는 방식은 동일합니다.

Option<i32>를 매개변수로 받아서, 내부에 값이 있으면 그 값에 1을 더하는 함수를 작성하고 싶다고 칩시다.

만일 내부에 값이 없으면, 이 함수는 None 값을 반환하고 다른 어떤 연산도 수행하는 시도를 하지 않아야 합니다.

match 덕분에 이 함수는 매우 쉽게 작성할 수 있고, 예제 5-5처럼 보일 것입니다.

예제 5-5: Option<i32> 상에서 match를 이용하는 함수
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);

plus_one의 첫 번째 실행을 좀 더 자세히 시험해봅시다.

plus_one(five)가 호출될 때, plus_one의 본문 안에 있는 변수 x는 값 Some(5)를 갖게 될 것입니다.

그런 다음 각각의 매치 갈래에 대하여 이 값을 비교합니다.

            None => None,

Some(5) 값은 패턴 None과 매칭되지 않으므로, 다음 갈래로 계속 갑니다.

            Some(i) => Some(i + 1),

Some(5)가 Some(i)랑 매칭되나요?

그렇습니다!

동일한 배리언트를 갖고 있습니다.

Some 내부에 담긴 값은 i에 바인딩되므로, i는 값 5를 갖습니다.

그런 다음 매치 갈래 내의 코드가 실행되므로, i의 값에 1을 더한 다음 최종적으로 6을 담은 새로운 Some 값을 생성합니다.

이제 x가 None인 예제 5-5에서의 plus_one의 두 번째 호출을 살펴봅시다.

match 안으로 들어와서 첫 번째 갈래와 비교합니다.

            None => None,

매칭되었군요!

더할 값이 없으므로 이 match 평가가 끝나고, 함수는 =>의 우측 편에 있는 None 값을 반환합니다.

첫 번째 갈래에 매칭되었으므로, 다른 갈래와는 비교하지 않습니다.

예시 원 plus_one의 정의와 호출을 한 파일로 모으면 다음과 같습니다. 작은 입력값에서 나온 Some 결과와 부재 결과를 따로 읽습니다.

plus_one의 입력과 반환을 한 파일에서 비교하기
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)입니다. 0도 실제 숫자이므로 None이 되지 않습니다. 철저성은 두 배리언트를 덮었다는 보장이지, i+1의 산술 범위를 보장하는 기능은 아닙니다. 원 함수에는 덧셈 결과가 i32 범위에 들어온다는 전제가 필요합니다.

범위 초과도 결과로 정하려면 별도 정책을 작성합니다. 다음 함수는 checked_add의 공식 계약을 사용하여 범위를 넘는 덧셈에도 None을 반환합니다.

산술 범위까지 정한 별도 plus_one
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));
}

MAX는 2147483647이므로 첫 결과는 Some(2147483647), 다음 두 결과는 None입니다. 이 함수는 입력 부재와 산술 범위 초과라는 서로 다른 원인을 같은 결과로 모았습니다. 원인을 구별해야 한다면 반환 계약도 그 구별을 담도록 설계해야 합니다.

match와 열거형을 조합하는 것은 다양한 경우에 유용합니다.

여러분은 러스트 코드에서 이러한 패턴을 많이 보게 될 겁니다.

열거형에 대한 match, 내부의 데이터에 변수 바인딩, 그런 다음 그에 대한 수행 코드 말이지요.

처음에는 약간 까다롭지만, 일단 익숙해지면 이를 모든 언어에서 쓸 수 있게 되기를 바랄 것입니다.

이것은 꾸준히 사용자들이 가장 좋아하는 기능입니다.

매치는 철저합니다

우리가 논의할 필요가 있는 match의 다른 관점이 있습니다.

갈래의 패턴들은 모든 가능한 경우를 다루어야 합니다.

plus_one 함수의 아래 버전을 고려해 봅시다.

버그가 있고 컴파일되지 않지만요.

    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            Some(i) => Some(i + 1),
        }
    }

여기서는 None 케이스를 다루지 않았고, 따라서 이 코드는 버그를 일으킬 것입니다.

다행히도 러스트는 이 버그를 어떻게 잡는지 알고 있습니다.

이 코드의 컴파일을 시도하면, 아래와 같은 에러를 얻게 됩니다.

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
 --> src/main.rs:3:15
  |
3 |         match x {
  |               ^ pattern `None` not covered
  |
note: `Option<i32>` defined here
 --> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:518:1
  |
  = note.
/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:522:5: 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!(),
  |

For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` due to previous error

러스트의 매치는 철저합니다(exhaustive).

발생할 수 있는 경우 중 놓친 게 있음을 아는 것은 물론, 어떤 패턴을 놓쳤는가도 알고 있죠.

따라서 유효한 코드를 만들려면 모든 가능성을 샅샅이 다루어야 합니다.

이로써 발생하는 장점은 Option<T>에서도 드러납니다.

None 케이스를 다루는 것을 깜박하면 러스트가 알아채고 알려주기 때문에, 이 match에서 값의 부재 처리를 빠뜨린 코드는 컴파일되지 않습니다.

포괄 패턴과 _ 자리표시자

열거형을 사용하면서 특정한 몇 개의 값들에 대해 특별한 동작을 하지만, 그 외의 값들에 대해서는 기본 동작을 취하도록 할 수도 있습니다.

어떤 게임을 구현하는 중인데 주사위를 굴려서 3이 나오면 플레이어는 움직이는 대신 새 멋진 모자를 얻고, 7을 굴리면 플레이어는 그 모자를 잃게 된다고 생각해봅시다.

그 외의 값들에 대해서는 게임판 위에서 해당 숫자만큼 칸을 움직입니다.

이러한 로직을 구현한 match를 볼 것인데, 실제로 이를 구현하는 것은 이 예제의 범위를 벗어나므로 임의의 값 대신 하드코딩된 주사위 눈 결과를 사용하고, 그 밖의 로직들은 본문 없는 함수로 작성하겠습니다.

    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 함수에 넘기는 데 사용합니다.

match는 놓친 경우를 컴파일 단계에서 드러내고, 모든 가능성을 의식적으로 처리하게 만듭니다.

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() {}

여기에서는 러스트에게 명시적으로 앞의 갈래에 매칭되지 않은 어떠한 값도 사용하지 않을 것이며, 어떠한 코드도 실행하지 않기를 원한다고 명시적으로 알려준 것입니다.

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

패턴 뒤에는 if와 불리언 식을 붙여 매치 가드(match guard)를 만들 수 있습니다. 패턴이 먼저 맞은 뒤 가드가 평가되며, 가드가 false이면 해당 갈래의 본문을 실행하지 않고 같은 갈래의 남은 | 대안 또는 다음 갈래의 패턴을 검사합니다.

let number = Some(8);

match number {
    Some(n) if n % 2 == 0 => println!("짝수: {n}"),
    Some(n) => println!("홀수: {n}"),
    None => println!("값 없음"),
}

패턴이 바인딩한 n은 그 패턴 뒤의 가드와 갈래 표현식에서 사용할 수 있습니다. 바인딩 방식은 패턴에 따라 이동, 복사, 참조가 되며, 가드가 있는 갈래에서는 가드를 평가하는 동안 대응 부분을 공유 참조로 읽고 가드가 성공한 뒤에만 값의 이동 또는 복사가 일어납니다.

가드가 붙은 더 좁은 Some(n) 갈래를 먼저 두고, 가드 없는 Some(n) 갈래를 뒤에 두면 남은 Some 값을 처리할 수 있습니다. 컴파일러는 가드 식의 참·거짓으로 철저성을 판단하지 않으므로, 가드 없는 Some(n)과 마지막 None 갈래가 함께 있어야 Option<i32>의 모든 경우가 덮입니다.

가드의 참·거짓을 따라가기 위 가드 발췌를 main 안에 넣고 number를 Some(9)로 바꾸면, 첫 Some(n) 패턴 자체는 맞지만 n%2==0은 false입니다. 그래서 두 번째 Some(n) 갈래가 선택되어 홀수: 9를 출력합니다. None이면 두 Some 패턴이 모두 맞지 않아 값 없음을 출력합니다. 가드는 패턴이 맞지 않은 값을 새로 바인딩하지 않습니다.

스스로 갈래와 계산 조건 확인하기 가드 예제의 number를 Some(0)과 None으로 각각 바꾸어 선택 갈래와 출력을 적으세요. 가드 없는 Some(n) 갈래를 첫 줄로 옮기면 Some(8)이 어느 갈래로 가는지도 생각합니다. 별도 plus_one_checked에 Some(-1)을 전달한 결과를 원 plus_one의 작은 입력 결과와 비교하세요.

순서·가드·철저성 해설

Some(0)은 첫 가드까지 통과하여 짝수: 0을, None은 마지막 갈래에서 값 없음을 출력합니다. 가드 없는 Some(n)을 먼저 놓으면 Some(8)도 그 갈래가 먼저 잡아 홀수: 8이라는 잘못된 메시지를 만들고, 뒤의 가드 갈래는 도달하지 못합니다. 경고를 통해 순서를 다시 확인할 수 있습니다. plus_one_checked(Some(-1))도 Some(0)입니다. 배리언트 누락 검사와 산술 범위 검사는 다른 계약입니다.

패턴과 매칭에 관한 더 많은 내용이 17장에 있습니다.

지금은 match 표현식이 좀 장황할 경우에 유용한 if let 문법으로 넘어가겠습니다.

match를 작성할 때는 패턴 순서, 값 바인딩, 가드, Option<T> 처리, 포괄 패턴을 한 번에 점검하면 컴파일러의 철저함 요구를 더 쉽게 만족할 수 있습니다.

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 갈래
  • 다른 갈래: 실행하지 않음
  • 철저성은 별도 컴파일 계약이며 런타임에 모든 갈래를 수행하는 뜻이 아니다.