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
열거형의 새로운 정의는 두 개의 V4
와 V6
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();
러스트에는 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을 너무 많이 사용하는 문제를 제한하고 러스트 코드의 안정성을 높이기 위한 러스트의 의도된 디자인 결정사항입니다.