3

最近、私はTrieデータ構造を実装していましたが、ノードがさまざまなタイプのデータを格納したり、その実装を変更したりできると判断したため、Node<T>. 次に、Trie を構築するためのアルゴリズムに取りかかると、Node に関するより詳細な知識が必要であることに気付きました。そのため、ジェネリック クラスがINodeインターフェイスを使用するように制限しました。これにより柔軟性が向上しますが、ジェネリック クラスのコンテキストでは正しくないと感じました。

ジェネリック クラスには、インターフェイスを実装するクラスとは異なるユース ケースがあります。たとえばList<T>、アルゴリズムは、関連する一連の抽象化に依存せずに機能します。インターフェースを実装するクラスはポリモーフィズム/DI を必要とする場合がありますが、インターフェースはより特化されます。

T がより特殊化されたインターフェイスを実装する可能性があるジェネリック クラス T を他のユーザーが適用するのはどのような状況ですか?

Tが操作/データを実際に公開する必要がない場合はジェネリッククラスが使用されると思いましたが、TがIDisposableまたはその他のより一般的なインターフェイスを実装する場合にジェネリッククラスを使用できることがわかります。

これらの点を明確にするのに何か助けはありますか?

4

3 に答える 3

5

インターフェイス制約のあるジェネリックを使用するか、インターフェイス型の非ジェネリックを使用するかの選択に直面した場合、ジェネリック引数として渡される型の一部またはすべてが値型である場合にのみ、ジェネリック + インターフェイスを使用します。これにより、私の実装では、私の を処理するときにコストのかかるボックス化とボックス化解除が必要になるのを防ぐことができstructます。

たとえば、インターフェイスがIComparable.

ジェネリック クラスに機能を提供する別の方法は、値と共にデリゲートを渡すことです。たとえば、このようなことを計画している場合

interface IScoreable {
    decimal GetScore(object context);
}
class Node<T> where T : IScoreable {
    ...
    void DoSomething(T data) {
        var score = data.GetScore(someContext);
        ...
    }
}

これを行うこともできます:

class Node<T> {
    private Func<T,object,decimal> scorer;
    public Node(Func<T,object,decimal> scorer) {
        this.scorer = scorer;
    }
    ...
    void DoSomething(T data) {
        var score = scorer(data, someContext);
        ...
    }
}

2 番目の解決策では、スコアリングされるタイプからスコアリング機能を「切り離す」ことができますが、呼び出し元がもう少しコードを書く必要があります。

于 2013-10-03T08:39:28.360 に答える
4

ジェネリック引数に制約を設定することに問題はありません。一般的な引数を持つことは、「これは何に対しても機能する」ことを意味するのではなく、コードが意味を成す方法が複数あることを意味します。

のような完全に一般的な概念を実際に公開するかもしれませんがList<T>、一部のコンテキストでのみ意味を持つ概念を公開する可能性があります ( Nullable<T>null 非許容エンティティに対してのみ意味をなすなど)。

制約は、クラスがどのような状況下で意味を持つかを世界に伝えるために使用するメカニズムであり、その (制約された) 引数を合理的な方法で実際に使用できるようにしますDisposeIDisposable

これの極端な例は、コンテキストが非常に制約されている場合です。つまり、可能な実装が 2 つしかない場合はどうでしょうか。実際、現在のコードベースにはそのケースがあり、ジェネリックを使用しています。いくつかのデータ ポイントに対して何らかの処理を行う必要がありますが、現在 (および近い将来) は 2 種類のデータ ポイントしかありません。これは、原則として、私が使用するコードです。

interface IDataPoint 
{ 
   SomeResultType Process();
}

class FirstKindDataPoint : IDataPoint 
{
   SomeResultType Process(){...}
};

class SecondKindDataPoint : IDataPoint 
{
   SomeResultType Process(){...}
};

class DataPointProcessor<T> where T: IDataPoint
{
   void AcquireAndProcessDataPoints(){...}
}

この制約されたコンテキストでも、これは理にかなっています。なぜなら、プロセッサが 1 つしかないため、処理するロジックは 1 つだけであり、同期を維持するために 2 つの別個のプロセッサを使用する必要はありません。

このようにして、プロセッサ内にList<T>and のAction<T>代わりにList<IDataPoint>andAction<IDataPoint>を含めることができますが、これは私のシナリオでは正しくありません。より具体的なデータ型用のプロセッサが必要なためですIDataPoint

である限り、何でも処理するプロセッサが必要な場合はIDataPoint、その汎用性を取り除き、IDataPointコード内で単純に使用するのが理にかなっているかもしれません。

さらに、@dasblinkenlight の回答で提起された点は非常に有効です。ジェネリック パラメーターが構造体とクラスの両方である場合、ジェネリックを使用するとボックス化が回避されます。

于 2013-10-03T08:29:10.370 に答える
2

ジェネリックは通常、インターフェイスまたは基本クラス (およびこれを含むobject) を使用するだけでは不十分な場合に使用されます。たとえば、関数の戻り値が単なるインターフェイスではなく元の型であることが心配な場合や、パラメーターが渡すのは、特定の型で動作する式である場合があります。

したがって、反対側からロジックにアプローチする場合。型の制限に関する決定は、関数パラメーターの型を選択するときと同じ決定でなければなりません。

于 2013-10-03T08:44:53.590 に答える