rokdocs
v0.1

3.4. 대입과 복합 대입

대입 (=) 과 복합 대입 (+=, -= 등) 은 변수, 필드, 또는 인덱싱된 자리의 값을 갱신하는 표현식입니다. 문법은 다음과 같이 정의됩니다.

assign_expr     ::= lvalue assign_op expr
assign_op       ::= "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|="
                  | "^=" | "<<=" | ">>="
lvalue          ::= identifier
                  | lvalue "." identifier
                  | lvalue "." int_lit                 // tuple position
                  | lvalue "[" expr "]"                // index
                  | lvalue "[" range_expr "]"          // slice

3.4.1. 단순 대입

단순 대입은 다음과 같은 모양을 갖습니다.

x = expr;
obj.field = expr;
arr[i] = expr;

좌변 (lvalue) 은 재할당이 가능한 자리이고, 우변 (rvalue) 은 임의의 표현식이며, 그 결과 값은 단위 타입 (()) 입니다. 대입 자체는 표현식이지만 결과가 unit 이기 때문에 보통 문장으로 쓰게 됩니다. 덕분에 C 의 if (x = read()) 같은 패턴은 사용 빈도가 낮으므로 unit 결과로 자연스럽게 차단됩니다.

좌변 (lvalue) 의 종류

좌변에 올 수 있는 형태와 그 의미는 다음과 같습니다.

형태예의미
단순 변수x = ...변수의 슬롯에 값을 씁니다
필드 접근obj.field = ...객체의 필드에 값을 씁니다
튜플 위치 접근pair.0 = ...tuple struct / 튜플의 위치에 값을 씁니다
인덱싱arr[i] = ...인덱싱 자리에 값을 씁니다
슬라이싱arr[1:3] = ...슬라이스 영역에 값을 씁니다

이 가운데 인덱싱 및 슬라이싱 대입은 인덱싱과 슬라이싱 의 쓰기 트레잇으로 desugar 됩니다.

좌변이 될 수 없는 자리

다음과 같은 자리들은 좌변이 될 수 없습니다.

1 = 2;                    // 리터럴은 좌변 아님
foo() = 2;                // 호출의 결과는 일반적으로 좌변 아님
(x + 1) = 2;              // 산술 결과는 좌변 아님

좌변이 될 수 있는지 여부는 컴파일 타임에 검사되며, 형태 (모양) 만으로 결정됩니다. 런타임 검사는 따로 수행하지 않습니다.

3.4.2. 변수의 새 선언과 재할당

키워드 의 let 키워드와 결합하여 다음 두 형태가 구분됩니다.

let x = 1;       // 새 선언, 'let' 필수
x = 2;           // 재할당, 'let' 없이

새 변수를 만들려면 let 을 붙여야 하고, 기존 변수의 슬롯에 다시 쓰려면 let 없이 적습니다. let 없이 선언되지 않은 이름에 대입하면 컴파일 에러가 납니다. 정확한 규칙은 변수 선언 에서 정의합니다.

counter = counter + 1;     // counter 가 선언되어 있어야 OK
coutner = ...;             // ERROR: 'coutner' not declared. Did you mean 'counter'?

이러한 강제는 오타가 새 변수를 만들어 버리는 함정 (Julia 와 Python 에서 흔한 버그) 을 차단합니다.

3.4.3. 복합 대입

복합 대입은 연산과 대입을 한 줄에 묶어 표현하는 형태입니다.

x += 1;            // x = x + 1 과 의미상 동일
x -= 1;
x *= 2;
arr[i] += 1;

구두점과 연산자 토큰 의 복합 대입 토큰 (+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=) 은 모두 같은 패턴을 따릅니다.

desugar 와 좌변 평가

복합 대입은 다음과 같이 desugar 됩니다. 타입이 OpAssign 트레잇을 구현하면 a += b 는 그것을 부르고, 아니면 a = a + b 입니다 (stdlib 2.4.1). 모든 연산자가 같습니다.

lvalue OP= rhs       →   OpAssign::op_assign(ref lvalue, rhs)   // 구현되어 있으면
                     →   lvalue = (lvalue OP rhs)               // 아니면

이때 좌변이 두 번 평가되지 않도록 컴파일러가 보장합니다.

arr[expensive_index()] += 1;
// expensive_index() 는 한 번만 호출됨

좌변이 순수한 변수나 필드라면 그대로 두 번 참조하지만, 좌변이 호출이나 계산을 포함하는 경우에는 그 부분을 임시 변수로 묶어 한 번만 평가합니다. 이 보장 덕분에 부작용이 있는 인덱스나 메서드가 의도치 않게 두 번 호출되는 함정을 차단할 수 있습니다.

트레잇 매핑

각 복합 대입은 별개의 Assign 트레잇 패밀리로 매핑됩니다.

x += y       →   <typeof(x) as AddAssign<typeof(y)>>::add_assign(ref x, y)
x -= y       →   <typeof(x) as SubAssign<typeof(y)>>::sub_assign(ref x, y)

(AddAssign, SubAssign, MulAssign, DivAssign, ... 등이 있습니다.)

이 트레잇은 제자리 갱신 (in-place update) 으로 구현될 수 있으므로, x = x + y 보다 효율적인 구현이 가능합니다. 예를 들어 큰 컬렉션의 원소 추가가 새 컬렉션 생성 없이 수행됩니다.

AddAssign 구현은 선택입니다: 없으면 x += y 는 x = x + y 로 대체됩니다. x + y 자체에는 언제나 Add 가 필요합니다.

시그니처

AddAssign 등의 시그니처는 다음과 같은 모양을 갖습니다.

trait AddAssign<Rhs = Self> {
    #[infix("+=")]
    fn add_assign(ref self, rhs: Rhs);
}

다른 연산자처럼 *Assign 트레잇도 #[infix("op=")] 로 등록합니다 (3.1). ref self 로 좌변의 슬롯을 참조하며, 반환 타입은 없고 결과는 unit 입니다. ref self 의 정확한 의미와 좌변 슬롯의 갱신 의미는 함수 사양에서 정의합니다.

3.4.4. 다중 대입

C 와 Python 식의 연쇄 대입 (a = b = c = 1) 은 rok 에서 허용되지 않습니다.

a = b = c = 1;             // ERROR

대입의 결과 값이 unit 이므로, b = c = 1 의 결과가 unit 이 되어 a = (unit) 이 되어 버리고, 따라서 의미가 없어집니다. 의도한 의미를 적으려면 다음처럼 분리해서 적습니다.

c = 1; b = 1; a = 1;

또는 분해 대입을 활용하면 됩니다.

3.4.5. 표현식 안에서의 대입

대입은 표현식이고 결과는 unit 이므로 표현식 자리에 끼워 넣을 수 있지만, 그렇게 할 가치는 거의 없습니다.

let x = (y = 1);           // x = unit (의미가 거의 없음)

이러한 패턴은 권장되지 않으며, 도구나 린터가 경고를 낼 수 있습니다.

문장 자리가 아닌 곳에서 대입은 괄호 (y = 1) 로 감싸거나 match arm 의 본체로 적습니다. 괄호 없는 let x = y = 1; 은 지원하지 않습니다.

GitHub에서 이 페이지 고치기