共変性と反変性の違いを理解するのに苦労しています。
6 に答える
問題は、「共変性と反変性の違いは何ですか?」です。
共変性と反変性は、セットの1つのメンバーを別のメンバーに関連付けるマッピング関数のプロパティです。より具体的には、マッピングは、そのセットの関係に関して共変または反変である可能性があります。
すべての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>
。
関係を保持するが逆にするマッピングは、反変マッピングと呼ばれます。
繰り返しますが、これは明らかに正しいはずです。2匹の動物を比較できるデバイスは2匹のトラを比較することもできますが、2匹のトラを比較できるデバイスは必ずしも2匹の動物を比較できるとは限りません。
これが、C#4の共分散と反変性の違いです。共分散は割り当て可能性の方向性を維持します。共変性はそれを逆転させます。
例をあげるのがおそらく最も簡単です-それは確かに私がそれらを覚えている方法です。
共分散
正規の例: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>
それを回転させます。これは、戻り値と同じように、メソッドの実装から呼び出し元のコードに値を渡すことができるという点で「出力」です。通常、この種のことは起こりません、幸いなことに:)
私の投稿が、言語に依存しないトピックの見方を得るのに役立つことを願っています。
社内トレーニングでは、すばらしい本「Smalltalk、Objects and Design(Chamond Liu)」を使用して、次の例を言い換えました。
「一貫性」とはどういう意味ですか?アイデアは、高度に置換可能なタイプを使用してタイプセーフなタイプ階層を設計することです。この一貫性を実現するための鍵は、静的に型付けされた言語で作業する場合、サブタイプベースの適合性です。(ここでは、リスコフの置換原則(LSP)について大まかに説明します。)
実用的な例(擬似コード/ C#では無効):
共分散:静的型付けで「一貫して」卵を産む鳥を想定しましょう:鳥が卵を産むタイプの場合、鳥のサブタイプは卵のサブタイプを産むのではないでしょうか?たとえば、タイプDuckがDuckEggを配置すると、一貫性が与えられます。なぜこれが一貫しているのですか?そのような表現で
Egg anEgg = aBird.Lay();
は、参照aBirdは、BirdまたはDuckインスタンスによって合法的に置き換えられる可能性があるためです。戻り型は、Lay()が定義されている型と共変であると言います。サブタイプのオーバーライドは、より特殊なタイプを返す場合があります。=>「彼らはより多くを提供します。」</p>共変性:ピアニストが静的型付けで「一貫して」演奏できると仮定しましょう。ピアニストがピアノを演奏する場合、彼女はグランドピアノを演奏できますか?むしろ名手がグランドピアノを演奏しませんか?(注意してください;ねじれがあります!)これは一貫性がありません!そのような表現では
aPiano.Play(aPianist);
、aPianoをPianoまたはGrandPianoインスタンスで合法的に置き換えることはできませんでした。グランドピアノは名人だけが演奏できます。ピアニストは一般的すぎます。GrandPianosは、より一般的なタイプでプレイできる必要があります。そうすれば、プレイは一貫します。パラメータタイプは、Play()が定義されているタイプと反変であると言います。サブタイプのオーバーライドは、より一般化されたタイプを受け入れる場合があります。=>「必要なものが少なくて済みます。」</p>
C#に戻る:
C#は基本的に静的に型付けされた言語であるため、共変または反変である必要がある型のインターフェイスの「場所」(パラメーターや戻り型など)は、その型の一貫した使用/開発を保証するために明示的にマークする必要があります、LSPを正常に機能させるため。動的に型付けされた言語では、LSPの一貫性は通常問題ではありません。言い換えると、型で動的型のみを使用した場合、.Netインターフェイスとデリゲートの共変および反変の「マークアップ」を完全に取り除くことができます。-しかし、これはC#の最良の解決策ではありません(パブリックインターフェイスでは動的を使用しないでください)。
理論に戻る:
記述された適合性(共変リターンタイプ/反変パラメータータイプ)は、理論上の理想です(言語EmeraldおよびPOOL-1によってサポートされています)。一部のオブジェクト指向言語(Eiffelなど)は、別のタイプの一貫性、特にを適用することを決定しました。また、共変パラメータタイプは、理論上の理想よりも現実をよりよく説明しているためです。静的に型付けされた言語では、多くの場合、「ダブルディスパッチ」や「ビジター」などのデザインパターンを適用することで、望ましい一貫性を実現する必要があります。他の言語は、いわゆる「多重ディスパッチ」またはマルチメソッド(これは基本的に実行時に関数のオーバーロードを選択することです。たとえばCLOSを使用)、または動的型付けを使用して目的の効果を取得します。
CoとContraの分散はかなり論理的なものです。言語型システムは、実際のロジックをサポートすることを強制します。例で理解するのは簡単です。
共分散
たとえば、花を購入したいと思っていて、あなたの街に2つの花屋があります。ローズショップとデイジーショップです。
誰かに「花屋はどこ?」と聞くと。誰かがバラ屋さんを教えてくれますが、大丈夫ですか?はい、バラは花なので、花を買いたいならバラを買うことができます。誰かがデイジーショップの住所をあなたに返信した場合も同じです。
これは共分散の例です。一般的な値(関数の結果として返される)を生成する場合、は、のサブクラスであるにA<C>
キャストできます。共分散はプロデューサーに関するものであるため、C#は共分散にキーワードを使用します。A<B>
C
B
A
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();
}
共変性
たとえば、ガールフレンドに花を贈りたいと思っていて、ガールフレンドはどんな花も好きです。彼女をバラが好きな人、またはデイジーが好きな人と見なすことができますか?はい、彼女が花を愛するなら、彼女はバラとデイジーの両方を愛するでしょうから。
これは反変性の例です。ジェネリック値を消費する場合、はにキャストできます。ここで、はのサブクラスです。共変性は消費者に関するものであるため、C#は共変性にキーワードを使用します。A<B>
A<C>
C
B
A
in
タイプ:
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());
リンク
コンバーターデリゲートは、違いを理解するのに役立ちます。
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();
組織内に2つのポジションがあると考えてください。アリスは椅子のカウンターです。そして、ボブは同じ椅子の店主です。
共変性。ボブはテーブルを店に持って行かず、椅子だけを保管しているため、家具の店主を指名することはできません。しかし、紫色の椅子は椅子なので、彼を紫色の椅子の店主と名付けることができます。これはIBookkeeper<in T>
、より具体的なタイプに割り当てることを許可します。in
オブジェクトへのデータフローを表します。
コヴァリナス。それどころか、アリスの役割に影響を与えないので、アリスを家具のカウンターと名付けることができます。しかし、彼女が赤い椅子以外の椅子を数えないことを期待するので、彼女に赤い椅子のカウンターという名前を付けることはできませんが、彼女はそれらを数えます。これはICounter<out T>
、より具体的ではなく、より具体的でないものへの暗黙の変換を許可します。out
オブジェクトからのデータフローを表します。
そして、不変性とは、両方を実行できない場合です。