[rust] 6. 열거형과 패턴 매칭

About_work·2024년 6월 29일
0

rust

목록 보기
8/16
  • 열거형 (enumerations, 줄여서 enums)
  • 하나의 타입이 가질 수 있는 배리언트 (variant) 들을 열거함으로써 타입을 정의할 수 있도록 합니다.
  • match 표현식의 패턴 매칭을 통해 열거형의 값에 따라 다른 코드를 쉽게 실행할 수 있는 방법
  • 마지막으로, 코드에서 열거형을 편하고 간결하게 다루기 위한 관용 표현인 if let 구문 배우기

1. 열거형 정의하기

  • v4, v6는 근본적으로 IP 주소이기 때문에, 이 둘은 코드에서 모든 종류의 IP 주소에 적용되는 상황을 다룰 때 동일한 타입으로 처리되는 것이 좋습니다.
enum IpAddrKind {
    V4,
    V6,
}
  • v4, v6: variant

1.1. 열거형 값

  • 아래처럼, IpAddrKind의 두 개의 배리언트에 대한 인스턴스를 만들 수 있습니다:
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;
  • 열거형을 정의할 때의 식별자로 네임스페이스가 만들어져서, 각 배리언트 앞에 이중 콜론(::)을 붙여야 한다는 점을 주의하세요.
fn route(ip_kind: IpAddrKind) {}

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
    enum IpAddr {
        V4(String),
        V6(String),
    }

    let home = IpAddr::V4(String::from("127.0.0.1"));

    let loopback = IpAddr::V6(String::from("::1"));
  • 각 열거형 배리언트의 이름이 해당 열거형 인스턴스의 생성자 함수처럼 된다는 것이죠.
    • 생성자: 인스턴스를 초기화하고 생성하는 함수/메서드
  • 즉, IpAddr::V4()는 String 인수를 입력받아서 IpAddr 타입의 인스턴스 결과를 만드는 함수
  • 열거형을 정의한 결과로써 이러한 생성자 함수가 자동적으로 정의

  • 구조체 대신 열거형을 사용하면 또 다른 장점이 있습니다.
  • 각 배리언트는 다른 타입과 다른 양의 연관된 데이터를 가질 수 있습니다.
    enum IpAddr {
        V4(u8, u8, u8, u8),
        V6(String),
    }

    let home = IpAddr::V4(127, 0, 0, 1);

    let loopback = IpAddr::V6(String::from("::1"));
  • 그러나, 누구나 알듯이 IP 주소와 그 종류를 저장하는 것은 흔하기 때문에, 표준 라이브러리에 정의된 것을 사용할 수 있습니다!
  • 위에서 정의하고 사용했던 것과 동일한 열거형과 배리언트를 갖고 있지만, 배리언트에 포함된 주소 데이터는 두 가지 다른 구조체로 되어 있으며, 각 배리언트마다 다르게 정의하고 있습니다:
struct Ipv4Addr {
    // --생략--
}

struct Ipv6Addr {
    // --생략--
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}
  • 이 코드로 알 수 있듯, 열거형 배리언트에는 어떤 종류의 데이터라도 넣을 수 있습니다.
    • 다른 열거형마저도 포함할 수 있죠!

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}
  • Quit은 연관된 데이터가 전혀 없습니다.
  • Move은 구조체처럼 이름이 있는 필드를 갖습니다.
  • Write은 하나의 String을 가집니다.
  • ChangeColor는 세 개의 i32을 가집니다.
  • 위 예제 처럼, 배리언트로 열거형을 정의하는 것은 다른 종류의 구조체들을 정의하는 것과 비슷

  • 구조체에 impl을 사용해서 메서드를 정의한 것처럼, 열거형에도 정의할 수 있습니다.
    impl Message {
        fn call(&self) {
            // 메서드 본문이 여기 정의될 것입니다
        }
    }

    let m = Message::Write(String::from("hello"));
    m.call();
  • 메서드 본문에서는 self를 사용하여 호출한 열거형의 값을 가져올 것입니다.
  • 이 예제에서 생성한 변수 m은 Message::Write(String::from("hello")) 값을 갖게 되고,
    • 이 값은 m.call()이 실행될 때 call 메서드 안에서 self가 될 것입니다.

