Sized (크기가 정해진 타입)
역참조 강제 변환(Deref coercion)을 살펴본 뒤에도 &str에는 아직 우리가 모르는 비밀이 더 숨겨져 있습니다.
메모리 레이아웃에 대한 이전 논의를 떠올려 보면, &str 역시 스택에서 단일 usize 크기의 포인터로만 표현될 것이라고 생각하기 쉽습니다. 하지만 실제로는 그렇지 않습니다. &str은 포인터와 함께 **메타데이터(metadata)**를 추가로 저장하는데, 바로 가리키고 있는 슬라이스의 길이 정보입니다. 이전 섹션에서 보았던 예시를 다시 살펴봅시다.
let mut s = String::with_capacity(5);
s.push_str("Hello");
// `String`에서 문자열 슬라이스 참조를 만듭니다.
// 첫 번째 바이트를 건너뜁니다.
let slice: &str = &s[1..];
메모리 구조는 다음과 같습니다.
s slice
+---------+--------+----------+ +---------+--------+
스택 | 포인터 | 길이 | 용량 | | 포인터 | 길이 |
| | | 5 | 5 | | | | 4 |
+----|----+--------+----------+ +----|----+--------+
| s |
| |
v |
+---+---+---+---+---+ |
힙(Heap): | H | e | l | l | o | |
+---+---+---+---+---+ |
^ |
| |
+--------------------------------+
어떻게 이런 일이 가능한 걸까요?
동적 크기 타입 (Dynamically Sized Types)
str은 **동적 크기 타입(Dynamically Sized Types, DST)**입니다.
DST는 컴파일 시점에 그 크기를 미리 알 수 없는 타입을 말합니다. &str처럼 DST를 참조할 때는 가리키는 데이터에 대한 추가 정보가 반드시 필요합니다. 이런 참조를 **팻 포인터(fat pointer)**라고 부릅니다.
&str의 경우에는 슬라이스의 길이를 함께 저장하는 것이죠. 앞으로 학습하면서 DST의 다른 예시들도 더 만나보게 될 것입니다.
Sized 트레이트
Rust의 표준 라이브러리(std)에는 Sized라는 트레이트가 정의되어 있습니다.
pub trait Sized {
// 이 트레이트는 비어 있으며, 구현해야 할 메서드가 없습니다.
}
어떤 타입의 크기를 컴파일 시점에 알 수 있다면, 그 타입은 Sized 트레이트를 구현한다고 봅니다. 즉, DST가 아니라는 뜻이죠.
마커 트레이트 (Marker Traits)
Sized는 우리가 처음으로 접하는 **마커 트레이트(marker trait)**의 예시입니다. 마커 트레이트는 별도의 메서드를 구현할 필요가 없는 트레이트입니다. 즉, 어떤 동작(behavior)을 정의하기 위한 것이 아닙니다. 그저 해당 타입이 특정 속성을 가지고 있음을 컴파일러에게 알려주는(marking) 역할만 합니다. 컴파일러는 이 정보를 바탕으로 특정 기능을 활성화하거나 최적화를 수행합니다.
자동 트레이트 (Auto Traits)
특히 Sized는 **자동 트레이트(auto trait)**이기도 합니다. 개발자가 직접 구현할 필요 없이, 컴파일러가 타입 정의를 보고 자동으로 구현해 줍니다.
예제
우리가 지금까지 다뤘던 대부분의 타입은 Sized입니다. u32, String, bool 등이 그 예입니다.
반면 str은 방금 살펴본 것처럼 Sized가 아닙니다. 하지만 &str 자체는 Sized입니다! 컴파일 시점에 그 크기를 확실히 알 수 있기 때문이죠. 포인터용 usize 하나와 길이용 usize 하나, 총 두 개의 usize 크기를 가집니다.
Exercise
The exercise for this section is located in 04_traits/08_sized