#8 열거형과 패턴 매칭

Pt J·2020년 8월 18일
0

[完] Rust Programming

목록 보기
10/41
post-thumbnail

이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.

다양한 언어에서 열거형을 지원하지만 세부적인 기능은 조금씩 차이가 있다.
Rust의 열거형은 함수형 언어의 대수식과 유사하다.

열거형 Enum

열거형은 구조체와 마찬가지로 여러 개의 필드로 이루어져 있다.
정확히는, 열거형의 필드는 Variant라고 부르는데, 공식 번역에서는 열것값이라고 하는 모양이다.
메서드를 구현할 수 있다는 것도 구조체와 열거형의 비슷한 점이다.
구조체와의 차이는, 구조체 인스턴스는 모든 필드의 값을 가지고 있지만
열거형 인스턴스는 하나의 열것값에 해당하는 값을 가지고 있다.
그리고 열것값은 자료형을 명시하지 않으며 열거형의 이름이 그 자료형이 된다.
열겨형은 여러 값 중 하나를 선택적으로 가질 수 있는 경우에 유용하다.

예를 들어, IP주소를 V4 또는 V6으로 저장한다고 할 때
그 중 무엇인지 나타내기 위해 다음과 같은 열거형을 사용할 수 있다.

enum IpAddrKind {
    V4,
    V6,
}

이들은 IpAddrKind::V4, IpAddrKind::V6과 같은 형태로 사용된다.

IP주소의 주소값은 열거형과 별개의 문자열에 저장하여
그들을 필드로 하는 구조체를 만들어 사용할 수도 있겠지만
열거형 내에 값을 넣을 수 있다.
그리고 같은 열거형 내의 열거값이 서로 다른 종류와 개수의 자료형의 값을 가질 수도 있다.
즉 다음과 같은 형태뿐만 아니라

enum IpAddrKind {
    V4(String),
    V6(String),
}

다음과 같은 형태의 열거형도 가능하다는 것이다.

enum IpAddrKind {
    V4(u8, u8, u8, u8),
    V6(String),
}

열것값은 어떤 자료형이든 담을 수 있으며 심지어 구조체나 다른 열거형도 담을 수 있다.

Option 열거형

표준 라이브러리가 제공하며 보편적으로 사용되는 열거형으로 Option이라는 녀석이 있다.
이 녀석에 대한 이야기를 하기 전에
Rust에는 NULL이 존재하지 않는다는 이야기를 먼저 해야할 것 같다.
NULL 값의 창시자 Tony Toare는 강연에서 다음과 같이 말한 바가 있다.

I call it my billion-dollar mistake. At that time, I was designing the first comprehensive type system for references in an object-oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

대략, 더 쉽게 구현하기 위해 NULL을 만들었지만 그것이 큰 취약성을 야기했고,
자신은 그것을 큰 실수, 십억짜리 실수라고 여긴다는 이야기다.

물론 유효하지 않거나 존재하지 않는 값을 표현하기 위한 개념 자체는 유용하다.
다만 NULL 값을 NULL이 아닌 값처럼 사용하려고 할 경우 에러와 취약성을 야기한다.
이건 개념 상의 문제라기 보다는 구현 상의 문제다.
따라서 Rust는 NULL 대신 어떤 값의 존재 여부를 표현하기 위한 열거형을 마련했다.
그것이 표준 라이브러리에 정의된 Option<T>다.

enum Option<T> {
    Some(T),
    None,
}

Option<T><T>는 제네릭이라는 녀석인데 나중에 따로 이야기할테니
일단 이것으로 인해 자료형에 상관 없이 사용 가능하다는 것 정도만 알고 넘어가자.
이 녀석은 매우 유용해서 Prelude에도 포함되어 있기에 use 없이 사용할 수 있다.
Option:: 접두어 없이 SomeNone을 직접 사용하는 게 가능하다.
Some의 경우 그것이 가진 값을 통해 <T>를 알 수 있으니 상관 없지만
None을 사용할 땐 그 변수가 어떤 Option 자료형인지 명시해주어야 한다.

그렇다면 Option::NoneNULL보다 나은 점은 무엇일까?

Option은 그 값이 Some에 해당하는 순간에도 일반 변수와 연산을 할 수 없다.
u32 변수와 Some(u32) 변수의 연산을 시도하면 오류가 발생한다.
따라서 실수로라도 Some이 아닌 None과 연산을 시도할 일이 없다.
Option이 가진 값이 Some(T)라면 이것을 사용하기 전에 T로 변환해야 하며
None이라면 그대로 연산하지 않고 이에 대한 예외처리를 해주어야 한다.

패턴 매칭 Pattern Matching

열것값 중 특정 열것값에 해당할 때 이 작업을 수행하라,
하는 코드를 작성할 때 유용하게 사용할 수 있는 것이 흐름 제어 연산자 match다.
matchmatch 비교대상 {} 블록 안에 여러 개의 arm을 가지고 있으며
그 중 하나의 arm이 선택적으로 수행된다.
arm패턴 => 표현식으로 구성되며 패턴비교대상과 일치할 때 그 표현식이 실행된다.
그리고 그 표현식의 결과값이 전체 match 표현식의 결과값이 된다.
표현식이 둘 이상의 문장으로 이루어질 경우 중괄호를 사용해야 한다.

