Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADT와 제네릭 인자

ADT라는 용어는 “대수적 데이터 타입(Algebraic data type)“을 뜻하며, Rust에서는 구조체, 열거형 또는 공용체를 가리킵니다.

ADT의 표현

MyStruct<u32>와 같은 타입을 예로 들어 봅시다. 여기서 MyStruct는 다음과 같이 정의되어 있습니다.

struct MyStruct<T> { x: u8, y: T }

MyStruct<u32> 타입은 TyKind::Adt의 인스턴스가 됩니다.

Adt(&'tcx AdtDef, GenericArgs<'tcx>)
//  ------------  ---------------
//  (1)            (2)
//
// (1) represents the `MyStruct` part
// (2) represents the `<u32>`, or "substitutions" / generic arguments

이 인스턴스는 두 부분으로 구성됩니다.

  • AdtDef는 구조체/열거형/공용체를 참조하지만, 그 타입 매개변수에 대한 값은 포함하지 않습니다. 이 예시에서는 인자 u32가 없는 MyStruct 부분이 이에 해당합니다. (HIR에서는 구조체, 열거형, 공용체가 서로 다르게 표현되지만, ty::Ty에서는 모두 TyKind::Adt로 표현된다는 점에 유의하십시오.)
  • GenericArgs는 제네릭 매개변수를 대체할 값들의 목록입니다. MyStruct<u32> 예시에서는 [u32]와 같은 목록이 만들어집니다. 제네릭과 치환에 대해서는 잠시 후에 더 자세히 살펴보겠습니다.

AdtDef와 DefId

소스 코드에 정의된 모든 타입에는 고유한 DefId가 있습니다(이 장을 참고하십시오). 여기에는 ADT와 제네릭도 포함됩니다. 위에서 제시한 MyStruct<T> 정의에는 DefId가 두 개 있습니다. 하나는 MyStruct에 대한 것이고, 다른 하나는 T에 대한 것입니다. 위 코드는 u32에 대해 새로운 DefId를 생성하지 않는다는 점에 주목하십시오. u32는 그 코드에서 정의되지 않고 참조되기만 하기 때문입니다.

AdtDef는 대략 유용한 헬퍼 메서드를 많이 갖춘 DefId의 래퍼라고 할 수 있습니다. AdtDef와 DefId는 본질적으로 일대일 관계입니다. tcx.adt_def(def_id) 쿼리를 사용하면 DefId에 대한 AdtDef를 얻을 수 있습니다. 'tcx 라이프타임에서 알 수 있듯이 AdtDef는 모두 인터닝되어 있습니다.

질문: 왜 AdtDef “내부“에서 대체하지 않습니까?

제네릭 구조체를 (AdtDef, args)로 표현한다는 점을 떠올려 보십시오. 그렇다면 왜 굳이 이러한 방식을 사용합니까?

타입을 표현하는 다른 방법으로, 모든 타입이 이미 대체된 완전히 대체된 형태의 AdtDef를 항상 새로 만드는 방식을 택할 수도 있었습니다. 이 방식이 덜 번거로워 보입니다. 그러나 (AdtDef, args) 방식에는 이에 비해 몇 가지 장점이 있습니다.

첫째, (AdtDef, args) 방식은 효율성 면에서 이점이 있습니다.

struct MyStruct<T> {
  ... 100s of fields ...
}

// Want to do: MyStruct<A> ==> MyStruct<B>

이와 같은 예시에서는 A에 대한 단 하나의 참조를 B로 바꾸기만 하면 MyStruct<A>를 MyStruct<B> 등으로 매우 저렴하게 인스턴스화할 수 있습니다. 하지만 모든 필드를 즉시 인스턴스화한다면 AdtDef의 모든 필드를 순회하며 각 필드의 타입을 모두 갱신해야 할 수도 있으므로 훨씬 더 많은 작업이 필요할 수 있습니다.

좀 더 깊이 들어가면, 이는 Rust의 구조체가 명목적 타입이라는 사실에 대응합니다. 즉, 구조체는 그 이름으로 정의되며, 그 내용은 타입 자체 “안에” 담겨 다니는 것이 아니라 해당 이름의 정의로부터 색인된다는 뜻입니다.

GenericArgs 타입

제네릭 타입 MyType<A, B, …>가 주어지면 MyType에 대한 제네릭 인자 목록을 저장해야 합니다.

