오늘은 Rust의 match, if let에 대해 알아보고자 합니다.
match 제어 흐름 구조
rust는 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,
}
}
rust의 match는 다른 언어들의 switch문과 비슷합니다.
match에는 갈래(arm)들이 있고, 하나의 갈래는 패턴과 코드 두 부분으로 이루어져 있습니다.
예시에서는 첫 번째 갈래에는 값 Coin::Penny로 되어있는 패턴이 있고, 그 뒤에 패턴과 실행되는 코드를 구분해주는 => 연산자가 있습니다.
match표현식이 실행될 때, 결과값을 각 갈래의 패턴에 대해서 순차적으로 비교하다가 만일 어떤 패턴이 그 값과 매칭되면, 그 패턴과 연관된 코드가 실행됩니다.
만일 그 패턴이 값과 매칭되지 않는다면, 동전 분류기와 비슷하게 다음 갈래로 실행을 계속합니다.
여러 줄의 코드를 실행시키고 싶다면 중괄호를 사용하고, 그렇게 되면 갈래 뒤에 붙이는 쉼표는 옵션이 됩니다.
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
값을 바인딩하는 패턴
매치 갈래의 또 다른 유용한 기능은 패턴과 매칭된 값들의 일부분을 바인딩할 수 있다는 것입니다.
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --생략--
}
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
}
}
}
만약게 value_in_cents(Coin::Quarter(UsState::Alaska))를 호출했다면, coin은 Coin::Quarter(UsState::Alaska)가 됩니다.
state에 대한 바인딩은 값 UsState::Alaska가 되고, 이 바인딩은 println! 표현식에서 사용할 수 있고, 따라서 Quarter에 대한 Coin열거형 배리언트로부터 주에 대한 내부 값을 얻습니다.
Option를 이용하는 매칭
이전에 Option 값을 사용하려면 Some일 떄 실행돼서, Some 내의 T 값을 얻을 수 있는 코드가 필요하다고 하였습니다.
Option의 배리언트를 match표현식으로 비교하면 다음과 같습니다.
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);
plus_one(five)가 호출될 때, plus_one 본문 안에 있는 변수 x는 값 Some(5)를 갖게 될 것이고, Some(i)에 매칭되어 최종적으로 6을 담은 새로운 Some값으 생성합니다.
plue_one(None)의 경우 match안에 들어오면 첫 번째 갈래와 비교하여 매칭되어 None 값을 반환합니다.
매치는 철저합니다.
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
다음과 같이 작성된 코드의 경우 에러를 출력하게 됩니다.
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:3:15
|
3 | match x {
| ^ pattern `None` not covered
|
note: `Option<i32>` defined here
--> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:518:1
|
= note:
/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:522:5: 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
|
4 ~ Some(i) => Some(i + 1),
5 ~ None => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` due to previous error
rust의 매치는 발생할 수 있는 경우 중 놓친 게 있음을 아는 것은 물론, 어떤 패턴을 놓쳤는가도 알고 있죠.
None 케이스를 다루는 것을 깜빡하더라도 rust가 알아채고 알려주기 때문에, 앞서 설명했던 null일지도 모를 값을 가지고 있어서 발생할 수 있는 실수를 불가능하게 만듭니다.
포괄 패턴과 _ 자리 표시자
열거형을 사용하면서 특정한 몇 개의 값들에 대해 특별한 동작을 하지만, 그 외의 값들에 대해서는 기본 동작을 취하게 할 수 있습니다.
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) {}
나머지 모든 가능한 값을 다루는 마지막 갈래에 대한 패턴은 other 라는 이름을 가진 변수입니다.
u8이 가질 수 있는 모든 값을 나열하지 않았음에도 이 코드는 컴파일 되는데, 그 이유는 특별하게 나열되지 않은 나머지 모든 값에 대해 마지막 other 패턴이 매칭되기 때문입니다.
이러한 포괄(catch-all) 패턴은 match의 철저함을 만족시킵니다.
포괄적인 갈래는 마지막에 위치시켜야 하며, 만약 포괄 패턴 뒤에 갈래를 추가하면 rust는 이에 대해 경고를 줍니다.
만약 match에서 3이나 7같은 값이 아닌 다른 값이 들어온 경우, 그 값을 활용하지 않는다면 other대신에 _를 사용할 수 있습니다.
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() {}
또한 3이나 7같은 값이 아닌 경우, 아무일도 일어나지 않도록 하기 위해서는 다음과 같이 가능합니다.
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
if let을 사용한 간결한 제어 흐름
if let 문법은 if와 let을 조합하여 하나의 패턴만 매칭시키고 나머지 경우는 무시하도록 값을 처리하는 간결한 방법을 제공합니다.
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {}", max),
_ => (),
}
match를 활용하여 다음과 같이 표현한 경우, if let을 이용하여 같은 동작을 하면서, 코드를 더 짧게 표현할 수 있습니다.
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {}", max);
}
if let은 =로 구분된 패턴과 표현식을 입력받아 match와 동일한 방식으로 작동되는데, 여기서 표현식은 match에 주어지는 것이고 패턴은 이 match의 첫 번째 갈래와 같습니다.
if let을 이용하면 덜 타이핑하고, 덜 들여쓰기 하고, 보일러 플레이트 코드를 덜 쓰게 됩니다.
if let은 한 패턴에 매칭될 때만 코드를 실행하고 다른 경우 무시하는 match문을 작성할 때 사용하는 문법 설탕(syntax sugar)이라고 생각하시면 됩니다.
if let과 함께 else를 match 표현식에서 사용한 _를 사용하여 표현 가능합니다.
let mut count = 0;
match coin {
Coin::Quarter(state) => println!("State quarter from {:?}!", state),
_ => count += 1,
}
물론 다음과 같이 작성도 가능합니다.
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}
프로그램이 match로 표현하기에 너무 장황한 로직을 하는 경우라면, if let를 활용하시면 됩니다.
정리
- 타 언어의 switch문이 rust에서는 match문이 된다.
- 특정 case가 아닌 경우에 해당 변수를 사용해야 한다면 other로, 변수를 사용하지 않아도 된다면 _으로 표현하여 사용이 가능합니다.
- 하나의 조건문만 필요로 하는데 match로 사용하기에 너무 코드가 길고 불편하다면 if let을 활용하여 가능합니다.
'Rust' 카테고리의 다른 글
Rust 설치부터 실행까지 (use, pub use, as, mod) - 16 (0) | 2025.01.15 |
---|---|
Rust 설치부터 실행까지 (crate, 절대 경로, 상대 경로, super, use, pub) - 15 (0) | 2025.01.13 |
Rust 설치부터 실행까지 (열거형, Option) - 13 (0) | 2025.01.12 |
Rust 설치부터 실행까지 (메서드) - 12 (0) | 2025.01.09 |
Rust 설치부터 실행까지 (구조체, 디버깅) - 11 (0) | 2025.01.07 |
댓글