协变性大致上是指,在使用“简单”类型的复杂类型中,反映继承关系的能力。
例如,我们总是可以将Cat
的实例视为Animal
的实例。如果ComplexType
是协变的,那么ComplexType<Cat>
可以被视为ComplexType<Animal>
。
我想知道:协变性的“类型”有哪些,它们与C#有什么关系(是否支持)?
代码示例会很有帮助。
例如,一种类型是返回类型协变性,Java支持,但C#不支持。
我希望有函数式编程经验的人也能参与讨论!
协变性大致上是指,在使用“简单”类型的复杂类型中,反映继承关系的能力。
例如,我们总是可以将Cat
的实例视为Animal
的实例。如果ComplexType
是协变的,那么ComplexType<Cat>
可以被视为ComplexType<Animal>
。
我想知道:协变性的“类型”有哪些,它们与C#有什么关系(是否支持)?
代码示例会很有帮助。
例如,一种类型是返回类型协变性,Java支持,但C#不支持。
我希望有函数式编程经验的人也能参与讨论!
以下是我的思考:
更新
通过阅读Eric Lippert指出并撰写的许多文章以及建设性的评论,我改进了回答:
返回类型协变性:
Java(>=5)[1]
和C++支持[2]
,但不支持C#(Eric Lippert说明了为什么不支持,以及你可以怎么做):
class B {
B Clone();
}
class D: B {
D Clone();
}
接口协变[3]
- 受 C# 支持
BCL 将通用的 IEnumerable
接口定义为协变接口:
IEnumerable<out T> {...}
class Animal {}
class Cat : Animal {}
IEnumerable<Cat> cats = ...
IEnumerable<Animal> animals = cats;
IEnumerable
是“只读”的 - 无法向其中添加元素。IList<T>
的定义,可以通过使用 .Add()
进行修改:public interface IEnumerable<out T> : ... //covariant - notice the 'out' keyword
public interface IList<T> : ... //invariant
委托协变通过方法组实现[4]
- 在C#中得到支持。
class Animal {}
class Cat : Animal {}
class Prog {
public delegate Animal AnimalHandler();
public static Animal GetAnimal(){...}
public static Cat GetCat(){...}
AnimalHandler animalHandler = GetAnimal;
AnimalHandler catHandler = GetCat; //covariance
}
“纯”委托协变性[5 - 预泛型版本发布的文章]
- 在C#中支持。
BCL对于不带参数并返回某个值的委托定义是协变的:
public delegate TResult Func<out TResult>()
Func<Cat> getCat = () => new Cat();
Func<Animal> getAnimal = getCat;
数组的协变 - 在C#中支持,但以一种有缺陷的方式[6]
[7]
。string[] strArray = new[] {"aa", "bb"};
object[] objArray = strArray; //covariance: so far, so good
//objArray really is an "alias" for strArray (or a pointer, if you wish)
//i can haz cat?
object cat == new Cat(); //a real cat would object to being... objectified.
//now assign it
objArray[1] = cat //crash, boom, bang
//throws ArrayTypeMismatchException
[8]
BCL定义的委托接受一个参数且返回空值是逆变的。public delegate void Action<in T>(T obj)
请耐心等待。让我们定义一下马戏团动物训练师 - 他可以被告知如何训练动物(通过给他一个与该动物配合的Action
)。
delegate void Trainer<out T>(Action<T> trainingAction);
Trainer<Cat> catTrainer = (catAction) => catAction(new Cat());
Trainer<Animal> animalTrainer = catTrainer;
// covariant: Animal > Cat => Trainer<Animal> > Trainer<Cat>
//define a default training method
Action<Animal> trainAnimal = (animal) =>
{
Console.WriteLine("Training " + animal.GetType().Name + " to ignore you... done!");
};
//work it!
animalTrainer(trainAnimal);
A < B => Action<A> > Action<B> (1)
将上面的A
和B
替换为Action<A>
和Action<B>
,得到:
Action<A> < Action<B> => Action<Action<A>> > Action<Action<B>>
or (flip both relationships)
Action<A> > Action<B> => Action<Action<A>> < Action<Action<B>> (2)
,-------------(1)--------------.
A < B => Action<A> > Action<B> => Action<Action<A>> < Action<Action<B>> (4)
`-------------------------------(2)----------------------------'
但是我们的 Trainer<T>
委托实际上是一个 Action<Action<T>>
:
Trainer<T> == Action<Action<T>> (3)
A < B => ... => Trainer<A> < Trainer<B>
objArray[1] = new object();
,虽然在编译时是合法的,但在运行时会抛出异常。从这个意义上讲,与其他示例不同,数组协变性是有问题的。详情请参阅此链接:http://blogs.msdn.com/b/ericlippert/archive/2007/10/17/covariance-and-contravariance-in-c-part-two-array-covariance.aspx - Chris Sinclairdelegate R D<R>(); ... D<Banana> db = ()=>new Banana(); D<Fruit> df = db;
。 - Eric Lippert这最好通过更通用的结构类型来解释。考虑以下内容:
元组在其组件类型上是协变的,即 (T1,T2) < (U1,U2) 当且仅当 T1 < U1 且 T2 < U2(其中“<”表示子类型)。
函数在结果方面是协变的,在参数方面是逆变的,即 (T1 -> T2) < (U1 -> U2) 当且仅当 U1 < T1 且 T2 < U2。
可变类型是不变的,即仅当 T = U 时 Mut(T) < Mut(U)。
所有这些规则都是最常见的正确子类型化规则。
现在,像主流语言中所熟知的对象或接口类型可以被解释为一种奇特的元组形式,其中包含其方法作为函数,以及其他东西。例如,接口
interface C<T, U, V> {
T f(U, U)
Int g(U)
Mut(V) x
}
C(T, U, V) = ((U, U) -> T, U -> Int, Mut(V))
f、g 和 x 分别对应元组的第一、第二和第三个组成部分。
根据上述规则可知,如果 T < T' 且 U' < U 且 V = V',则 C(T, U, V) < C(T', U', V')。这意味着通用类型 C 在 T 上是协变的,在 U 上是逆变的,在 V 上是不变的。
另一个例子:
interface D<T> {
Int f(T)
T g(Int)
}
是
D(T) = (T -> Int, Int -> T)
在这里,只有当 T < T' and T' < T 时,D(T) < D(T')。通常情况下,只有当T = T'时才会出现这种情况,因此D实际上在T中是不变的。
还有第四种情况,有时被称为“双变性”,即同时具有协变和逆变。例如:
interface E<T> { Int f(Int) }
System.Generic.Collections
命名空间中的.NET接口都是协变的。 - Aron