배열 표현식과 배열 인덱스 표현식
배열 표현식과 배열 인덱스 표현식
같은 타입의 값 여러 개를 순서대로 묶어 하나로 다루고 싶을 때가 있어요. Rust에서는 배열(array) 이 딱 그 용도예요. 이번에는 배열을 어떻게 만들고, 어떻게 인덱스로 접근하는지 살펴볼게요.
출처: Rust Reference
본문
배열 표현식 (Array expressions)
배열 표현식(array expression) 은 배열을 만들어 내요. 배열 표현식은 두 가지 형태가 있어요.
ArrayExpression → [ ArrayElements? ]
ArrayElements →
Expression ( , Expression )* ,?
| Expression ; Expression
첫 번째 형태는 배열의 값을 하나씩 모두 나열하는 방식이에요. 문법은 대괄호(square brackets) 로 감싼, 같은 타입의 표현식들을 쉼표로 구분한 목록이에요. 이 형태는 적어 놓은 순서 그대로의 값들을 담은 배열을 만들어 내요.
두 번째 형태는 세미콜론(;)으로 구분된 두 개의 표현식을 대괄호로 감싼 형태예요. 여기서 ; 앞의 표현식을 반복 피연산자(repeat operand) 라고 하고, ; 뒤의 표현식을 길이 피연산자(length operand) 라고 해요.
길이 피연산자에는 제약이 있어요. 유추된 상수(inferred const) 이거나 usize 타입의 상수 표현식(예: 리터럴이나 상수 아이템)이어야 해요.
#![allow(unused)]
fn main() {
const C: usize = 1;
let _: [u8; C] = [0; 1]; // Literal.
let _: [u8; C] = [0; C]; // Constant item.
let _: [u8; C] = [0; _]; // Inferred const.
let _: [u8; C] = [0; (((_)))]; // Inferred const.
}
여기서 한 가지 알아둘 점이 있어요. 배열 표현식 안에서 유추된 상수는 표현식으로 파싱(parse) 되지만, 의미상으로는 별도의 상수 제네릭 인자(const generic argument) 로 취급돼요.
이 두 번째 형태는 길이 피연산자의 값만큼의 길이를 가진 배열을 만들고, 각 요소는 반복 피연산자의 복사본이에요. 즉 [a; b]는 a라는 값을 b개 담은 배열을 만들어 내요.
길이 피연산자의 값이 1보다 크다면, 반복 피연산자의 타입이 Copy를 구현해야 하거나, const 블록 표현식이거나, 상수 아이템으로 가는 경로(path) 여야 해요.
반복 피연산자가 const 블록이나 상수 아이템 경로라면, 그건 길이 피연산자에 지정된 횟수만큼 평가돼요. 그 값이 0이라면 const 블록이나 상수 아이템은 아예 평가되지 않아요. 반면 const 블록도 상수 아이템도 아닌 표현식은 정확히 한 번만 평가되고, 그 결과가 길이 피연산자의 값만큼 복사돼요.
예시를 몇 개 볼게요.
#![allow(unused)]
fn main() {
[1, 2, 3, 4];
["a", "b", "c", "d"];
[0; 128]; // array with 128 zeros
[0u8, 0u8, 0u8, 0u8,];
[[1, 0, 0], [0, 1, 0], [0, 0, 1]]; // 2D array
const EMPTY: Vec<i32> = Vec::new();
[EMPTY; 2];
}
배열·슬라이스 인덱스 표현식 (Array and slice indexing expressions)
배열 타입이나 슬라이스 타입의 값은 그 뒤에 usize 타입의 표현식을 대괄호로 감싼 형태(인덱스)를 붙여서 인덱싱할 수 있어요. 배열이 가변(mutable)이라면, 그 결과로 얻은 메모리 위치에 할당할 수도 있어요.
IndexExpression → Expression [ Expression ]
배열·슬라이스가 아닌 다른 타입에서는 인덱스 표현식 a[b]가 *std::ops::Index::index(&a, b)와 동등해요. 가변 위치 표현식(mutable place expression) 맥락에서는 *std::ops::IndexMut::index_mut(&mut a, b)와 동등하죠. 단, 예외가 하나 있는데, 인덱스 표현식이 임시값 수명 연장(temporary lifetime extension) 을 겪을 때는 인덱싱되는 표현식 a의 임시 스코프도 함께 연장돼요. 메서드와 마찬가지로, Rust는 a에 역참조 연산을 반복해서 넣어 구현체를 찾아내기도 해요.
여기서 흥미로운 대조를 보여 주는 예시가 있어요. 첫 번째 코드는 vec![()]의 결과를 담는 임시값이 블록 끝까지 살아남도록 연장돼서, 이후 문장에서 x를 계속 쓸 수 있어요.
#![allow(unused)]
fn main() {
// The temporary holding the result of `vec![()]` is extended to
// live to the end of the block, so `x` may be used in subsequent
// statements.
let x = &vec![()][0];
x;
}
반면 두 번째 코드는 임시값이 문장 끝에서 바로 드롭되기 때문에, 이후에 y를 쓰면 오류가 나요.
#![allow(unused)]
fn main() {
// The temporary holding the result of `vec![()]` is dropped at the
// end of the statement, so it's an error to use `y` after.
let y = &*std::ops::Index::index(&vec![()], 0); // ERROR
y;
}
배열과 슬라이스의 인덱스는 0부터 시작(zero-based) 해요. 그리고 배열 접근은 상수 표현식이라, 상수 인덱스 값이 있으면 경계를 컴파일 타임에 검사할 수 있어요. 그게 아니라면 런타임에 검사가 이뤄지고, 실패하면 그 스레드는 패닉 상태(panicked state) 가 돼요.
#![allow(unused)]
fn main() {
// lint is deny by default.
#![warn(unconditional_panic)]
([1, 2, 3, 4])[2]; // Evaluates to 3
let b = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
b[1][2]; // multidimensional array indexing
let x = (["a", "b"])[10]; // warning: index out of bounds
let n = 10;
let y = (["a", "b"])[n]; // panics
let arr = ["a", "b"];
arr[10]; // warning: index out of bounds
}
배열과 슬라이스가 아닌 타입에서도 Index와 IndexMut 트레이트를 구현하면 배열 인덱스 표현식을 쓸 수 있어요.
더 알아보기 (Learn more)
- Rust Reference - Array expressions — 본문 원문
- Rust Reference - Types (Array) — 배열 타입 자체에 대한 설명