共分散と反分散の実世界の例


162

現実の世界で共分散と反変をどのように使用するかを理解するのに少し問題があります。

これまでのところ、私が見た唯一の例は、同じ古い配列の例です。

object[] objectArray = new string[] { "string 1", "string 2" };

それが他の場所で使用されているのを見ることができれば、開発中にそれを使用できるようにする例を見るとよいでしょう。


1
(私自身の)質問に対するこの回答で共分散を探索します:共分散タイプ:例によって。あなたはそれが面白いと思うでしょう、そしてうまくいけば有益です。
Cristian Diaconescu 2013年

回答:


109

クラスPersonと、それから派生するクラスTeacherがあるとします。IEnumerable<Person>引数として取る操作がいくつかあります。Schoolクラスには、を返すメソッドがありますIEnumerable<Teacher>。共分散を使用すると、その結果を、を使用するメソッドに直接使用することができIEnumerable<Person>、より派生型の少ない(より一般的な)型をより派生型に置き換えることができます。逆直観では、直観的には、より派生的な型が指定されているより一般的な型を使用できます。

MSDNのGenericsのCovarianceおよびContravarianceも参照してください。

クラス

public class Person 
{
     public string Name { get; set; }
} 

public class Teacher : Person { } 

public class MailingList
{
    public void Add(IEnumerable<out Person> people) { ... }
}

public class School
{
    public IEnumerable<Teacher> GetTeachers() { ... }
}

public class PersonNameComparer : IComparer<Person>
{
    public int Compare(Person a, Person b) 
    { 
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : Compare(a,b);
    }

    private int Compare(string a, string b)
    {
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : a.CompareTo(b);
    }
}

使用法

var teachers = school.GetTeachers();
var mailingList = new MailingList();

// Add() is covariant, we can use a more derived type
mailingList.Add(teachers);

// the Set<T> constructor uses a contravariant interface, IComparer<in T>,
// we can use a more generic type than required.
// See https://msdn.microsoft.com/en-us/library/8ehhxeaf.aspx for declaration syntax
var teacherSet = new SortedSet<Teachers>(teachers, new PersonNameComparer());

14
@FilipBartuzi-私がこの答えを書いたときのように、あなたは大学で働いていて、それは実世界の例です。
tvanfosson 2014

5
質問に答えず、C#でco / contra分散を使用する例を示さない場合、これをどのように回答としてマークできますか?
barakcaf 2016年

@barakcafが反変の例を追加しました。共分散の例が表示されなかった理由がわからない-おそらくコードを下にスクロールする必要があった-が、私はその周りにいくつかのコメントを追加した。
tvanfosson

@tvanfossonコードではco / contraを使用しているため、宣言方法は示していません。この例では、ジェネリック宣言でのin / outの使用法を示していませんが、他の答えは示しています。
barakcaf 2016年

それで、私がそれを正しく理解すると、C#でのLiskovの置換原理を可能にするのは共分散です。
Miguel Veloso

136
// Contravariance
interface IGobbler<in T> {
    void gobble(T t);
}

// Since a QuadrupedGobbler can gobble any four-footed
// creature, it is OK to treat it as a donkey gobbler.
IGobbler<Donkey> dg = new QuadrupedGobbler();
dg.gobble(MyDonkey());

// Covariance
interface ISpewer<out T> {
    T spew();
}

// A MouseSpewer obviously spews rodents (all mice are
// rodents), so we can treat it as a rodent spewer.
ISpewer<Rodent> rs = new MouseSpewer();
Rodent r = rs.spew();

完全を期して…

// Invariance
interface IHat<T> {
    void hide(T t);
    T pull();
}

// A RabbitHat…
IHat<Rabbit> rHat = RabbitHat();

// …cannot be treated covariantly as a mammal hat…
IHat<Mammal> mHat = rHat;      // Compiler error
// …because…
mHat.hide(new Dolphin());      // Hide a dolphin in a rabbit hat??

