回答:
問題は、「共分散と反分散の違いは何ですか」です。
共分散と反分散は、セットの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 ⇒ Y
、IC<X> ⇐ IC<Y>
です。
関係を保持するが逆にするマッピングは反変マッピングと呼ばれます。
繰り返しますが、これは明らかに正しいはずです。2匹の動物を比較できるデバイスは2匹のトラも比較できますが、2匹のタイガーを比較できるデバイスは、必ずしも2匹の動物を比較できるわけではありません。
これがC#4の共分散と反分散の違いです。共分散は代入可能性の方向を維持します。反変はそれを逆にします。
IEnumerable<Tiger>
にIEnumerable<Animal>
安全?キリンをに入力する方法がないためIEnumerable<Animal>
です。なぜ我々は、変換することができますIComparable<Animal>
にIComparable<Tiger>
?からキリンを取り出す方法がないからIComparable<Animal>
です。理にかなっていますか?
例を示すのがおそらく最も簡単でしょう。それが確かに私がそれらを覚えている方法です。
共分散
正規の例: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>
。これは、値はメソッドの実装から渡すことができるという点で、「出力」だ方にだけ戻り値缶のように、呼び出し側のコード。通常、このようなことは起こりません、幸いなことに:)
Action<T>
がまだT
出力位置でのみ使用されている」ことを理解していません。Action<T>
戻り値の型は無効T
ですが、出力としてどのように使用できますか?それともそれが何を意味するのでしょうか?それは何も返さないので、ルールに違反することは決してないことがわかりますか?
私の投稿が言語にとらわれないトピックの見方を理解するのに役立つことを願っています。
私たちの内部トレーニングについては、素晴らしい本「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タイプにキャストされる実装内に共分散が存在すると思いますか?いずれにせよ、私の混乱を解消してください。
DuckEgg Lay()
はEgg Lay()
C#での有効なオーバーライドではありません。C#は共変の戻り値型をサポートしていませんが、JavaとC ++はサポートしています。C#のような構文を使用して、理論上の理想を説明しました。C#では、BirdとDuckに共通のインターフェイスを実装させる必要があります。この場合、Layは共変の戻り値(つまり、仕様外)の型を持つように定義されているため、問題が相互に適合します。
extends
、コンシューマーsuper
」用です。
コンバーターデリゲートは、違いを理解するのに役立ちます。
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();
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());