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


20

前回ジェネリッククラスを書いたときのことは覚えていません。何かを考えてから必要になると思うたびに、結論を出します。

この質問に対する2番目の答えは、明確化を求めることでした(まだコメントできないので、新しい質問をしました)。

ジェネリックが必要な場合の例として、与えられたコードを取り上げましょう。

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

型の制約があります: IBusinessObject

私の通常の考え方は次のとおりです。クラスはuse IBusinessObjectに制限されているため、これを使用するクラスもそうRepositoryです。リポジトリはこれらを保存します。IBusinessObjectほとんどの場合、これのクライアントはインターフェースをRepository介してオブジェクトを取得および使用したいと思うでしょうIBusinessObject。それではなぜ

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

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

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

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

回答:


24

最初に純粋なパラメトリック多相性について説明し、後で有界多相性について説明します。

パラメトリック多型

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

たとえば、それを保存および取得できます。あなたが既に言及したのは、コレクションです。リストまたは配列にアイテムを保存するために、アイテムについて何も知る必要がありません。リストまたは配列は、タイプを完全に無視できます。

しかし、Maybeタイプはどうですか?あなたがそれに精通してMaybeいないなら、多分値を持っているかもしれないし、そうでないかもしれない型です。どこで使用しますか?たとえば、ディクショナリからアイテムを取得する場合:アイテムがディクショナリにない可能性があるという事実は例外的な状況ではないため、アイテムが存在しない場合は例外をスローしないでください。代わりに、のサブタイプのインスタンスを返します。このインスタンスにはMaybe<T>、との2つのサブタイプがNoneありSome<T>ます。int.ParseMaybe<int>、例外またはint.TryParse(out bla)ダンス全体をスローする代わりに、実際に返す必要がある何かの別の候補です。

さて、あなたはそれMaybeがゼロまたは1つの要素しか持つことができないリストのようなちょっとしたものであると主張するかもしれません。したがって、ちょっとしたコレクションです。

では、どうでしょうTask<T>?これは、将来のある時点で値を返すことを約束するタイプですが、必ずしも現在値を持っているとは限りません。

それともどうFunc<T, …>?型を抽象化できない場合、ある型から別の型への関数の概念をどのように表現しますか?

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

有界ポリモーフィズム

それでは、境界のあるポリモーフィズムについて話しましょう。有界ポリモーフィズムは、基本的にパラメトリックポリモーフィズムとサブタイプポリモーフィズムが出会う場所です。タイプコンストラクターのタイプパラメーターを完全に無視する代わりに、タイプを特定のタイプのサブタイプにバインド(または制約)できます。

コレクションに戻りましょう。ハッシュテーブルを取ります。上記で、リストはその要素について何も知る必要がないと述べました。ハッシュテーブルはそうです:ハッシュテーブルをハッシュできることを知る必要があります。(注: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では、あなたが得るBankAccount背中を、のようなメソッドとプロパティを持つOwnerAccountNumberBalanceDepositWithdraw、などあなたが働くことができる何か。さて、他のケースは?aを入力しBankAccountますがSerializable、プロパティが1つしかないを取得しますAsString。あなたはそれで何をするつもりですか?

また、有界ポリモーフィズムを使用して実行できるいくつかの巧妙なトリックもあります。

F境界の多型

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はこの方向に向かっています。これはジェネリックで始まり、それらを削除しようとする最初の主要な言語です。これは、JavaやC#などとまったく逆です。

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

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

型の抽象化が役立つことを理解しています。「実生活」での使用は見ていません。Func <>およびTask <>およびAction <>はライブラリタイプです。おかげで私interface IFoo<T> where T : IFoo<T>も思い出しました。それは明らかに実際のアプリケーションです。例は素晴らしいです。しかし、何らかの理由で私は満足していません。私はむしろ、それが適切であるときとそうでないときを考えてみたい。ここでの回答はこのプロセスにある程度貢献していますが、私はまだこのすべてについて不愉快だと感じています。言語レベルの問題はもう長い間気にしないので、それは奇妙です。
-jungle_mole

素晴らしい例。クラスルームに戻ったような気がしました。+1
Chef_Code

1
@Chef_Code:それがcompめ言葉であることを願っています:-P-
ヨルグW

はい、そうです。私はすでにコメントした後、それがどのように知覚されるかについて考えました。だから誠意を確認するために...はい、それは何も賛辞です。
Chef_Code

14

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

いいえ。あなたはあまりにも多くのことを考えているRepository、それはどこほとんど同じ。しかし、それはジェネリックの目的ではありません。それらはユーザーのためにあります

ここで重要なのは、リポジトリ自体がより一般的ではないということです。これにより、ユーザーはよりspecialized-すなわち、そのあることだRepository<BusinessObject1>Repository<BusinessObject2>私は取る場合こと、異なったタイプであり、さらにはRepository<BusinessObject1>、私が知っている私が取得することBusinessObject1からバックGet

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


ありがとう、それは理にかなっています。しかし、この高く評価されている言語機能を使用するのは、IntelliSenseをオフにしているユーザーを支援するのと同じくらい簡単ですか?(私は少し大げさだけど、私はあなたのポイントを得ると確信している)
jungle_mole

@zloidooraque:また、IntelliSenseは、リポジトリに保存されているオブジェクトの種類を認識しません。しかし、はい、代わりにキャストを使用する場合は、ジェネリックなしで何でもできます。
ジェキシサイド

@gexicideがポイントです。共通のインターフェイスを使用する場合、キャストを使用する必要がある場所がわかりません。「使用Object」と言ったことはありません。また、コレクションを記述するときにジェネリックを使用する理由も理解しています(DRY原則)。おそらく、私の最初の質問は、コレクションコンテキスト以外でジェネリックを使用することに関するものだったはずです。
jungle_mole15年

@zloidooraque:環境とは関係ありません。場合インテリセンスはあなたを伝えることができないIBusinessObjectですBusinessObject1BusinessObject2。知らない派生型に基づいてオーバーロードを解決することはできません。間違った型を渡すコードを拒否することはできません。Intellisenseにはまったく何もできない、より強力なタイピングが100万ビットあります。優れたツールサポートは素晴らしい利点ですが、コアの理由とは関係ありません。
-DeadMG

@DeadMG、それが私のポイントです:インテリセンスはそれを行うことができません:ジェネリックを使用してください、そうすることができますか?それは重要ですか?インターフェースでオブジェクトを取得するとき、なぜダウンキャストするのですか?あなたがそれをするなら、それは悪いデザインですよね?そしてなぜ「オーバーロードの解決」とは何ですか?ユーザーは、適切なメソッドの呼び出しをシステムに委任する場合、派生型に基づいてメソッドを呼び出すかどうかを決定してはなりません(ポリモーフィズム)。そして、これは再び質問につながります:ジェネリックはコンテナの外でも有用ですか?私はあなたと議論していません、本当にそれを理解する必要があります。
jungle_mole

12

「ほとんどの場合、このリポジトリのクライアントは、IBusinessObjectインターフェースを介してオブジェクトを取得および使用したいでしょう」

いいえ、彼らはしません。

IBusinessObjectには次の定義があると考えてみましょう。

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

すべてのビジネスオブジェクト間で唯一の共有機能であるため、IDを定義するだけです。そして、2つの実際のビジネスオブジェクトがあります。PersonとAddress(Personにはストリートがなく、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が発生します)、不必要にオーバーヘッドがキャストされます...どちらのパフォーマンスもチェックしないコンパイル時間に注意してください(そうすべきではありません)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.