// It also cannot be treated contravariantly as a cottontail hat…
IHat<CottonTail> cHat = rHat;  // Compiler error
// …because…
rHat.hide(new MarshRabbit());
cHat.pull();                   // Pull a marsh rabbit out of a cottontail hat??

138
私はこの現実的な例が好きです。私は先週ロバのゴブリングコードを書いていただけで、共分散ができてとても良かったです。:-)
Eric Lippert

4
上記の@javadbaでのコメントは、共分散と反変とは何であるかをEricLippertに指示しているので、おばあちゃんが卵を吸う方法を教えている現実的な共変の例です!:p
iAteABug_And_iLiked_it

1
質問は、反変と共分散が何をすることができるかを尋ねたのではなく、なぜそれを使う必要があるのかを尋ねまし。あなたの例はどちらも必要としないので、実用的とはほど遠いです。QuadrupedGobblerを作成してそれをそれ自体として扱い(それをIGobbler <Quadruped>に割り当てる)、それでもロバをゴブリングできます(クワッドを必要とするゴブルメソッドにロバを渡すことができます)。反変は必要ありません。QuadrupedGobblerをDonkeyGobblerとして扱うことができるのはすばらしいことですが、この場合、QuadrupedGobblerがすでにDonkeysを食い尽くすことができるのに、なぜ必要なのでしょうか。
wired_in

1
@wired_inロバだけが気になるときは、もっと一般的であることで邪魔になることがあります。たとえば、むさぼり食われるロバを供給する農場がある場合、これはと表すことができますvoid feed(IGobbler<Donkey> dg)。代わりにIGobbler <Quadruped>をパラメーターとして受け取った場合、ロバのみを食べるドラゴンを渡すことができません。
Marcelo Cantos 2016

1
パーティーに遅れましたが、これは私がSOの周りで見た中で最も書かれた例についてです。ばかげている間、完全に理にかなっています。私は答えでゲームをアップしなければなりません...
ジェシーウィリアムズ

120

違いを理解するのに役立つようにまとめました

public interface ICovariant<out T> { }
public interface IContravariant<in T> { }

public class Covariant<T> : ICovariant<T> { }
public class Contravariant<T> : IContravariant<T> { }

public class Fruit { }
public class Apple : Fruit { }

public class TheInsAndOuts
{
    public void Covariance()
    {
        ICovariant<Fruit> fruit = new Covariant<Fruit>();
        ICovariant<Apple> apple = new Covariant<Apple>();

        Covariant(fruit);
        Covariant(apple); //apple is being upcasted to fruit, without the out keyword this will not compile
    }

    public void Contravariance()
    {
        IContravariant<Fruit> fruit = new Contravariant<Fruit>();
        IContravariant<Apple> apple = new Contravariant<Apple>();

        Contravariant(fruit); //fruit is being downcasted to apple, without the in keyword this will not compile
        Contravariant(apple);
    }

    public void Covariant(ICovariant<Fruit> fruit) { }

    public void Contravariant(IContravariant<Apple> apple) { }
}

tldr

ICovariant<Fruit> apple = new Covariant<Apple>(); //because it's covariant
IContravariant<Apple> fruit = new Contravariant<Fruit>(); //because it's contravariant

10
これは、これまでに見た中で最も明確で簡潔なものです。素晴らしい例です。
Rob L

6
親が親であるContravariance場合、果物はどのようにして(例では)リンゴにダウンキャストできますか?FruitApple
トビアスマーシャル

@TobiasMarschallは、「ポリモーフィズム」についてさらに勉強する必要があることを意味します
snr

56

inキーワードとoutキーワードは、インターフェイスおよびデリゲートのコンパイラのキャストルールをジェネリックパラメーターで制御します。

interface IInvariant<T> {
    // This interface can not be implicitly cast AT ALL
    // Used for non-readonly collections
    IList<T> GetList { get; }
    // Used when T is used as both argument *and* return type
    T Method(T argument);
}//interface

interface ICovariant<out T> {
    // This interface can be implicitly cast to LESS DERIVED (upcasting)
    // Used for readonly collections
    IEnumerable<T> GetList { get; }
    // Used when T is used as return type
    T Method();
}//interface

