블록 표현식

블록 표현식

Rust 코드의 몸통은 대부분 { }로 감싼 블록(block) 이에요. 함수 본문도, if의 각 분기도, 루프 본문도 모두 블록 표현식이죠. 블록은 단순히 코드를 묶는 괄호가 아니라, 제어 흐름 표현식이면서 동시에 항목과 변수 선언을 위한 익명 네임스페이스 스코프 역할을 해요.

출처: Rust Reference - Block expressions

본문

BlockExpression ->
    {
        InnerAttribute*
        Statements?
    }

BlockExpressionNoInnerAttributes ->
    {
        Statements?
    }

Statements ->
      Statement+
    | Statement+ ExpressionWithoutBlock
    | ExpressionWithoutBlock

제어 흐름 표현식으로서 블록은 내부의 비-항목 선언문(statement)들을 순서대로 실행한 다음, 마지막의 선택적인 표현식을 실행해요. 익명 네임스페이스 스코프로서 항목 선언은 블록 내부에서만 유효하고, let으로 선언한 변수는 다음 문장부터 블록 끝까지 유효해요.

블록의 문법은 {, 내부 속성들, 여러 문장, 그리고 최종 피연산자(final operand) 라고 부르는 선택적 표현식, 마지막으로 } 순서로 구성돼요.

문장과 세미콜론

문장은 보통 세미콜론이 필요하지만, 두 가지 예외가 있어요.

  • 항목 선언문은 세미콜론이 필요 없어요.
  • 표현식 문장은 바깥 표현식이 제어 흐름 표현식이 아닌 이상 보통 세미콜론이 필요해요.

문장 사이에 세미콜론이 하나 더 들어가는 것도 허용되지만, 그런 추가 세미콜론은 의미에 영향을 주지 않아요.

블록의 값

블록을 평가할 때, 항목 선언문을 제외한 각 문장이 순서대로 실행돼요. 그리고 마지막 피연산자가 있다면 그게 실행돼요.

  • 블록에 마지막 피연산자가 있으면, 블록의 타입과 값은 그 피연산자의 타입과 값이 돼요.
#![allow(unused)]
fn main() {
let x: u8 = { 0u8 }; // `0u8`이 마지막 피연산자예요.
assert_eq!(x, 0);
let x: u8 = { (); 0u8 }; // 위와 동일해요.
assert_eq!(x, 0);
}
  • 블록에 마지막 피연산자가 없고 발산(diverges)하지 않으면, 블록은 unit 타입과 unit 값을 가져요.
#![allow(unused)]
fn main() {
let x: () = {}; // 마지막 피연산자가 없어요.
assert_eq!(x, ());
let x: () = { 0u8; }; // 위와 동일해요.
assert_eq!(x, ());
}
  • 블록에 마지막 피연산자가 없고 발산하면, 블록은 never 타입(!)을 가지며 (타입이 비어 있으므로) 최종 값이 없어요.
#![allow(unused)]
fn main() {
fn f() -> ! { loop {}; } // 발산하고 마지막 피연산자가 없어요.
//          ^^^^^^^^^^^^
// 함수의 몸통은 블록 표현식이에요.
}

여기서 주의할 점이 하나 있어요. 마지막 피연산자가 없는 것명시적으로 unit 타입의 마지막 피연산자가 있는 것은 서로 달라요. 예를 들어 아래 블록은 발산하지만, 타입은 never가 아니라 unit이에요.

#![allow(unused)]
fn main() {
fn f() -> ! { loop {}; () } // ERROR: Mismatched types.
//          ^^^^^^^^^^^^^^^ This block has unit type.
}

또한 제어 흐름 표현식으로서, 블록 표현식이 표현식 문장의 바깥 표현식이라면 기대 타입은 ()예요. 단, 바로 뒤에 세미콜론이 붙으면 예외예요.

발산(diverging) 블록

블록이 발산한다(diverge) 는 것은, 도달 가능한 모든 제어 흐름 경로에 발산 표현식이 포함된 경우를 말해요. 단, 그 표현식이 읽히지 않는 place 표현식이라면 예외예요.

