표현식

표현식 (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) 중 하나로 나타날 수 있습니다. 표현식의 평가는 그 자신의 범주와 놓인 맥락 둘 다에 의존해요.

위치 표현식메모리 위치를 나타내는 표현식이에요. 다음과 같은 것들이 위치 표현식입니다.

그 외의 모든 표현식은 값 표현식이에요. 값 표현식은 실제 값을 나타내는 표현식입니다.

위치 표현식 맥락

다음 맥락들이 위치 표현식 맥락이에요.

참고: 역사적으로 위치 표현식을 lvalue, 값 표현식을 rvalue라고 불렀어요.

할당 대상 표현식 (Assignee expression)

할당 대상 표현식은 할당(assignment) 표현식의 왼쪽 피연산자에 나타나는 표현식이에요. 구체적으로는:

할당 대상 표현식 안에서는 임의의 괄호 사용이 허용됩니다.

이동(move)되는 타입과 복사(copy)되는 타입

위치 표현식이 값 표현식 맥락에서 평가되거나, 패턴에서 값으로 바인딩되면, 그 메모리 위치에 저장된 값을 나타냅니다.

그 값의 타입이 Copy를 구현한다면 값이 복사돼요.

나머지 상황에서 그 타입이 Sized라면 값을 **이동(move)**할 수 있어요.

다음 위치 표현식들만 이동될 수 있습니다.

지역 변수로 평가되는 위치 표현식에서 이동한 뒤에는 그 위치가 초기화되지 않은(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);
}

다음 표현식들에서 암시적 빌림이 일어날 수 있어요.

오버로딩 트레이트 (Overloading traits)

아래의 많은 연산자와 표현식은 std::ops 또는 std::cmp의 트레이트를 사용해 다른 타입에 대해 오버로드될 수 있어요. 이 트레이트들은 core::opscore::cmp에도 같은 이름으로 존재합니다.

표현식 속성 (Expression attributes)

바깥 속성(Outer attribute)은 표현식 앞에서 몇 가지 특정한 경우에만 허용돼요.

그리고 다음 앞에서는 절대 허용되지 않아요.

더 알아보기