타나기
타나기 월드
타나기
전체 방문자
오늘
어제
  • 분류 전체보기 (90)
    • ⚙️ Rust (20)
      • 👀 Tutorial (20)
    • 🗿 Embedded (11)
      • 🐧 OS for ARM (11)
    • 💻 Study (37)
      • 🧩 알고리즘 (37)
    • 🏄🏽‍♂️ Life (21)
      • 🍚 타나구루망 (20)
      • 💡 Light (1)

인기 글

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
타나기

타나기 월드

⚙️ Rust/👀 Tutorial

[Rust / 튜토리얼] 15. 러스트에서 match 를 이용한 분기.

2022. 6. 6. 18:16

match 를 이용한 분기

코딩을 하다 보면 if/else 문을 여러개 사용해야 될 상황이 있다. 이럴 때 무식하게 if/else를 남발하는 것은 그다지 권장되는 상황이 아니다. 오히려 표창좀 그만 남발 하라고 욕을 먹지 않으면 다행이다.

다른 언어에선 switch 라는 키워드를 사용해 이런 상황을 해결한다. switch는 표창을 날리는 것 보다 훨씬 빠르고, 직관적이며 우하하다. 하지만 평범하지 않은 우리의 러스트 언어는 switch 키워드는 제공하지 않으며, match라는 키워드를 통해 이를 해결한다.

분기문을 사용하는 대부분의 경우는 한줄기의 큰 흐름을 진행하는 중 자잘한 세부 줄기로 진행하기 위함이다. 우리가 카페에 갔다고 가정 해 보자. 카페에 도착해서 메뉴판을 보고 주문을 한다. 라는 것은 큰 흐름이고, 대부분의 사람들이 그런 행동을 한다. 하지만 어떤 메뉴를 고를지는 알 수 없다. 여기서 메뉴를 고르는 행동이 match 키워드를 사용하는 순간 이라고 할 수 있다. 이런 상황을 코드로 나타내 보자.

#[derive(Debug)]
enum Menu {
    Coffee,
    Latte,
    Water,
    Cookie,
    Cake,
}

fn get_cost(menu: Menu) -> u32 {
    match menu {
        Menu::Coffee => 1000,
        Menu::Latte => 1500,
        Menu::Water => 500,
        Menu::Cookie => 2000,
        Menu::Cake => 4000,
    }
}

fn main() {
    let menu = Menu::Coffee;
    let cost = get_cost(menu);

    println!("Cost is {}", cost);
}
warning: `rust` (bin "rust") generated 4 warnings
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/rust`
Cost is 1000

지금 커피를 마시고 있기 때문에 커피를 주문한 것으로 코드를 작성해 보았다.

get_cost 함수의 match 키워드 아래에 각 메뉴들이 하나하나 작성되어 있다. 함수의 인자로 전달 된 menu가 Coffee이기 때문에 Menu::Coffee => 1000, 코드가 실행된다. =>를 기준으로 왼쪽이 매칭되는 키워드, 오른쪽이 매칭 되었을 때 실행되는 코드이다.

여기쯤에서 우리는 if/else와 차이점을 발견 할 수 있다. 그건 분기문의 표현식이 True/False 인 bool 형식이 아니라는 것이다. match에는 어떤 타입이든 올 수 있다. 마치 우리가 직접 만든 Menu 열거형을 사용한 것처럼 말이다.

매칭 되었을 때 꼭 한줄의 코드만 실행하는 것은 아니다. 만약 더 많은 코드를 실행하고 싶다면 {}으로 Scope를 만들어 주면 많은 양의 코드도 실행 할 수 있다. 하지만 분기마다 코드를 많이 작성 하는것은 직관적이지 못하고 우아하지 않으므로, 작성 해야 될 코드가 길다면 따로 함수로 만들어 작성 하는 것이 더 좋다. 여기에선 간단한 인사말을 출력 하도록 해 보자.

fn get_cost(menu: Menu) -> u32 {
    match menu {
        Menu::Coffee => {
            println!("Thank you for order");
            1000
        },
        Menu::Latte => 1500,
        Menu::Water => 500,
        Menu::Cookie => 2000,
        Menu::Cake => 4000,
    }
}
warning: `rust` (bin "rust") generated 4 warnings
    Finished dev [unoptimized + debuginfo] target(s) in 0.35s
     Running `target/debug/rust`
Thank you for order
Cost is 1000

값을 바인딩 (binding) 하는 패턴

저번 장에서 러스트에서의 열거형은 값을 가지고 있을 수 있다는 것을 알게 되었다. 그럼 그 값들을 match에서 쓸 수 있으리란 것은 당연한 것일지도 모른다. 카페에 가면 특정 음료들은 할인을 해주기도 한다. 그 할인율을 값으로 가지고 있는 메뉴들을 작성해 값을 바인딩 하는 것을 연습해 보자.

#[derive(Debug)]
enum Menu {
    Coffee(u32),
    Latte,
    Water,
    Cookie,
    Cake,
}

fn get_cost(menu: Menu) -> u32 {
    match menu {
        Menu::Coffee(discount) => {
            println!("Thank you for order");
            1000 * (100 - discount) / 100
        },
        Menu::Latte => 1500,
        Menu::Water => 500,
        Menu::Cookie => 2000,
        Menu::Cake => 4000,
    }
}

fn main() {
    let menu = Menu::Coffee(10);
    let cost = get_cost(menu);

    println!("Cost is {}", cost);
}
Thank you for order
Cost is 900

커피를 주문하면 10%의 할인을 하도록 main에서 작성했다. 이 값을 사용하기 위해 match 되는 부분을 Menu::Coffee(discount) 로 바꾸어 discount 변수로 가져 오도록 했다. 그럼 의도한 대로 discount 변수는 10 이라는 값을 가지게 된다. 만약 enum 이 값을 가지고 있는데, match에서 이를 사용하지 않는다면 컴파일러는 에러를 출력할 것이다. enum 에 값이 존재한다면 반드시 이를 명시해 주자.

Option를 이용한 매칭

이전에 Option을 공부하며 i32 + Option<i32> 의 식이 실행되지 않는것을 확인 했을 것이다. 이는 Option<i32>가 i32와 다른 타입이고, 계산을 위해선 i32로 변환 해주어야 했기 때문이었다. 여기서 match를 사용한다면 None인지 아닌지를 확인 하며 값을 가져올 수 있다! None 이 아니면 값을 리턴하는 함수를 만들어 보자.

fn get_value(value: Option<i32>) -> i32 {
    match value {
        Some(i) => i,
        None => 0
    }
}

fn main() {
    let value: Option<i32> = Some(5);

    println!("value is {}", get_value(value));
}
value is 5

결과를 보면 의도한 대로 5를 출력 하는 것을 알 수 있다. 약간 번거롭긴 하지만, 값이 존재하는지 확인하는 꽤 훌륭한 방법이다.

match 에는 예외가 없다.

match는 모든 경우의 수를 포함해야 한다. 앞에서 카페 메뉴를 예시로 든 것을 다시 떠올려 보자. 메뉴판엔 분명히 water 라는 항목이 있다. 그런데 match에 water가 존재하지 않는다면 정상적으로 주문이 이루어 지지 않을 것이다! 우리의 깐깐하기 그지없는 러스트가 이런 허술함을 허용해 줄 리가 없다. 우리가 메뉴 하나를 적는 것을 깜빡 한다면 이런 오류가 발생 할 것이다.


#[derive(Debug)]
enum Menu {
    Coffee(u32),
    Latte,
    Water,
    Cookie,
    Cake,
}

fn get_cost(menu: Menu) -> u32 {
    match menu {
        Menu::Coffee(discount) => {
            println!("Thank you for order");
            1000 * (100 - discount) / 100
        },
        Menu::Latte => 1500,
        // Menu::Water => 500,
        Menu::Cookie => 2000,
        Menu::Cake => 4000,
    }
}

fn main() {
    let menu = Menu::Coffee(10);
    let cost = get_cost(menu);

    println!("Cost is {}", cost);
}
Running `cargo build --bin=rust --package=rust --message-format=json`...
   Compiling rust v0.1.0 (/Users/seunghye/Documents/juneun/rust)
error[E0004]: non-exhaustive patterns: `Water` not covered
  --> src/main.rs:12:11
   |
12 |     match menu {
   |           ^^^^ pattern `Water` not covered
   |
note: `Menu` defined here
  --> src/main.rs:6:5
   |
3  | enum Menu {
   |      ----
...
6  |     Water,
   |     ^^^^^ not covered
   = note: the matched value is of type `Menu`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
   |
20 ~         Menu::Cake => 4000,
21 ~         Water => todo!(),
   |
error: aborting due to previous error
For more information about this error, try `rustc --explain E0004`.
error: could not compile `rust` due to 2 previous errors

정말로 에러가 나는지 확인하기 위해 water 케이스를 주석처리 해 지워보았다. 그랬더니 정말 친절하기 그지 없게도 컴파일 오류에 todo!() 라고 알려 준다. 이런 부분들은 모두 코드의 안정성을 위해서이다. 만약 Option을 match하는 경우, 우리는 또다시 None 에 대해 처리 하는 것을 빼먹을 수도 있다. 그렇다면 코드는 또다시 어디에 선가 버그를 발생시킬 가능성이 있기 때문에 이를 사전에 차단한다. 이 모든게 Null 때문이다.

_ 변경자 (placeholder)

match를 사용하다 보면 공통적으로 처리되는 부분에 대해서 한꺼번에 처리 하고싶은 경우도 있을 것이다. 예를들어 카페의 메뉴에서 쿠키와 케이크가 3000원으로 동일할 경우 따로 코드를 작성하지 않아도 될 것이다. 이럴 때 사용하는 것이 _ 기호이다. 이 변경자는 위에서 나열되지 않은 모든 경우에 대해 처리하게 해 준다. 그럼 쿠키와 케이크가 같은 가격일 경우 어떻게 처리 하는지 코드를 봐보자.

#[derive(Debug)]
enum Menu {
    Coffee(u32),
    Latte,
    Water,
    Cookie,
    Cake,
}

fn get_cost(menu: Menu) -> u32 {
    match menu {
        Menu::Coffee(discount) => {
            println!("Thank you for order");
            1000 * (100 - discount) / 100
        },
        Menu::Latte => 1500,
        Menu::Water => 500,
        _ => 3000,
    }
}

fn main() {
    let cookie = Menu::Cookie;
    let cake = Menu::Cake;

    println!("Cookie {} / Cake {}", get_cost(cookie), get_cost(cake));
}
Cookie 3000 / Cake 3000

결과값이 모두 3천원인 것을 확인 할 수 있다. 분기로 이동 할 때 맨 마지막에 작성된 _ 코드와 매칭되어 실행된다. 이렇게 _ 를 이용하면 모든 경우에 대해 한꺼번에 처리할 수 있어서 enum variant가 많을 때 유용하다.

이제 다음번에는 match와 더불어 유용하게 사용되는 if let에 대해 알아보자.

저작자표시 (새창열림)

'⚙️ Rust > 👀 Tutorial' 카테고리의 다른 글

[Rust / 튜토리얼] 17. 러스트의 모듈 (mod)  (0) 2022.06.13
[Rust / 튜토리얼] 16. 러스트의 let if 의 사용법  (0) 2022.06.08
[Rust / 튜토리얼] 13. 러스트의 열거형과 Option, None 그리고 Some  (0) 2022.06.03
[Rust / 튜토리얼] 13. 러스트의 메소드.  (0) 2022.06.01
[Rust / 튜토리얼] 12. 러스트 구조체를 이용한 예제 프로그래밍  (0) 2022.05.29
    '⚙️ Rust/👀 Tutorial' 카테고리의 다른 글
    • [Rust / 튜토리얼] 17. 러스트의 모듈 (mod)
    • [Rust / 튜토리얼] 16. 러스트의 let if 의 사용법
    • [Rust / 튜토리얼] 13. 러스트의 열거형과 Option, None 그리고 Some
    • [Rust / 튜토리얼] 13. 러스트의 메소드.
    타나기
    타나기
    #include<all>

    티스토리툴바