learn

Rust의 Statements와 Expressions

Rust의 Statements와 Expressions

Rust의 가장 큰 문법적 특징 중 하나는 statement와 expression의 구분이 엄격하다는 점이다. 이 구분은 what-is-expression-based-language|expression-based 언어 철학의 직접적인 결과물이며, 함수 작성·세미콜론 사용·제어 흐름 표현 모두에 영향을 미친다.

statement와 expression이라는 개념 자체가 무엇인지에 대한 일반론은 what-are-statements-and-expressions|별도의 노트에 정리되어 있다. 이 노트는 그 개념이 Rust에서는 구체적으로 어떻게 분류되는가에 집중한다.

정의

  • Statement: 어떤 동작을 수행하지만 값을 반환하지 않는다
  • Expression: 평가되어 값을 만들어낸다

Rust는 이 둘을 명확히 구분한다.

let y = 6;

이 전체 줄은 statement다. let y = 6 자체가 어떤 값을 가지지 않는다.

C++와 비교하면 차이가 분명해진다.

// C++: assignment가 expression이라 chaining 가능
int x;
int y = (x = 5);  // x = 5 가 5라는 값을 가짐 → y도 5
// Rust: let은 statement. 다른 expression의 일부가 될 수 없음
let y = (let x = 5);  // 컴파일 에러

C++에서는 assignment가 expression이라 chaining이 가능하지만, Rust에서 let은 명백히 statement이므로 다른 expression 안에 들어갈 수 없다.

Statement의 3가지 종류

Rust Reference 기준으로 statement는 정확히 세 종류가 있다.

1. let statement (declaration statement)

가장 명확한 statement. 변수 바인딩이다.

let x = 5;
let mut y: i32 = 10;
let (a, b) = (1, 2);  // pattern destructuring도 let statement

2. Item declaration statement

fn, struct, enum, const, static, type alias, mod, use, impl, trait 같은 item을 선언하는 것. 이런 item들은 함수 본문 안에서도 선언할 수 있고, 그렇게 함수 본문 안에 놓이면 item declaration statement가 된다.

Item이란? Rust에서 item은 *프로그램의 구성 요소(component)*를 가리키는 공식 용어다. 함수 정의, 타입 정의, 상수 정의, 모듈 정의처럼 프로그램의 구조를 정의하는 최상위 구성 단위들이 모두 item이다. C++의 함수 정의·클래스 정의·typedef·namespace 정의를 통칭하는 단어가 따로 없는데, Rust는 이걸 "item"이라는 이름으로 묶었다.

fn main() {
    fn helper() { println!("hi"); }   // 함수 안에서 함수 정의
    struct Point { x: i32, y: i32 }   // 함수 안에서 struct 정의
    const MAX: u32 = 100;             // 함수 안에서 const 정의

    helper();
}

이렇게 함수 안에서 선언된 item은 그 함수 스코프 안에서만 보이고, 외부에서 접근할 수 없다. C++의 local class와 비슷한 효과다.

3. Expression statement

Expression 뒤에 세미콜론(;)을 붙여 statement로 변환한 것. 자세한 차이는 아래 별도 섹션에서 다룬다.

5 + 6;           // expression이지만 ;가 붙어 statement
println!("hi");  // 매크로 호출 expression + ;
foo();           // 함수 호출 expression + ;

정리

종류예시
let statementlet x = 5;
Item declaration statementfn foo() {}, struct Point;, const MAX: u32 = 100;
Expression statement5 + 6;, println!("hi");, foo();

이 세 가지 외에는 모두 expression이다.

Expression의 종류

값을 만드는 모든 식이 expression이다. Rust Reference가 정의하는 expression을 카테고리로 묶으면 다음과 같다.

Literal expression

값이 그대로 적힌 것.

5
3.14
"hello"
true
'a'

Path expression

이름으로 값을 가리키는 것. 변수나 상수, 정적 변수, 모듈 경로 등.

x          // 변수
MAX        // 상수
std::f64::PI

Operator expression

연산자를 쓰는 것.

a + b      // 산술
x == y     // 비교
&value     // 참조 생성
*ptr       // 역참조
!flag      // 논리 부정

Call expression

함수, 메서드, 매크로를 호출하는 것.

foo(1, 2)         // 함수 호출
"hi".to_string()  // 메서드 호출
println!("hi")    // 매크로 호출

Tuple / Array / Struct expression

복합 데이터를 만드는 것.

(1, 2, 3)                 // tuple
[1, 2, 3]                 // array
Point { x: 1, y: 2 }      // struct

Block expression

중괄호로 둘러싸인 것. 안에 statement를 여러 개 넣고, 마지막 expression이 블록 전체의 값이 된다.

let y = {
    let x = 3;
    x + 1   // 세미콜론 없음 → block의 값이 됨
};
// y == 4

{ ... } 블록의 마지막 줄이 세미콜론 없는 expression이면, 그게 곧 블록 전체의 값이 된다. C++의 GCC 확장인 statement expression(({ ...; x; }))과 비슷한 개념이지만, Rust에서는 이것이 표준 문법이며 어디서나 통용된다.

Control flow expression

제어 흐름이 값을 가진다 — 이게 Rust의 시그니처 같은 특징이다.

if cond { 1 } else { 2 }
match x { 0 => "zero", _ => "other" }
loop { if done { break 42; } }

if, match, loop뿐 아니라 return, break도 expression이다. 흥미롭게도 이들의 타입은 ! (never type)이라 어떤 위치에든 들어갈 수 있다.