1.2. Option 열거형이 null 값보다 좋은 점들

  • 표준 라이브러리에 포함된 열거형 중에서 굉장히 유용하고 자주 사용되는 Option 열거형
  • 비어있는 리스트로부터 첫 번째 아이템을 요청한다면 아무 값도 얻을 수 없을 것입니다.
  • 이 개념을 타입 시스템으로 표현한다는 것은 처리해야 하는 모든 경우를 처리했는지 컴파일러가 확인할 수 있다는 의미입니다.
  • 러스트는 다른 언어들에서 흔하게 볼 수 있는 널 (null) 개념이 없습니다.
    • 널은 값이 없음을 표현하는 하나의 값입니다.
    • 널 개념이 존재하는 언어에서, 변수의 상태는 둘 중 하나입니다. 널인 경우와, 널이 아닌 경우죠.
    • 널 값으로 발생하는 문제는, 널 값을 널이 아닌 값처럼 사용하려고 할 때 여러 종류의 에러가 발생할 수 있다는 것입니다.
    • 널의 문제는 실제 개념에 있기보다, 특정 구현에 있습니다.
  • 이처럼 러스트에는 널이 없지만, 값의 존재 혹은 부재의 개념을 표현할 수 있는 열거형이 있습니다.
  • 그 열거형이 바로 Option<T>이며, 다음과 같이 표준 라이브러리에 정의되어 있습니다:
enum Option<T> {
    None,
    Some(T),
}
  • Option<T> 열거형은 너무나 유용하기 때문에, 러스트에서 기본으로 임포트하는 목록인 프렐루드에도 포함
  • 이것의 배리언트 또한 프렐루드에 포함되어 있습니다:
    • 프렐루드: 자주 사용하는 라이브러리의 아이템들을 포함하는 모듈
  • 따라서 Some, None 배리언트 앞에 Option::도 붙이지 않아도 됩니다.
  • 하지만 Option<T>는 여전히 그냥 일반적인 열거형이며, Some(T)와 None도 여전히 Option<T>의 배리언트 입니다.
  • <T> 문법은 아직 다루지 않은 러스트의 기능입니다. 이것은 제네릭 타입 매개변수 (generic type parameter) 이며, 제네릭에 대해서는 10장에서 더 자세히 다룰 것입니다.
  • 지금은 <T>라는 것이 아래의 역할을 한다는 것만 알아두면 됩니다.
    • Option 열거형의 Some 배리언트가 어떤 타입의 데이터라도 담을 수 있게 한다는 것,
    • T의 자리에 구체적인 타입을 집어넣는 것이 전체 Option<T> 타입을 모두 다른 타입으로 만든다는 것
    let some_number = Some(5);
    let some_char = Some('e');

    let absent_number: Option<i32> = None;
  • some_number의 타입은 Option<i32>입니다.
  • some_char의 타입은 Option<char>이고 둘은 서로 다른 타입입니다.
  • Some 배리언트 내에 어떤 값을 명시했기 때문에 러스트는 이 타입들을 추론할 수 있습니다.
  • absent_number에 대해서는 전반적인 Option 타입을 명시하도록 해야 합니다: None 값만 봐서는 동반되는 Some 배리언트가 어떤 타입의 값을 가질지 컴파일러가 추론할 수 없기 때문입니다.
    • 위 예제에서는 absent_number가 Option<i32> 타입임을 명시했습니다.

  • Option<T>와 T(T는 어떤 타입이던 될 수 있음)이 다른 타입이기 때문에, 컴파일러는 Option<T> 값을 명백하게 유효한 값처럼 사용하지 못하도록 합니다.
    • 예를 들면, 아래 코드는 Option<i8>에 i8을 더하려고 하고 있으므로 컴파일되지 않습니다:
    let x: i8 = 5;
    let y: Option<i8> = Some(5);

    let sum = x + y;

