仍然对协方差和逆变与输入/输出感到困惑

Jie*_*eng 45 c# covariance

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

这里开始

协方差允许在API中替换"更大"(更不具体)类型,其中原始类型仅用于"输出"位置(例如,作为返回值).逆变量允许在API中替换"较小"(更具体)类型,其中原始类型仅用于"输入"位置.

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

关于这in/out件事.我可以说in当我需要写它时,我会使用它,out当它只读它时.并且in意味着反方差,out协方差.但从上面的解释......

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

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

我知道这个问题已经被问到但仍然很困惑.

Aki*_*nos 56

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

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

     Fruit
      / \
Banana   Apple
Run Code Online (Sandbox Code Playgroud)

您创建两个对象:

Apple a = new Apple();
Banana b = new Banana();
Run Code Online (Sandbox Code Playgroud)

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

Fruit f = (Fruit)a;
Fruit g = (Fruit)b;
Run Code Online (Sandbox Code Playgroud)

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

但是,您不能像处理派生类那样处理基类

a = (Apple)f; //This is incorrect
Run Code Online (Sandbox Code Playgroud)

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

假设您创建了两个列表:

List<Fruit> fruitList = new List<Fruit>();
List<Banana> bananaList = new List<Banana>();
Run Code Online (Sandbox Code Playgroud)

你可以这样做......

fruitList.Add(new Apple());
Run Code Online (Sandbox Code Playgroud)

fruitList.Add(new Banana());
Run Code Online (Sandbox Code Playgroud)

因为当你将它们添加到列表中时,它基本上是对它们进行类型转换.你可以这样想到......

fruitList.Add((Fruit)new Apple());
fruitList.Add((Fruit)new Banana());
Run Code Online (Sandbox Code Playgroud)

但是,对相反的情况应用相同的逻辑会引发一些危险信号.

bananaList.Add(new Fruit());
Run Code Online (Sandbox Code Playgroud)

是相同的

bannanaList.Add((Banana)new Fruit());
Run Code Online (Sandbox Code Playgroud)

因为您不能像派生类那样处理基类,所以会产生错误.

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

这是Fruit课程

public class Fruit
{
    public Fruit()
    {
        a = 0;
    }
    public int A { get { return a; } set { a = value } }
    private int a;
}
Run Code Online (Sandbox Code Playgroud)

这是香蕉课

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;
}
Run Code Online (Sandbox Code Playgroud)

所以想象你再次创建了两个对象

Fruit f = new Fruit();
Banana ba = new Banana();
Run Code Online (Sandbox Code Playgroud)

记住香蕉有两个变量"a"和"b",而Fruit只有一个变量"a".所以当你这样做的时候......

f = (Fruit)b;
f.A = 5;
Run Code Online (Sandbox Code Playgroud)

您创建一个完整的Fruit对象.但如果你这样做......

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

问题是你没有创建一个完整的Banana类.没有声明/初始化所有数据成员.

现在我已经从洗澡回来了,让我的自己吃了一个小吃,这里有点复杂.

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

让我们创建两个新类:

public class Base
public class Derived : Base
Run Code Online (Sandbox Code Playgroud)

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

现在让我们定义两个函数

public Base DoSomething(int variable)
{
    return (Base)DoSomethingElse(variable);
}  
public Derived DoSomethingElse(int variable)
{
    // Do stuff 
}
Run Code Online (Sandbox Code Playgroud)

这有点像"out"的工作方式,你应该总是能够使用派生类,就好像它是一个基类,让我们将它应用于一个接口

interface MyInterface<T>
{
    T MyFunction(int variable);
}
Run Code Online (Sandbox Code Playgroud)

out/in之间的关键区别在于Generic用作返回类型或方法参数,这是前一种情况.

让我们定义一个实现这个接口的类:

public class Thing<T>: MyInterface<T> { }
Run Code Online (Sandbox Code Playgroud)

然后我们创建两个对象:

MyInterface<Base> base = new Thing<Base>;
MyInterface<Derived> derived = new Thing<Derived>;
Run Code Online (Sandbox Code Playgroud)

如果你这样做:

base = derived;
Run Code Online (Sandbox Code Playgroud)

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

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

base = (MyInterface<Base>)derived; // #1
Run Code Online (Sandbox Code Playgroud)

要么

interface MyInterface<out T>  // #2
{
    T MyFunction(int variable);
}
Run Code Online (Sandbox Code Playgroud)

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

interface MyInterface<T>
{
    int MyFunction(T variable); // T is now a parameter
}
Run Code Online (Sandbox Code Playgroud)

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

public int DoSomething(Base variable)
{
    // Do stuff
}  
public int DoSomethingElse(Derived variable)
{
    return DoSomething((Base)variable);
}
Run Code Online (Sandbox Code Playgroud)

