仍然对协方差和逆变而感到困惑。进出

时间:2010-08-10 02:31:09

标签: c# covariance

好的,我在stackoverflow上看了一下这个主题,看了this& this,但仍然有点混淆co / contra-variance。

来自here

  

协方差允许“更大”(更少)   特定的)类型要替换   只有原始类型的API   用于“输出”位置(例如   返回值)。逆变允许   一个“更小”(更具体)的类型   在API中替换   原始类型仅用于   “输入”位置。

我知道它与类型安全有关。

关于in/out的事情。我可以说当我需要写入时使用in,而当它只读时可以使用outin表示反差,out协方差。但是从上面的解释......

here

  

例如,List<Banana>不能   被视为List<Fruit>因为   list.Add(new Apple())有效   列表但不适用于List<Banana>

所以不应该是,如果我要使用in /我要写入该对象,它必须更大更通用。

我知道这个问题已被提出但仍然非常困惑。

6 个答案:

答案 0 :(得分:53)

我必须长时间地思考如何解释这个问题。解释似乎与理解它一样困难。

想象一下,你有一个基类水果。你有两个子类Apple和Banana。

     Fruit
      / \
Banana   Apple

您创建了两个对象:

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

对于这两个对象,您可以将它们转换为Fruit对象。

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

您可以将派生类视为它们的基类。

但是你不能像处理派生类那样对待基类

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

让我们将它应用于List示例。

假设您创建了两个列表:

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

因为您无法像派生类那样处理基类,所以会产生错误。

如果您的问题是导致错误的原因,我也会解释。

这是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;
}

假设您再次创建了两个对象

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

记住香蕉有两个变量“a”和“b”,而Fruit只有一个变量“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类。没有声明/初始化所有数据成员。

现在我已经从淋浴间回来,让自己吃点小吃,这会让它变得有点复杂。

事后来说,在进入复杂的事情时我应该放弃这个比喻

让我们创建两个新类:

public class Base
public class Derived : Base

他们可以做任何你喜欢的事情

现在让我们定义两个函数

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> { }

然后我们创建两个对象:

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

如果你这样做:

base = derived;

您会收到类似“无法隐式转换自...”的错误

你有两个选择,1)显式转换它们,或者2)告诉编译器隐式转换它们。

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

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

如果您的界面如下所示,则会出现第二种情况:

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

再次将它与两个功能联系起来

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
}

当泛型仅用作接口方法的返回类型时,基本上使用out。用于何时将其用作Method参数。使用委托时也适用相同的规则。

有一些奇怪的例外,但我不会在这里担心它们。

对于提前发生的任何粗心错误,我们深表抱歉=)

答案 1 :(得分:42)

C#4.0中的协方差和逆变指的是使用派生类而不是基类的能力。 in / out关键字是编译器提示,用于指示类型参数是否将用于输入和输出。

协方差

C#4.0中的协方差由out关键字辅助,这意味着使用out类型参数的派生类的泛型类型是正常的。因此

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

由于AppleFruitList<Apple>可以安全地用作IEnumerable<Fruit>

逆变

Contravariance是in关键字,它表示输入类型,通常在委托中。原理是一样的,这意味着委托可以接受更多的派生类。

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

这意味着,如果我们有Func<Fruit>,则可以将其转换为Func<Apple>

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

如果它们基本上是同一个东西,为什么它们被称为共同/逆变?

因为即使原理是相同的,从派生到基础的安全转换,当在输入类型上使用时,我们可以安全地将较少派生的类型(Func<Fruit>)转换为更加派生的类型({{1 }}),这是有道理的,因为任何带Func<Apple>的函数也可以Fruit

答案 2 :(得分:7)

协方差很容易理解。这很自然。逆变性更令人困惑。

仔细看看example from MSDN。了解SortedList如何期望IComparer,但它们正在传递ShapeAreaComparer:IComparer。 Shape是“更大”类型(它在被调用者的签名中,而不是调用者),但是逆变量允许“较小”类型 - Circle - 替换ShapeAreaComparer中通常采用Shape的任何位置。< / p>

