오늘은 Rust의 열거형(enumerations)에 대해 알아보고자 합니다.
열거형
열거형은 하나의 타입이 가질 수 있는 베리언트(variant)들을 열거함으로써 타입을 정의할 수 있도록 합니다.
IP 주소의 경우 v4나 v6 중 하나면 될 수 있는데, 이러한 특성은 열거형 자료 구조에 적합합니다.
v4,v6는 근본적으로 IP 주소이기 때문에, 이 둘은 코드에서 모든 종류의 IP 주소에 적용되는 상황을 다룰 때 동일한 타입으로 처리되는 것이 좋습니다.
enum IpAddrKind {
V4,
V6,
}
열거형 값
위에 선언한 IpAddrKind의 두 개의 배리언트에 대한 인스턴스를 다음과 같이 만들 수 있습니다.
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
이 방식은 IpAddrKind::V4, IpAddrKind::V6가 모두 IpAddrKind 타입이라는 것을 표현할 수 있기 때문에 유용합니다.
IpAddrKind 타입을 인수로 받는 함수의 경우 다음과 같이 작성할 수 있습니다.
fn route(ip_kind: IpAddrKind) {}
route(IpAddrKind::V4);
route(IpAddrKind::V6);
이전에 배운 구조체와 열거형을 사용하면 다음과 같이 활용할 수 있습니다.
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind,
address: String,
}
let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
각 열거형 배리언트에 데이터를 직접 넣는 방식을 사용해서 열거형을 구조체의 일부로 사용하는 방식보다 더 간결하게 동일한 개념을 표현할 수 있습니다.
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
만약 v4는 0 ~ 255 사이의 숫자 4개로 된 구성요소를 원하고, 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"));
열거형 배리언트에는 문자열, 숫자 타입, 구조체 등 어떤 종류의 데이터라도 넣을 수 있습니다.
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
구조체에서 impl을 사용해서 메서드를 정의한 것처럼, 열거형에서도 정의할 수 있습니다.
impl Message {
fn call(&self) {
// 메서드 본문이 여기 정의될 것입니다
}
}
let m = Message::Write(String::from("hello"));
m.call();
Option 열거형이 null 값보다 좋은 점들
Option은 값이 어떤 것일 수도 있고, 아무것도 아닐 수도 있음을 표현하는 열거형 입니다.
러스트는 다른 언어들에서 흔하게 볼 수 있는 null 개념이 없습니다.
null 값으로 발생하는 문제는, null 값을 null이 아닌 값처럼 사용하려고 할 때 여러 종류의 에러가 발생할 수 있다는 것입니다.
하지만, '현재 어떠한 이유로 인해 유효하지 않거나, 존재하지 않는 하나의 값'이라는 null이 표현하려고 하는 개념은 여전이 유용합니다.
rust에는 null이 없지만, 값의 존재 혹은 부재의 개념을 표현할 수 있는 Option라는 열거형이 있습니다.
enum Option<T> {
None,
Some(T),
}
Option 열거형에는 기본적으로 임포트하는 목록인 프렐루드가 포함되어 있어 Some,None 배리언트 앞에 Option::도 붙이지 않아도 됩니다.
하지만 Option는 여전히 그냥 일반적인 열거형이며, Some(T)와 None도 여전히 Option의 배리언트 입니다.
문법은 제네릭 타입 매개변수(generic type parameter)이며, Option 열거형의 Some 배리언트가 어떤 타입의 데이터라도 담을 수 있게 한다는 것입니다.
또한 T 자리에 구체적인 타입을 집어넣는 것이 전체 Option 타입을 모두 다른 타입으로 만듭니다.
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
None 값을 얻게 되면, 얻은 값이 유효하지 않다는, 어떤 면에서는 null과 같은 의미를 갖습니다.
Option와 T는 다른 타입이기 때문에, 컴파일러는 Option 값을 명백하게 유효한 값처럼 사용하지 못하도록 합니다.
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
다음과 같은 경우, x는 i8의 값을 y는 Option의 값을 가져, 서로 다른 타입이므로 + 연산을 수행할 수 없습니다.
error[E0277]: cannot add `Option<i8>` to `i8`
--> src/main.rs:5:17
|
5 | let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
= help: the following other types implement trait `Add<Rhs>`:
<&'a i8 as Add<i8>>
<&i8 as Add<&i8>>
<i8 as Add<&i8>>
<i8 as Add>
For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` due to previous error
rust에서 i8과 같은 타입을 가질 때, 컴파일러는 항상 유효한 값을 갖고 있다고 보장할 것입니다.
그러나, Option의 경우 값이 있을지 없을지에 대해 걱정할 필요가 있으며, 컴파일러는 값을 사용하기 전에 이런 경우가 처리되었는지 확인해 줄 것입니다.
다음 연산을 수행하기 위해서는 Option의 타입을 T 타입으로 변환해야 합니다.
이런 방식은 null로 인해 발생하는 가장 흔한 문제인, null인데 null이 아니라고 가정하는 상황을 발견하는 데 도움이 됩니다.
null일 수 있는 값을 사용하기 위해서는 명시적으로 값의 타입을 Option로 만들어 줘야 합니다.
Option가 아닌 모든 곳은 값이 null이 아니라고 안전하게 가정할 수 있습니다.
이것은 null을 너무 많이 사용하는 문제를 제한하고 rust 코드의 안전성을 높이기 위해 의도된 rust의 디자인 결정 사항입니다.
정리
- 각 타입들을 모아서 사용할 수 있는 열거형이 존재하며, 구조체처럼 메서드를 구현해서 사용할 수 있음
- rust에서는 Option 타입을 활용해 해당 값이 있는지 없는지에 대해 판단이 필요함
- Option와 T는 서로 다른 타입으로, 두 타입에 대한 연산이 불가능함
'Rust' 카테고리의 다른 글
Rust 설치부터 실행까지 (crate, 절대 경로, 상대 경로, super, use, pub) - 15 (0) | 2025.01.13 |
---|---|
Rust 설치부터 실행까지 (match, if let) - 14 (0) | 2025.01.12 |
Rust 설치부터 실행까지 (메서드) - 12 (0) | 2025.01.09 |
Rust 설치부터 실행까지 (구조체, 디버깅) - 11 (0) | 2025.01.07 |
Rust 설치부터 실행까지 (슬라이스) - 10 (0) | 2025.01.06 |
댓글