Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

if letlet...else 로 더 간결하게 제어 흐름 쓰기

if let 문법은 iflet 을 합쳐, 하나의 패턴과 맞는 값만 처리하고 나머지는 무시하는 일을 덜 장황하게 표현할 수 있게 해 줍니다. 예를 들어 목록 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}"),
        _ => (),
    }
}
Listing 6-6: 값이 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) 이며, maxSome 안쪽 값에 바인딩됩니다. 그 뒤 if let 블록 본문에서, 대응하는 match arm에서 했던 것과 똑같이 max 를 사용할 수 있습니다. if let 블록 안의 코드는 값이 해당 패턴과 매칭될 때만 실행됩니다.

if let 을 사용하면 타이핑도 줄고, 들여쓰기도 줄며, 보일러플레이트 코드도 줄어듭니다. 하지만 그 대가로 match 가 강제하던 완전성 검사를 잃게 됩니다. 즉, 어떤 경우를 빠뜨렸는지 컴파일러가 확인해 주지 않습니다. 따라서 matchif 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 letelse 식을 사용할 수도 있습니다.

#[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}");
    }
}
Listing 6-7: 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}");
    }
}
Listing 6-8: 값을 만들어 내거나 조기에 반환하기 위해 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}");
    }
}
Listing 6-9: 함수 안의 흐름을 더 명확하게 만드는 let...else 사용하기

이 방식은 if let 처럼 두 분기의 제어 흐름이 크게 갈라지지 않으면서도, 함수 본문이 주로 “행복한 경로(happy path)” 에 머무르게 해 줍니다.

어떤 상황에서 프로그램 로직이 match 로 표현하기엔 너무 장황하다면, if letlet...else 역시 러스트 도구 상자 안에 있다는 점을 기억해 두세요.

정리

이제 우리는 enum을 사용해, 열거된 값 집합 중 하나가 될 수 있는 사용자 정의 타입을 만드는 방법을 배웠습니다. 또한 표준 라이브러리의 Option<T> 타입이 타입 시스템을 사용해 오류를 막는 데 어떻게 도움을 주는지도 보았습니다. enum 값 안에 데이터가 있다면, 처리해야 할 경우 수에 따라 matchif let 을 사용해 그 값을 꺼내 쓰면 됩니다.

이제 여러분의 러스트 프로그램은 구조체와 enum을 사용해 도메인 개념을 더 정확히 표현할 수 있게 되었습니다. API에 사용할 사용자 정의 타입을 만들면 타입 안전성도 함께 얻습니다. 컴파일러가 함수가 기대하는 타입의 값만 함수에 들어오도록 보장해 주기 때문입니다.

이제 사용자가 쉽게 쓸 수 있고, 동시에 정말 필요한 것만 정확히 드러내는 잘 정리된 API를 제공하기 위해, 러스트의 모듈로 넘어가 보겠습니다.