interface IContravariant<in T> {
    // This interface can be implicitly cast to MORE DERIVED (downcasting)
    // Usually means T is used as argument
    void Method(T argument);
}//interface

class Casting {

    IInvariant<Animal> invariantAnimal;
    ICovariant<Animal> covariantAnimal;
    IContravariant<Animal> contravariantAnimal;

    IInvariant<Fish> invariantFish;
    ICovariant<Fish> covariantFish;
    IContravariant<Fish> contravariantFish;

    public void Go() {

        // NOT ALLOWED invariants do *not* allow implicit casting:
        invariantAnimal = invariantFish; 
        invariantFish = invariantAnimal; // NOT ALLOWED

        // ALLOWED covariants *allow* implicit upcasting:
        covariantAnimal = covariantFish; 
        // NOT ALLOWED covariants do *not* allow implicit downcasting:
        covariantFish = covariantAnimal; 

        // NOT ALLOWED contravariants do *not* allow implicit upcasting:
        contravariantAnimal = contravariantFish; 
        // ALLOWED contravariants *allow* implicit downcasting
        contravariantFish = contravariantAnimal; 

    }//method

}//class

// .NET Framework Examples:
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable { }
public interface IEnumerable<out T> : IEnumerable { }


class Delegates {

    // When T is used as both "in" (argument) and "out" (return value)
    delegate T Invariant<T>(T argument);

    // When T is used as "out" (return value) only
    delegate T Covariant<out T>();

    // When T is used as "in" (argument) only
    delegate void Contravariant<in T>(T argument);

    // Confusing
    delegate T CovariantBoth<out T>(T argument);

    // Confusing
    delegate T ContravariantBoth<in T>(T argument);

    // From .NET Framework:
    public delegate void Action<in T>(T obj);
    public delegate TResult Func<in T, out TResult>(T arg);

}//class

魚が動物のサブタイプであると仮定します。ところで素晴らしい答え。
Rajan Prasad

48

以下は、継承階層を使用した簡単な例です。

単純なクラス階層を考えると:

ここに画像の説明を入力してください

そしてコードでは:

public abstract class LifeForm  { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }

不変性(つまり、ジェネリック型パラメーターまたはキーワードで装飾されていない*inout

一見、このような方法

public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
    foreach (var lifeForm in lifeForms)
    {
        Console.WriteLine(lifeForm.GetType().ToString());
    }
}

...異種のコレクションを受け入れる必要があります:(それを行います)

