web-dev-qa-db-ja.com

共変性と反変性とイン/アウトについてまだ混乱しています

わかりました、stackoverflowでこのトピックについて少し読んで、 thisthis を見ましたが、それでもco/contra-varianceについて少し混乱しています。

から ここ

共分散により、元の型が「出力」位置でのみ使用される(たとえば、戻り値として)APIで「より大きな」(特定性の低い)型を置き換えることができます。共変性により、元のタイプが「入力」位置でのみ使用されるAPIで、「より小さな」(より具体的な)タイプを置き換えることができます。

私はそれが型安全性と関係があることを知っています。

_in/out_のことについて。書き込む必要がある場合はinを使用し、読み取り専用の場合はoutを使用すると言えますか。 inは逆分散、out共分散を意味します。しかし、上記の説明から...

および ここ

たとえば、list.Add(new Apple())はListには有効ですが、_List<Banana>_には有効でないため、_List<Fruit>_を_List<Banana>_として扱うことはできません。

in /を使用してオブジェクトに書き込む場合は、より大きく、より一般的である必要があります。

私はこの質問がされたことを知っていますが、それでも非常に混乱しています。

47
Jiew Meng

C#4.0の共変性と反変性はどちらも、基本クラスの代わりに派生クラスを使用する機能を指します。 in/outキーワードは、タイプパラメータが入力と出力に使用されるかどうかを示すコンパイラのヒントです。

共分散

C#4.0の共分散は、outキーワードによって支援されます。これは、out型パラメーターの派生クラスを使用するジェネリック型がOKであることを意味します。したがって、

IEnumerable<Fruit> fruit = new List<Apple>();

AppleFruitであるため、List<Apple>IEnumerable<Fruit>として安全に使用できます。

共変性

共変性はinキーワードであり、通常はデリゲートでの入力タイプを示します。原則は同じです。つまり、デリゲートはより多くの派生クラスを受け入れることができます。

public delegate void Func<in T>(T param);

これは、Func<Fruit>がある場合、それをFunc<Apple>に変換できることを意味します。

Func<Fruit> fruitFunc = (fruit)=>{};
Func<Apple> appleFunc = fruitFunc;

それらが基本的に同じものであるのに、なぜそれらは共変性/反変性と呼ばれるのですか?

原理は同じですが、派生型からベースへの安全なキャストであるため、入力型で使用すると、派生型の少ない型(Func<Fruit>)を派生型の型(Func<Apple>)に安全にキャストできます。 、これは理にかなっています。Fruitを取る関数は、Appleを取ることもできるからです。

45
Igor Zevaka

私はこれをうまく説明する方法について長くそして一生懸命考えなければなりませんでした。説明するのは理解するのと同じくらい難しいようです。

基本クラスのFruitがあると想像してください。そして、2つのサブクラスAppleとバナナがあります。

     Fruit
      / \
Banana   Apple

2つのオブジェクトを作成します。

Apple a = new Apple();
Banana b = new Banana();

これらのオブジェクトの両方について、Fruitオブジェクトにタイプキャストできます。

Fruit f = (Fruit)a;
Fruit g = (Fruit)b;

派生クラスは、基本クラスであるかのように扱うことができます。

ただし、基本クラスを派生クラスのように扱うことはできません。

a = (Apple)f; //This is incorrect

これをリストの例に適用してみましょう。

2つのリストを作成したとします。

List<Fruit> fruitList = new List<Fruit>();
List<Banana> bananaList = new List<Banana>();

あなたはこのようなことをすることができます...

fruitList.Add(new Apple());

そして

fruitList.Add(new Banana());

これは、リストに追加するときに基本的に型キャストするためです。あなたはそれをこのように考えることができます...

fruitList.Add((Fruit)new Apple());
fruitList.Add((Fruit)new Banana());

ただし、逆の場合に同じロジックを適用すると、いくつかの危険信号が発生します。

bananaList.Add(new Fruit());

と同じです

bannanaList.Add((Banana)new Fruit());

基本クラスを派生クラスのように扱うことはできないため、これによりエラーが発生します。

なぜこれがエラーを引き起こすのかという質問があった場合に備えて、それについても説明します。

これがフルーツクラスです

public class Fruit
{
    public Fruit()
    {
        a = 0;
    }
    public int A { get { return a; } set { a = value } }
    private int a;
}

これがバナナのクラスです

