열거자 (enumerations)

이 포스트는 Rust를 처음 공부하면서 정리한 내용입니다. 해당 포스트는 개인적으로 기억하기 위한 메모 성격에 가깝습니다. “러스트 프로그래밍 공식 가이드 (2018, 제이펍)” 서적을 참고하였으며, 러스트 공식 사이트 에서 책과 동일한 내용을 찾을 수 있습니다. 따라서 더 자세한 내용을 찾으시면 위의 링크를 참고하시면 좋습니다.

열거자, 또는 C언어에서의 열거형은 자주 쓰이는 기능 중 하나입니다. C에서의 열거형은 아래와 같이 선언하고 사용합니다.

// C/C++
enum 열거형이름 {
    1 = 초깃값,
    2,
    3
};

위와 같이 선언하여 변수와 값으로 이용하기도 하지만, 초기값이 지정되면 다음으로 등장하는 값은 자동적으로 1씩 증가하여 할당되기 때문에 #define을 대신하여 사용되기도 합니다.

enum {
    SUNDAY, // 0
    MONDAY, // 1
    TUESDAY, // 2
    WEDNESDAY, // 3
    ... // etc.
};

반면, Rust에서의 열거자는 데이터 타입에 더 가까운 개념으로 이용됩니다.

열거자 정의

두 가지의 IP 주소 방식을 다루어야 하는 프로그램을 작성한다고 할 때의 예시를 들어보겠습니다. 열거자는 아래와 같이 정의합니다. 이때 IpAddrKind의 값 V4V6는 열거자의 열거값(Variants) 이라고 합니다.

IpAddrKind으로 열거자를 정의하고 foursix와 같이 인스턴스를 생성할 수 있습니다. 물론, 열거자 타입의 함수를 정의하고 호출할 수도 있습니다.

enum IpAddrKind {
    V4,
    V6
}

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

fn route(ip_kind: IpAddrKind) {
    ... // Some logic
}

열거자의 값 지정

앞서 언급했듯이, Rust에서의 열거자는 C와는 다르게 데이터를 표현할 수 있는 기능을 가지고 있습니다. IP의 종류 뿐만 아니라 주소까지 같이 표현해야 한다고 할 때 struct를 이용하면 아래와 같이 나타낼 수 있을겁니다. IpAddr는 열거자 타입인 kind 필드와 문자열 타입인 addr 필드를 정의하고 localhost 인스턴스에의 각 멤버에 값을 할당하고 있습니다. 두 개의 타입이 IpAddr으로 묶여있기 때문에 열거자와 관련된 값을 한 인스턴스를 사용하여 처리할 수 있습니다.

enum IpAddrKind {
    V4,
    V6
}

struct IpAddr {
    kind: IpAddrKind, // enum 타입
    addr: String // 문자열 타입
}

let localhost = IpAddr {
    kind: IpAddrKind::V4,
    addr: String::from("127.0.0.1")
};

그러나 Rust 에서는 구조체 안에 열거자를 넣지 않고 열거자만 사용하여 더 간단하게 값을 표현하는 기능을 제공합니다. 아래의 코드에서는 V4V6의 값을 정의함과 동시에 연관된 값String으로 명시하고 있습니다.

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

let localhost = IpAddrKind::V4(String::from("127.0.0.1"));

위의 예시에서는 문자열 타입을 지정하였지만, 열거자의 값에는 문자열, 숫자, 구조체 등 어떤 종류의 데이터도 저장할 수 있습니다. 심지어는 다른 열거자의 값을 저장해도 무방합니다.

아래의 예시는 개별 값을 각각 다른 타입으로 정의한 Message 열거자입니다.

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

Message의 예시에서 각기 다른 타입을 명시하고 있습니다.

  • Quit은 연관 데이터를 전혀 갖지 않습니다.
  • Move는 익명 구조체(Anonymous struct)를 포함합니다.
  • Write값은 하나의 String 값을 포함하고 있습니다.
  • ChangeColor 값은 세 개의 i32 값을 포함합니다.

위의 Message를 각기 다른 struct 타입으로 구현하게 되면 다른 타입의 Message를 매개변수로 하는 함수를 쉽게 정의할 수 없게 됩니다.