예를 들어, 동전의 종류에 따라 그 가치를 센트로 반환하는 함수를 다음과 같이 구현할 수 있다.

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

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

값을 bind하는 패턴

match 표현식의 패턴은 열것값으로부터 어떤 값을 받아올 수 있다.
이것이 어떤 식으로 수행되는지 확인하기 위해 위 예제를
값을 가진 열것값이 포함된 열거형으로 수정해보자.
25센트 동전에는 미국의 50개 주의 고유한 디자인이 존재하는데
이 정보를 Quarter 열것값에 추가하도록 하겠다.
어떤 주인지에 대한 정보도 열거형으로 작성한다.

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska, 
    // ... snip
}

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

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

지난 시간에 언급된 디버그 애노테이션을 통해 값을 출력하도록 하였다.
이렇게 함수에 매개변수 사용하듯 열것값이 가진 값을 사용할 수 있다.

Option 매칭

이번엔 아까 언급된 Option 열거형을 패턴 매칭을 통해 처리하는 방법을 알아보자.
Some일 경우 값을 1 증가시키고 None일 경우 그냥 두는 함수를 작성해보겠다.

peter@hp-laptop:~/rust-practice$ mkdir chapter06
peter@hp-laptop:~/rust-practice$ cd chapter06/
peter@hp-laptop:~/rust-practice/chapter06$ cargo new option_match
     Created binary (application) `option_match` package
peter@hp-laptop:~/rust-practice/chapter06$ cd option_match/
peter@hp-laptop:~/rust-practice/chapter06/option_match$ vi src/main.rs

src/main.rs

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

    println!("five: {}", print_option(five));
    println!("six: {}", print_option(six));
    println!("none: {}", print_option(none));
}

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

fn print_option(x: Option<i32>) -> String {
    match x {
        None => String::from("None"),
        Some(i) => format!("{}", i),
    }
}
peter@hp-laptop:~/rust-practice/chapter06/option_match$ cargo run
   Compiling option_match v0.1.0 (/home/peter/rust-practice/chapter06/option_match)
    Finished dev [unoptimized + debuginfo] target(s) in 0.22s
     Running `target/debug/option_match`
five: 5
six: 6
none: None
peter@hp-laptop:~/rust-practice/chapter06/option_match$

match 표현식을 통해 열것값이 Some인 경우에만 연산을 수행하도록 할 수도 있고
열것값에서 그것이 가진 값을 뽑아낼 수도 있다.

자리지정자 Placeholder

match 표현식은 반드시 열거형의 모든 열것값에 대한 경우의 수를 커버해야 한다.
그렇지 않다면 match 표현식이 처리하지 못하는 값이 들어왔을 때 오류가 발생할 수 있다.
물론 Rust는 이러한 오류를 사전 방지하여 컴파일 할 때 먼저 오류를 띄워준다.
이로서 Rust는 None에 대한 처리를 강제하게 되고
NULL이 가질 수 있는 실수로부터 보호한다.

그런데 때로는 모든 값을 다 처리하고 싶지 않은 경우도 있다.
이럴 땐 자리지정자 _를 통해 나머지를 뭉뚱그려 처리할 수 있다.
예를 들어, 어떤 match 표현식의 맨 아래에 _ => ()를 추가하면
나머지 경우 아무것도 하지 않겠다는 것을 의미한다.
_는 '모든 값'을 의미하므로 맨 아래에 작성하도록 하자.

if let

어떤 경우에는 단 하나의 패턴만 처리하고 나머지는 무시하고 싶을 때가 있다.
이럴 땐 match 표현식을 사용하면 쓸데없이 복잡해보이기만 한다.
모든 경우의 수를 고려해야 한다는 점에서 _ => ()를 항상 포함해야 하니 말이다.
따라서 이런 경우에는 match 표현식이 아닌 if let을 사용하는 게 유리하다.
if letif 패턴 = 비교대상 {} 형식으로 작성한다.

if let을 사용하면 다음과 같은 코드를

let some_u8_value = Some(0u8);
match some_u8_value {
    Some(3) => println!("three"),
    _ => (),
}

다음과 같이 보다 가독성 높고 간결하게 표현할 수 있다.

if let Some(3) = some_u8_value {
    println!("three");
}

만약 단 하나의 패턴만 처리하는 게 아니라 두 가지 중 양자택일이라면,
그러니까 특정 값이거나 아니거나에 따라 구분된다면
if let 구문 뒤에 else 구문을 통해 _ => {}에 대한 처리를 해줄 수 있다.

패턴 매칭에 대한 건 나중에 좀 더 자세하게 다룰 일이 있을 것이다.

이 포스트의 내용은 공식문서의 6장 Enums and Pattern Matching에 해당합니다.

profile
Peter J Online Space - since July 2020

0개의 댓글