web-dev-qa-db-ja.com

共分散と反分散の違い

共分散と反分散の違いを理解するのに苦労しています。

143
jane doe

問題は、「共分散と反分散の違いは何ですか?」です。

共分散と反分散はセットの1つのメンバーを別のメンバーに関連付けるマッピング関数のプロパティです。より具体的には、マッピングは、そのセットのrelationに関して共変または反変になります。

すべてのC#タイプのセットの次の2つのサブセットを検討してください。最初:

{ Animal, 
  Tiger, 
  Fruit, 
  Banana }.

次に、この明らかに関連するセット:

{ IEnumerable<Animal>, 
  IEnumerable<Tiger>, 
  IEnumerable<Fruit>, 
  IEnumerable<Banana> }

最初のセットから2番目のセットへのマッピング操作があります。つまり、最初のセットの各Tについて、2番目のセットの対応するタイプはIEnumerable<T>。または、短い形式では、マッピングはT → IE<T>。これは「細い矢印」であることに注意してください。

これまで私と一緒に?

ここで、関係を考えてみましょう。最初のセットのタイプのペアの間には割り当て互換性関係があります。タイプTigerの値は、タイプAnimalの変数に割り当てることができるため、これらのタイプは「割り当て互換」と呼ばれます。 「X型の値をY型の変数に割り当てることができます」と短い形式で書きましょう:X ⇒ Y。これは「太い矢印」であることに注意してください。

したがって、最初のサブセットでは、すべての割り当て互換性の関係があります。

Tiger  ⇒ Tiger
Tiger  ⇒ Animal
Animal ⇒ Animal
Banana ⇒ Banana
Banana ⇒ Fruit
Fruit  ⇒ Fruit

特定のインターフェイスの共変割り当ての互換性をサポートするC#4では、2番目のセットの型のペア間に割り当ての互換性の関係があります。

IE<Tiger>  ⇒ IE<Tiger>
IE<Tiger>  ⇒ IE<Animal>
IE<Animal> ⇒ IE<Animal>
IE<Banana> ⇒ IE<Banana>
IE<Banana> ⇒ IE<Fruit>
IE<Fruit>  ⇒ IE<Fruit>

マッピングT → IE<T>割り当て互換性の存在と方向を保持。つまり、X ⇒ Y、それからIE<X> ⇒ IE<Y>

太い矢印の両側に2つのものがある場合、両側を対応する細い矢印の右側にあるものに置き換えることができます。

特定の関係に関してこのプロパティを持つマッピングは、「共変マッピング」と呼ばれます。これは理にかなっているはずです。動物のシーケンスが必要な場合は、タイガーのシーケンスを使用できますが、逆は当てはまりません。一連のトラが必要な場合、一連の動物を必ずしも使用することはできません。

それは共分散です。次に、すべてのタイプのセットのこのサブセットを検討します。

{ IComparable<Tiger>, 
  IComparable<Animal>, 
  IComparable<Fruit>, 
  IComparable<Banana> }

これで、最初のセットから3番目のセットへのマッピングT → IC<T>

C#4の場合:

IC<Tiger>  ⇒ IC<Tiger>
IC<Animal> ⇒ IC<Tiger>     Backwards!
IC<Animal> ⇒ IC<Animal>
IC<Banana> ⇒ IC<Banana>
IC<Fruit>  ⇒ IC<Banana>     Backwards!
IC<Fruit>  ⇒ IC<Fruit>

つまり、マッピングT → IC<T>には、存在は保持するが方向を逆にする割り当て互換性があります。つまり、X ⇒ Y、次にIC<X> ⇐ IC<Y>

リレーションが保存するが逆になるマッピングはcontravariantマッピングと呼ばれます。

繰り返しますが、これは明らかに正しいはずです。 2匹の動物を比較できるデバイスは2匹のトラを比較することもできますが、2匹のトラを比較できるデバイスは必ずしも2匹の動物を比較できるわけではありません。

これが、C#4の共分散と反分散の違いです。共分散preserves割り当て可能性の方向。反分散それ。

254
Eric Lippert

おそらく例を挙げるのが最も簡単でしょう-それは確かに私がそれらを覚えている方法です。

共分散

標準的な例:IEnumerable<out T>Func<out T>

IEnumerable<string>からIEnumerable<object>に、またはFunc<string>からFunc<object>に変換できます。値はout fromこれらのオブジェクトのみです。

APIから値を取得するだけで、特定の何か(stringなど)を返す場合、その返された値をより一般的な型(objectなど)として扱うことができるため、これは機能します。 。

反分散

標準的な例:IComparer<in T>Action<in T>

IComparer<object>からIComparer<string>に、またはAction<object>からAction<string>に変換できます。値はintoこれらのオブジェクトのみになります。

APIが一般的なもの(objectなど)を想定している場合は、より具体的なもの(stringなど)を指定できるため、今回は機能します。

より一般的に