#![allow(unused)]
fn main() {
#![ feature(never_type) ]
fn no_control_flow() -> ! {
    // 조건문이 없으므로 이 함수 몸통 전체가 발산해요.
    loop {}
}

fn control_flow_diverging() -> ! {
    // 모든 경로가 발산하므로 함수 몸통 전체가 발산해요.
    if true {
        loop {}
    } else {
        loop {}
    }
}

fn control_flow_not_diverging() -> () {
    // 어떤 경로는 발산하지 않으므로 이 블록 전체는 발산하지 않아요.
    if true {
        ()
    } else {
        loop {}
    }
}

// 참고: 여기서는 불안정한 never 타입(nightly 전용)을 사용했어요.
// stable Rust에서도 이 상황은 만날 수 있지만 예시가 더 복잡해져요.
struct Foo {
    x: !,
}

fn make<T>() -> T { loop {} }

fn diverging_place_read() -> ! {
    let foo = Foo { x: make() };
    // place 표현식을 읽으면 발산하는 블록이 돼요.
    let _x = foo.x;
}
}
#![allow(unused)]
fn main() {
#![ feature(never_type) ]
fn make<T>() -> T { loop {} }
struct Foo {
    x: !,
}
fn diverging_place_not_read() -> ! {
    let foo = Foo { x: make() };
    // `_`에 할당하는 것은 place를 읽지 않음을 뜻해요.
    let _ = foo.x;
} // ERROR: Mismatched types.
}

블록은 항상 값 표현식이며, 마지막 피연산자를 값 표현식 컨텍스트에서 평가해요. 이 성질을 이용해 정말로 필요할 때 값을 강제로 이동(move)시킬 수도 있어요. 아래 예시는 s가 블록 표현식 안에서 이동됐기 때문에 consume_self 호출이 실패해요.

#![allow(unused)]
fn main() {
struct Struct;

impl Struct {
    fn consume_self(self) {}
    fn borrow_self(&self) {}
}

fn move_by_block_expression() {
    let s = Struct;

    // 블록 표현식에서 `s`의 값을 이동시켜요.
    (&{ s }).borrow_self();

    // `s`가 이동됐기 때문에 실행에 실패해요.
    s.consume_self();
}
}

async 블록

async 블록은 future로 평가되는 블록 표현식의 변형이에요. 블록의 마지막 표현식(있으면)이 future의 결과 값을 결정해요.

AsyncBlockExpression -> async move? BlockExpression

async 블록을 실행하는 것은 클로저 표현식을 실행하는 것과 비슷해요. 즉각적인 효과는 익명 타입 하나를 만들어 반환한다는 점이죠. 클로저가 std::ops::Fn 계열 트레잇을 구현하는 타입을 반환하는 반면, async 블록이 반환하는 타입은 std::future::Future 트레잇을 구현해요. 이 타입의 실제 데이터 형식은 명시되어 있지 않아요.

rustc가 생성하는 future 타입은 대략 await 지점마다 하나의 variant를 갖는 enum과 비슷해요. 각 variant는 해당 지점에서 재개하는 데 필요한 데이터를 저장하죠.

  • 에디션 차이: async 블록은 Rust 2018부터 사용할 수 있어요.
  • 캡처 모드: async 블록은 클로저와 같은 캡처 모드로 환경의 변수를 캡처해요. async { .. }는 각 변수의 캡처 모드를 블록 내용에서 추론하고, async move { .. }는 참조된 모든 변수를 결과 future로 이동시켜요.
  • async 컨텍스트: async 블록은 future를 만들므로, 그 안에 await 표현식을 담을 수 있는 async 컨텍스트를 정의해요. async 함수의 본문도 이 async 블록으로 정의되죠.

async 블록은 클로저처럼 함수 경계처럼 작동해요. 따라서 ? 연산자와 return 표현식은 둘러싼 함수가 아니라 future의 출력에 영향을 줘요. async 블록 안에서 return <expr>을 쓰면 <expr>의 결과가 future의 출력이 되고, <expr>?가 오류를 전파하면 그 오류가 future의 결과로 전파되죠.

