共分散と逆分散の違い


回答:


266

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

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

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

{ Animal, 
  Tiger, 
  Fruit, 
  Banana }.

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

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

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

これまでのところ私と?

次に、関係について考えてみましょう。ある代入互換の関係最初のセット内の型のペア間で。typeの値はtype 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 ⇒ YIC<X> ⇐ IC<Y>です。

関係を保持するが逆にするマッピングは反変マッピングと呼ばれます。

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

これがC#4の共分散と反分散の違いです。共分散は代入可能性の方向を維持します。反変はそれを逆にします。


4
私のような人にとっては、共変ではないものと反変ではないもの、および両方ではないものを示す例を追加した方がいいでしょう。
bjan 2015

2
@Bargitta:とても似ています。違いは、C#は明確なサイト差異を使用し、Javaは呼び出しサイト差異を使用することです。したがって、物事が異なる方法は同じですが、開発者が「これをバリアントにする必要がある」と言っているところは異なります。ちなみに、両方の言語の機能の一部は同じ人によって設計されました!
Eric Lippert 2016年

2
@AshishNegi:矢印を「次のように使用できる」と読みます。「トラを比較することができるものとして、動物を比較することができるものを使用することができます」。今意味がありますか?
Eric Lippert

1
@AshishNegi:いいえ、そうではありません。 IEnumerableは、TがIEnumerableのメソッドの戻りにのみ現れるため、共変です。 また、TはIComparable のメソッドの仮パラメーターとしてのみ表示されるため、IComparableは反変です。
Eric Lippert

2
@AshishNegi:これらの関係の根底にある論理的な理由について考えたいと思います。 なぜ我々は変換することができますIEnumerable<Tiger>IEnumerable<Animal>安全?キリンをに入力する方法がないためIEnumerable<Animal>です。なぜ我々は、変換することができますIComparable<Animal>IComparable<Tiger>?からキリンを取り出す方法がないからIComparable<Animal>です。理にかなっていますか?
Eric Lippert、2016

111

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

共分散

正規の例:IEnumerable<out T>Func<out T>

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

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

反変

正規の例:IComparer<in T>Action<in T>

IComparer<object>からIComparer<string>、またはAction<object>に変換できAction<string>ます。値が唯一行くこれらのオブジェクト。

今回は、APIが一般的な(などobject)を期待している場合、より具体的なもの(など)を与えることができるため、機能しますstring

より一般的に

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

「出力位置」は思ったほど単純ではないため、混乱を招く可能性があります。タイプのパラメータは、出力位置でAction<T>のみ使用さTれていますAction<T>。これは、値はメソッドの実装から渡すことができるという点で、「出力」だ方にだけ戻り値缶のように、呼び出し側のコード。通常、このようなことは起こりません、幸いなことに:)


1
私のような人にとっては、共変ではないものと反変ではないもの、および両方ではないものを示す例を追加した方がいいでしょう。
bjan 2015

1
@ジョンスキートニースの例、私は「タイプのパラメータAction<T>がまだT出力位置でのみ使用されている」ことを理解していません。Action<T>戻り値の型は無効Tですが、出力としてどのように使用できますか?それともそれが何を意味するのでしょうか?それは何も返さないので、ルールに違反することは決してないことがわかりますか?
Alexander Derck 2016

2
違いを学習するためにこの優れた答えに再び戻ってくる私の将来の自分にとって、これはあなたが望む行です:「APIから値を取得するだけで、何かを返すので、[共分散]は機能します特定の(文字列のような)場合、その戻り値をより一般的なタイプ(オブジェクトのような)として扱うことができます。
Matt Klein

これらすべての中で最も混乱する部分は、共分散または反分散のいずれかで、方向(インまたはアウト)を無視すると、とにかく、より具体的な変換からより一般的な変換が得られるということです。私は意味:のために、「あなたは(オブジェクトのような)より一般的なタイプのように、その戻り値を扱うことができ、」共分散のために「APIは、あなたがそれを(文字列のような)何かもっと具体的に与えることができます(オブジェクトのような)一般的なものを期待している」:とcontravariance。私にとって、これらは同じようなサウンドです!
XMight 2017年

@AlexanderDerck:なぜ私が以前に返信しなかったのかわからない。私はそれが不明確であることに同意し、それを明確にするよう努めます。
ジョンスキート2017年

16

私の投稿が言語にとらわれないトピックの見方を理解するのに役立つことを願っています。

私たちの内部トレーニングについては、素晴らしい本「Smalltalk、Objects and Design(Chamond Liu)」で作業し、次の例を言い換えました。

「一貫性」とはどういう意味ですか?考え方は、非常に置換可能な型を使用して型保証された型階層を設計することです。この一貫性を得るための鍵は、静的に型付けされた言語で作業する場合のサブタイプベースの準拠です。(ここでは、Liskov Substitution Principle(LSP)について高レベルで説明します。)

