Rust 공식 문서를 참고한 글입니다.
열거형 Enum
우리가 이전에 Rectangle이라는 구조체를 선언하고 width, height 필드를 가지도록 했다. 그런데 Rust에서는 구조체보다는 enum 을 사용하는 것이 더 적절하다. 왜 구조체보다 열거형이 적절한지 알아보자.
우리가 IP 주소를 v4 또는 v6을 사용한다고 할 때, 아래처럼 나타낼 수 있다.
enum IpAddrKind {
v4,
v6
}
이것을 사용하려면, IpAddrKind::v4로 접근할 수 있다. 이것을 사용해서 함수에 적용할 수도 있다.
// 함수 정의
fn route(ip_kind: IpAddrKind) {}
// 함수 호출
route(IpAddrKind::V4);
route(IpAddrKind::V6);
IP 주소 값은 열거형과 별개의 문자열로 저장해서 구조체로 묶어 사용할 수도 있지만, 열거형 안에 값을 넣을 수 있다. 열거형 안에 서로 다른 종류와 개수의 자료형도 넣을 수 있다.
enum IpAddr {
V4(String),
V6(String),
}
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
Option 열거형
option 열거형은 표준 라이브러리에서 제공하고 보편적으로 사용되는 열거형이다. 여기에서 알아야 하는 개념은 Rust는 다른 언어와 다르게 Null 기능이 없다는 사실이다. Null이 없다는 말이다. 다른 언어에서는 변수들이 null이거나 null 이 아닌 상태거나 둘 중 하나다. 이때 null로 인한 문제는 null 인 값을 null 이 아니라고 판단하고 사용할 때 발생한다.
null은 현재 무효하거나 존재하지 않은 상태로 그 개념 자체는 유용하다. 그러나 여기에서는 개념이 아니라 구현 방식 문제다. Rust는 null을 사용하지 않지만 값이 존재하거나 존재함을 나타내는 방법으로 enum을 사용한다. 표준 라이브러리에 정의된 Option<T>이다.
enum Option<T> {
None,
Some(T),
}
<T>로 작성하여 자료형을 각각 명시하지 않아도 된다. prelude에도 포함되어 있어서 Option:: 없이도 Some, none을 직접 사용할 수 있다. <T>는 아직 다루지 않았기 때문에 자료형을 구체적으로 명시하지 않아도 사용할 수 있다는 것만 알고 넘어가면 된다.
예제를 통해 이해해보자.
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
some_number와 some_char은 각각 Option<i32>, Option<char> 타입이다. Some variant를 사용했기 때문에 Rust는 타입을 추론할 수 있다. absent_number의 경우, None만 보고 타입을 추론할 수 없기 때문에 Option을 명시해주어야 한다.
그렇다면, Option<T>가 null 로 표현하는 것보다 더 나은 점이 무엇일까?
Option은 값이 Some에 해당하는 순간에도 일반 변수와 연산할 수 없다. u32 타입 변수와 Some(u32) 타입 변수와 연산을 시도하면 오류가 발생한다. Rust는 u32와 같은 타입의 값을 가지면 컴파일러는 항상 유효한 값을 가지고 있음을 보장한다. 그래서 null인지 확인할 필요없이 사용할 수 있다. 그러나 Option<u32>는 값이 없을 수도 있기 때문에 컴파일러는 값을 사용하기 전에 null인지 여부를 확인하도록 한다. 그래서 Option<T>를 꺼내서 T 타입의 연산을 수행하려면 Option<T>를 T로 변환한다. 정리하면 만약 null 인 값을 가지게 하려면 타입을 Option<T>로 명시해야 하고, 사용하기 전에는 null인 경우를 처리해야 한다.
패턴 매칭 The Match Control Flow Construct
Rust는 match를 사용해서 여러 개의 블록을 패턴을 비교해서 코드를 실행시킨다. match는 동전 분류기처럼 동작한다. 값은 match문에서 각각의 패턴을 통해 검사되고 패턴과 일치하면 일치한 경우의 코드를 실행해 match문을 빠져나간다.
match문을 사용해서 동전 분류 프로그램을 작성해보자.
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,
}
}
coin이 enum형 Coin 타입에 따라서 패턴과 일치하는 경우 함수가 동작한다.
값을 bind하는 패턴
match 문의 패턴은 열거값에서 어떤 값을 받아올 수 있다.
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
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라는 변수를 추가해서 바인딩한다.
Option<T>와 매칭
Option 열거형을 패턴 매칭으로 처리하는 방법을 알아보자. 값이 존재하면 1을 더하는 프로그램이다.
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);
five의 경우, plus_one 함수에서 Some(i)과 일치하여 Some(i+1) 이 리턴된다. 리턴된 값을 변수 six에 바인딩된다. 변수 none의 경우, None 과 일치하기 때문에 None이라는 것이 리턴된다. 그래서 Some인 경우에만 연산을 수행하도록 설정할 수 있다.
Placeholder
enum 타입을 사용할 때 특정한 값을 받아오고 나머지는 기본 옵션으로 지정해두는 방식을 택할 수 있다. 예를 들어서 우리가 주사위를 던지고 나온 수에 따라 게임 규칙을 설정한다. 3이 나오면 움직이지 않는 대신에 새로운 모자를 얻고, 7이 나오면 모자를 빼앗는다. 그외 나머지 수들은 나온 수만큼 이동한다. 이때 게임 로직을 match문을 사용해 구현해보자.
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인 경우 모자를 뺏고 나머지 수는 이동하는 경우를 구현했다. match 문을 통해서 모든 경우의 수가 처리되는 걸 나열할 수 없기 때문에 other을 사용해 나머지를 처리했다. _를 사용해서도 처리할 수 있다.
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
lf let
하나의 패턴만 처리하고 나머지는 무시하고 싶을 때가 있다. 이런 경우 match를 사용하면 약간 복잡해질 수 있다. 모든 경우의 수를 고려해야 하기 때문에 _ => ()를 처리해줘야 하기 때문이다. 이런 경우에는 match 대신에 if let을 작성하는 것이 유리하다. if 패턴 = 비교대상 {}으로 작성할 수 있다.
그래서 match로 구현된 것을 if let으로 변경할 수 있다.
// match 표현식
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {max}"),
_ => (),
}
// if let
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
}
'언어 > Rust' 카테고리의 다른 글
[Rust] 8. 컬렉션 (0) | 2024.10.09 |
---|---|
[Rust] 7. 프로젝트 관리 (1) | 2024.10.09 |
[Rust] 5-2. 메서드 (0) | 2024.10.09 |
[Rust] 5-1. 구조체 사용 (0) | 2024.10.09 |
[Rust] 4-3. Slice Type (0) | 2024.09.28 |