이 번 장에서는 열거(enumerations) 에 대해 살펴볼 것입니다. 열거형(enums) 이라고도 합니다.
열거형은 하나의 타입이 가질 수 있는 variant들을 열거함으로써 타입을 정의할 수 있도록 합니다.
- 우선, 하나의 열거형을 정의하고 사용해 봄으로써, 어떻게 열거형에 의미와 함께 데이터를 담을 수 있는지 보여줄 것입니다.
- 다음으로,
Option
이라고 하는 특히 유용한 열거형을 자세히 볼 텐데, 이것은 어떤 값을 가질 수도 있고, 갖지 않을 수도 있습니다. - 그 다음으로, 열거형의 값에 따라 쉽게 다른 코드를 실행하기 위해
match
표현식에서 패턴 매칭을 사용하는 방법을 볼 것입니다. - 마지막으로, 코드에서 열거형을 편하고 간결하게 다루기 위한 관용 표현인
if let
구문을 다룰 것입니다.
열거형은 다른 언어들에서도 볼 수 있는 특징이지만, 열거형으로 할 수 있는 것들은 언어마다 다릅니다. p 러스트의 열거형은 F#, OCaml, Haskell 과 같은 함수형 언어의 대수 데이터 타입과 가장 비슷합니다.
6.1 열거형 정의하기
IP 주소를 다루는 프로그램을 만들어 보면서, 어떤 상황에서 열거형이 구조체보다 유용하고 적절한지 알아보겠습니다.
- 현재 사용되는 IP 주소 표준은 IPv4, IPv6 두 종류입니다(앞으로 v4, v6 로 표기하겠습니다).
- 즉, 우리가 만들 프로그램에서 다룰 IP 종류 역시 v4, v6 가 전부입니다.
- 이번엔 단 두 가지뿐이긴 하지만, 이처럼 가능한 모든 variant들을 죽 늘어놓는 것을
열거
라고 표현합니다. - IP 주소는 반드시 v4나 v6 중 하나만 될 수 있는데, 이러한 특성은 열거형 자료 구조에 적합합니다.
- 왜냐하면, 열거형의 값은 여러 variant 중 하나만 될 수 있기 때문입니다.
- v4, v6 는 근본적으로 IP 주소이기 때문에, 이 둘은 코드에서 모든 종류의 IP 주소에 적용되는 상황을 다룰 때 동일한 타입으로 처리되는 것이 좋습니다.
enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
route(IpAddrKind::V4);
route(IpAddrKind::V6);
}
fn route(ip_kind: IpAddrKind) {}
이제 IpAddrKind
은 우리의 코드 어디에서나 쓸 수 있는 데이터 타입이 되었습니다.
열거형 값
아래처럼 IpAddrKind
의 두 개의 variant 에 대한 인스턴스를 만들 수 있습니다:
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
- 열거형을 정의할 때의 식별자로 네임스페이스가 만들어져서, 각 variant 앞에 콜론(
:
) 두 개를 붙여야 한다는 점을 알아두세요. - 이 방식은
IpAddrKind::V4
,IpAddrKind::V6
가 모두IpAddrKind
타입이라는 것을 표현할 수 있다는 장점이 있습니다.
이제 IpAddrKind
타입을 인자로 받는 함수를 정의해봅시다:
fn route(ip_kind: IpAddrKind) {}
그리고, variant 중 하나를 사용해서 함수를 호출할 수 있습니다
route(IpAddrKind::V4);
route(IpAddrKind::V6);
열거형을 사용하면 이점이 더 있습니다. IP 주소 타입에 대해 더 생각해 볼 때, 지금으로써는 실제 IP 주소 데이터 를 저장할 방법이 없습니다. 단지 어떤 종류 인지만 알 뿐입니다.
5장에서 구조체에 대해 방금 공부했다고 한다면, 이 문제를 Listing 6-1에서 보이는 것처럼 풀려고 할 것입니다:
fn main() {
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"),
};
}
Listing 6-1: struct
를 사용해서 IP 주소의 데이터와 IpAddrKind
variant 저장하기
IpAddrKind
(이전에 정의한 열거형) 타입kind
필드와String
타입address
필드를 갖는IpAddr
를 정의하고, 인스턴스를 두 개 생성했습니다.- 첫 번째
home
은kind
의 값으로IpAddrKind::V4
을 갖고 연관된 주소 데이터로127.0.0.1
를 갖습니다. - 두 번째
loopback
은IpAddrKind
의 다른 variant 인V6
을 값으로 갖고, 연관된 주소로::1
를 갖습니다. kind
와address
의 값을 함께 사용하기 위해 구조체를 사용했습니다.- 그렇게 함으로써 variant 가 연관된 값을 갖게 되었습니다.
각 열거형 variant 에 데이터를 직접 넣는 방식을 사용해서 열거형을 구조체의 일부로 사용하는 방식보다 더 간결하게 동일한 개념을 표현할 수 있습니다.
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 에 직접 데이터를 붙임으로써, 구조체를 사용할 필요가 없어졌습니다.
- 구조체 대신 열거형을 사용할 때의 또 다른 장점이 있습니다.
- 각 variant 는 다른 타입과 다른 양의 연관된 데이터를 가질 수 있습니다.
- v4 타입의 IP 주소는 항상 0 ~ 255 사이의 숫자 4개로 된 구성요소를 갖게 될 것입니다.
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 주소를 저장하기 위해 코드상에서 열거형을 정의하는 몇 가지 방법을 살펴봤습니다.
- 그러나, 누구나 알듯이 IP 주소와 그 종류를 저장하는 것은 흔하기 때문에, 표준 라이브러리에 사용할 수 있는 정의가 있습니다! 표준 라이브러리에서
IpAddr
를 어떻게 정의하고 있는지 살펴봅시다. - 위에서 정의하고 사용했던 것과 동일한 열거형과 variant 를 갖고 있지만, variant 에 포함된 주소 데이터는 두 가지 다른 구조체로 되어 있으며, 각 variant 마다 다르게 정의하고 있습니다:
#![allow(unused)]
fn main() {
struct Ipv4Addr {
// --생략--
}
struct Ipv6Addr {
// --생략--
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
}
이 코드로 알 수 있듯, 열거형 variant 에는 어떤 종류의 데이터건 넣을 수 있습니다. 문자열, 숫자 타입, 구조체 등은 물론, 다른 열거형마저도 포함할 수 있죠!
이건 여담이지만, 러스트의 표준 라이브러리 타입은 여러분 생각보다 단순한 경우가 꽤 있습니다.
현재 스코프에 표준 라이브러리를 가져오지 않았기 때문에, 표준 라이브러리에 IpAddr
정의가 있더라도, 동일한 이름의 타입을 만들고 사용할 수 있습니다.
열거형의 다른 예제를 살펴봅시다. 이 예제에서는 각 variant 에 다양한 유형의 타입들이 포함되어 있습니다:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
이 열거형에는 다른 데이터 타입을 갖는 네 개의 variant 가 있습니다:
Quit
은 연관된 데이터가 전혀 없습니다.Move
은 익명 구조체를 포함합니다.Write
은 하나의String
을 포함합니다.ChangeColor
는 세 개의i32
을 포함합니다.
variant 로 열거형을 정의하는 것은 다른 종류의 구조체들을 정의하는 것과 비슷합니다. 열거형과 다른 점은 struct
키워드를 사용하지 않는다는 것과 모든 variant 가 Message
타입으로 그룹화된다는 것입니다. 아래 구조체들은 이전 열거형의 variant 가 갖는 것과 동일한 데이터를 포함할 수 있습니다:
struct QuitMessage; // unit struct
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32); // tuple struct
각기 다른 타입을 갖는 여러 개의 구조체를 사용한다면, 이 메시지 중 어떤 한 가지를 인자로 받는 함수를 정의하기 힘들 것입니다. Listing 6-2 에 정의한 Message
열거형은 하나의 타입으로 이것이 가능합니다.
- 열거형과 구조체는 한 가지 더 유사한 점이 있습니다.
- 구조체에
impl
을 사용해서 메소드를 정의한 것처럼, 열거형에도 정의할 수 있습니다. - 여기
Message
열거형에 에 정의한call
이라는 메소드가 있습니다:
fn main() {
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
// method body would be defined here
}
}
let m = Message::Write(String::from("hello"));
m.call();
}
열거형의 값을 가져오기 위해 메소드 안에서 self
를 사용할 것입니다.
이 예제에서 생성한 변수 m
은 Message::Write(String::from("hello"))
값을 갖게 되고, 이 값은 m.call()
이 실행될 때, call
메소드 안에서 self
가 될 것입니다.
이제 표준 라이브러리에 포함된 열거형 중 유용하고 굉장히 자주 사용되는 Option
열거형을 살펴봅시다: