好的,我在stackoverflow上看了一下这个主题,看了this& this,但仍然有点混淆co / contra-variance。
来自here
协方差允许“更大”(更少) 特定的)类型要替换 只有原始类型的API 用于“输出”位置(例如 返回值)。逆变允许 一个“更小”(更具体)的类型 在API中替换 原始类型仅用于 “输入”位置。
我知道它与类型安全有关。
关于in/out
的事情。我可以说当我需要写入时使用in
,而当它只读时可以使用out
。 in
表示反差,out
协方差。但是从上面的解释......
和here
例如,
List<Banana>
不能 被视为List<Fruit>
因为list.Add(new Apple())
有效 列表但不适用于List<Banana>
。
所以不应该是,如果我要使用in
/我要写入该对象,它必须更大更通用。
我知道这个问题已被提出但仍然非常困惑。
答案 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>();
由于Apple
是Fruit
,List<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 { }
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
修饰符的界面呢?让我们看看:
让我们以不变性开头(没有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 (狗),每条狗都是哺乳动物,因此与兼容。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()
-会返回动物。并非每个动物都是哺乳动物,所以它不兼容。out
修饰符的限制,这不再是问题!如果我们尝试:ICovariant<Mammal> covariantMammal = (ICovariant<Dog>)null
怎么办?
ICovariant<Mammal>.Get()
希望有哺乳动物的人,ICovariant<Dog>.Get()
-返回 Dog (狗),每条狗都是哺乳动物,因此与兼容。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>
,则它声明两件事:
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
那么我们可以同时使用in
和out
泛型修饰符吗? -很明显不是。
为什么?回顾一下in
和out
修饰符施加的限制。如果我们想让我们的通用类型参数既协变又是协变的,我们基本上会说:
T
T
从本质上讲,这将使我们的通用接口非通用。
您可以使用我的把戏:)
答案 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,
}
}
}