var myAnimals = new List<LifeForm>
{
    new Giraffe(),
    new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra

ただし、より派生した型のコレクションを渡すと失敗します。

var myGiraffes = new List<Giraffe>
{
    new Giraffe(), // "Jerry"
    new Giraffe() // "Melman"
};
PrintLifeForms(myGiraffes); // Compile Error!

cannot convert from 'System.Collections.Generic.List<Giraffe>' to 'System.Collections.Generic.IList<LifeForm>'

どうして?ジェネリックパラメーターIList<LifeForm>は共変ではない ため、IList<T>不変であるためIList<LifeForm>、パラメーター化された型がT必要なコレクション(IListを実装する)のみを受け入れますLifeForm

のメソッドの実装PrintLifeFormsが悪意のあるものである(ただし、同じメソッドシグネチャを持っている)場合、コンパイラがパスを妨げる理由List<Giraffe>が明らかになります。

 public static void PrintLifeForms(IList<LifeForm> lifeForms)
 {
     lifeForms.Add(new Zebra());
 }

以来IList許可が追加または要素の除去、の任意のサブクラスは、LifeFormこのようにパラメータを追加することができlifeForms、そしてメソッドに渡された派生型の任意のコレクションの種類に違反します。(ここでは、悪意のあるメソッドがをに追加しようとZebraしますvar myGiraffes)。幸い、コンパイラーはこの危険から私たちを守ってくれます。

共分散(で装飾されたパラメーター化されたタイプのジェネリックout

共分散は不変コレクションで広く使用されています(つまり、新しい要素をコレクションに追加またはコレクションから削除できない場合)

上記の例の解決策は、共変なジェネリックコレクションタイプが使用されるようにすることですIEnumerable(例:として定義IEnumerable<out T>)。IEnumerableコレクションに変更するメソッドはありません。また、out共分散の結果として、サブタイプがのコレクションをLifeFormメソッドに渡すことができます。

public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
    foreach (var lifeForm in lifeForms)
    {
        Console.WriteLine(lifeForm.GetType().ToString());
    }
}

PrintLifeForms今で呼び出すことができZebrasGiraffesかつ任意のIEnumerable<>任意のサブクラスのLifeForm

反変(で装飾されたパラメーター化された型を持つジェネリックin

反変は、関数がパラメーターとして渡されるときによく使用されます。

以下Action<Zebra>は、パラメーターとしてを受け取り、Zebraの既知のインスタンスでそれを呼び出す関数の例です。

public void PerformZebraAction(Action<Zebra> zebraAction)
{
    var zebra = new Zebra();
    zebraAction(zebra);
}

予想通り、これはうまく機能します:

var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra

直感的に、これは失敗します:

var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction); 

cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'

しかし、これは成功します

var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal

そしてこれも成功します:

var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba

どうして?Actionはとして定義されているためAction<in T>、つまりでありcontravariant、の場合、これは「最大」aになるAction<Zebra> myActionことmyActionができますが、のAction<Zebra>派生スーパークラスが少なくZebraても許容されます。

これは最初は直感的ではないかもしれませんが(たとえばAction<object>、を必要なパラメータとして渡すにAction<Zebra>はどうすればよいですか?)、ステップをアンパックすると、呼び出された関数(PerformZebraAction)自体がデータ(この場合はZebraインスタンス) )関数に-データは呼び出し元のコードからのものではありません。

このように高次関数を使用するという逆のアプローチのため、Actionが呼び出されるまでにZebrazebraAction関数に対して呼び出されるのはより派生したインスタンスです(パラメーターとして渡されます)が、関数自体はあまり派生型を使用しません。


7
これは、さまざまな分散オプションの優れた説明です。これは、例を通して説明し、コンパイラーがin / outキーワードなしで制限または許可する理由も明らかにするためです
Vikhram

反変にin使用されるキーワードはどこにありますか?
javadba

