Generic Variance

IEnumerable<string> strings = new List<string> { "a", "b", "c" };
IEnumerable<object> objects = strings;

The line 2 of the above code tried to convert IEnumerable<string> to IEnumerable<object>. You can see that the generic type is converted from string to object. This feature is called Generic Variance.

Generic Modifier

There’re two modifiers which can go with the generic type: in and out. Generic modifier can only appear on delegates or interface. This is limited by CLR, I’ll explain it later.
A type can be declared with out modifier only if it defines the type of a method’s return type (property getter) and not of a method’s parameters (property setter).
A type can be declared with in modifier only if it defines the type of a method’s parameters (property setter) and not of a method’s return type (property getter).
If a type is declared without modifier, it could define both a method’s parameters or a method’s return type.

variance

The out keyword specifies that the type parameter is Covariant (协变). Covariance occurs when the type parameter is covariant.
The in keyword specifies that the type parameter is Contravariant (逆变). Contravariance occurs when the type parameter is contravariance.
If the type parameter comes with no modifier, it’s Invariant (不变). Invariance occurs when the type parameter is invariant.

// Covariant
public interface IEnumerator<out T>
{
    T Current { get; }
}

// Contravariant
public delegate void Action<in T>(T obj);

// Invariant
public interface IList<T>
{
    T this[int index] { get; set; }
    void Add(T item);
    // ......
}

Restrictions

进行Variance Conversion的时候,其实只需要把握住“严进宽出”的原则就好。

  • 对于Covariance,该type只会出现在output的位置。首先output必然满足其继承的更宽泛的type (父类)。其次,output未必能满足更严格的type (子类)。所以,covariance只可以向宽泛的方向转换。
    该output在后面的使用时,也确实是被转换为宽泛type使用的 (Implicit Reference Conversion),output转换方向和variance的转换方向是一直的,这就是为什么叫做协变。
  • 对于Contravariance,该type只会出现在input的位置。在delegate或者interface的implement中,可能会用到该type的某些feature。更严格的type (子类),继承了全部的feature,在implement中不会有问题。而更宽泛的type (父类),不一定有全部需要的feature。所以,contravariance只可以向严格的方向转换。
    该input在implement中,还是被转换为宽泛type使用的 (Implicit Reference Conversion),input的转换方向和variance的转换方向相反,所以叫逆变。
  • 对于Invariance,该type既可能是output,也可能是input。于是,转换的方向必须“既宽泛又严格”。唯一的方案就是不转换,即所谓的Identity Conversion——转换为同样的type。daynamic和object被认为是equivalent的。
// Covariance
IEnumerable<string> covFrom = new List<string> { "a", "b" };
IEnumerable<object> covTo = covFrom;
foreach (object obj in covTo)
{
    Console.WriteLine(obj.GetHashCode());
}

// Contravariance
Func<object, int> contravFrom = obj => obj.GetHashCode();
Func<string, int> contravTo = contravFrom;
Console.WriteLine(contravTo("abc"));

// Invariance
IList<string> invFrom = new List<string> { "a", "b" };
IList<string> invTo = invFrom;

回过头来在谈谈in和out为什么只能用在interface或delegate上,而不可以用在class和struct上。
class和struct之于interface,区别在于该entity是仅仅描述行为,还是也能够存放数据。如果该instance本身需要存储数据,那么就要提供read/write的双向操作,否则该instance就变成write only/read only的。
Write only没有实际使用的意义。
Read only基本上就是immutable type,为了实现这个需求,大概要在CLR的实现上付出非常大的成本。Microsoft基于CP考虑,选择了限制使用。

Leave a comment