実際の例(疑似コード/ C#では無効):

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

  • 反変:ピアニストが静的なタイピングで「一貫して」演奏できるピアノを想定しましょう。ピアニストがピアノを演奏すると、グランドピアノを演奏できるでしょうか?名手がグランドピアノを演奏するのではなく、(警告されます;ひねりがあります!)これは矛盾しています!なぜなら、そのような表現では、aPiano.Play(aPianist);aPianoは、PianoまたはGrandPianoインスタンスによって合法的に置き換えることができないからです。GrandPianoはVirtuosoだけが演奏できます。ピアニストは一般的すぎます。GrandPianosは、より一般的なタイプで再生できる必要があります。そうすれば、再生は一貫します。パラメータタイプは、Play()が定義されているタイプと反変であると言います。サブタイプのオーバーライドは、より一般化されたタイプを受け入れる場合があります。=>「必要なものは少ない。」

C#に戻る:
C#は基本的に静的に型付けされた言語であるため、共変または反変である必要がある型のインターフェイスの「位置」(パラメーターと戻り型など)は、その型の一貫した使用/開発を保証するために明示的にマークする必要があります、LSPを正常に動作させるため。動的に型付けされた言語では、通常、LSPの一貫性は問題になりません。つまり、型で動的な型のみを使用した場合、.Netインターフェイスとデリゲートで共変の「マークアップ」を完全に取り除くことができます。-しかし、これはC#の最適なソリューションではありません(パブリックインターフェイスでダイナミックを使用しないでください)。

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


あなたは、サブタイプのオーバーライドがより専門的なタイプを返すかもしれないと言います。しかし、それは完全に真実ではありません。をBird定義する場合はpublic abstract BirdEgg Lay();、実装するDuck : Bird 必要があります。public override BirdEgg Lay(){}そのため、BirdEgg anEgg = aBird.Lay();なんらかの分散があるアサーションは、単に正しくありません。説明のポイントの前提として、今ではポイント全体がなくなっています。代わりに、DuckEggが暗黙的にBirdEgg out / returnタイプにキャストされる実装内に共分散が存在すると思いますか?いずれにせよ、私の混乱を解消してください。
Suamere

1
短く言えば、あなたは正しいです!混乱させて申し訳ありません。DuckEgg Lay()Egg Lay() C#での有効なオーバーライドではありません。C#は共変の戻り値型をサポートしていませんが、JavaとC ++はサポートしています。C#のような構文を使用して、理論上の理想を説明しました。C#では、BirdとDuckに共通のインターフェイスを実装させる必要があります。この場合、Layは共変の戻り値(つまり、仕様外)の型を持つように定義されているため、問題が相互に適合します。
ニコ

1
@ Jon-Skeetの回答に対するMatt-Kleinのコメントの類似点として、「私の将来の自己へ」:ここでの私にとっての最良のテイクアウトは、「より多くを提供する」(特定)と「より少ない必要」(特定)です。「必要な量が少なく、多くを提供する」というのは、優れたニーモニックです。これは、それほど具体的でない指示(一般的な要求)を要求し、それでもより具体的なもの(実際の作業成果物)を提供したいという仕事に似ています。どちらの方法でも、サブタイプの順序(LSP)は途切れません。
カルフス

@karfus:ありがとうございましたが、覚えているように、別のソースからの「必要なものは少なく、もっと多く提供する」という考えを言い換えました。それは、私が上記で参照したLiuの本だったのかもしれません。ところで Javaでは、人々はニーモニックを「PECS」に削減しました。これは、差異を宣言する構文的な方法に直接関係します。PECSは「プロデューサーextends、コンシューマーsuper」用です。
ニコ

5

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

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

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

TInputメソッドに特定性低い型が渡される場合の反変を表します。

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();

0

CoとContraの分散はかなり論理的なものです。言語型システムは、現実の論理をサポートすることを強制します。例で理解しやすいです。

共分散

たとえば、花を購入したいのですが、市内に2つのフラワーショップがあります。

誰かに「花屋はどこ?」と尋ねたら 誰かがローズショップの場所を教えてくれても大丈夫でしょうか?はい、バラは花なので、花を買いたいならバラを買うことができます。デイジーショップの住所を誰かが返信した場合も同様です。

これはの例である共分散:あなたはキャストに許可されているA<C>A<B>、どこCのサブクラスであるB場合、A一般的な値(関数から結果としてリターン)を生成します。共分散はプロデューサーに関するものです。そのため、C#outは共分散にキーワードを使用します。

タイプ:

class Flower {  }
class Rose: Flower { }
class Daisy: Flower { }

interface FlowerShop<out T> where T: Flower {
    T getFlower();
}

class RoseShop: FlowerShop<Rose> {
    public Rose getFlower() {
        return new Rose();
    }
}

class DaisyShop: FlowerShop<Daisy> {
    public Daisy getFlower() {
        return new Daisy();
    }
}

質問は「フラワーショップはどこですか」、回答は「ローズショップはそこ」です。

static FlowerShop<Flower> tellMeShopAddress() {
    return new RoseShop();
}

反変

たとえば、あなたはガールフレンドに花を贈りたいと思っていて、ガールフレンドはどんな花も好きです。彼女をバラが好きな人、またはヒナギクが好きな人と見なすことができますか?そうです、もし彼女が花を愛するなら、彼女はバラとデイジーの両方を愛するでしょう。

これはの例ですcontravariance:あなたはキャストに許可されているA<B>A<C>CのサブクラスをされBた場合、A消費一般的な値です。in反変はコンシューマーに関するものです。そのため、C#は反変にキーワードを使用します。

タイプ:

interface PrettyGirl<in TFavoriteFlower> where TFavoriteFlower: Flower {
    void takeGift(TFavoriteFlower flower);
}

class AnyFlowerLover: PrettyGirl<Flower> {
    public void takeGift(Flower flower) {
        Console.WriteLine("I like all flowers!");
    }
}

花を愛するあなたのガールフレンドをバラを愛する誰かと考えて、彼女にバラを与えます:

PrettyGirl<Rose> girlfriend = new AnyFlowerLover();
girlfriend.takeGift(new Rose());

リンク集

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