public class Banana: Fruit
{
   public Banana(): Fruit() // This calls the Fruit constructor
   {
       // By calling ^^^ Fruit() the inherited variable a is also = 0; 
       b = 0;
   }
   public int B { get { return b; } set { b = value; } }
   private int b;
}

もう一度2つのオブジェクトを作成したと想像してください

Fruit f = new Fruit();
Banana ba = new Banana();

バナナには2つの変数「a」と「b」がありますが、フルーツには1つの「a」しかないことに注意してください。だからあなたがこれをするとき...

f = (Fruit)b;
f.A = 5;

完全なFruitオブジェクトを作成します。しかし、もしあなたがこれをするなら...

ba = (Banana)f;
ba.A = 5;
ba.B = 3; //Error!!!: Was "b" ever initialized? Does it exist?

問題は、完全なBananaクラスを作成していないことです。すべてのデータメンバーが宣言/初期化されているわけではありません。

今、私はシャワーから戻って、少し複雑になるここで自分自身に軽食を取りました。

後から考えると、複雑なものに入るときに比喩を落とすべきでした

2つの新しいクラスを作成しましょう:

public class Base
public class Derived : Base

彼らはあなたが好きなことをすることができます

次に、2つの関数を定義しましょう

public Base DoSomething(int variable)
{
    return (Base)DoSomethingElse(variable);
}  
public Derived DoSomethingElse(int variable)
{
    // Do stuff 
}

これは、「out」がどのように機能するかのようなもので、派生クラスを基本クラスであるかのように常に使用できるはずです。これをインターフェイスに適用しましょう。

interface MyInterface<T>
{
    T MyFunction(int variable);
}

Out/inの主な違いは、Genericが戻り値の型またはメソッドパラメータとして使用される場合です。これは前者の場合です。

このインターフェースを実装するクラスを定義しましょう:

public class Thing<T>: MyInterface<T> { }

次に、2つのオブジェクトを作成します。

MyInterface<Base> base = new Thing<Base>;
MyInterface<Derived> derived = new Thing<Derived>;

あなたがこれをした場合:

base = derived;

「暗黙的に...から変換できません」のようなエラーが発生します。

2つの選択肢があります。1)明示的に変換するか、2)コンパイラに暗黙的に変換するように指示します。

base = (MyInterface<Base>)derived; // #1

または

interface MyInterface<out T>  // #2
{
    T MyFunction(int variable);
}

2番目のケースは、インターフェイスが次のようになっている場合に発生します。

interface MyInterface<T>
{
    int MyFunction(T variable); // T is now a parameter
}

それを再び2つの機能に関連付ける

public int DoSomething(Base variable)
{
    // Do stuff
}  
public int DoSomethingElse(Derived variable)
{
    return DoSomething((Base)variable);
}

うまくいけば、状況がどのように逆転したかがわかりますが、本質的に同じタイプの変換です。

同じクラスを再度使用する

public class Base
public class Derived : Base
public class Thing<T>: MyInterface<T> { }

と同じオブジェクト

MyInterface<Base> base = new Thing<Base>;
MyInterface<Derived> derived = new Thing<Derived>;

それらを等しく設定しようとすると

base = derived;

あなたのコンパイラーは再びあなたに怒鳴ります、あなたは以前と同じオプションを持っています

base = (MyInterface<Base>)derived;

または

interface MyInterface<in T> //changed
{
    int MyFunction(T variable); // T is still a parameter
}

基本的に、ジェネリックがインターフェイスメソッドの戻り値の型としてのみ使用される場合に使用します。メソッドパラメータとして使用する場合に使用します。デリゲートを使用する場合にも同じルールが適用されます。

奇妙な例外がありますが、ここではそれらについて心配するつもりはありません。

事前に不注意な間違いをしてすみません=)

56
Akinos

このトピックについての私の見解を共有させてください。

免責事項:nullの割り当ては無視してください。コードを比較的短くするために使用しており、コンパイラが何を伝えたいかを確認するのに十分です。

クラスの階層から始めましょう:

_class Animal { }

class Mammal : Animal { }

class Dog : Mammal { }
_

次に、いくつかのインターフェースを定義して、inおよびout汎用修飾子が実際に何をするかを説明します。

_interface IInvariant<T>
{
    T Get(); // ok, an invariant type can be both put into and returned
    void Set(T t); // ok, an invariant type can be both put into and returned
}

