ジェネリック型ではなく関連型を使用するのが適切なのはいつですか?


108

この質問、問題は、関連する型にジェネリック型パラメータを使用しての試みを変更することで解決できることを生じました。そのため、「ここでは関連するタイプの方が適しているのはなぜですか?」という質問があり、詳細を知りたくなりました。

関連するタイプを導入RFCは言います:

このRFCは、以下によって特性マッチングを明確にしています。

  • すべての形質型パラメータを処理する入力タイプ、および
  • 出力タイプである関連タイプを提供します

RFCは動機付けの例としてグラフ構造を使用しおり、これはドキュメントでも使用されていますが、型パラメーター化バージョンよりも関連する型バージョンの利点を十分に理解していないことを認めます。主なことは、distanceメソッドがEdgeタイプを気にする必要がないことです。これはいいですが、関連する型がある理由は少し浅いようです。

関連付けられた型は実際に使用するとかなり直感的であることがわかりましたが、自分のAPIでそれらをどこでいつ使用するかを決定するときに苦労しています。

コードを作成するとき、ジェネリック型パラメーターよりも関連する型を選択する必要があるのはいつですか?

回答:


75

これはRustプログラミング言語の第2版​​で触れられています。ただし、もう少し詳しく見てみましょう。

簡単な例から始めましょう。

特性メソッドを使用するのが適切なのはいつですか?

遅延バインディングを提供する方法は複数あります。

trait MyTrait {
    fn hello_word(&self) -> String;
}

または:

struct MyTrait<T> {
    t: T,
    hello_world: fn(&T) -> String,
}

impl<T> MyTrait<T> {
    fn new(t: T, hello_world: fn(&T) -> String) -> MyTrait<T>;

    fn hello_world(&self) -> String {
        (self.hello_world)(self.t)
    }
}

実装/パフォーマンス戦略を無視して、上記の両方の抜粋により、ユーザーは動的な方法hello_worldで動作方法を指定できます。

1つの違い(意味的に)は、trait実装が、特定の型をT実装するtrait場合、hello_world常に同じ動作をすることを保証するのに対し、struct実装ではインスタンスごとに異なる動作を許可することです。

メソッドの使用が適切かどうかは、ユースケースに依存します!

関連付けられたタイプを使用するのが適切なのはいつですか?

trait上記のメソッドと同様に、関連する型はレイトバインディングの形式です(コンパイル時に発生します)。これにより、のユーザーは、trait特定のインスタンスに対してどの型に置き換えるかを指定できます。それが唯一の方法ではありません(したがって問題です):

trait MyTrait {
    type Return;
    fn hello_world(&self) -> Self::Return;
}

または:

trait MyTrait<Return> {
    fn hello_world(&Self) -> Return;
}

上記のメソッドの遅延バインディングと同等です。

  • 最初のものは、与えられたものSelfReturn関連付けられた単一の
  • 二つ目は、代わりに、実現可能MyTraitのためのSelf複数のためにReturn

どの形式がより適切であるかは、単一性を強制することが理にかなっているかどうかによって異なります。例えば:

  • Deref unicityがないとコンパイラーは推論中に狂ってしまうため、関連する型を使用します
  • Add 2つの引数が与えられると論理的な戻り値の型が存在すると考えられているため、関連付けられた型を使用します

ご覧のとおりDeref、明らかなユースケース(技術的な制約)ですが、のケースAddはあまり明確ではありません。i32 + i32どちらか一方i32またはComplex<i32>コンテキストに応じて譲歩することには意味があるのでしょうか?それでも、著者は判断を下して、追加の戻り値の型のオーバーロードは不要であると判断しました。

私の考えでは、正解はありません。それでも、unicityの引数を超えて、関連する型は指定する必要のあるパラメーターの数を減らすため、トレイトの使用を容易にすることを述べます。したがって、通常のトレイトパラメーターを使用する柔軟性の利点が明らかでない場合は、関連するタイプから始めることをお勧めします。


4
少し簡略化してみましょう: trait/struct MyTrait/MyStruct1つimpl MyTrait forまたはを許可しますimpl MyStruct。 汎用であるためtrait MyTrait<Return>、複数implのを使用できます。Returnどのタイプでもかまいません。一般的な構造体は同じです。
ポールセバスチャンマノール

2
あなたの答えは、「The Rust Programming Language」よりもはるかに理解しやすいと思います
drojf 2018年

「最初の1つは、特定のSelfに関連付けられた単一のReturnがあることを強制します。」これは当面の意味では当てはまりますが、一般的な特性を使用してサブクラス化することで、この制限を回避できます。おそらく、統一性は提案にすぎず、強制されることはありません
joel

36

関連付けられた型はグループ化メカニズムであるため、型をグループ化することに意味がある場合に使用する必要があります。

Graphドキュメントで紹介した形質はその一例です。あなたはGraphをジェネリックにしたいのですが、特定の種類のを取得したらGraphNodeEdgeタイプを変更する必要はもうありません。特定のケースでGraphは、単一の実装内でこれらのタイプを変更したくはなく、実際には常に同じであることが望まれます。それらはグループ化されているか、関連付けられていると言うこともできます。


4
理解するのに少し時間がかかりました。私にとっては、いくつかのタイプを一度に定義することに似ています。エッジとノードはグラフから理解できません。
tafia
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.