breakcontinue 키워드는 async 블록 밖으로 분기할 수 없어요. 그래서 아래 코드는 불법이에요.

#![allow(unused)]
fn main() {
loop {
    async move {
        break; // error[E0267]: `break` inside of an `async` block
    }
}
}

const 블록

const 블록은 본문이 런타임 대신 컴파일 타임에 평가되는 블록 표현식의 변형이에요.

ConstBlockExpression -> const BlockExpression

const 블록을 쓰면 새 상수 항목을 정의하지 않고도 상수 값을 정의할 수 있어요. 그래서 인라인 상수(inline consts) 라고도 불러요. 상수 항목과 달리 타입 추론도 되므로 타입을 명시할 필요가 없어요. 그리고 자유 상수 항목과 달리 스코프 안의 제네릭 매개변수를 참조할 수도 있어요.

#![allow(unused)]
fn main() {
fn foo<T>() -> usize {
    const { std::mem::size_of::<T>() + 1 }
}
}

위 코드는 아래와 동등해요.

#![allow(unused)]
fn main() {
fn foo<T>() -> usize {
    {
        struct Const<T>(T);
        impl<T> Const<T> {
            const CONST: usize = std::mem::size_of::<T>() + 1;
        }
        Const::<T>::CONST
    }
}
}

런타임에서 const 블록 표현식이 실행된다면, 반환 값을 무시하더라도 그 상수가 반드시 평가되는 것이 보장돼요.

#![allow(unused)]
fn main() {
fn foo<T>() -> usize {
    // 이 코드가 실행된다면, 어서션은 반드시 컴파일 타임에 평가됐어요.
    const { assert!(std::mem::size_of::<T>() > 0); }
    // 여기서 이 타입이 zero-sized가 아니라고 가정하는 unsafe 코드를 쓸 수 있어요.
    /* ... */
    42
}
}

반대로 런타임에서 실행되지 않으면, 평가되거나 안 될 수 있어요.

#![allow(unused)]
fn main() {
if false {
    // 패닉은 프로그램이 빌드될 때 발생할 수도, 아닐 수도 있어요.
    const { panic!(); }
}
}

unsafe 블록

코드 블록 앞에 unsafe 키워드를 붙이면 안전하지 않은 연산을 허용해요.

UnsafeBlockExpression -> unsafe BlockExpression

언제 unsafe를 써야 하는지에 대한 자세한 내용은 unsafe 블록 문서를 참고해요.

#![allow(unused)]
fn main() {
unsafe {
    let b = [13u8, 17u8];
    let a = &b[0] as *const u8;
    assert_eq!(*a, 13);
    assert_eq!(*a.offset(1), 17);
}

unsafe fn an_unsafe_fn() -> i32 { 10 }
let a = unsafe { an_unsafe_fn() };
}

라벨이 있는 블록 표현식

라벨이 있는 블록 표현식은 "루프와 그 밖의 break 가능한 표현식" 섹션에서 다뤄요.

블록 표현식의 속성

내부 속성은 다음 상황에서 블록 표현식의 여는 중괄호 바로 다음에 허용돼요.

  • 함수와 메서드 본문
  • 루프 본문(loop, while, for)
  • 문장으로 쓰인 블록 표현식
  • 배열 표현식, 튜플 표현식, 호출 표현식, 튜플 스타일 구조체 표현식의 요소로 쓰인 블록 표현식
  • 다른 블록 표현식의 꼬리 표현식으로 쓰인 블록 표현식

블록 표현식에서 의미를 갖는 속성은 cfg와 린트 검사 속성이에요. 예를 들어 아래 함수는 unix 플랫폼에서 true, 그 외 플랫폼에서 false를 반환해요.

#![allow(unused)]
fn main() {
fn is_unix_platform() -> bool {
    #[cfg(unix)] { true }
    #[cfg(not(unix))] { false }
}
}

더 알아보기 (Learn more)