希望你看到情况如何逆转但基本上是同一类型的转换.

再次使用相同的类

public class Base
public class Derived : Base
public class Thing<T>: MyInterface<T> { }
Run Code Online (Sandbox Code Playgroud)

和相同的对象

MyInterface<Base> base = new Thing<Base>;
MyInterface<Derived> derived = new Thing<Derived>;
Run Code Online (Sandbox Code Playgroud)

如果你试图将它们设置为相等

base = derived;
Run Code Online (Sandbox Code Playgroud)

你的编辑会再次对你大喊大叫,你有和以前一样的选择

base = (MyInterface<Base>)derived;
Run Code Online (Sandbox Code Playgroud)

要么

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

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

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

对不起任何疏忽的错误提前=)

  • 实际上你可以在那个环境中说'a =(Apple)f`.-1对于错误的帖子,甚至没有触及op的问题.哦,少了`a`和`b`和'aa` ...... (3认同)
  • @Tyler我对C#不是那么热,但不是这个C#? (3认同)

Igo*_*aka 47

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

协方差

C#4.0中的协方差由out关键字辅助,这意味着使用out类型参数的派生类的泛型类型是可以的.于是

IEnumerable<Fruit> fruit = new List<Apple>();
Run Code Online (Sandbox Code Playgroud)

既然Apple是一个Fruit,List<Apple>可以安全地用作IEnumerable<Fruit>

逆变

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

public delegate void Func<in T>(T param);
Run Code Online (Sandbox Code Playgroud)

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

Func<Fruit> fruitFunc = (fruit)=>{};
Func<Apple> appleFunc = fruitFunc;
Run Code Online (Sandbox Code Playgroud)

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

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

  • 最好称之为行动.Func令人困惑:) (6认同)

And*_*Gis 11

让我分享一下我对这个主题的看法.

免责声明:忽略空分配,我使用它们来保持代码相对较短,它们只是足以看到编译器想要告诉我们的内容.

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

class Animal { }

class Mammal : Animal { }

class Dog : Mammal { }
Run Code Online (Sandbox Code Playgroud)

现在定义一些接口,以说明实际修饰符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
}
Run Code Online (Sandbox Code Playgroud)

好吧,如果他们限制我们,为什么还要使用接口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()- 返回一只,每只狗都是哺乳动物,所以它是兼容的.
  • 无论谁打电话都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
Run Code Online (Sandbox Code Playgroud)

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

好的,让我们来看看我们如何利用它.


协方差(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()- 返回一只,每只狗都是哺乳动物,所以它是兼容的.
  • 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
Run Code Online (Sandbox Code Playgroud)

逆差(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
Run Code Online (Sandbox Code Playgroud)

顺便说一句,这感觉有点违反直觉,不是吗?

// 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
Run Code Online (Sandbox Code Playgroud)

为什么不兼得?

那么我们可以使用两者inout泛型修饰符吗?- 显然不是.

为什么?回顾一下限制措施inout修饰符所施加的限制.如果我们想使我们的泛型类型参数既有协变性又有逆变性,我们基本上会说:

  • 我们的接口的所有方法都没有返回 T
  • 我们的接口的所有方法都不接受 T

这基本上会使我们的通用接口非通用.

怎么记住它?

你可以用我的技巧:)

  1. "covariant"短于"contravaraint",这与其修饰符的长度相反(分别为"out"和"in")
  2. contra varaint有点直觉(参见上面的例子)

  • 我总是在用抽象语法解释代码范式时遇到问题,我倾向于在那里落后。所以你的解释是一种新的方法,因此是我真正完全理解的第一个方法,它是完美的!谢谢!你应该写书! (2认同)

Ric*_*ein 7

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

从MSDN中仔细查看此示例.了解SortedList如何期望IComparer,但它们正在传递ShapeAreaComparer:IComparer.Shape是"更大"的类型(它在被调用者的签名中,而不是调用者),但是逆变量允许"较小"类型 - Circle - 替换ShapeAreaComparer中通常采用Shape的任何位置.

希望有所帮助.


ABa*_*pai 5

在进入主题之前,我们先快速回顾一下:

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

协方差:协方差允许您在需要基类型对象的情况下传递派生类型对象协方差可以应用于委托、泛型、数组、接口等。

逆变: 逆变应用于参数。它允许将带有基类参数的方法分配给需要派生类参数的委托

看看下面的简单示例:

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, 

        }
    }
}
Run Code Online (Sandbox Code Playgroud)


wog*_*les 5

在琼斯的话:

协方差允许在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;    
Run Code Online (Sandbox Code Playgroud)

转换器代表帮助我理解它:

delegate TOutput Converter<in TInput, out TOutput>(TInput input);
Run Code Online (Sandbox Code Playgroud)

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();
Run Code Online (Sandbox Code Playgroud)