# cannot add `Option<i8>` to `i8`
  • 바꿔 말하면, T에 대한 연산을 수행하기 전에 Option<T>를 T로 변환해야 합니다.
  • 이런 방식은 널로 인해 발생하는 가장 흔한 문제인, 실제로는 널인데 널이 아니라고 가정하는 상황을 발견하는 데 도움이 됩니다.
  • 널일 수 있는 값을 사용하기 위해서는 명시적으로 값의 타입을 Option<T>로 만들어 줘야 합니다.
  • 이것은 널을 너무 많이 사용하는 문제를 제한하고 러스트 코드의 안정성을 높이기 위해 의도된 러스트의 디자인 결정 사항입니다.
  • 그래서, Option<T> 타입인 값을 사용할 때 Some 배리언트에서 T 값을 가져오려면 어떻게 해야 하냐고요?
    • Option<T> 열거형이 가진 메서드는 많고, 저마다 다양한 상황에서 유용하게 쓰일 수 있습니다.
    • 그러니 한번 문서에서 여러분에게 필요한 메서드를 찾아보세요.
  • 일반적으로, Option<T> 값을 사용하기 위해서는 각 배리언트를 처리할 코드가 필요할 겁니다.
    • Some(T) 값일 때만 실행돼서 내부의 T 값을 사용하는 코드도 필요할 테고,
      = None 값일 때만 실행될, T 값을 쓸 수 없는 코드도 필요할 겁니다.
      = match 표현식은 열거형과 함께 사용할 때 이런 작업을 수행하는 제어 흐름 구조로,
    • 열거형의 배리언트에 따라 다른 코드를 실행하고 매칭되는 값 내부의 데이터를 해당 코드에서 사용할 수 있습니다.

2. match 제어 흐름 구조

  • 일련의 패턴에 대해 어떤 값을 비교한 뒤 어떤 패턴에 매칭되었는지를 바탕으로 코드를 수행하도록 해줍니다.
  • TODO: 패턴은 리터럴 값, 변수명, 와일드카드 등 다양한 것으로 구성될 수 있으며, 전체 종류 및 각각의 역할은 18장에서 배울 예정입니다.
  • match의 힘은 패턴의 표현성으로부터 오며 -> 컴파일러는 모든 가능한 경우가 처리되는지 검사합니다.

  • match 표현식을 동전 분류기와 비슷한 종류로 생각해 보세요. 동전들은 다양한 크기의 구멍들이 있는 트랙으로 미끄러져 내려가고, 각 동전은 그것에 맞는 첫 번째 구멍을 만났을 때 떨어집니다.
  • 먼저 match 키워드 뒤에 표현식을 써줬는데, 위의 경우에는 coin 값입니다.
  • 이는 if 에서 사용하는 조건식과 매우 유사하지만, 큰 차이점이 있습니다.
  • if를 사용할 경우에는 조건문에서 부울린 값을 반환해야 하지만, 여기서는 어떤 타입이든 가능
  • match 갈래 (arm) 들입니다. 하나의 갈래는 패턴과 코드 두 부분으로 이루어져 있습니다.
    • 여기서의 첫 번째 갈래에는 값 Coin::Penny로 되어있는 패턴이 있고
    • 그 뒤에 패턴과 실행되는 코드를 구분해 주는 => 연산자가 있습니다.
    • 위의 경우에서 코드는 그냥 값 1입니다.
    • 각 갈래는 그다음 갈래와 쉼표로 구분됩니다.
  • 각 갈래와 연관된 코드는 표현식이고, 이 매칭 갈래에서의 표현식의 결과로써 생기는 값은 전체 match 표현식에 대해 반환되는 값

2.1. 값을 바인딩하는 패턴

  • 매치 갈래의 또 다른 유용한 기능: 패턴과 매칭된 값들의 일부분을 바인딩할 수 있다는 것
// 예제 6-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 변수는 그 쿼터 동전의 주에 대한 값에 바인딩될 것입니다.
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
        }
    }
}

2.2. Option<T>를 이용하는 매칭

    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);

2.3. 매치는 철저합니다.

  • 갈래의 패턴들은 모든 가능한 경우를 다루어야 합니다. plus_one 함수의 아래 버전을 고려해 봅시다. 버그가 있고 컴파일되지 않지만요:
  • 여기서는 None 케이스를 다루지 않았고, 따라서 이 코드는 버그를 일으킬 것입니다.
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            Some(i) => Some(i + 1),
        }
    }