interface IContravariant<in T>
{
    //T Get(); // compilation error, cannot return a contravariant type
    void Set(T t); // ok, a contravariant type can only be **put into** our class (hence "in")
}

interface ICovariant<out T>
{
    T Get(); // ok, a covariant type can only be **returned** from our class (hence "out")
    //void Set(T t); // compilation error, cannot put a covariant type into our class
}
_

では、なぜinおよびout修飾子を使用するインターフェースをわざわざ使用するのですか?restrict us?どれどれ:


不変性

不変性から始めましょう(inなし、out修飾子なし)

不変性実験

_IInvariant<Mammal>_を検討してください

  • IInvariant<Mammal>.Get()-哺乳類を返します
  • IInvariant<Mammal>.Set(Mammal)-哺乳類を受け入れます

試してみるとどうなりますか:IInvariant<Mammal> invariantMammal = (IInvariant<Animal>)null

  • IInvariant<Mammal>.Get()を呼び出す人は誰でも哺乳類を期待しますが、IInvariant<Animal>.Get()-は動物を返します。すべての動物が哺乳類であるとは限らないので、互換性がないです。
  • IInvariant<Mammal>.Set(Mammal)を呼び出す人は誰でも、哺乳類が渡されることを期待しています。 IInvariant<Animal>.Set(Animal)any動物(哺乳類を含む)を受け入れるので、互換性
  • [〜#〜]結論[〜#〜]:そのような割り当ては互換性がない

そして、試してみるとどうなりますか:IInvariant<Mammal> invariantMammal = (IInvariant<Dog>)null

  • IInvariant<Mammal>.Get()を呼び出す人は誰でも哺乳類を期待し、IInvariant<Dog>.Get()-はDogを返します。すべての犬は哺乳類なので、互換性です。
  • IInvariant<Mammal>.Set(Mammal)を呼び出す人は誰でも、哺乳類が渡されることを期待しています。 IInvariant<Dog>.Set(Dog)のみ犬を受け入れるので(そしてすべての哺乳類を犬としてではない)、それは互換性がないです。
  • [〜#〜]結論[〜#〜]:そのような割り当ては互換性がない

私たちが正しいかどうかを確認しましょう

_IInvariant<Animal> invariantAnimal1 = (IInvariant<Animal>)null; // ok
IInvariant<Animal> invariantAnimal2 = (IInvariant<Mammal>)null; // compilation error
IInvariant<Animal> invariantAnimal3 = (IInvariant<Dog>)null; // compilation error

IInvariant<Mammal> invariantMammal1 = (IInvariant<Animal>)null; // compilation error
IInvariant<Mammal> invariantMammal2 = (IInvariant<Mammal>)null; // ok
IInvariant<Mammal> invariantMammal3 = (IInvariant<Dog>)null; // compilation error

IInvariant<Dog> invariantDog1 = (IInvariant<Animal>)null; // compilation error
IInvariant<Dog> invariantDog2 = (IInvariant<Mammal>)null; // compilation error
IInvariant<Dog> invariantDog3 = (IInvariant<Dog>)null; // ok
_

これ1つIS重要:ジェネリック型パラメーターがクラス階層で上位か下位かによって、ジェネリック型自体が互換性がない)であることに注意してください。さまざまな理由で

では、どうすればそれを悪用できるかを調べてみましょう。


共分散(out

out汎用修飾子を使用すると共分散があります(上記を参照)

タイプが_ICovariant<Mammal>_のようになっている場合、次の2つのことを宣言します。

  • 私のメソッドのいくつかは哺乳類を返します(したがってout汎用修飾子)-これは退屈です
  • 私のメソッドはどれも哺乳類を受け入れません-これは実際の制限によって課せられたout汎用修飾子であるため、これは興味深いです

out修飾子の制限からどのように利益を得ることができますか?上記の「不変性実験」の結果を振り返ってください。共分散について同じ実験を行うとどうなるか見てみましょう。

共分散実験

試してみるとどうなりますか:ICovariant<Mammal> covariantMammal = (ICovariant<Animal>)null

  • ICovariant<Mammal>.Get()を呼び出す人は誰でも哺乳類を期待しますが、ICovariant<Animal>.Get()-は動物を返します。すべての動物が哺乳類であるとは限らないので、互換性がないです。
  • ICovariant.Set(哺乳類) -out修飾子の制限により、これは問題ではなくなりました。
  • [〜#〜]結論[〜#〜]そのような割り当ては互換性がない

そして、試してみるとどうなりますか:ICovariant<Mammal> covariantMammal = (ICovariant<Dog>)null

  • ICovariant<Mammal>.Get()を呼び出す人は誰でも哺乳類を期待し、ICovariant<Dog>.Get()-はDogを返します。すべての犬は哺乳類なので、互換性です。
  • ICovariant.Set(哺乳類) -out修飾子の制限により、これは問題ではなくなりました。
  • [〜#〜]結論[〜#〜]そのような割り当ては[〜#〜]互換性[〜#〜]

コードで確認しましょう:

_ICovariant<Animal> covariantAnimal1 = (ICovariant<Animal>)null; // ok
ICovariant<Animal> covariantAnimal2 = (ICovariant<Mammal>)null; // ok!!!
ICovariant<Animal> covariantAnimal3 = (ICovariant<Dog>)null; // ok!!!

ICovariant<Mammal> covariantMammal1 = (ICovariant<Animal>)null; // compilation error
ICovariant<Mammal> covariantMammal2 = (ICovariant<Mammal>)null; // ok
ICovariant<Mammal> covariantMammal3 = (ICovariant<Dog>)null; // ok!!!

ICovariant<Dog> covariantDog1 = (ICovariant<Animal>)null; // compilation error
ICovariant<Dog> covariantDog2 = (ICovariant<Mammal>)null; // compilation error
ICovariant<Dog> covariantDog3 = (ICovariant<Dog>)null; // ok
_

共変性(in

in汎用修飾子を使用すると、反変性が発生します(上記を参照)

タイプが_IContravariant<Mammal>_のようになっている場合、次の2つのことを宣言します。

  • 私のメソッドのいくつかは哺乳類を受け入れます(したがって、in汎用修飾子)-これは退屈です
  • 私のメソッドはどれも哺乳類を返しません-これは実際の制限inジェネリック修飾子によって課せられるので興味深いです

共変性実験

試してみるとどうなりますか:IContravariant<Mammal> contravariantMammal = (IContravariant<Animal>)null

  • IContravariant<Mammal>.Get() -in修飾子の制限により、これは問題ではなくなりました。
  • IContravariant<Mammal>.Set(Mammal)を呼び出す人は誰でも、哺乳類が渡されることを期待しています。 IContravariant<Animal>.Set(Animal)any動物(哺乳類を含む)を受け入れるので、互換性
  • [〜#〜]結論[〜#〜]:そのような割り当ては[〜#〜]互換性[〜#〜]

そして、試してみるとどうなりますか:IContravariant<Mammal> contravariantMammal = (IContravariant<Dog>)null

  • IContravariant<Mammal>.Get() -in修飾子の制限により、これは問題ではなくなりました。
  • IContravariant<Mammal>.Set(Mammal)を呼び出す人は誰でも、哺乳類が渡されることを期待しています。 IContravariant<Dog>.Set(Dog)のみ犬を受け入れるので(そしてすべての哺乳類を犬としてではない)、それは互換性がないです。
  • [〜#〜]結論[〜#〜]:そのような割り当ては互換性がない

コードで確認しましょう:

_IContravariant<Animal> contravariantAnimal1 = (IContravariant<Animal>)null; // ok
IContravariant<Animal> contravariantAnimal2 = (IContravariant<Mammal>)null; // compilation error
IContravariant<Animal> contravariantAnimal3 = (IContravariant<Dog>)null; // compilation error

IContravariant<Mammal> contravariantMammal1 = (IContravariant<Animal>)null; // ok!!!
IContravariant<Mammal> contravariantMammal2 = (IContravariant<Mammal>)null; // ok
IContravariant<Mammal> contravariantMammal3 = (IContravariant<Dog>)null; // compilation error

IContravariant<Dog> contravariantDog1 = (IContravariant<Animal>)null; // ok!!!
IContravariant<Dog> contravariantDog2 = (IContravariant<Mammal>)null; // ok!!!
IContravariant<Dog> contravariantDog3 = (IContravariant<Dog>)null; // ok
_

ところで、これは少し直感に反しているように感じますね。

_// obvious
Animal animal = (Dog)null; // ok
Dog dog = (Animal)null; // compilation error, not every Animal is a Dog

// but this looks like the other way around
IContravariant<Animal> contravariantAnimal = (IContravariant<Dog>) null; // compilation error
IContravariant<Dog> contravariantDog = (IContravariant<Animal>) null; // ok
_

なぜ両方ではない?

では、inoutの両方の汎用修飾子を使用できますか? -明らかにない

どうして? inおよびout修飾子が課す制限を振り返ってください。ジェネリック型パラメーターを共変と反変の両方にしたい場合は、基本的に次のように言います。

  • インターフェイスのどのメソッドもTを返しません
  • 私たちのインターフェースのどのメソッドもTを受け入れません

これは本質的に私たちのジェネリックインターフェースを非ジェネリックにします。

それを覚える方法は?

あなたは私のトリックを使うことができます:)

  1. 「covariant」は「contravaraint」よりも短く、これは反対修飾子の長さ(それぞれ「out」と「in」)になります。
  2. contra varaintは少しですcounter直感的です(上記の例を参照)
8
Andrzej Gis

共分散は非常に理解しやすいです。それは当然です。共変性はもっと紛らわしいです。

これをよく見てください MSDNの例 。 SortedListがIComparerをどのように期待するかを確認しますが、ShapeAreaComparer:IComparerを渡します。 Shapeは「大きい」タイプ(呼び出し元ではなく呼び出し先の署名にあります)ですが、共変性により、通常はShapeを使用するShapeAreaComparerのすべての場所を「小さい」タイプ(Circle)に置き換えることができます。

お役に立てば幸いです。

ジョンズの言葉で:

共分散により、元の型が「出力」位置でのみ使用される(たとえば、戻り値として)APIで「より大きな」(あまり具体的でない)型置換されるが可能になります。共変性により、元の型が「入力」位置でのみ使用されるAPIで「より小さな」(より具体的な)型置換されるが可能になります。

彼の説明は最初は紛らわしいと思いましたが、C#プログラミングガイドの例と組み合わせて、置き換えられることが強調されていることは一度私には理にかなっています。

// Covariance.   
IEnumerable<string> strings = new List<string>();  
// An object that is instantiated with a more derived type argument   
// is assigned to an object instantiated with a less derived type argument.   

// Assignment compatibility is preserved.   
IEnumerable<object> objects = strings;

// Contravariance.             
// Assume that the following method is in the class:   
// static void SetObject(object o) { }   
Action<object> actObject = SetObject;  
// An object that is instantiated with a less derived type argument   
// is assigned to an object instantiated with a more derived type argument.   

// Assignment compatibility is reversed.   
Action<string> actString = actObject;    

コンバーターデリゲートは私がそれを理解するのを助けます:

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

トピックに入る前に、簡単に復習しましょう。

基本クラス参照は派生クラスオブジェクトを保持できますが、その逆はできません。

Covariance:Covarianceを使用すると、基本型オブジェクトが期待される派生型オブジェクトを渡すことができます。Covarianceは、デリゲート、ジェネリック、配列、インターフェイスなどに適用できます。

Contravariance: Contravarianceはパラメーターに適用されます。これにより、基本クラスのパラメーターを持つメソッドを、派生クラスのパラメーターを期待するデリゲートに割り当てることができます。

以下の簡単な例を見てください。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CovarianceContravarianceDemo
{
    //base class
    class A
    {

    }

    //derived class
    class B : A
    {

    }
    class Program
    {
        static A Method1(A a)
        {
            Console.WriteLine("Method1");
            return new A();
        }

        static A Method2(B b)
        {
            Console.WriteLine("Method2");
            return new A();
        }

        static B Method3(B b)
        {
            Console.WriteLine("Method3");
            return new B();
        }

        public delegate A MyDelegate(B b);
        static void Main(string[] args)
        {
            MyDelegate myDel = null;
            myDel = Method2;// normal assignment as per parameter and return type

            //Covariance,  delegate expects a return type of base class
            //but we can still assign Method3 that returns derived type and 
            //Thus, covariance allows you to assign a method to the delegate that has a less derived return type.
            myDel = Method3;
            A a = myDel(new B());//this will return a more derived type object which can be assigned to base class reference

            //Contravariane is applied to parameters. 
            //Contravariance allows a method with the parameter of a base class to be assigned to a delegate that expects the parameter of a derived class.
            myDel = Method1;
            myDel(new B()); //Contravariance, 

        }
    }
}
3
ABajpai