열거형(enumerateion, 줄여서 enum) 은 하나의 타입이 가질 수 있는 배리언트(variant)들을 열거함으로써 타입을 정의할 수 있도록 합니다.
열거형 정의하기
열거형은 어떤 값이 여러 개의 가능한 값의 집합 중 하나라는 것을 나타내는 방법을 제공
열거형 값
1
2
3
4
| enum IpAddrKind {
V4,
V6
}
|
- enum (열거형)
- 하나의 타입을 정의하는 틀 (
IpAddrKind)
- variant (배리언트)
- enum 이 가질 수 있는 구체적인 값 (
V4, V6)
1
2
| let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
|
- 열거형을 정의할 때의 식별자로 네임스페이스가 만들어져서 각 배리언트 앞에 이중콜론(::) 을 붙여야 함
1
| fn route(ip_kind: IpAddrKind) {}
|
IpAddrKind 타입을 인수로 받는 함수를 정의
1
2
| route(IpAddrKind::V4);
route(IpAddrKind::V6);
|
1
2
3
4
| struct IpAddr {
kind: IpAddrKind,
address: String,
}
|
kind와 address의 값을 함께 사용하기 위해 구조체를 사용
1
2
3
4
5
6
7
| enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
|
- 구조체 대신 열거형 배리언트에 데이터를 직접 넣는 방식을 사용
- 열거형의 각 배리언트에 직접 데이터를 붙임으로 써 구조체 사용을 줄일 수 있음
- 열거형의 동작에 대한 다른 세부사항을 살펴보기가 좀 더 쉬워짐
- 열거형을 정의하게 되면 생성자 함수가 자동적으로 정의됨
IpAddr::V4(String::from("127.0.0.1")) 와 같이 String 인수를 입력받아서 IpAddr 타입의 인스턴스를 만들 수 있음
1
2
3
4
5
6
7
| enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
|
- 각 배리언트는 다른 타입과 다른 양(amount) 의 연관된 데이터를 가질 수 있음
- 열거형 배리언트에는 어떤 종류의 데이터라도 넣을 수 있음
1
2
3
4
5
6
| enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
|
Message 열거형에는 다른 데이터 타입을 갖는 네 개의 배리언트가 있음Quit 은 연관된 데이터가 전혀 없음Move 는 구조체처럼 이름이 있는 필드를 갖음Write 는 하나의 String을 가짐ChangeColor 는 세 개의 i32 를 가짐
1
2
3
4
5
6
7
| struct QuitMessage;
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String);
struct ChangeColorMessage(i32, i32, i32);
|
- 각기 다른 타입을 갖는 여러 개의 구조체를 사용한다면, 이 메시지 중 어떤 한가지를 인수로 받는 함수를 정의하기가 힘듦
1
2
3
4
5
6
7
8
| impl Message {
fn call(&self) {
// 메서드 본문이 여기 정의될 것입니다.
}
}
let m = Message:Write(String::from("hello"));
m.call();
|
- 열거형에도
impl 을 사용해서 메서드를 정의할 수 있음 - 메서드 본문에서는
self를 사용하여 호출한 열거형 값을 가져올 수 있음 - 변수
m은 Message:Write(String::from("hello")) 값을 갖게 됨- 이 값은
m.call() 이 실행될 때 call 메서드 안에서 self 가 됨
Option 열거형이 널값보다 좋은 점들
Option은 표준 라이브러리에 정의되어 있으며 값이 있거나 없을 수 있는 상황을 표현하는 열거형(enum)
- 러스트는 Null 개념이 존재하지 않음
- 따라서 값의 존재 혹은 부재의 개념을 표현할 수 있어야 하기 때문에
Option 열거형이 사용됨- 널을 너무 많이 사용하는 문제를 제한하고, 러스트 코드의 안정성을 높이기 위해 의도된 러스트의 디자인 결정 사항
- 대부분의 언어에서는 존재와 부재의 개념을
not null 또는 null 로 표현함
1
2
3
4
| enum Option<T> { // T는 제네릭으로 어떤 타입도 담을 수 있음
Some(T), // 값이 있을 때
None, // 값이 없을 때
}
|
- 러스트에서 기본으로 임포트하는 목록인 프렐루드에 포함되어 있음
- 배리언트들 또한 프렐루드에 포함 (따라서 배리언트 앞에
Option:: 을 사용하지 않아도 됨)
<T>는 제네릭을 의미하며 현재는 어떤 타입의 데이터도 담을 수 있음을 의미하는 것으로 이해하고 추후 설명T의 자리에 구체적인 타입을 집어넣으면 Option<T> 타입을 모두 다른 타입으로 만듦1
2
3
| let some_number = Some(5); // Option<i32> 로 추론
let some_char = Some('e'); // Option<char> 로 추론
let absent_number: Option<i32> = None;
|
Some배리언트 내에 어떤 값을 명시했기 때문에 러스트는 이 타입들을 추론할 수 있음absent_number는 Option타입을 명시하도록 해야함None 값만 봐서는 동반되는 Some 배리언트가 어떤 타입의 값을 가질 지 컴파일러가 추론할 수 없기 때문- 타입 명시로 인해
Some 값을 얻게 되면 값이 존재한다는 것과 해당 값이 Some 내에 있다는 것을 알 수 있음
1
2
3
4
| let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
|
Option<T> 와 T가 다른 타입이기 때문에, 컴파일러는 Option<T>값을 명백하게 유효한 값처럼 사용하지 못하도록 함Option<i8> 에 i8 을 더하려 하므로 컴파일되지 않음
1
2
3
4
5
6
7
| 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`
|
- 위 에러 메시지는 러스트가
Option<i8>과 i8 을 어떻게 더해야 할지 모름을 의미 Option<i8> 타입은 값이 있을지 없을지 확인이 필요하며, 컴파일러는 값을 사용하기 전에 이런 경우가 처리되었는지 확인함- 즉
Option<i8> 을 i8 로 변환해야 함
- 이런 방식은 널로 인해 발생하는 가장 흔한 문제인, 실제로는 널인데 널이 아니라고 가정하는 상황을 발견한데 도움이 됨
- 널이 아닌 값을 갖는다는 가정을 놓치는 경우에 대한 위험 요소가 제거되면, 코드에 더 확신을 가질 수 있음
- 널일 수 있는 값을 사용하기 위해서는 명시적으로 값의 타입을
Option<T>로 만들고, 값을 사용할 때 명시적으로 널인 경우에 대해 처리
match 제어 흐름 구조
러스트는 match 라고 불리는 매우 강력한 제어 흐름 연산자를 가지고 있으며 이는 일련의 패턴에 대해 어떤 값을 비교한 뒤 어떤 패턴에 매칭되었는지를 바탕으로 코드를 수행하도록 해줌
- 패턴
- 패턴의 표현성
- 컴파일러는 모든 가능한 경우가 처리되는지 검사
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
|
match 뒤에는 표현식이 오며 위 경우에는 coin값이 사용됨match 갈래- 하나의 갈래는 패턴과 코드 두 부분으로 이루어짐
=> 전에 사용된 부분이 패턴=> 는 패턴과 실행되는 코드를 구분해주는 연산자=> 후에 사용된 부분이 실행되는 코드 또는 그냥 값
- 각 갈래는
, 로 구분됨
match 표현식이 실행될 때, 결괏값을 각 갈래의 패턴에 대해서 순차적으로 비교- 패턴이 그 값과 매칭되면 그 패턴과 연관된 코드가 실행
- 패턴이 그 값과 매칭되지 않으면, 다음 갈래로 실행을 계속함
- 각 갈래와 연관된 코드는 표현식이고, 이 매칭 갈래에서의 표현식의 결과로서 생기는 값은 전체
match 표현식에 대해 반환되는 값
1
2
3
4
5
6
7
8
9
10
11
| fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
|
- 각 갈래가 그냥 값을 반환하는 경우 코드 블록을 사용할 필요가 없음
- 갈래 내에서 여러 줄의 코드를 실행시키고 싶다면 중괄호를 사용하고, 쉼표는 옵션이 됨
값을 바인딩하는 패턴
매치 갈래는 패턴과 매칭된 값들의 일부분을 바인딩할 수 있으며 열거형의 배리언트로 부터 어떤 값들을 추출할 수 있는 방법을 제공
1
2
3
4
5
6
7
8
9
10
11
12
13
| #[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --생략--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
|
- 열거형의 배리언트로 부터 어떤 값들을 추출할 수 있는 방법
- Quarter 배리언트는 내부에
UsState 값을 들고 있도록 구성
1
2
3
4
5
6
7
8
9
10
11
| fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
}
}
}
|
- 매치 표현식 내에서 배리언트
Coin::Quarter 의 값과 매칭되는 패턴에 state 라는 이름의 변수를 추가Coin::Quarter 이 매치될 때, state 변수는 그 쿼터 동전의 주(UsState)에 대한 값에 바인딩
value_in_cents(Coin::Quarter(UsState::Alaska)) 와 같이 호출하면, coin은 Coin::Quarter(UsState::Alaska) 값을 갖게 됨Quarter 갈래에서 state 에 바인딩 된 값은 UsState::Alaska 가 됨
Option를 이용하는 매칭
1
2
3
4
5
6
7
8
9
10
| fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
|
Option<i32> 타입을 인수로 받는 plus_one 함수를 정의Option<i32> 타입은 값이 있을지 없을지 확인이 필요하며, 컴파일러는 값을 사용하기 전에 이런 경우가 처리되었는지 확인함- 즉
Option<i32> 을 i32 로 변환해야 함
plus_one 함수는 값이 있으면 1을 더한 Some 배리언트를 반환(Some(6))하고, 값이 없으면 None 을 반환
match는 철저합니다
match에서 갈래의 패턴들은 모든 가능한 경우를 다루어야 함
- 러스트의 매치는 철저합니다(exhaustive)
1
2
3
4
5
| fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
|
None 케이스를 다루지 않았기 때문에 위 코드는 버그를 일으킴
1
2
3
4
5
6
7
8
9
10
11
| |
7 | match x {
| ^ pattern `None` not covered
|
...
= note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
8 ~ Some(i) => Some(i + 1),
9 ~ None => todo!(),
|
|
- 발생할 수 있는 경우 중 놓친게 있음을 아는 것은 물론, 어떤 패턴을 놓쳤는가도 알고 있음
- 따라서, 유효한 코드를 만들려면 모든 가능성을 샅샅이 다루어야 함
- 위 예시와 같이
None 케이스를 다루는 것을 깜빡하더라도 러스트가 알아채고 알려줌- 널일지도 모르는 값을 가지고 있어서 발생할 수 있는 실수를 불가능하게 만듦
포괄 패턴과 _ 자리표시자
포괄 패턴(Catch-all Pattern)
match 표현식에서 아직 처리되지 않은 모든 나머지 경우를 포괄적으로 처리하는 패턴
- 특정 값이 아닌, 나머지 전부를 매칭하는 역할을 함
- 포괄 패턴은
_ 를 사용할 수도 있고, 변수명을 지정해서 사용할 수도 있음
1
2
3
4
5
6
7
8
9
10
| let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
|
- 처음 두 갈래에서의 패턴은 3과 7 리터럴 값을 가지며, 나머지 모든 가능한 값을 다루는 마지막 갈래에 대한 패턴은
other 라는 이름을 가진 변수- 위 코드는 컴파일이 가능
- 나열되지 않은 나머지 모든 값에 대해 마지막 패턴이 매칭되기 때문
- 패턴은 순차적으로 평가되므로 마지막에 포괄적인 갈래를 위치 시켜야 함
- 포괄 패턴 뒤에 있는 갈래는 결코 실행될 수 없음
_ 자리표시자
포괄 패턴이 필요한데 그 포괄 패턴의 값을 사용할 필요가 없는 경우에 사용하는 패턴
- 러스트에 해당 값을 사용하지 않겠다는 것을 알려주므로, 사용되지 않는 변수에 대한 경고를 띄우지 않음
1
2
3
4
5
6
7
8
9
10
| let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
|
- 철저함에 대한 요구사항을 충족
- 마지막 갈래에서 나머지 모든 값에 대해 명시적으로 무시
1
2
3
4
5
| match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
|
- 러스트에게 명시적으로 앞의 갈래에 매칭되지 않은 어떠한 값도 사용하지 않을 것임 알려줌
() 를 통해 어떠한 코드도 실행하지 않기를 원한다고 명시적으로 알려줌
if let을 사용한 간결한 제어 흐름
if let 문법은 if와 let을 조합하여 하나의 패턴만 매칭시키고 나머지 경우는 무시하도록 값을 처리하는 간결한 방법을 제공
if let 장점- 타이핑을 줄일 수 있음
- 보일러플레이트 코드를 덜 쓰게 됨
if let 단점match가 강제했던 철저한 검사를 안하게 됨
- 즉,
if let은 한 패턴에 매칭될 때만 코드를 실행하고 다른 경우는 무시하는 match 문을 작성할 때 사용하는 문법 설탕(syntax sugar)- 간결함을 얻는 것이 철저한 검사를 안하게 되는 것에 대한 적절한 거래인지 잘 판단해서 사용할 것
1
2
3
4
5
| let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {}", max),
_ => (),
}
|
- 값이
Some 이면 패턴 내에 있는 max에 Some 배리언트의 값을 바인딩하고 출력 None 값에 대해서는 아무것도 처리하지 않음match 표현식을 만족시키려면 딱 하나의 배리언트 처리 후 _ => () 를 붙여야 하는데, 이는 다소 성가신 보일러 플레이트 코드
1
2
3
4
| let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {}", max);
}
|
- 위 예시의 코드를 제거하기 위해
if let 사용 if let 은 =로 구분된 패턴과 표현식을 입력받음match 와 동일한 방식으로 작동- 표현식은
match에 주어지는 것 - 패턴은 이
match의 첫 번째 갈래- 위 예시에서 패턴은
Some(max)이고 max는 Some내에 있는 값에 바인딩 됨match 갈래 안에서 max를 사용했던 것과 같은 방식으로 if let 본문 블록 내에서 max 를 사용할 수 있음
1
2
3
4
5
6
| let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}
|
else뒤에 나오는 코드 블록은 match 표현식에서 _ 케이스 뒤에 나오는 코드 블록과 동일