if let과 let...else로 표현하는 간결한 제어 흐름
if let과 let...else로 표현하는 간결한 제어 흐름
match는 강력하지만, 처리할 경우가 하나뿐일 때는 오히려 장황해지기 쉬워요. if let은 이런 상황에서 if와 let을 합쳐 한 패턴만 골라내는 코드를 짧게 만들어 주고, 값이 있으면 계산하고 없으면 기본값을 돌려주는 자주 쓰는 패턴은 let...else가 훨씬 명확하게 다뤄 줘요.
하나의 패턴만 매치할 때: if let
if let 구문은 하나의 패턴과 매치되는 값을 다루고 나머지는 무시하고 싶을 때 쓰는 간결한 표현이에요. Listing 6-6의 프로그램을 볼게요. config_max 변수에 담긴 Option<u8> 값을 match로 처리하지만, 실제로 코드를 실행하고 싶은 건 값이 Some 변형일 때뿐이에요.
fn main() {
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {max}"),
_ => (),
}
}
값이 Some이면 패턴의 max 변수에 값을 바인딩해 그 값을 출력해요. None 값은 아무것도 하고 싶지 않죠. 그런데 match 표현을 만족시키려면 단 하나의 변형을 처리하고 나서도 _ => ()를 붙여야 하니, 그 보일러플레이트 코드가 은근 짜증나요.
대신 if let을 쓰면 훨씬 짧게 표현할 수 있어요. 아래 코드는 Listing 6-6의 match와 똑같이 동작해요.
fn main() {
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
}
}
if let 구문은 패턴과 표현식을 등호(=)로 나눠 받아요. 동작 방식은 match와 같아요. 표현식이 match에 주어지고 패턴이 그 첫 번째 arm이 되는 거예요. 여기서 패턴은 Some(max)이고, max는 Some 안의 값에 바인딩돼요. 그러면 if let 블록 안에서 match arm에서 max를 쓴 것과 똑같이 max를 쓸 수 있어요. if let 블록의 코드는 값이 패턴과 매치될 때만 실행돼요.
if let을 쓰면 타이핑도 줄고 들여쓰기도 줄고 보일러플레이트도 줄어요. 하지만 그 대가로 match가 강제하던 exhaustive checking(모든 경우를 처리했는지 컴파일러가 확인하는 것)을 잃어요. 더는 어떤 경우를 빠뜨리지 않았는지 보장받을 수 없으니, match와 if let 중 무엇을 쓸지는 지금 상황과, 간결함을 얻는 대신 exhaustive checking을 잃는 게 적절한 교환인지 판단해서 정하면 돼요. 다시 말해 if let은 "값이 하나의 패턴과 매치될 때만 코드를 실행하고 나머지는 모두 무시하는 match"의 문법 설탕(syntax sugar)이라고 생각하면 돼요.
if let에는 else도 붙일 수 있어요. else에 딸린 코드 블록은 그 if let ... else와 동등한 match 표현에서 _ 케이스에 딸린 블록과 같아요. Listing 6-4의 Coin enum 정의에서 Quarter 변형이 UsState 값도 들고 있었던 걸 떠올려 볼게요. 동전들을 세면서 quarter인 동전의 주(state)도 알리고 싶다면, match 표현으로는 이렇게 짤 수 있어요.
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn main() {
let coin = Coin::Penny;
let mut count = 0;
match coin {
Coin::Quarter(state) => println!("State quarter from {state:?}!"),
_ => count += 1,
}
}
아니면 if let ... else 표현으로 이렇게 쓸 수도 있어요.
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn main() {
let coin = Coin::Penny;
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {state:?}!");
} else {
count += 1;
}
}
let...else로 "해피 패스" 유지하기
값이 있을 때는 어떤 계산을 하고, 없으면 기본값을 돌려주는 패턴은 아주 흔해요. UsState 값을 지닌 동전 예시로 계속해 볼게요. quarter의 주가 얼마나 오래됐는지에 따라 재밌는 말을 하고 싶다면, UsState에 주의 나이를 확인하는 메서드를 도입하면 돼요.
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
impl UsState {
fn existed_in(&self, year: u16) -> bool {
match self {
UsState::Alabama => year >= 1819,
UsState::Alaska => year >= 1959,
// -- snip --
}
}
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn describe_state_quarter(coin: Coin) -> Option<String> {
if let Coin::Quarter(state) = coin {
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
} else {
None
}
}
fn main() {
if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
println!("{desc}");
}
}
이대로라면 동전의 종류를 if let으로 매치하면서 조건 본문 안에 state 변수를 도입할 수 있는데, 그게 Listing 6-7입니다.
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
impl UsState {
fn existed_in(&self, year: u16) -> bool {
match self {
UsState::Alabama => year >= 1819,
UsState::Alaska => year >= 1959,
// -- snip --
}
}
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn describe_state_quarter(coin: Coin) -> Option<String> {
if let Coin::Quarter(state) = coin {
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
} else {
None
}
}
fn main() {
if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
println!("{desc}");
}
}
동작은 하죠. 하지만 일이 if let 문의 본문 안으로 밀려 들어가서, 최상위 분기들이 어떻게 서로 관련되는지 따라가기가 어려워질 수 있어요. 그리고 표현식이 값을 만들어낸다는 점을 활용해 if let에서 state를 만들어내거나 아예 조기 반환(early return)하는 방식도 있어요. Listing 6-8이 그런 접근이고, match로도 비슷하게 할 수 있어요.
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
impl UsState {
fn existed_in(&self, year: u16) -> bool {
match self {
UsState::Alabama => year >= 1819,
UsState::Alaska => year >= 1959,
// -- snip --
}
}
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn describe_state_quarter(coin: Coin) -> Option<String> {
let state = if let Coin::Quarter(state) = coin {
state
} else {
return None;
};
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}
fn main() {
if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
println!("{desc}");
}
}
이것도 나름대로 따라가기 조금 번거로워요. if let의 한쪽 분기는 값을 만들어내고, 다른 쪽 분기는 함수에서 아예 반환해 버리니까요. 이렇게 흔한 패턴을 더 멋지게 표현하기 위해 Rust에는 let...else가 있어요. let...else 구문은 왼쪽에 패턴, 오른쪽에 표현식을 받아요. 생긴 건 if let과 비슷하지만 if 분기가 없고 else 분기만 있어요. 패턴이 매치되면 패턴에서 나온 값을 바깥 스코프에 바인딩하고, 매치되지 않으면 프로그램은 else arm으로 흘러가는데 그 arm은 함수에서 반드시 반환해야 해요.
Listing 6-9를 보면 Listing 6-8을 if let 대신 let...else로 바꾸면 어떻게 되는지 알 수 있어요.
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
impl UsState {
fn existed_in(&self, year: u16) -> bool {
match self {
UsState::Alabama => year >= 1819,
UsState::Alaska => year >= 1959,
// -- snip --
}
}
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn describe_state_quarter(coin: Coin) -> Option<String> {
let Coin::Quarter(state) = coin else {
return None;
};
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}
fn main() {
if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
println!("{desc}");
}
}
이렇게 하면 if let에서처럼 두 분기가 확연히 다른 제어 흐름을 가질 필요 없이, 함수 본문의 메인 흐름("해피 패스")을 그대로 유지할 수 있어요.
프로그램 로직을 match로 표현하기엔 너무 장황하다고 느껴지는 상황이 있다면, if let과 let...else도 여러분의 러스트 도구 상자에 있다는 걸 기억해 주세요.
요약 (Summary)
지금까지 enum으로 열거된 값 중 하나가 될 수 있는 사용자 정의 타입을 만드는 방법을 다뤘어요. 표준 라이브러리의 Option<T> 타입이 타입 시스템을 활용해 오류를 예방하도록 도와준다는 것도 확인했죠. enum 값이 내부에 데이터를 품고 있을 때는 처리해야 할 경우의 수에 따라 match 또는 if let으로 그 값을 추출해 쓸 수 있어요.
이제 러스트 프로그램은 struct와 enum으로 여러분의 도메인에 있는 개념을 표현할 수 있게 됐어요. API에 사용할 사용자 정의 타입을 만드는 것은 타입 안전성을 보장해 줘요. 컴파일러가 각 함수가 기대하는 타입의 값만 함수에 전달되도록 확인해 주니까요.
사용하기 쉽고 사용자가 정말 필요로 하는 것만 정확히 노출하는 잘 정리된 API를 제공하기 위해, 이제 러스트의 모듈로 넘어가 볼게요.