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 では struct、enum、または union を指します。

ADT の表現

MyStruct<u32> のような型の例を考えてみましょう。ここで MyStruct は次のように定義されています。

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

MyStruct<u32>TyKind::Adt のインスタンスになります。

Adt(&'tcx AdtDef, GenericArgs<'tcx>)
//  ------------  ---------------
//  (1)            (2)
//
// (1) は `MyStruct` の部分を表します
// (2) は `<u32>`、つまり「置換」/ ジェネリック引数を表します

2 つの部分があります。

  • AdtDef は struct/enum/union を参照しますが、その型 パラメーターに対する値は含みません。 この例では、これは引数 u32 なしMyStruct の部分です。 (HIR では struct、enum、union は異なる形で表現されますが、ty::Ty では、 それらはすべて TyKind::Adt を使って表現されることに注意してください。)
  • GenericArgs は、ジェネリックパラメーターに置換される値のリストです。 MyStruct<u32> の例では、[u32] のようなリストになります。 ジェネリクスと置換については、少し後で詳しく掘り下げます。

AdtDefDefId

ソースコード内で定義されたすべての型には、一意の DefId があります(この 章を参照)。 これには ADT とジェネリクスが含まれます。 上で示した MyStruct<T> の定義では、 2 つの DefId があります。1 つは MyStruct 用で、もう 1 つは T 用です。 上のコードは u32 用の新しい DefId を生成しないことに注意してください。 なぜなら、それはそのコード内で定義されていない(参照されているだけ)からです。

AdtDef は、多くの便利なヘルパーメソッドを持つ DefId のラッパーのようなものです。 AdtDefDefId の間には、基本的に 1 対 1 の関係があります。 tcx.adt_def(def_id) クエリを使って、DefId に対する AdtDef を取得できます。 AdtDef はすべて intern されており、これは 'tcx ライフタイムによって示されています。

質問: なぜ AdtDef の「内部」で置換しないのか?

ジェネリック struct を (AdtDef, args) で表現することを思い出してください。 では、なぜこの仕組みをわざわざ使うのでしょうか?

別の方法として、型を表現する際に、すべての型がすでに置換された、 完全に置換済みの AdtDef の形を常に新しく作成する、という選択もできたはずです。 これは手間が少ないように見えます。 しかし、(AdtDef, args) という仕組みには、これよりいくつか利点があります。

まず、(AdtDef, args) の仕組みには効率上の利点があります。

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

// やりたいこと: MyStruct<A> ==> MyStruct<B>

このような例では、A への 1 つの参照を B に置き換えるだけで、 MyStruct<A>MyStruct<B>(など)として非常に低コストにインスタンス化できます。 しかし、すべてのフィールドを eager にインスタンス化した場合、 AdtDef 内のすべてのフィールドを走査して、 それらすべての型を更新しなければならない可能性があるため、はるかに多くの作業になることがあります。

もう少し深く言うと、これは Rust における struct が nominalであることに対応します。 つまり、それらはその名前によって定義されるということです(そして、その内容はその名前の定義からインデックス付けされ、 型そのものの「内部」に一緒に保持されるわけではありません)。

GenericArgs

ジェネリック型 MyType<A, B, …> が与えられた場合、MyType のジェネリック引数のリストを保存する必要があります。

rustc では、これは GenericArgs を使って行われます。 GenericArgs は、ジェネリック項目に対するジェネリック引数のリストを表す GenericArg のスライスへの thin pointer です。 たとえば、2 つの型パラメーター KV を持つ struct HashMap<K, V> が与えられた場合、型 HashMap<i32, u32> を表すために使われる GenericArgs&'tcx [tcx.types.i32, tcx.types.u32] によって表されます。

GenericArg は概念的には 3 つのバリアントを持つ enum で、型引数、const 引数、ライフタイム引数にそれぞれ 1 つずつ対応します。 実際には、これは GenericArgKind によって表現され、GenericArg はそれを GenericArgKind に変換するメソッドを持つ、より空間効率の良いバージョンです。

実際の GenericArg struct は、型、ライフタイム、または const を、下位 2 ビットに discriminant を格納した intern 済みポインターとして保存します。 GenericArgs の実装そのものに取り組んでいる場合を除き、通常は GenericArg を直接扱う必要はなく、代わりに GenericArg::unpack() メソッドを介して取得できる安全な GenericArgKind 抽象を利用するべきです。

場合によっては GenericArg を構築しなければならないことがあります。これは Ty/Const/Region::into() または GenericArgKind::pack によって行えます。

// ジェネリック引数を unpack および pack する例。
fn deal_with_generic_arg<'tcx>(generic_arg: GenericArg<'tcx>) -> GenericArg<'tcx> {
    // 生の `GenericArg` を安全に扱うために unpack する。
    let new_generic_arg: GenericArgKind<'tcx> = match generic_arg.unpack() {
        GenericArgKind::Type(ty) => { /* ... */ }
        GenericArgKind::Lifetime(lt) => { /* ... */ }
        GenericArgKind::Const(ct) => { /* ... */ }
    };
    // `GenericArgKind` を pack してジェネリック args リストに格納する。
    new_generic_arg.pack()
}

すべてをまとめると、次のようになります。

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

Foo 型エイリアス内に書かれた MyStruct<U> については、次のように表現します。

  • MyStruct に対する AdtDef(および対応する DefId)があります。
  • リスト [GenericArgKind::Type(Ty(u32))] を含む GenericArgs があります。
  • そして最後に、上に挙げた AdtDefGenericArgs を持つ TyKind::Adt があります。

ネストしたジェネリック args

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] になります。

generics_of クエリによって返される ty::Generics 型は、ネストした階層がどのように リストへ平坦化されるかに関する情報を含み、GenericArgs リスト内のどのインデックスがどの ジェネリックに対応するかを把握できるようにします。 その動作の一般的な考え方は、外側から内側へ(例では TT2 より前)、左から右へ (T2T3 より前)というものですが、いくつかの複雑な点があります。

  • trait には暗黙の Self ジェネリックパラメーターがあり、これは最初(つまり 0 番目)のジェネリックパラメーターです。Self はすべての状況でジェネリックパラメーターを意味するわけではないことに注意してください。Res::SelfTyAliasRes::SelfCtor を参照してください。
  • early-bound ジェネリックパラメーターのみが含まれ、late-bound ジェネリクスは含まれません。
  • … などです …

平坦化がどのように動作するかの正確な詳細については、ty::Generics を確認してください。