블록 표현식
블록 표현식
Rust 코드의 몸통은 대부분 { }로 감싼 블록(block) 이에요. 함수 본문도, if의 각 분기도, 루프 본문도 모두 블록 표현식이죠. 블록은 단순히 코드를 묶는 괄호가 아니라, 제어 흐름 표현식이면서 동시에 항목과 변수 선언을 위한 익명 네임스페이스 스코프 역할을 해요.
본문
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의 결과로 전파되죠.
break와 continue 키워드는 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)
- Rust Reference - Expressions — 전체 표현식 목록.
- Rust Reference - Scopes — 블록이 만드는 스코프 규칙 상세.
- Rust Reference - unsafe blocks — unsafe 블록의 자세한 규칙.