// non-exhaustive patterns: `None` not covered
  • 러스트의 매치는 철저합니다 (exhaustive).
  • 발생할 수 있는 경우 중 놓친 게 있음을 아는 것은 물론, 어떤 패턴을 놓쳤는가도 알고 있죠.

2.4. 포괄패턴과 _ 자리표시자

  • 그 외의 값들에 대해서는 기본 동작을 취하도록 할 수도 있습니다.
    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) {}
  • u8이 가질 수 있는 모든 값을 나열하지 않았음에도 이 코드는 컴파일 되는데,
    • 그 이유는 특별하게 나열되지 않은 나머지 모든 값에 대해 마지막 패턴이 매칭될 것이기 때문입니다.
  • 이러한 포괄 (catch-all) 패턴은 match의 철저함을 만족시킵니다.
  • 패턴들은 순차적으로 평가되므로 마지막에 포괄적인 갈래를 위치시켜야 한다는 점을 기억

  • 포괄 패턴이 필요한데 그 포괄 패턴의 값을 사용할 필요는 없는 경우에 쓸 수 있는 패턴도 있습니다:
  • _는 어떠한 값이라도 매칭되지만, 그 값을 바인딩하지는 않는 특별한 패턴입니다.
  • 이는 러스트에게 해당 값을 사용하지 않겠다는 것을 알려주므로, 러스트는 사용되지 않는 변수에 대한 경고를 띄우지 않을 것입니다.
    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() {}
  • 아무 일도 일어나지 않게 하려면
    • _ 갈래에 (‘튜플 타입’에서 다루었던) 유닛 값을 사용하여 표현할 수 있습니다:
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => (),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
  • 어떠한 값도 사용하지 않을 것이며, 어떠한 코드도 실행하지 않기를 원한다고 명시적으로 알려준 것입니다

3. if let을 사용한 간결한 제어 흐름

  • if let 문법은 if와 let을 조합하여 하나의 패턴만 매칭시키고 나머지 경우는 무시하도록 값을 처리하는 간결한 방법을 제공
enum Option<T> {
    None,
    Some(T),
}

    let config_max = Some(3u8);
    match config_max {
        Some(max) => println!("The maximum is configured to be {}", max),
        _ => (),
    }
  • match 표현식을 만족시키려면 딱 하나의 배리언트 처리 후 _ => ()를 붙여야 하는데, 이는 다소 성가신 보일러 플레이트 코드입니다.
  • if let을 이용하여 이 코드를 더 짧게 쓸 수 있습니다.
  • if let은 =로 구분된 패턴과 표현식을 입력받습니다.
  • 하지만, match가 강제했던 철저한 검사를 안하게 되었습니다.
  • match와 if let 사이에서 선택하는 것은
    • 여러분의 특정 상황에서 여러분이 하고 있는 것에 따라, 그리고
    • 간결함을 얻는 것이 철저한 검사를 안하게 되는 것에 대한 적절한 거래인지에 따라 달린 문제
  • 즉, if let은 한 패턴에 매칭될 때만 코드를 실행하고 다른 경우는 무시하는 match 문을 작성할 때 사용하는 문법 설탕 (syntax sugar) 이라고 생각하시면 됩니다.

  • if let과 함께 else를 포함시킬 수 있습니다.
  • else 뒤에 나오는 코드 블록은, match 표현식에서 _ 케이스 뒤에 나오는 코드 블록과 동일
    let mut count = 0;
    match coin {
        Coin::Quarter(state) => println!("State quarter from {:?}!", state),
        _ => count += 1,
    }
-----
    let mut count = 0;
    if let Coin::Quarter(state) = coin {
        println!("State quarter from {:?}!", state);
    } else {
        count += 1;
    }
  • 만일 여러분의 프로그램이 match로 표현하기에는 너무 장황한 로직을 가지고 있는 경우라면, 러스트 도구 상자에 if let도 있음을 기억하세요.

profile
새로운 것이 들어오면 이미 있는 것과 충돌을 시도하라.

0개의 댓글