3. 표현식 개요
이 장은 표현식과 문장의 형태, 그리고 모든 제어 구조를 표현식으로 둔 결정의 귀결을 정의합니다. 구체적인 표현식 종류와 그 의미는 후속 문서에서 차례대로 다루게 됩니다.
목차
- 3.1. 연산자 디스패치
- 3.2. 우선순위와 결합성
- 3.3. 브로드캐스트 연산자
- 3.4. 대입과 복합 대입
- 3.5. 멤버 접근과 메서드 호출
- 3.6. 인덱싱과 슬라이싱
- 3.7. 범위 표현식
- 3.8. 옵셔널 체이닝과 null 합체
- 3.9. 블록 표현식
- 3.10.
if표현식 - 3.11. 패턴
- 3.12.
match표현식 - 3.13. 루프 표현식
- 3.14. 에러 제어 흐름
- 3.15. 분해 대입 (destructuring)
- 3.16.
let-else - 3.17.
using표현식
표현식과 문장
표현식 (expression) 은 값을 만드는 구조이며, 평가하면 어떤 타입의 값이 결과로 나오게 됩니다. 이에 비해 문장 (statement) 은 값을 만들지 않는 코드 단위로서, 문장 종결자 의 ; 로 끝납니다.
이 두 개념 사이에서 rok 은 다음과 같은 변환 규칙을 둡니다.
<문장> = <표현식> ';' // 표현식의 결과를 버리는 문장
즉 문장은 표현식에 종결자를 붙인 것이라고 볼 수 있습니다. 거의 모든 코드 구조가 표현식 형태로 값을 만들고, 그 값을 폐기하면 문장이 됩니다.
제어 구조 = 표현식
이 설계에서 더 나아가, if, match, loop, while, for, try 의 모든 제어 구조 역시 표현식입니다. 다음 예시는 이러한 제어 구조들이 그대로 값을 만들어내는 모습을 보여 줍니다.
let a = if cond { x } else { y };
let b = match v {
1 => "one",
2 => "two",
_ => "other",
};
let c = loop {
let v = compute();
if v.converged { break v; }
};
let d = { let a = 1; let b = 2; a + b };
이러한 결정은 두 가지 귀결을 낳습니다. 첫째로 어떤 식 자리에든 그대로 끼워 넣을 수 있어, 함수 인자, 변수 초기값, 다른 표현식의 일부 등 어디에든 자유롭게 들어갑니다. 둘째로 ternary 같은 별도 형태가 필요 없게 되며, 따라서 cond ? x : y 같은 별도 토큰을 두지 않고 if cond { x } else { y } 가 그 자리를 채웁니다.
각 제어 구조가 어떤 값을 반환하는지에 대한 정확한 규칙은 해당 사양 문서에서 정의됩니다.
분기 결과 타입
표현식의 결과 타입과 그 값을 받는 슬롯의 타입은 별개로 결정됩니다.
식의 결과 타입
제어 구조의 모든 분기가 같은 타입의 값을 만들면, 그 식 자체의 결과 타입은 그 타입입니다. 분기들이 서로 다른 타입의 값을 만들 경우에는 받는 쪽이 정한 타입 (슬롯의 명시 타입, 매개변수 타입, 반환 타입) 이 있어야 하며, 각 분기는 그 타입과 호환되어야 합니다. 받는 쪽의 타입이 없으면 컴파일 에러가 발생합니다. 자동 합 타입 추론과 자동 공통 슈퍼타입 승급, 예를 들어 i32 가 i64 나 f64 로 묵시적으로 승급되는 일은 모두 허용되지 않습니다.
if cond { 1i32 } else { 2i32 } // 식의 결과 타입: i32
if cond { 1i32 } else { 2.0f64 } // 받는 쪽의 타입이 i32 | f64 등을 정해야 함
합 타입은 명시로, 승급은 없음
두 경우는 거부되는 이유가 다릅니다. 합 타입을 자동으로 추론하지 않는 까닭은, 분기 하나의 실수가 조용히 합 타입을 만들어 이후 코드 전체로 퍼지는 일을 막기 위함입니다. 합 타입이 필요하면 let d: i32 | str = if ... 처럼 받는 쪽에 명시하면 됩니다. 반면 자동 슈퍼타입 승급은 한 타입이 다른 타입으로 변환되는 것이며 (i32 → f64), 정밀도나 표현 손실이 발생할 수 있어 받는 쪽의 타입이 있어도 허용되지 않습니다.
이러한 구분은 언어와 표준 라이브러리의 경계 의 명시성 원칙과도 일관됩니다. 변환이 필요한 경우라면 형 변환 의 세 구문, 즉 as, .into(), .try_into() 로 명시해야 합니다.
식 결과를 슬롯이 받기
표현식의 결과를 변수 슬롯이 받을 때, 슬롯의 타입은 변수 선언 의 규칙을 따릅니다. 타입을 명시하면 그 타입이 받는 쪽의 타입이 되고, 생략하면 식의 결과 타입으로 추론합니다.
// 타입 명시 없음 → 슬롯은 식의 결과 타입으로 추론
let a = if cond { 1i32 } else { 2i32 }; // a: i32
let b = if cond { 1i32 } else { 2.0f64 }; // ERROR: 분기 타입 불일치 - 타입 명시 필요
// any 슬롯 → 어떤 분기 조합이든 받음
let g: any = if cond { 1i32 } else { 2.0f64 }; // OK
// 합 타입 슬롯 → 식의 결과가 슬롯의 항 집합에 포함되어야
let c: i32 | f64 = if cond { 1i32 } else { 2.0f64 }; // OK
let d: i32 | f64 = if cond { 1i32 } else { "hi" }; // ERROR: str 이 슬롯에 없음
// 단일 구체 타입 슬롯 → 식의 결과가 정확히 그 타입이어야
let e: i32 = if cond { 1i32 } else { 2i32 }; // OK
let f: i32 = if cond { 1i32 } else { 2.0f64 }; // ERROR: f64 분기가 i32 에 안 맞음
슬롯 호환 표
식의 결과 타입과 받는 슬롯 사이의 호환 관계는 다음 표와 같이 정리됩니다.
| 받는 슬롯 | 식의 결과 타입 | 허용 |
|---|---|---|
any (: any 명시) | 무엇이든 | 항상 |
합 타입 T1 | T2 | ... | 식의 결과 항이 슬롯의 항 집합에 포함 | 허용 |
단일 구체 타입 T | 식의 결과가 정확히 T | 허용 |
단일 구체 타입 T | 식의 결과에 T 외 다른 항이 있음 | 거부 |
| 타입 명시 생략 | 모든 분기가 같은 타입 | 허용 (그 타입으로 추론) |
| 타입 명시 생략 | 분기 타입이 서로 다름 | 거부 |
점진과 정적
이러한 호환 규칙 위에서 사용자는 세 가지 모드를 선택할 수 있습니다. 먼저 점진 모드 (let x: any = ...) 에서는 슬롯이 any 가 되어 어떤 분기 조합도 자유롭게 받을 수 있고, 디스패치는 런타임 태그 검사로 이루어지므로 빠른 탐색에 적합합니다. 다음으로 정적 합 타입 모드 (let x: T1 | T2 = ...) 에서는 슬롯이 명시 합 타입이며 분기 결과가 항 집합 안에 들어가야 하는데, 타입 좁히기와 패턴 매칭 완전성 검사가 가능하므로 안정성과 성능에 적합합니다. 마지막으로 정적 단일 타입 모드 (let x: T = ... 또는 타입을 생략한 let x = ...) 에서는 슬롯이 단일 타입이고 모든 분기가 그 타입이어야 하며, 최대 성능과 완전한 정적 분석이 가능합니다.
이 세 모드는 같은 코드 안에서 자유롭게 섞일 수 있으며, 각 변수마다 사용자가 선택합니다.
표현식과 문장의 구분
대부분의 코드 줄은 다음 두 형태 중 하나가 됩니다.
<선언>; // let / fn / struct / ... 의 정의 - 문장
<제어구조 또는 식>; // 표현식의 결과를 버리는 문장
if / match / 루프 / 블록처럼 } 로 끝나는 식은 문장 자리에서 ; 를 생략할 수 있습니다 (1.3.4).
다만 블록 안의 마지막 식은 예외에 해당합니다.
{
let a = 1;
let b = 2;
a + b // 종결자 없음 - 이 블록의 값
}
블록의 마지막 식만은 종결자 없이 둘 수 있고, 이때 그 식이 블록 전체의 값이 됩니다. 이에 대한 자세한 규칙은 블록 표현식 에서 정의됩니다.
부작용
표현식은 평가 도중에 부작용을 일으킬 수 있는데, I/O, 변수 재할당, 외부 호출 등이 그 예입니다. 평가 순서는 후속 문서에서 정의되는 연산자 우선순위와 함수 호출 평가 순서에 따라 결정됩니다.
같은 표현식 안에서 부작용으로 인한 의존이 있다면 그 의존은 명시적 순서, 즉 좌에서 우로, 그리고 깊이 우선의 순서를 따르게 됩니다. 컴파일러는 이 순서를 임의로 바꾸지 않습니다.