web-dev-qa-db-ja.com

ジェネリックスvs共通インターフェース?

前回ジェネリッククラスをいつ書いたか覚えていません。必要だと思うたびに私はそれが必要だと思うたびに、私は必要ないと結論を下します。

この質問 の2番目の回答により、説明を求められました(まだコメントできないため、新しい質問を作成しました)。

それで、ジェネリックが必要な場合の例として、与えられたコードを見てみましょう:

public class Repository<T> where T : class, IBusinessOBject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

タイプ制約があります:IBusinessObject

私の通常の考え方は、クラスがIBusinessObjectを使用するように制約されているため、このRepositoryを使用するクラスもそうであるということです。リポジトリはこれらのIBusinessObjectsを保存します。このRepositoryのクライアントは、IBusinessObjectインターフェースを介してオブジェクトを取得して使用する可能性があります。それで、なぜ

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

つまり、この例は別のコレクションタイプであり、ジェネリックコレクションはクラシックであるため、良い例ではありません。この場合、型制約も奇妙に見えます。

実際、class Repository<T> where T : class, IBusinessbBjectの例は、class BusinessObjectRepositoryによく似ています。これは、ジェネリックが修正するために作成されるものです。

重要な点は、ジェネリックはコレクション以外のすべてに適しているか、型制約はクラス内でジェネリック型パラメーターの代わりにこの型制約を使用するのと同じように特殊化されていないか?

22
jungle_mole

まず、純粋なパラメトリック多態性について説明し、後で制限付き多態性について説明します。

パラメトリック多型

パラメトリック多態性とはどういう意味ですか?まあ、それは型、またはむしろ型コンストラクタが型によってパラメータ化されたことを意味しますタイプはパラメーターとして渡されるため、事前にそれが何であるかを知ることはできません。これに基づいて仮定を行うことはできません。さて、それが何であるかわからない場合は、どのように使用しますか?あなたはそれで何ができますか?

たとえば、それを格納して取得することができます。それはあなたがすでに言及したケースです:コレクション。リストまたは配列にアイテムを格納するために、アイテムについて何も知る必要はありません。リストまたは配列は、タイプに完全に気付かない可能性があります。

しかし、Maybeタイプについてはどうでしょうか?慣れていない場合は、Maybeは、おそらく値を持つタイプと持たないタイプです。どこで使用しますか?たとえば、ディクショナリからアイテムを取得する場合、アイテムがディクショナリにない可能性があるという事実は例外的な状況ではないため、アイテムがそこにない場合でも例外をスローするべきではありません。代わりに、_Maybe<T>_のサブタイプのインスタンスを返します。これには、Noneと_Some<T>_の2つのサブタイプがあります。 _int.Parse_は、例外またはint.TryParse(out bla)ダンス全体をスローする代わりに、実際に_Maybe<int>_を返す必要があるものの別の候補です。

ここで、Maybeは、要素が0または1つしかないリストのようなものだと主張するかもしれません。このように、コレクションのようなものです。

次に、_Task<T>_はどうですか?これは、将来のある時点で値を返すことを約束するタイプですが、現在のところ必ずしも値があるとは限りません。

または_Func<T, …>_はどうですか?型を抽象化できない場合、関数の概念をある型から別の型にどのように表現しますか?

または、より一般的には、抽象化と再利用がソフトウェアエンジニアリングの2つの基本操作であると考えると、なぜできないのか型を抽象化できるようにしたいですか?

有界多型

それでは、制限付きポリモーフィズムについて話しましょう。制限付きポリモーフィズムは、基本的にパラメトリックポリモーフィズムとサブタイプポリモーフィズムが出会う場所です。型コンストラクターがその型パラメーターについて完全に気づかない代わりに、bind(または制約)を行うことができます指定された型のサブタイプになる型。