希望有所帮助。

答案 3 :(得分:6)

让我分享我对这个话题的看法。

免责声明:忽略空分配,我使用它们来使代码保持相对简短,它们足以查看编译器要告诉我们什么。

让我们从类的层次结构开始:

class Animal { }

class Mammal : Animal { }

class Dog : Mammal { }

现在定义一些接口,以说明inout泛型修饰符的实际作用:

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
}

好吧,如果限制我们,为什么还要使用带有inout修饰符的界面呢?让我们看看:


不变性

让我们以不变性开头(没有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)接受任何动物(包括哺乳动物),因此它是兼容
  • 结论:此类分配不兼容

如果我们尝试:IInvariant<Mammal> invariantMammal = (IInvariant<Dog>)null怎么办?

  • 呼叫IInvariant<Mammal>.Get()希望有哺乳动物的人,IInvariant<Dog>.Get()-返回 Dog (狗),每条狗都是哺乳动物,因此与兼容
  • li>
  • 呼叫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

这一点很重要:值得注意的是,根据类层次结构中泛型类型参数的较高还是较低,泛型类型本身由于不同的原因而互不兼容

好吧,让我们找出如何利用它。


协方差(out

使用out泛型修饰符(见上文)时,您具有协方差

如果我们的类型看起来像:ICovariant<Mammal>,则它声明两件事:

  • 我的某些方法返回了哺乳动物(因此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 (狗),每条狗都是哺乳动物,因此与兼容
  • li>
  • ICovariant.Set(哺乳动物)-由于out修饰符的限制,这不再是问题!
  • 结论,这种分配是 COMPATIBLE

让我们用以下代码进行确认:

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>,则它声明两件事:

  • 我的某些方法接受哺乳动物(因此in泛型修饰符)-这很无聊
  • 我的任何方法都不返回哺乳动物-尽管这很有趣,因为这是 in通用修饰符
  • 施加的实际限制

协方差实验

如果我们尝试:IContravariant<Mammal> contravariantMammal = (IContravariant<Animal>)null,怎么办?

  • IContravariant<Mammal>.Get() -由于in修饰符的限制,这不再是问题!
  • 呼叫IContravariant<Mammal>.Set(Mammal)的人都希望可以传递哺乳动物。由于IContravariant<Animal>.Set(Animal)接受任何动物(包括哺乳动物),因此它是兼容
  • 结论:这样的分配是兼容

如果我们尝试: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泛型修饰符吗? -很明显不是

为什么?回顾一下inout修饰符施加的限制。如果我们想让我们的通用类型参数既协变又是协变的,我们基本上会说:

  • 我们接口的任何方法都不会返回T
  • 我们界面的任何方法都不接受T

从本质上讲,这将使我们的通用接口非通用

如何记住它?

您可以使用我的把戏:)

  1. “协变”短于“ contravaraint”,并且与之相反与其修饰符的长度(分别为“ out”和“ in”)
  2. 相反变量有点计数器直观(请参见上面的示例)

答案 4 :(得分:5)

用Jons的话来说:

  

协方差允许更大的&#34; (不太具体)在API中键入替换,其中原始类型仅用于&#34;输出&#34;位置(例如作为返回值)。逆变性允许更小的&#34; (更具体)在API中键入替换,其中原始类型仅用于&#34;输入&#34;位置。

我发现他的解释起初很混乱 - 但是对于我来说,一旦被强调替换是有意义的,结合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表示协方差,其中方法返回更具体的类型

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

答案 5 :(得分:3)

在谈到主题之前,让我们快速回顾一下:

基类引用可以保存派生类对象但反之亦然。

<强>协方差: Covariance允许您传递派生类型对象,其中需要基类型对象 协方差可以应用于委托,通用,数组,接口等。

<强>逆变: 逆变量应用于参数。它允许将具有基类参数的方法分配给期望派生类的参数的委托

看看下面的简单示例:

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, 

        }
    }
}