インターフェイスIFoo<T>がある場合、Tで共変にできます(つまり、Tが出力位置(戻り値型など)でのみ使用される場合は、IFoo<out T>として宣言できます。 Tが入力位置(パラメータータイプなど)でのみ使用される場合、T(つまりIFoo<in T>)で反変になる可能性があります。

「出力位置」は見た目ほど単純ではないため、混乱を招く可能性があります-タイプAction<T>のパラメーターは、出力位置でTのみを使用します-Action<T>の矛盾あなたが私の意味を見るなら、それを丸くします。これは、値がメソッドの実装から渡すことができるという点で「出力」です。戻り値ができるように、呼び出し元のコードtowardsです。通常、この種のものは、幸いなことに出てこない:

106
Jon Skeet

私の投稿が言語に依存しないトピックのビューを取得するのに役立つことを願っています。

内部トレーニングのために、私はすばらしい本「Smalltalk、Objects and Design(Chamond Liu)」と協力し、次の例を言い換えました。

「一貫性」とはどういう意味ですか?考え方は、高度に代替可能な型を持つ型保証型階層を設計することです。この一貫性を得るための鍵は、静的に型付けされた言語で作業する場合、サブタイプベースの適合性です。 (ここでは、リスコフ置換原理(LSP)について高レベルで説明します。)

実用例(C#での擬似コード/無効):

  • 共分散:静的型付けで「一貫して」卵を産む鳥を想定しましょう:型鳥が卵を産む場合、鳥のサブタイプは卵のサブタイプを産みませんか?例えば。タイプDuckがDuckEggを配置すると、一貫性が与えられます。なぜこれが一貫しているのですか?そのような式:Egg anEgg = aBird.Lay();では、参照aBirdを合法的にBirdまたはDuckインスタンスに置き換えることができるためです。戻り値の型は、Lay()が定義されている型と共変であると言います。サブタイプのオーバーライドは、より特殊なタイプを返す場合があります。 =>「より多くを提供します。」

  • 矛盾:ピアニストが静的な型付けで「一貫して」演奏できるピアノを想定しましょう:ピアニストがピアノを演奏する場合、彼女はグランドピアノを演奏できますか?むしろ名人がグランドピアノを演奏しませんか? (注意してください、ねじれがあります!)これは矛盾しています!そのような式では:aPiano.Play(aPianist); aPianoは、PianoまたはGrandPianoインスタンスによって法的に置き換えることができませんでした!グランドピアノは名人だけが演奏でき、ピアニストは一般的すぎます! GrandPianosは、より一般的なタイプでプレイ可能でなければならず、プレイは一貫しています。パラメーターの型は、Play()が定義されている型に対して反変であると言います。サブタイプのオーバーライドは、より一般化されたタイプを受け入れる場合があります。 =>「必要なものは少ない。」

C#に戻る:
[。入力して、LSPを正常に動作させます。動的に型付けされた言語では、通常、LSPの一貫性は問題になりません。つまり、型で動的な型のみを使用した場合、.Netインターフェイスとデリゲートで共変および反変の「マークアップ」を完全に取り除くことができます。 -しかし、これはC#での最適なソリューションではありません(パブリックインターフェイスでは動的を使用しないでください)。

理論に戻る:
記述された適合性(共変の戻り値の型/反変のパラメータ型)は理論上の理想です(言語EmeraldおよびPOOL-1でサポートされています)。一部のOOP言語(Eiffelなど)は、別の種類の一貫性、特にまた、共変パラメータのタイプも、理論的な理想よりも現実をよりよく説明しているためです。静的に型付けされた言語では、多くの場合、「ダブルディスパッチ」や「訪問者」などの設計パターンを適用することで、目的の一貫性を実現する必要があります。他の言語は、いわゆる「複数ディスパッチ」またはマルチメソッドを提供します(これは基本的に実行時で関数オーバーロードを選択します(例:CLOSで))、または動的型付けを使用して目的の効果を取得します。

15
Nico

デリゲートにメソッドを割り当てる場合、メソッドシグネチャはデリゲートのシグネチャと完全に一致する必要があります。そうは言っても、共分散と反分散により、メソッドのシグネチャをデリゲートのシグネチャに一致させる際にある程度の柔軟性が得られます。

これを参照できます 共分散、反分散、およびそれらの違いを理解するための記事

4
Ranganatha

コンバーターデリゲートは、違いを理解するのに役立ちます。

delegate TOutput Converter<in TInput, out TOutput>(TInput input);

TOutputは、共分散を表します。メソッドはより具体的なタイプを返します。

TInputcontravarianceを表します。ここでメソッドは特定のタイプが少ないに渡されます。

public class Dog { public string Name { get; set; } }
public class Poodle : Dog { public void DoBackflip(){ System.Console.WriteLine("2nd smartest breed - woof!"); } }

public static Poodle ConvertDogToPoodle(Dog dog)
{
    return new Poodle() { Name = dog.Name };
}

List<Dog> dogs = new List<Dog>() { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } };
List<Poodle> poodles = dogs.ConvertAll(new Converter<Dog, Poodle>(ConvertDogToPoodle));
poodles[0].DoBackflip();
3
woggles