열거자 메서드

열거자는 struct와 마찬가지로 impl 블록을 이용해 메서드를 정의할 수 있습니다.

impl Message {
    fn call(&self) { ... } // Some function
}

let msg = Message::Write(String::from("Hello Rust!"));

msg.call();

Option 열거자와 Null

Null의 구현

Option은 Rust가 제공하는 표훈 라이브러리 열거자입니다. Option 열거자는 다양한 곳에서 활용이 가능한데, 어떠한 값이 존재하거나 존재하지 않는 경우를 포함하는 상황에서도 이용 가능하도록 디자인 되었기 때문입니다. Type 시스템이 이런 타입을 제공한다는 것은 모든 경우의 수를 처리하고 있는지 컴파일러가 확인이 가능하다는 것을 의미합니다. 따라서 이 열거자를 사용하면 타 프로그래밍 언어에서 Null로 인해 발생하는 버그를 방지할 수 있습니다.

Rust는 Null값의 개념이 존재하지 않습니다. 일반적으로 다른 프로그래밍 언어에서 Null 이란 존재하지 않는 값, 또는 아무런 값도 갖지 않는 경우를 의미합니다. 따라서 Null의 상태 또는 Non-Null 의 상태 두 가지만 존재할 수 있습니다. 여기서 흔히 발생하는 문제점은 Null 값이지만 Null 값이 아닌 것처럼 사용하려고 하는 경우가 대부분입니다. 그러나 이 개념은 현재에도 유용하기 때문에 Rust에서는 Null을 지원하지 않는 대신 Option 을 통하여 구현하고 있습니다.

Option<T>은 표준라이브러리에서 다음과 같이 구현되어 있습니다. 간단한 enum 타입입니다.

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

Option<T>prelude (모든 Rust 프로그램에서 자동적으로 import 시키는 것들, std::prelude)에 포함되어 있기 때문에 명시적으로 범위로 가져올 필요는 없습니다. 즉, Option:: 의 접두어 없이 직접 Some(T)이나 None 값을 사용할 수 있습니다.

Some과 None

Some(T)NoneOption의 열거값입니다. 여기서 T라 함은 제네릭(Generic) 타입의 변수를 의미하며, C++의 template를 떠올리시면 쉽게 이해가 가능합니다. 여기서 Some은 일단 어떠한 타입의 데이터도 저장할 수 있다는 점을 알고 넘어가겠습니다.

아래는 Some에 숫자와 문자열 값을 저장하는 Option 열거자의 예시입니다.

let some_num = Some(5);
let some_str = Some(String::from("Hello Rust!"));

let absent_num: Option<i32> = None;

Some과는 다르게 None 값을 이용하려면 Option<T>의 타입이 무엇인지 알려주어야 합니다. 당연하게도 T를 명시하지 않으면 OptionSome이 무엇을 저장해야 하는지 컴파일러는 알 수가 없기 때문입니다.

한편, 아래와 같은 코드는 작동하지 않습니다. 이는 어떻게 보면 당연하게도, 하나는 i8의 타입이고 하나는 Option<i8> 타입이기 때문입니다. 컴파일러는 i8 타입의 값을 가지고 있다는 것은 이 값이 항상 유효한 값이라고 가정합니다. 따라서 연산 전에 유효한 값인지 Null 검사를 할 필요가 없습니다. 그러나 Option 타입이라면 Null일 가능성이 존재하기 때문에 값이 없는 경우도 처리하려 합니다.

// 문제의 코드
let x: i8 = 5;
let y: Option<i8> = Some(5);

let z = x + y; // 여기서 에러가 발생!

정리하자면 Option<T> 타입이 아닌 경우에는 Null로 간주하지 않을 것이므로 더욱 자신있게 코드를 작성할 수 있게 됩니다. 반대로 Null을 가질 가능성이 있다면 Option 열거자를 사용하여야 합니다. 만일 이 값이 Non-Null의 경우라면 Some에 담겨있는 값을 추출한 후 사용해야 합니다. 이는 공식 문서에서 확인 가능합니다.

// Example
let x = Some("air");
assert_eq!(x.unwrap(), "air");

태그:

카테고리:

업데이트: