if let과 let...else 로 더 간결하게 제어 흐름 쓰기
if let 문법은 if 와 let 을 합쳐, 하나의 패턴과 맞는 값만 처리하고 나머지는
무시하는 일을 덜 장황하게 표현할 수 있게 해 줍니다. 예를 들어 목록 6-6의 프로그램은
config_max 변수에 들어 있는 Option<u8> 값을 match 로 처리하지만, 실제로
코드를 실행하고 싶은 경우는 값이 Some variant일 때뿐입니다.
fn main() {
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {max}"),
_ => (),
}
}
Some 일 때만 코드를 실행하고 싶은 match값이 Some 이면, 패턴 안에서 그 값을 max 변수에 바인딩해 Some 안쪽 값을
출력합니다. None 값에 대해서는 아무 것도 하고 싶지 않습니다. 그런데 match 식을
완전하게 만들려면, 하나의 variant만 처리하고도 _ => () 같은 코드를 덧붙여야
합니다. 이건 성가신 보일러플레이트죠.
대신 if let 을 사용하면 훨씬 더 짧게 같은 일을 쓸 수 있습니다. 다음 코드는
목록 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 를 사용할 수 있습니다.
if let 블록 안의 코드는 값이 해당 패턴과 매칭될 때만 실행됩니다.
if let 을 사용하면 타이핑도 줄고, 들여쓰기도 줄며, 보일러플레이트 코드도 줄어듭니다.
하지만 그 대가로 match 가 강제하던 완전성 검사를 잃게 됩니다. 즉, 어떤 경우를
빠뜨렸는지 컴파일러가 확인해 주지 않습니다. 따라서 match 와 if let 중 어느 것을
선택할지는, 지금 상황에서 더 간결한 코드를 얻는 것이 완전성 검사를 포기할 만한 가치가
있는지에 따라 달라집니다.
다르게 말하면, if let 은 하나의 패턴과 매칭될 때만 코드를 실행하고, 나머지 모든
값은 무시하는 match 의 문법적 설탕이라고 생각할 수 있습니다.
if let 에는 else 도 붙일 수 있습니다. else 와 연결된 코드 블록은, 같은 일을
하는 match 식에서 _ 케이스와 함께 둘 코드와 동일한 역할을 합니다. 목록 6-4의
Coin enum 정의를 떠올려 봅시다. 거기서 Quarter variant는 UsState 값을 함께
가지고 있었습니다. 만약 quarter가 아닌 동전을 셀 필요도 있고, quarter라면 어떤
주의 동전인지도 출력하고 싶다면 다음처럼 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 로 “행복한 경로” 유지하기
흔한 패턴 하나는 값이 존재할 때 어떤 계산을 수행하고, 그렇지 않으면 기본값을 반환하는
것입니다. 주 값이 들어 있는 동전 예제를 계속 사용해 보면, 만약 어떤 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}");
}
}
그런 다음 목록 6-7처럼 if let 을 사용해 동전 종류를 매칭하고, 조건 본문 안에서
state 변수를 도입할 수 있습니다.
#[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 안에 조건을 중첩해, 그 주가 1900년에 존재했는지 검사하기이 코드는 원하는 일을 하기는 하지만, 작업이 if let 본문 안으로 밀려 들어갔습니다.
해야 할 일이 더 복잡해지면, 최상위 분기들이 서로 어떤 관계인지 따라가기가 어려워질 수
있습니다. 또는 식이 값을 만들어 낸다는 사실을 이용해, if let 에서 state 를
만들어 내거나 조기에 반환할 수도 있습니다. 목록 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 사용하기하지만 이 코드도 나름대로 따라가기 불편합니다. if let 의 한 분기는 값을 만들고,
다른 분기는 함수에서 바로 반환해 버리기 때문입니다.
이런 흔한 패턴을 더 보기 좋게 표현하기 위해, 러스트는 let...else 를 제공합니다.
let...else 문법은 왼쪽에 패턴, 오른쪽에 식을 받는다는 점에서 if let 과 매우
비슷하지만, if 분기는 없고 else 분기만 있습니다. 패턴이 맞으면, 패턴 안의 값은
바깥 스코프에 바인딩됩니다. 패턴이 맞지 않으면, 프로그램 흐름은 else arm으로
들어가며, 그 arm은 반드시 함수에서 반환해야 합니다.
목록 6-9는 목록 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}");
}
}
let...else 사용하기이 방식은 if let 처럼 두 분기의 제어 흐름이 크게 갈라지지 않으면서도, 함수 본문이
주로 “행복한 경로(happy path)” 에 머무르게 해 줍니다.
어떤 상황에서 프로그램 로직이 match 로 표현하기엔 너무 장황하다면, if let 과
let...else 역시 러스트 도구 상자 안에 있다는 점을 기억해 두세요.
정리
이제 우리는 enum을 사용해, 열거된 값 집합 중 하나가 될 수 있는 사용자 정의 타입을
만드는 방법을 배웠습니다. 또한 표준 라이브러리의 Option<T> 타입이 타입 시스템을
사용해 오류를 막는 데 어떻게 도움을 주는지도 보았습니다. enum 값 안에 데이터가
있다면, 처리해야 할 경우 수에 따라 match 나 if let 을 사용해 그 값을 꺼내 쓰면
됩니다.
이제 여러분의 러스트 프로그램은 구조체와 enum을 사용해 도메인 개념을 더 정확히 표현할 수 있게 되었습니다. API에 사용할 사용자 정의 타입을 만들면 타입 안전성도 함께 얻습니다. 컴파일러가 함수가 기대하는 타입의 값만 함수에 들어오도록 보장해 주기 때문입니다.
이제 사용자가 쉽게 쓸 수 있고, 동시에 정말 필요한 것만 정확히 드러내는 잘 정리된 API를 제공하기 위해, 러스트의 모듈로 넘어가 보겠습니다.