代码改变世界

Effective C# 学习笔记(二十九)在范型中的协变和逆变

2011-07-17 17:34  小郝(Kaibo Hao)  阅读(490)  评论(0编辑  收藏  举报

协变:若一个返回值类型可以用该类型的子类型(继承类型)替代,我们就说该类型是可协变的

逆变:若一个参数的类型可以用该类型的父类型(被继承类型)替代,我们就说该类型是被逆变的。

在你的范型声明、使用中,注意支持协变(Covariance)和逆变(Contravariance)。编译器会捕捉你使用不当的逆变或协变行为。

举例说明协变和逆变:

这里有个抽象类CelestialBody,有Planet、Moon、Asteroid三个类型继承自该基类:

abstract public class CelestialBody

{

public double Mass { get; set; }

public string Name { get; set; }

// elided

}

public class Planet : CelestialBody

{

// elided

}

public class Moon : CelestialBody

{

// elided

}

public class Asteroid : CelestialBody

{

// elided

}

//下面这个方法以协变的方式来处理CeletialBody数组

public static void CoVariantArray(CelestialBody[] baseItems)

{

foreach (var thing in baseItems)

Console.WriteLine("{0} has a mass of {1} Kg",

thing.Name, thing.Mass);

}

//但下面这两种赋值操作是不安全的,编译可通过,但在运行时会抛出异常System.ArrayTypeMismatchException: 尝试访问类型与数组不兼容的元

素。

public static void UnsafeVariantArray(

CelestialBody[] baseItems)

{

baseItems[0] = new Asteroid { Name = "Hygiea", Mass = 8.85e19 };

}

CelestialBody[] spaceJunk = new Asteroid[5];

spaceJunk[0] = new Planet();

 

当范型这个特性引入C#中时,其对协变逆变的支持的很苛刻的。直到C# 4.0版本才添加了新的关键字(协变out  和逆变 in)来修饰范型接口的类型参数,才使这些范型可以更好的支持协变、逆变。

让我们看一个例子:

public static void CoVariantGeneric(

IEnumerable<CelestialBody> baseItems)

{

foreach (var thing in baseItems)

Console.WriteLine("{0} has a mass of {1} Kg",

thing.Name, thing.Mass);

}

上面这段代码用IEnumerable<T>作为形参类型,其可以用List<Planet>类型的对象作为参数。这是因为IEnumerable<T>利用了out关键字来限制T类型的使用范围。即类型IEnumberable<T>类型只能被用来作为返回值、属性的get访问器即部分代理的参数类型。IEnumberable<T>定义代码如下:

public interface IEnumerable<out T> : IEnumerable

{

IEnumerator<T> GetEnumerator();

}

public interface IEnumerator<out T> :

IDisposable, IEnumerator

{

T Current { get; }

// MoveNext(), Reset() inherited from IEnumerator

}

 

适用范围:

协变:方法的返回值的类型,属性的get访问器的类型、部分代理中的参数类型

逆变:方法参数的类型,属性的set访问器的类型,部分代理中的参数类型

之前说的部分代理中的参数类型,其规范由BCL定义如下(这里只列出了部分重载):

public delegate TResult Func<out TResult>();

public delegate TResult Func<in T, out TResult>(T arg);

public delegate TResult Func<in T1, T2, out TResult>(T1 arg1,T2 arg2);

public delegate void Action<in T>(T arg);

public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);

public delegate void Action<in T1, in T2, T3>(T1 arg1,T2 arg2, T3 arg3);

另外注意协变、逆变的声明上下文:

//协变接口

  public interface ICovariantDelegates<out T>

  {

        T GetAnItem();

        Func<T> GetAnItemLater();

        void GiveAnItemLater(Action<T> whatToDo);

  }


    在ICovariantDelegates<out T>接口中,T类型被声明为协变类型,而在该接口的

GiveAnItemLater方法中 Action<T>作为了该方法的参数,此时的T应为in 修饰的逆变类型参数,但这里的T是和上下文相关的,它只对ICovariantDelegates接口负责,也就是说尽管Action<in T>被声明为逆变的,T在GiveAnItemLater中的Action<T>声明是合法的。下例中的ActOnAnItemLater也是如此。

 //逆变接口

  public interface IContravariantDelegates<in T>

  {

        void ActOnAnItem(T item);

        void GetAnItemLater(Func<T> item);

        Action<T> ActOnAnItemLater();

  }