표현식
표현식 (Expressions)
Rust 프로그램에서 실제 '일'이 일어나는 곳이 바로 표현식(expression)이에요. 표현식은 항상 값을 만들어내고, 그 과정에서 부수 효과(side effect)를 가질 수 있지요. Rust는 문(statement)보다 표현식 중심이라서, 이 장을 이해하면 코드의 실행 구조가 어떻게 결정되는지가 잡혀요.
출처: Rust Reference
본문
문법
Expression →
ExpressionWithoutBlock
| ExpressionWithBlock
ExpressionWithoutBlock →
OuterAttribute* ExpressionWithoutBlockNoAttrs
ExpressionWithoutBlockNoAttrs →
LiteralExpression
| PathExpression
| OperatorExpression
| GroupedExpression
| ArrayExpression
| AwaitExpression
| IndexExpression
| TupleExpression
| TupleIndexingExpression
| StructExpression
| CallExpression
| MethodCallExpression
| FieldExpression
| ClosureExpression
| AsyncBlockExpression
| ContinueExpression
| BreakExpression
| RangeExpression
| ReturnExpression
| UnderscoreExpression
| MacroInvocation
ExpressionWithBlock →
OuterAttribute* ExpressionWithBlockNoAttrs
ExpressionWithBlockNoAttrs →
BlockExpression
| ConstBlockExpression
| UnsafeBlockExpression
| LoopExpression
| IfExpression
| MatchExpression
표현식의 두 역할
표현식은 두 가지 역할을 해요. 항상 값을 만들어내고, 그리고 효과(부수 효과)를 가질 수 있어요.
표현식은 값으로 **평가(evaluate)**되며, 평가 중에 효과를 갖습니다.
또한 많은 표현식은 **하위 표현식(sub-expression)**을 포함하는데, 이를 그 표현식의 **피연산자(operand)**라고 불러요.
각 종류의 표현식의 의미는 몇 가지를 결정합니다.
- 그 표현식을 평가할 때 피연산자를 평가할지 여부
- 피연산자를 평가하는 순서
- 피연산자의 값을 조합해 표현식의 값을 얻는 방식
이런 식으로, 표현식의 구조가 실행의 구조를 결정합니다. 블록도 그저 또 하나의 표현식일 뿐이라서, 블록·문장·표현식·블록이 서로 임의의 깊이로 재귀적으로 중첩될 수 있어요.
참고: 우리는 피연산자에 이름을 붙여서 이야기하지만, 이 이름은 안정적이지 않아서 나중에 바뀔 수 있어요.
표현식 우선순위 (Expression precedence)
Rust 연산자와 표현식의 우선순위는 강한 것에서 약한 것 순서로 다음과 같아요. 같은 우선순위의 이항 연산자는 결합성(associativity)이 주어진 순서로 묶여요.
| 연산자/표현식 | 결합성 |
|---|---|
| 경로(Paths) | |
| 메서드 호출(Method calls) | |
| 필드 표현식(Field expressions) | 왼쪽에서 오른쪽 |
| 함수 호출, 배열 인덱싱 | |
? |
|
단항 [-] [!] [*] 빌림(borrow) |
|
as |
왼쪽에서 오른쪽 |
[*] [/] [%] |
왼쪽에서 오른쪽 |
[+] [-] |
왼쪽에서 오른쪽 |
[<<] [>>] |
왼쪽에서 오른쪽 |
& |
왼쪽에서 오른쪽 |
^ |
왼쪽에서 오른쪽 |
| |
왼쪽에서 오른쪽 |
[==] [!=] [<] [>] [<=] >= |
괄호 필요 |
&& |
왼쪽에서 오른쪽 |
|| |
왼쪽에서 오른쪽 |
[..] ..= |
괄호 필요 |
[=] [+=] [-=] [*=] [/=] [%=] [&=] [|=] [^=] [<<=] >>= |
오른쪽에서 왼쪽 |
[return] [break] 클로저(closures) |
피연산자의 평가 순서 (Evaluation order of operands)
다음 목록의 표현식들은 모두 아래에서 설명할 동일한 방식으로 피연산자를 평가해요. (그 밖의 표현식들은 피연산자를 갖지 않거나, 각 페이지에서 설명하는 대로 조건부로 평가합니다.)
- 역참조(dereference) 표현식
- 오류 전파(error propagation) 표현식
- 부정(negation) 표현식
- 산술·논리 이항 연산자
- 비교 연산자
- 타입 캐스트 표현식
- 괄호로 묶인 표현식
- 배열 표현식
- await 표현식
- 인덱스 표현식
- 튜플 표현식
- 튜플 인덱스 표현식
- 구조체 표현식
- 호출 표현식
- 메서드 호출 표현식
- 필드 표현식
- break 표현식
- 범위 표현식
- return 표현식
이 표현식들의 피연산자는 표현식의 효과를 적용하기 전에 평가돼요. 여러 피연산자를 갖는 표현식은 소스 코드에 적힌 대로 왼쪽에서 오른쪽으로 평가됩니다.
참고: 어떤 하위 표현식이 어떤 표현식의 피연산자인지는 앞 절의 표현식 우선순위에 따라 결정돼요.
예를 들어, 아래 두 next 메서드 호출은 항상 같은 순서로 호출돼요.
#![allow(unused)]
fn main() {
// 배열 대신 vec을 사용 (이 예제가 작성될 당시 안정적인
// 소유 배열 반복자(owned array iterator)가 없었으므로, 참조를 피하기 위함)
let mut one_two = vec![1, 2].into_iter();
assert_eq!(
(1, 2),
(one_two.next().unwrap(), one_two.next().unwrap())
);
}
참고: 이것은 재귀적으로 적용되므로, 이 표현식들도 가장 안쪽에서 바깥쪽으로 평가되어요. 안쪽 하위 표현식이 없어질 때까지 형제는 무시됩니다.
위치 표현식과 값 표현식 (Place expressions and value expressions)
표현식은 두 가지 큰 범주로 나뉘어요: **위치 표현식(place expression)**과 값 표현식(value expression). 그리고 세 번째로 작은 범주인 **할당 대상 표현식(assignee expression)**도 있어요. 각 표현식 내에서 피연산자는 위치 맥락(place context) 또는 값 맥락(value context) 중 하나로 나타날 수 있습니다. 표현식의 평가는 그 자신의 범주와 놓인 맥락 둘 다에 의존해요.
위치 표현식은 메모리 위치를 나타내는 표현식이에요. 다음과 같은 것들이 위치 표현식입니다.
그 외의 모든 표현식은 값 표현식이에요. 값 표현식은 실제 값을 나타내는 표현식입니다.
위치 표현식 맥락
다음 맥락들이 위치 표현식 맥락이에요.
- 복합 할당(compound assignment) 표현식의 왼쪽 피연산자
- 단항 빌림, raw 빌림, 또는 역참조 연산자의 피연산자
- 필드 표현식의 피연산자
- 배열 인덱싱 표현식에서 인덱싱되는 피연산자
- 튜플 인덱싱 표현식의 튜플 피연산자
- 어떤 암시적 빌림의 피연산자
let문장의 초기화자(initializer)if let,match,while let표현식의 조사 대상(scrutinee)- 기능 업데이트(functional update) 구조체 표현식의 베이스
참고: 역사적으로 위치 표현식을 lvalue, 값 표현식을 rvalue라고 불렀어요.
할당 대상 표현식 (Assignee expression)
할당 대상 표현식은 할당(assignment) 표현식의 왼쪽 피연산자에 나타나는 표현식이에요. 구체적으로는:
- 위치 표현식
- 밑줄(Undercores)
- 할당 대상 표현식의 튜플
- 할당 대상 표현식의 슬라이스
- 할당 대상 표현식의 튜플 구조체
- 할당 대상 표현식의 구조체 (이름 있는 필드일 수도 있음)
- 유닛 구조체
할당 대상 표현식 안에서는 임의의 괄호 사용이 허용됩니다.
이동(move)되는 타입과 복사(copy)되는 타입
위치 표현식이 값 표현식 맥락에서 평가되거나, 패턴에서 값으로 바인딩되면, 그 메모리 위치에 저장된 값을 나타냅니다.
그 값의 타입이 Copy를 구현한다면 값이 복사돼요.
나머지 상황에서 그 타입이 Sized라면 값을 **이동(move)**할 수 있어요.
다음 위치 표현식들만 이동될 수 있습니다.
- 현재 빌려지지 않은 변수
- 임시 값(Temporary values)
- 이동될 수 있고
Drop을 구현하지 않은 위치 표현식의 필드 Box<T>타입의 표현식을 역참조한 결과로, 이동될 수도 있는 것
지역 변수로 평가되는 위치 표현식에서 이동한 뒤에는 그 위치가 초기화되지 않은(deinitialized) 상태가 되고, 다시 초기화하기 전까지 읽을 수 없어요.
이 외의 모든 경우에서 위치 표현식을 값 표현식 맥락에서 사용하는 것은 오류예요.
가변성 (Mutability)
위치 표현식이 할당되거나, 가변으로 빌려지거나, 암시적으로 가변 빌려지거나, ref mut를 포함하는 패턴에 바인딩되려면 **가변(mutable)**이어야 해요. 우리는 이런 것들을 **가변 위치 표현식(mutable place expression)**이라고 부르고, 반대는 **불변 위치 표현식(immutable place expression)**이라고 불러요.
다음 표현식들이 가변 위치 표현식 맥락이 될 수 있어요.
- 현재 빌려지지 않은 가변 변수
- 가변
static아이템 - 임시 값
- 필드(Field): 하위 표현식을 가변 위치 표현식 맥락에서 평가해요.
*mut T포인터의 역참조&mut T타입의 변수 또는 변수의 필드 역참조. 참고: 이건 다음 규칙의 요구사항에 대한 예외예요.DerefMut를 구현하는 타입의 역참조: 이때 역참조되는 값은 가변 위치 표현식 맥락에서 평가되어야 해요.IndexMut를 구현하는 타입의 배열 인덱싱: 이때 인덱싱되는 값(인덱스는 아님)을 가변 위치 표현식 맥락에서 평가해요.
임시 값 (Temporaries)
대부분의 위치 표현식 맥락에서 값 표현식을 사용하면, 이름 없는 임시 메모리 위치가 만들어지고 그 값으로 초기화돼요. 표현식은 대신 그 위치로 평가됩니다. 다만 static으로 승격(promoted)되는 경우는 예외예요. 임시 값의 드롭 스코프(drop scope)는 보통 바깥 문장의 끝이에요.
슈퍼 매크로 (Super macros)
일부 내장 매크로는 스코프가 확장될 수 있는 임시 값을 만들 수 있어요. 이런 임시 값을 슈퍼 임시 값(super temporary), 이런 매크로를 **슈퍼 매크로(super macro)**라고 불러요. 이 매크로의 호출을 슈퍼 매크로 호출 표현식, 매크로에 전달되는 인자를 **슈퍼 피연산자(super operand)**라고 해요.
참고: 슈퍼 매크로 호출 표현식이 확장 표현식(extending expression)이면, 그 슈퍼 피연산자들도 확장 표현식이고, 슈퍼 임시 값의 스코프도 확장돼요.
format_args!
포맷 문자열 인자를 제외하고, format_args!에 전달되는 모든 인자는 슈퍼 피연산자예요.
#![allow(unused)]
fn main() {
fn temp() -> String { String::from("") }
// 호출이 확장 표현식이고 인자가 슈퍼 피연산자이므로,
// 내부 블록도 확장 표현식이 되고,
// 따라서 그 마지막 표현식에서 만들어진 임시 값의 스코프도 확장된다.
let _ = format_args!("{}", { &temp() }); // OK
}
format_args!의 슈퍼 피연산자들은 암시적으로 빌려지므로 위치 표현식 맥락이에요. 값 표현식이 인자로 전달되면 슈퍼 임시 값을 만들어요.
#![allow(unused)]
fn main() {
fn temp() -> String { String::from("") }
let x = format_args!("{}", temp());
x; // <-- 임시 값이 확장되어 여기서 사용 가능
}
format_args! 호출의 확장은 때로 내부 슈퍼 임시 값을 만들기도 해요.
#![allow(unused)]
fn main() {
let x = {
// 이 호출은 내부 임시 값을 만든다.
let x = format_args!("{:?}", 0);
x // <-- 임시 값이 확장되어 여기서 사용 가능
}; // <-- 임시 값은 여기서 드롭
x; // ERROR
}
#![allow(unused)]
fn main() {
// 이 호출은 내부 임시 값을 만들지 않는다.
let x = { let x = format_args!("{}", 0); x };
x; // OK
}
참고:
format_args!가 내부 임시 값을 언제 만들고 언제 만들지 않는지에 대한 세부 사항은 **현재 미지정(unspecified)**이에요.
pin!
pin!의 인자는 슈퍼 피연산자예요.
#![allow(unused)]
fn main() {
use core::pin::pin;
fn temp() {}
// 위의 `format_args!`와 동일하게
let _ = pin!({ &temp() }); // OK
}
pin!의 인자는 값 표현식 맥락이고 슈퍼 임시 값을 만들어요.
#![allow(unused)]
fn main() {
use core::pin::pin;
fn temp() {}
// 인자는 슈퍼 임시 값으로 평가된다.
let x = pin!(temp());
// 임시 값이 확장되어 여기서 사용 가능
x; // OK
}
암시적 빌림 (Implicit borrows)
일부 표현식은 특정 피연산자를 암시적으로 빌려서 위치 표현식처럼 취급해요. 예를 들어 크기가 정해지지 않은 두 슬라이스의 동등성을 직접 비교할 수 있는 것은, == 연산자가 피연산자를 암시적으로 빌리기 때문이에요.
#![allow(unused)]
fn main() {
let c = [1, 2, 3];
let d = vec![1, 2, 3];
let a: &[i32];
let b: &[i32];
a = &c;
b = &d;
// ...
*a == *b;
// 동등한 형태:
::std::cmp::PartialEq::eq(&*a, &*b);
}
다음 표현식들에서 암시적 빌림이 일어날 수 있어요.
- 메서드 호출 표현식의 왼쪽 피연산자
- 필드 표현식의 왼쪽 피연산자
- 호출 표현식의 왼쪽 피연산자
- 배열 인덱싱 표현식의 왼쪽 피연산자
- 역참조 연산자(
*)의 피연산자 - 비교의 피연산자
- 복합 할당의 왼쪽 피연산자
format_args!의 인자(포맷 문자열 제외)
오버로딩 트레이트 (Overloading traits)
아래의 많은 연산자와 표현식은 std::ops 또는 std::cmp의 트레이트를 사용해 다른 타입에 대해 오버로드될 수 있어요. 이 트레이트들은 core::ops와 core::cmp에도 같은 이름으로 존재합니다.
표현식 속성 (Expression attributes)
바깥 속성(Outer attribute)은 표현식 앞에서 몇 가지 특정한 경우에만 허용돼요.
그리고 다음 앞에서는 절대 허용되지 않아요.
- 범위(Range) 표현식
- 이항 연산자 표현식 (ArithmeticOrLogicalExpression, ComparisonExpression, LazyBooleanExpression, TypeCastExpression, AssignmentExpression, CompoundAssignmentExpression)