[Chapter 6-1] Rust 열거형

hwwwa·2021년 10월 28일
0

🦀 Rust

목록 보기
15/25

열거형 정의하기

enum IpAddrKind {
    V4,
    V6,
}

위는 IpAddrKind 이라는 열거형을 정의하면서 포함할 수 있는 IP 주소인 V4 과 V6 를 나열하는 코드입니다. 이들은 열거형의 variants 라고 하며 열거형의 값은 variants 중 하나만 될 수 있습니다.

아래처럼 IpAddrKind 의 두 개의 variants에 대한 인스턴스를 만들 수 있습니다.

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

이제 IpAddrKind 타입을 인자로 받는 함수를 정의하고 호출할 수 있습니다.

fn route(ip_type: IpAddrKind) { }

route(IpAddrKind::V4);
route(IpAddrKind::V6);

하지만 지금의 코드로는 실제 IP 주소 데이터를 저장할 수 없습니다. 이때 구조체를 함께 사용할 수도 있지만, 더 간결하고 동일한 개념으로 표현할 수 있습니다. 아래의 코드에서 IpAddr 열거형의 새로운 정의는 두 개의 V4V6 variant 는 연관된 String 타입의 값을 갖게 됩니다.

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

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

let loopback = IpAddr::V6(String::from("::1"));

구조체보다 열거형을 사용할 때의 장점은 각 variant는 다른 타입과 다른 양의 연관된 데이터를 가질 수 있다는 점입니다. V4 주소에 4개의 u8 값을 저장하길 원하지만, V6 주소는 하나의 String 값으로 표현되길 원할 때, 구조체에서는 불가능하지만 열거형은 가능합니다.

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 주소와 그 종류를 저장하는 것은 흔하기 때문에 표준 라이브러리에 정의되어 있습니다.

열거형과 구조체는 한 가지 더 유사한 점이 있습니다. 구조체에 impl을 사용해 메소드를 정의한 것 처럼 열거형에도 정의 가능합니다.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

impl Message {
    fn call(&self) {
        // 메소드 내용은 여기 정의할 수 있습니다.
    }
}

let m = Message::Write(String::from("hello"));
m.call();

Option 열거형

러스트에는 null이 없지만 값의 존재 혹은 부재의 개념을 표현할 수 있는 열거형이 있습니다. 이 열거형은 Option<T> 이며, 다음과 같이 표준 라이브러리에 정의되어 있습니다.

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

Option<T> 열거형은 기본적으로 포함되어 있기 때문에 명시적으로 가져오지 않아도 사용할 수 있습니다. variants도 마찬가지입니다. Option::를 앞에 붙이지 않고 Some과 None을 바로 사용할 수 있습니다.

<T>는 러스트이 제너럭 타입 파라미터이며 이에 대해서는 10장에서 다루게 될 것입니다. 지금은 <T>가 Option 열거형의 Some variant가 어떤 타입의 데이터라도 가질 수 있다는 것을 의미한다는 것만 알고있으면 됩니다.

let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None

Some 이 아닌 None 을 사용한다면, Option<T> 가 어떤 타입을 가질지 러스트에게 알려줄 필요가 있습니다. 컴파일러는 None 만 보고는 Some variant 가 어떤 타입인지 추론할 수 없기 때문입니다.

None 값을 사용하면 유효한 값을 갖지 않기 때문에 어떤 면에서는 null과 같은 의미를 갖게 됩니다. 하지만 Option<T>와 T(T는 어떤 타입이든 될 수 있음)는 다른 타입이며, 컴파일러는 Option<T> 값을 명확하게 유효한 값처럼 사용하지 못하도록 하기 때문에 null을 갖는 것보다 낫습니다.

예를 들어 아래 코드는 서로 다른 타입인 Option<i8>에 i8을 더하려고 하기 때문에 컴파일되지 않습니다.

let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;
error[E0277]: the trait bound `i8: std::ops::Add<std::option::Option<i8>>` is not satisfied
 -->
  |
7 | let sum = x + y;
  |           ^^^^^
  |

러스트에서 i8과 같은 타입의 값을 가질 때, 컴파일러는 항상 유효한 값으르 갖고 있다는 것을 보장할 것입니다. 하지만 Option<i8>을 사용할 경우엔 값이 있을지 없을지에 대해 걱정할 필요가 있습니다. 다르게 이야기 하자면, T에 대한 연산을 수행하기 전에 Option<T>를 T로 변환해야 합니다. 일반적으로 이런 방식은 실제로 null 인데 null이 아니라고 가정하는 이슈를 발견할 수 있도록 해줍니다.

값의 타입이 Option<T>가 아닌 모든 곳은 값이 null이 아니라고 안전하게 가정할 수 있습니다. 이는 null을 너무 많이 사용하는 문제를 제한하고 러스트 코드의 안정성을 높이기 위한 러스트의 의도된 디자인 결정사항입니다.

0개의 댓글