[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개의 댓글

관련 채용 정보