동작하는 코드
신호등을 생각해보세요. 빨강, 노랑, 초록 중 하나의 상태만 가능하죠? 이런 "여러 가능성 중 하나"를 표현하는 게 열거형(enum) 이에요.
#[derive(Debug)]
enum TrafficLight {
Red,
Yellow,
Green,
}
fn action(light: &TrafficLight) {
match light {
TrafficLight::Red => println!("정지!"),
TrafficLight::Yellow => println!("주의!"),
TrafficLight::Green => println!("출발!"),
}
}
fn main() {
let light = TrafficLight::Red;
action(&light);
println!("현재 신호: {:?}", light);
}
match는 값을 보고 어떤 variant인지에 따라 다른 동작을 실행해요. 분류 기계라고 생각하면 돼요 — 값을 넣으면 모양에 따라 다른 칸으로 보내는 기계예요.
직접 수정하기
light를TrafficLight::Green으로 바꿔보세요. 출력이 달라지나요?match에서TrafficLight::Yellow줄을 삭제해보세요. 어떤 에러가 나나요?
"왜?" — 데이터를 가진 열거형
열거형의 진짜 힘은 각 variant에 데이터를 포함할 수 있다는 거예요.
#[derive(Debug)]
enum Shape {
Circle(f64), // 반지름
Rectangle(f64, f64), // 가로, 세로
Triangle(f64, f64, f64), // 세 변의 길이
}
fn describe(shape: &Shape) {
match shape {
Shape::Circle(r) => {
println!("원: 반지름 {r}");
}
Shape::Rectangle(w, h) => {
println!("직사각형: {w} x {h}");
}
Shape::Triangle(a, b, c) => {
println!("삼각형: 변 {a}, {b}, {c}");
}
}
}
fn main() {
let shapes = vec![
Shape::Circle(5.0),
Shape::Rectangle(10.0, 3.0),
Shape::Triangle(3.0, 4.0, 5.0),
];
for s in &shapes {
describe(s);
}
}
Shape 하나의 타입으로 원, 직사각형, 삼각형을 모두 담을 수 있어요. 구조체만으로는 이런 표현이 어려워요.
match는 빠짐없이 처리해야 해요
match의 가장 중요한 특징은 모든 가능성을 빠짐없이(exhaustive) 처리해야 한다는 거예요.
error[E0004]: non-exhaustive patterns: `Shape::Triangle(_, _, _)`
not covered
해석: "삼각형 경우를 처리하지 않았어요. match는 모든 가능성을 다뤄야 해요."
해결: 빠진 variant를 추가하거나, _(와일드카드)로 나머지를 처리하세요.
모든 경우를 일일이 처리하기 번거로울 때는 _로 나머지를 묶을 수 있어요.
fn is_circle(shape: &Shape) -> bool {
match shape {
Shape::Circle(_) => true,
_ => false, // 나머지는 전부 false
}
}
if let — 하나만 확인할 때
하나의 variant만 확인하고 나머지는 무시하고 싶을 때는 if let이 깔끔해요.
fn main() {
let shape = Shape::Circle(5.0);
// match로 쓰면
match &shape {
Shape::Circle(r) => println!("반지름: {r}"),
_ => {}
}
// if let으로 쓰면 — 훨씬 깔끔!
if let Shape::Circle(r) = &shape {
println!("반지름: {r}");
}
}
열거형에도 메서드를 추가할 수 있어요
구조체처럼 impl 블록으로 메서드를 넣을 수 있어요.
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle(a, b, c) => {
// 헤론의 공식
let s = (a + b + c) / 2.0;
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
}
}
}
더 알아보기: Option — Rust에는 null이 없어요
다른 언어에는 null이나 None이 있어서 "값이 없음"을 표현해요. 하지만 null을 잘못 다루면 프로그램이 터지죠.
Rust에는 null이 없어요! 대신 Option 이라는 열거형을 써요.
enum Option<T> {
Some(T), // 값이 있음
None, // 값이 없음
}
모듈 06에서 pop()의 결과가 Some(88)이었던 거 기억나세요? 그게 바로 Option이었어요!
fn main() {
let numbers = vec![1, 2, 3];
let first = numbers.first(); // Option<&i32>
match first {
Some(n) => println!("첫 번째: {n}"),
None => println!("비어있어요"),
}
}
Option에 대해서는 나중에 더 자세히 배울 거예요. 지금은 "Rust는 값이 없을 수도 있는 상황을 열거형으로 안전하게 표현한다"고 알아두세요.
연습 1. Coin 열거형을 만들고, 각 동전의 금액을 반환하는 value 메서드를 작성해보세요.
#[derive(Debug)]
enum Coin {
Ten, // 10원
Fifty, // 50원
Hundred, // 100원
FiveHundred,// 500원
}
impl Coin {
fn value(&self) -> i32 {
// 여기를 완성하세요
// match를 사용해서 각 variant에 맞는 금액을 반환
}
}
fn main() {
let coins = vec![
Coin::Hundred,
Coin::FiveHundred,
Coin::Fifty,
];
let total: i32 = coins.iter().map(|c| c.value()).sum();
println!("총액: {total}원");
// 출력: 총액: 650원
}
연습 2. 아래 Message 열거형의 process 함수를 완성해보세요.
#[derive(Debug)]
enum Message {
Quit,
Echo(String),
Move { x: i32, y: i32 },
}
fn process(msg: &Message) {
// 여기를 완성하세요
// Quit -> "종료합니다" 출력
// Echo(text) -> text 내용을 출력
// Move { x, y } -> "({x}, {y})로 이동" 출력
}
fn main() {
let messages = vec![
Message::Echo(String::from("안녕!")),
Message::Move { x: 10, y: 20 },
Message::Quit,
];
for msg in &messages {
process(msg);
}
}
힌트: match에서 구조체 variant는 Message::Move { x, y }처럼 분해해요.
Q1. match에서 모든 variant를 처리하지 않으면?
- A) 런타임 에러가 발생한다
- B) 컴파일 에러가 발생한다
- C) 처리되지 않은 variant는 무시된다
- D) 프로그램이 무한 루프에 빠진다
Q2. if let은 언제 사용하면 좋을까요?
- A) 모든 variant를 처리해야 할 때
- B) 하나의 variant만 확인하고 나머지는 무시할 때
- C) 열거형을 생성할 때
- D) 열거형에 메서드를 추가할 때
Q3. 아래 코드의 출력은?
enum Direction {
Up, Down, Left, Right,
}
fn main() {
let dir = Direction::Up;
let msg = match dir {
Direction::Up => "위",
Direction::Down => "아래",
_ => "옆",
};
println!("{msg}");
}
- A) "위"
- B) "아래"
- C) "옆"
- D) 컴파일 에러