`if` 표현식

if 표현식 (if expressions)

조건에 따라 다른 코드를 실행하고 싶을 때 쓰는 게 if 표현식이에요. Rust의 if 는 문(statement)이 아니라 표현식(expression) 이라서 값을 만들어 내요. 조건 여러 개를 이어 쓰는 방법부터 if let, 조건 체이닝까지 살펴볼게요.

출처: Rust Reference

본문

문법 (Syntax)

IfExpression → if Conditions BlockExpressionNoInnerAttributes
               ( else ( BlockExpressionNoInnerAttributes | IfExpression ) )?

Conditions → Expression except StructExpression
           | LetChain

LetChain → LetChainCondition ( && LetChainCondition )*

LetChainCondition → Expression except ExcludedConditions
                  | OuterAttribute* let Pattern = Scrutinee except ExcludedConditions

ExcludedConditions → StructExpression
                   | LazyBooleanExpression
                   | RangeExpr
                   | RangeFromExpr
                   | RangeInclusiveExpr
                   | AssignmentExpression
                   | CompoundAssignmentExpression

의미 (Semantics)

if 표현식의 문법은 하나 이상의 조건 피연산자(condition operand)가 && 로 이어지고, 그 뒤에 조건이 참일 때 실행할 블록(consequent block), 이어서 원하는 만큼의 else if 조건과 블록, 그리고 선택적인 마지막 else 블록이 오는 구조예요.

조건 피연산자는 불리언 타입을 가진 표현식이거나, 조건부 let 매치여야 해요.

실행 흐름을 정리하면 이래요.

  • 조건 피연산자가 모두 true 이고 모든 let 패턴이 자신의 scrutinee성공적으로 매치되면, consequent 블록이 실행되고 이후의 else ifelse 블록은 건너뛰어요.
  • 조건 피연산자 중 하나라도 false 이거나 let 패턴이 매치에 실패하면, consequent 블록을 건너뛰고 다음 else if 조건을 평가해요.
  • 모든 ifelse if 조건이 falseelse 블록을 실행해요.

if 표현식은 실행된 블록과 같은 값으로 평가되고, 어떤 블록도 실행되지 않았다면 () 로 평가돼요. 그리고 if 표현식은 모든 상황에서 같은 타입을 가져야 해요.

#![allow(unused)]
fn main() {
let x = 3;
if x == 4 {
    println!("x is four");
} else if x == 3 {
    println!("x is three");
} else {
    println!("x is something else");
}

// `if` can be used as an expression.
let y = if 12 * 15 > 150 {
    "Bigger"
} else {
    "Smaller"
};
assert_eq!(y, "Bigger");
}

위 예시에서 let y = if ... { ... } else { ... } 처럼 if 를 값으로 받을 수 있는 게 "표현식" 이라서 가능해요.

발산 (Divergence)

if 표현식은 조건 표현식이 발산(diverges)하거나 모든 분기가 발산하면 발산해요. 반환 타입이 ! 인 다음 예시를 볼게요.

#![allow(unused)]
fn main() {
fn diverging_condition() -> ! {
    // Diverges because the condition expression diverges
    if loop {} {
        ()
    } else {
        ()
    };
    // The semicolon above is important: The type of the `if` expression is
    // `()`, despite being diverging. When the final body expression is
    // elided, the type of the body is inferred to ! because the function body
    // diverges. Without the semicolon, the `if` would be the tail expression
    // with type `()`, which would fail to match the return type `!`.
}

fn diverging_arms() -> ! {
    // Diverges because all arms diverge
    if true {
        loop {}
    } else {
        loop {}
    }
}
}

if let 패턴 (if let patterns)

if 조건 안의 let 패턴은, 패턴이 성공적으로 매치되면 볼 수 있는 새 변수를 스코프로 바인딩해 줘요. 다음 예시를 볼게요.

#![allow(unused)]
fn main() {
let dish = ("Ham", "Eggs");

// This body will be skipped because the pattern is refuted.
if let ("Bacon", b) = dish {
    println!("Bacon is served with {}", b);
} else {
    // This block is evaluated instead.
    println!("No bacon will be served");
}

// This body will execute.
if let ("Ham", b) = dish {
    println!("Ham is served with {}", b);
}

if let _ = 5 {
    println!("Irrefutable patterns are always true");
}
}
  • ("Bacon", b) 패턴은 ("Ham", "Eggs") 와 매치되지 않으니 본문을 건너뛰고 else 로 가요.
  • ("Ham", b) 는 매치되면서 b"Eggs" 가 바인딩돼요.
  • _ 같은 무반박(irrefutable) 패턴은 항상 매치되기 때문에 그냥 true 로 취급돼요.

| 연산자로 여러 패턴을 지정할 수도 있어요. [match 표현식](https://doc.rust-lang.org/reference/expressions/match-expr.html) 에서의 | 와 의미가 같아요.

#![allow(unused)]
fn main() {
enum E {
    X(u8),
    Y(u8),
    Z(u8),
}
let v = E::Y(12);
if let E::X(n) | E::Y(n) = v {
    assert_eq!(n, 12);
}
}

여기서 vE::Y(12) 니까 E::X(n) | E::Y(n) 패턴 중 두 번째가 매치되고 n12 가 들어와요.

조건 체이닝 (Chains of conditions)

여러 조건 피연산자를 && 로 구분해 이어 쓸 수 있어요. && 지연 불리언 표현식 과 비슷하게, 각 피연산자는 왼쪽에서 오른쪽으로 평가되다가 어느 하나가 false 가 되거나 let 매치가 실패하면 이후 피연산자는 평가하지 않아요. 각 패턴의 바인딩은 다음 조건 피연산자와 consequent 블록에서 사용할 수 있도록 스코프에 들어와요.

#![allow(unused)]
fn main() {
fn single() {
    let outer_opt = Some(Some(1i32));

    if let Some(inner_opt) = outer_opt
        && let Some(number) = inner_opt
        && number == 1
    {
        println!("Peek a boo");
    }
}
}

위 코드는 체이닝을 쓰지 않고 중첩한 아래 코드와 동일해요.

#![allow(unused)]
fn main() {
fn nested() {
    let outer_opt = Some(Some(1i32));

    if let Some(inner_opt) = outer_opt {
        if let Some(number) = inner_opt {
            if number == 1 {
                println!("Peek a boo");
            }
        }
    }
}
}

조건 피연산자 중 하나라도 let 패턴이라면, 어떤 조건 피연산자도 || 지연 불리언 연산자 표현식이 될 수 없어요. let scrutinee와의 모호함·우선순위 때문이에요. || 표현식이 필요하다면 괄호로 감싸면 돼요.

#![allow(unused)]
fn main() {
let foo = Some(123);
let condition1 = true;
let condition2 = false;
if let Some(x) = foo
    // Parentheses are required here.
    && (condition1 || condition2)
{}
}

2024 에디션 차이 (2024 Edition differences)

2024 에디션 이전에는 let 체인(LetChain 문법)이 if 표현식에서 지원되지 않았어요.

더 알아보기 (Learn more)