rustc에서는 GenericArgs를 사용하여 이를 수행합니다. GenericArgs는 제네릭 항목의 제네릭 인자 목록을 나타내는 GenericArg 슬라이스를 가리키는 얇은 포인터입니다. 예를 들어, 두 개의 타입 매개변수 K와 V를 가진 struct HashMap<K, V>가 주어졌을 때, HashMap<i32, u32> 타입을 나타내는 데 사용되는 GenericArgs는 &'tcx [tcx.types.i32, tcx.types.u32]로 표현됩니다.

GenericArg는 개념적으로 타입 인자용, 상수 인자용, 라이프타임 인자용의 세 가지 변형을 가진 enum입니다. 실제로는 GenericArgKind로 표현되며, GenericArg는 이를 GenericArgKind로 변환하는 메서드를 갖춘, 공간 효율이 더 높은 버전입니다.

실제 GenericArg 구조체는 타입, 라이프타임 또는 상수를 인터닝된 포인터로 저장하며, 판별값은 하위 2비트에 저장됩니다. GenericArgs 구현 자체를 다루는 경우가 아니라면 일반적으로 GenericArg를 직접 다룰 필요가 없으며, 대신 GenericArg::unpack() 메서드로 얻을 수 있는 안전한 GenericArgKind 추상화를 사용하십시오.

경우에 따라 GenericArg를 직접 생성해야 할 수도 있는데, 이는 Ty/Const/Region::into() 또는 GenericArgKind::pack을 통해 할 수 있습니다.

// An example of unpacking and packing a generic argument.
fn deal_with_generic_arg<'tcx>(generic_arg: GenericArg<'tcx>) -> GenericArg<'tcx> {
    // Unpack a raw `GenericArg` to deal with it safely.
    let new_generic_arg: GenericArgKind<'tcx> = match generic_arg.unpack() {
        GenericArgKind::Type(ty) => { /* ... */ }
        GenericArgKind::Lifetime(lt) => { /* ... */ }
        GenericArgKind::Const(ct) => { /* ... */ }
    };
    // Pack the `GenericArgKind` to store it in a generic args list.
    new_generic_arg.pack()
}

이 모든 것을 종합하면 다음과 같습니다:

struct MyStruct<T>(T);
type Foo = MyStruct<u32>

Foo 타입 별칭에 작성된 MyStruct<U>는 다음과 같이 표현됩니다:

  • MyStruct에 대한 AdtDef(및 그에 대응하는 DefId)가 있습니다.
  • [GenericArgKind::Type(Ty(u32))] 목록을 담은 GenericArgs가 있습니다.
  • 마지막으로 위에 나열한 AdtDef와 GenericArgs를 가진 TyKind::Adt가 있습니다.

중첩된 제네릭 인자

struct MyStruct<T>(T);

impl<T> MyStruct<T> {
    fn func<T2, T3>() {}
}

fn main() {
    MyStruct::<u32>::func::<bool, char>();
}

MyStruct::<u32>::func::<bool, char> 구문은 튜플로 표현됩니다. 즉, func를 가리키는 DefId와, 이를 둘러싼 모든 제네릭 매개변수를 “순회“하는 GenericArgs 목록으로 이루어지며, 이 경우 목록은 [u32, bool, char]가 됩니다.

ty::Generics 타입(generics_of 쿼리가 반환함)은 중첩된 계층 구조가 어떻게 하나의 목록으로 평탄화되는지에 대한 정보를 담고 있으며, GenericArgs 목록의 어느 인덱스가 어느 제네릭에 대응하는지 알아낼 수 있게 해 줍니다. 동작 방식의 전반적인 원칙은 가장 바깥쪽에서 가장 안쪽으로(예제에서 T가 T2보다 먼저), 왼쪽에서 오른쪽으로(T2가 T3보다 먼저)이지만, 몇 가지 복잡한 점이 있습니다:

  • 트레이트에는 암묵적인 Self 제네릭 매개변수가 있으며, 이는 첫 번째(즉, 0번째) 제네릭 매개변수입니다. Self가 모든 상황에서 제네릭 매개변수를 의미하는 것은 아니라는 점에 유의하십시오. Res::SelfTyAlias와 Res::SelfCtor를 참고하십시오.
  • 조기 바인딩된 제네릭 매개변수만 포함되며, 후기 바인딩된 제네릭은 포함되지 않습니다.
  • … 그 밖에도 더 있습니다…

평탄화가 정확히 어떻게 동작하는지에 대한 자세한 내용은 ty::Generics를 확인하십시오.