上記の@javadbaは、入力タイプAction<in T>Func<in T, out TResult>反変です。(私の例では、既存の不変(リスト)、共変(IEnumerable)、反変(アクション、
関数

C#わかりませんので、わかりません。
javadba

Scalaでもかなり似ていますが、構文が異なるだけです。[+ T]はTで共変であり、[-T]はTで反変であり、Scalaは「between」制約と「Nothing」無差別サブクラスを強制できます。持っていません。
StuartLC

32
class A {}
class B : A {}

public void SomeFunction()
{
    var someListOfB = new List<B>();
    someListOfB.Add(new B());
    someListOfB.Add(new B());
    someListOfB.Add(new B());
    SomeFunctionThatTakesA(someListOfB);
}

public void SomeFunctionThatTakesA(IEnumerable<A> input)
{
    // Before C# 4, you couldn't pass in List<B>:
    // cannot convert from
    // 'System.Collections.Generic.List<ConsoleApplication1.B>' to
    // 'System.Collections.Generic.IEnumerable<ConsoleApplication1.A>'
}

基本的に、1つの型のEnumerableを取る関数がある場合は、明示的にキャストしないと、派生型のEnumerableを渡すことができませんでした。

ただし、トラップについて警告するだけです。

var ListOfB = new List<B>();
if(ListOfB is IEnumerable<A>)
{
    // In C# 4, this branch will
    // execute...
    Console.Write("It is A");
}
else if (ListOfB is IEnumerable<B>)
{
    // ...but in C# 3 and earlier,
    // this one will execute instead.
    Console.Write("It is B");
}

とにかくそれは恐ろしいコードですが、それは存在し、C#4での動作の変化は、このような構成を使用する場合、微妙でバグを見つけるのが困難になる可能性があります。


したがって、c#3では、より派生型のメソッドに、より派生型を渡すことができるため、これは何よりもコレクションに影響します。
かみそり

3
はい、大きな変更はIEnumerableがこれをサポートするようになりましたが、以前はサポートしていませんでした。
Michael Stum

4

MSDNから

次のコード例は、メソッドグループの共分散および反変のサポートを示しています。

static object GetObject() { return null; }
static void SetObject(object obj) { }

static string GetString() { return ""; }
static void SetString(string str) { }

static void Test()
{
    // Covariance. A delegate specifies a return type as object, 
    // but you can assign a method that returns a string.
    Func<object> del = GetString;

    // Contravariance. A delegate specifies a parameter type as string, 
    // but you can assign a method that takes an object.
    Action<string> del2 = SetObject;
}

4

反変

現実の世界では、動物用シェルターがウサギをホストするたびに動物になるため、ウサギ用のシェルターの代わりに動物用のシェルターをいつでも使用できます。ただし、アニマルシェルターの代わりにウサギシェルターを使用すると、そのスタッフはトラに食べられる可能性があります。

あなたが持っている場合は、そのコードでは、これは手段IShelter<Animal> animalsあなたは、単に書くことができるIShelter<Rabbit> rabbits = animals ならば、あなたが約束し、使用TIShelter<T>だけなのでようなメソッドのパラメータとして:

public class Contravariance
{
    public class Animal { }
    public class Rabbit : Animal { }

    public interface IShelter<in T>
    {
        void Host(T thing);
    }

    public void NoCompileErrors()
    {
        IShelter<Animal> animals = null;
        IShelter<Rabbit> rabbits = null;

        rabbits = animals;
    }
}

アイテムをより一般的なものに置き換えます。つまり、分散を減らすか、分散を導入します。

共分散

現実の世界では、ウサギのサプライヤーがウサギを与えるたびにそれが動物であるため、動物のサプライヤーではなくウサギのサプライヤーをいつでも使用できます。ただし、ウサギの供給業者の代わりに動物の供給業者を使用する場合、トラに食べられる可能性があります。

あなたが持っている場合、コードでは、この手段があることISupply<Rabbit> rabbitsは、単に書くことができるISupply<Animal> animals = rabbits ならば、あなたが約束し、使用T中のISupply<T>メソッドの戻り値はとても好きなだけのように:

public class Covariance
{
    public class Animal { }
    public class Rabbit : Animal { }

    public interface ISupply<out T>
    {
        T Get();
    }

    public void NoCompileErrors()
    {
        ISupply<Animal> animals = null;
        ISupply<Rabbit> rabbits = null;

        animals = rabbits;
    }
}

アイテムをより派生したものに置き換える、つまり、分散を大きくするか、coを導入する分散をます。

全体として、これはコンパイル時にチェックできるだけです、ジェネリック型を特定の方法で処理して型の安全性を維持し、誰も食べないように約束です。

あなたはこれにあなたの頭を二重巻きにするためにこれを読んでもらいたいかもしれません。


あなたは虎に食べられます それは
賛成の

あなたのコメントcontravarianceは興味深いです。私は運用要件を示すものとしてそれを読んでいます。より一般的なタイプは、それから派生したすべてのタイプのユースケースをサポートする必要があるということです。したがって、この場合、動物保護施設は、あらゆる種類の動物の保護をサポートできなければなりません。その場合、新しいサブクラスを追加すると、スーパークラスが壊れる可能性があります。つまり、サブタイプティラノサウルスレックスを追加すると、既存のアニマルシェルターが破壊される可能性があります。
javadba

(続く)。これは、構造的に明確に記述されている共分散とは大きく異なります。より具体的なサブタイプはすべて、スーパータイプで定義された操作をサポートしますが、必ずしも同じ方法であるとは限りません。
javadba

3

コンバーターデリゲートは、両方の概念が一緒に機能することを視覚化するのに役立ちます。

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