Closure expression

람다 함수.

|x| x + 1
|x, y| { x + y }

Range expression

범위.

0..10
0..=10

이 외에도 async, await 같은 비동기 expression도 있지만, 큰 그림은 위와 같다. 핵심은 statement 3종을 빼고 나머지는 거의 다 expression이라는 점이다.

Expression vs Expression statement: 왜 둘 다 필요한가

여기가 Rust 문법에서 가장 미묘한 지점이다. 같은 식이 세미콜론 하나 차이로 정체성이 달라진다.

같은 식, 다른 정체성

5 + 6        // ← expression. 값은 11.
5 + 6;       // ← expression statement. 값은 ().

같은 5 + 6인데:

  • 위는 expression이라 값(11)을 가지고, 다른 expression의 부품이 될 수 있다
  • 아래는 expression statement라 값이 사라지고(() unit type), 함수 본문에 한 줄로 놓일 뿐이다

왜 expression statement가 따로 필요한가

효과만 필요할 때를 위해서다.

println!("hello");   // 출력하는 게 목적. 반환값(())은 버려도 됨.
vec.push(42);        // 벡터에 추가가 목적. 반환값(())은 버려도 됨.
some_fn();           // 부작용이 목적. 반환값을 받지 않아도 됨.

이 식들은 expression이지만, 값보다는 부작용(side effect)이 목적이다. 그래서 세미콜론으로 statement화해서 *"이 식의 값은 신경 쓰지 않고, 효과만 일으킨다"*고 명시하는 것이다.

C에서 printf("hi");를 생각해보면 같은 패턴이다. printf는 int를 반환하는 함수지만, 보통 그 반환값(쓴 글자 수)은 받지 않고 그냥 호출하고 버린다. 이게 정확히 expression statement 패턴이다.

C++Rust
int x = foo();let x = foo(); (let statement, foo()는 expression)
foo(); (반환값 버림)foo(); (expression statement)

C++에서는 그냥 *"한 줄짜리 식"*으로 보이지만, Rust에서는 그게 명시적으로 expression을 statement로 변환하는 행위다.

함수 본문 안에서 본 차이

fn five() -> i32 {
    let x = 5;        // let statement. x에 5 바인딩.
    println!("hi");   // expression statement. 출력 효과.
    x                 // expression. 함수의 반환값.
}

세 줄 모두 함수 본문에 있지만 정체성이 모두 다르다.

  1. let statement — 변수 바인딩
  2. expression statement — 효과 발생, 값 버림
  3. expression (세미콜론 없음) — 함수의 반환값이 됨

만약 마지막 줄이 x;였다면 expression statement가 되어버려서 함수의 반환값이 ()가 되고 컴파일 에러가 난다. 이것이 Rust의 그 유명한 *"마지막 줄 세미콜론 함정"*이다.

세미콜론의 역할

이제 세미콜론이 왜 그렇게 중요한지가 명확해진다. 세미콜론은 단순한 구분자가 아니라, expression을 expression statement로 변환하는 연산자다.

fn five() -> i32 {
    5      // expression → 함수의 반환값이 됨, OK
}

fn five() -> i32 {
    5;     // expression statement → 반환값 없음 → 컴파일 에러
}          // expected `i32`, found `()`

5만 있을 때는 i32 expression이지만, 5;가 되는 순간 expression statement로 변하면서 값이 () (unit type)가 된다. C++에서는 return이 있어야 반환되니 헷갈릴 일이 없지만, Rust는 함수의 마지막 expression이 자동으로 반환값이 되므로 세미콜론 하나에 의미가 완전히 달라진다.

왜 이렇게 설계했을까

Expression-based 언어의 장점은 모든 것이 합성 가능하다는 것이다.

let x = if condition { 5 } else { 10 };

C++에서는 삼항연산자 condition ? 5 : 10을 따로 둬야 하지만, Rust는 if 자체가 expression이라 그냥 쓸 수 있다. match도 마찬가지다.

이 합성 가능성을 얻는 대신, Rust는 statement와 expression의 경계를 엄격하게 유지해야 했고, 그 결과로 세미콜론이 의미를 바꾸는 언어가 되었다. 이는 *"값을 가진 식은 합성 가능해야 한다"*는 원칙과 *"부작용만 필요한 식도 깔끔하게 표현해야 한다"*는 실용성 사이의 타협이다.

핵심 정리

구분StatementExpression
없음있음 (()을 포함한 어떤 타입)
종류let, item declaration, expression statementliteral, path, operator, call, tuple/array/struct, block, control flow, closure, range …
합성 가능❌ (다른 expression 안에 못 들어감)✅ (어디든 부품으로 들어감)
예시let y = 6;, fn foo() {}, 5 + 6;5 + 6, if x { 1 } else { 2 }, { ... }

C++에서 Rust로 넘어왔을 때 가장 자주 만나는 함정은 함수 마지막 줄에 세미콜론을 무심코 붙이는 것이다. 익숙해지면 if/match를 expression으로 쓰는 패턴이 매우 깔끔하게 느껴진다.


한 줄 요약

Rust의 코드는 결국 3종류의 statement(let, item, expression statement)와 다양한 expression의 조합이다. 세미콜론은 expression을 expression statement로 변환하는 연산자이며, 이 작은 차이가 함수 반환·블록 값·제어 흐름 표현 전반의 문법을 결정한다.