コレクションに戻りましょう。ハッシュテーブルを取る。上記では、リストはその要素について何も知る必要がないと述べました。まあ、ハッシュテーブルはそうします:それらをハッシュできることを知る必要があります。 (注:C#では、すべてのオブジェクトが等しいかどうかを比較できるのと同じように、すべてのオブジェクトがハッシュ可能です。ただし、すべての言語に当てはまるわけではなく、C#でも設計ミスと見なされることがあります。)

したがって、ハッシュテーブルのキータイプのタイプパラメータをIHashableのインスタンスに制約する必要があります。

_class HashTable<K, V> where K : IHashable
{
  Maybe<V> Get(K key);
  bool Add(K key, V value);
}
_

代わりにこれがあったと想像してください:

_class HashTable
{
    object Get(IHashable key);
    bool Add(IHashable key, object value);
}
_

あなたはそこから抜け出すvalueをどうしますか?あなたはそれで何かをすることはできません、あなたはそれがオブジェクトであることを知っているだけです。そしてそれを反復する場合、得られるのは、IHashable(1つのプロパティHashしかないのであまり役に立たない)と、object(これはさらに少ない)。

またはあなたの例に基づいて何か:

_class Repository<T> where T : ISerializable
{
    T Get(int id);
    void Save(T obj);
    void Delete(T obj);
}
_

アイテムはディスクに保存されるため、シリアル化可能である必要があります。しかし、代わりにこれがある場合はどうなりますか?

_class Repository
{
    ISerializable Get(int id);
    void Save(ISerializable obj);
    void Delete(ISerializable obj);
}
_

一般的なケースでは、BankAccountを挿入すると、BankAccountOwnerAccountNumberBalanceDepositWithdrawなどのメソッドとプロパティが返されます。さて、他のケース? BankAccountを入力しても、Serializableが返されます。これには、AsStringという1つのプロパティしかありません。どうするつもり?

制限付きポリモーフィズムでできる巧妙なトリックもいくつかあります。

F-bounded polymorphism

F制限付き数量化では、基本的に、型変数が制約に再び出現します。これは、状況によっては役立ちます。例えば。 ICloneableインターフェイスをどのように記述しますか?戻り値の型が実装クラスの型であるメソッドをどのように記述しますか? MyType機能を備えた言語では、それは簡単です。

_interface ICloneable
{
    public this Clone(); // syntax I invented for a MyType feature
}
_

制限付き多態性を持つ言語では、代わりに次のようなことができます。

_interface ICloneable<T> where T : ICloneable<T>
{
    public T Clone();
}

class Foo : ICloneable<Foo>
{
    public Foo Clone()
    {
        // …
    }
}
_

MyTypeバージョンほど安全ではないことに注意してください。誰かが「間違った」クラスをタイプコンストラクターに単純に渡すのを妨げるものがないからです。

_class EvilBar : ICloneable<SomethingTotallyUnrelatedToBar>
{
    public SomethingTotallyUnrelatedToBar Clone()
    {
        // …
    }
}
_

抽象型メンバー

結局のところ、抽象型のメンバーとサブタイプがある場合、実際には、パラメトリックなポリモーフィズムがまったくなくても、すべて同じことを実行できます。 Scalaはこの方向に向かっていて、genericsをで始めてそれらを削除しようとした最初の主要言語です、たとえばJavaおよびC#とはまったく逆です。

基本的に、Scalaでは、フィールド、プロパティ、メソッドをメンバーとして持つことができるように、型も持つことができます。フィールドやプロパティ、メソッドを抽象化したままサブクラスに実装できるように、型メンバーも抽象化したままにすることができます。 C#でサポートされている場合、次のような単純なListのコレクションに戻りましょう。

_class List
{
    T; // syntax I invented for an abstract type member
    T Get(int index) { /* … */ }
    void Add(T obj) { /* … */ }
}

class IntList : List
{
    T = int;
}
// this is equivalent to saying `List<int>` with generics
_
25
Jörg W Mittag

「このリポジトリのほとんどのクライアントは、IBusinessObjectインターフェイスを介してオブジェクトを取得して使用することを望んでいます」.

いいえ、ありません。

IBusinessObjectに次の定義があるとしましょう:

public interface IBusinessObject
{
  public int Id { get; }
}

すべてのビジネスオブジェクト間で唯一の共有機能であるため、IDを定義するだけです。そして、実際のビジネスオブジェクトには、PersonとAddressの2つがあります(Personsには住所がなく、Addressesにも名前がないので、両方の機能を備えた共通のインターフェースに両方を制約することはできません。これは、 インターフェイスの分割原理[〜#〜] solid [〜#〜] )の「I」

public class Person : IBusinessObject
{
  public int Id { get; private set; }
  public string Name { get; private set; }
}

public class Address : IBusinessObject
{
  public int Id { get; private set; }
  public string City { get; private set; }
  public string StreetName { get; private set; }
  public int Number { get; private set; }
}

さて、レポジトリのジェネリックバージョンを使用するとどうなるでしょうか。

public class Repository<T> where T : class, IBusinessObject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

ジェネリックリポジトリでGetメソッドを呼び出すと、返されるオブジェクトは厳密に型指定され、すべてのクラスメンバーにアクセスできます。

Person p = new Repository<Person>().Get(1);
int id = p.Id;
string name = p.Name;

Address a = new Repository<Address>().Get(1);
int id = a.Id;
string cityName = a.City;
int houseNumber = a.Number;

一方、非汎用リポジトリを使用する場合:

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

IBusinessObjectインターフェイスからのみメンバーにアクセスできます。

IBussinesObject p = new Repository().Get(1);
int id = p.Id; //OK
string name = p.Name; //Oooops, you dont have "Name" defined on the IBussinesObject interface.

したがって、次の行が原因で前のコードはコンパイルされません。

string name = p.Name;
string cityName = a.City;
int houseNumber = a.Number;

確かに、IBussinesObjectを実際のクラスにキャストできますが、ジェネリックスが許可するすべてのコンパイル時の魔法(将来的にはInvalidCastExceptionsにつながる)が失われ、不必要にキャストオーバーヘッドが発生します...そして、そうしなくてもどちらのパフォーマンスもチェックするコンパイル時間に注意してください(する必要があります)。後でキャストしても、最初からジェネリックを使用するよりもメリットはありません。

15

重要な点は、ジェネリックはコレクション以外のすべてに適していますか?クラス内でジェネリック型パラメーターの代わりにこの型制約を使用する場合と同様に、型制約はジェネリックを特殊化しないのですか?

いいえ。あなたはRepositoryについてあまり考えすぎており、isとほとんど同じです。しかし、それはジェネリック医薬品の目的ではありません。 sersのために用意されています。

ここで重要なのは、リポジトリ自体がより一般的であるということではありません。ユーザーがより専門的である、つまりRepository<BusinessObject1>およびRepository<BusinessObject2>はさまざまなタイプで、さらにRepository<BusinessObject1>、I know取得することBusinessObject1Getからバックアウトします。

単純な継承からこの強い型付けを提供することはできません。提案されたRepositoryクラスは、人々がさまざまな種類のビジネスオブジェクトのリポジトリを混乱させたり、正しい種類のビジネスオブジェクトが戻ってくることを保証したりするものではありません。

14
DeadMG