变体(协变与逆变)
变体的引入是为了提高泛型类型的变量在赋值时可以对类型进行兼容性转换,以扩展泛型的灵活性。下面看个例子:
public delegate void DoWork<T>(T arg);
........
DoWork<A> del1=delegate(A arg){//.......};
DoWork<B> del2=del1;
B bb=new B();
del2(bb);
其中A ,B是两个类,B类继承A类,即:public class A{.....} public class B:A{......}
上面的代码无法成功编译,因为类型无法完成转换。为了解决这一问题,也就引入了变体。
(1)协变与逆变
变体可以划分为协变和逆变,不管是协变还是逆变,从命名上可以看出其差别因该在于转换方向上。
对于协变与逆变,msdn 解释如下:
“协变”是指能够使用与原始指定的派生类型相比,派生程度更大的类型。
“逆变”则是指能够使用派生程度更小的类型。
当然,这已经解释得很清楚了,下面就我个人看法进行说明。
我们再来看上面的例子,如果作如下修改:
public delegate void DoWork<in T>(T arg);
........
DoWork<A> del1=delegate(A arg){//.......};
DoWork<B> del2=del1;
B bb=new B();
del2(bb);
修改代码后,则上面的代码就可以通过编译啦。其实这个例子就属于逆变,例子中泛型委托变量的转换方向和类型的隐式转换方向在形式上是“逆”的,方向相反,故称为“逆变”。
当然,协变食欲逆变是相对的,协变在形式上是泛型接口或泛型委托变量中赋值的方向与类型的隐式转换方向一致。下面来看个协变的例子:
//定义类型
public class A{}
public class B:A{}
//泛型接口
public interface ITest<out T>
{
T create();
}
//实现接口
public class Test<Y>:ITest<Y> where Y:new()
{
public Y create()
{
return new Y();
}
//下面进行代码赋值转换
ITest<B> t1=new Test<B>();
ITest<A> t2=t1;//赋值成功
A a=t2.create();
Console.WriteLine(a.GetType().Name);
}
上例中ITest<B>->ITest<A>,然而,因为B类型是从A类型派生的,可以隐式转换后赋值给A类型的变量,所以,泛型变量的赋值方向与类型参数所指定类型的隐式转换方向保持一致,即属于“协变”。
当然,变体仅仅支持泛型的接口和委托,不能将变体用于类和结构。
(2)类型参数的输入与输出
通过上面对协变与逆变的介绍,可知,泛型定义是可以使用变体的,方法是为类型参数添加in或者out修饰符。使用in修饰符修饰的类型可以理解为输入类型,这些类型只能用作输入参数;使用out修饰符表示类型为输出类型,一般用于方法的返回值。
下面来看个使用out的实例:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace My 8 { 9 #region 定义类 10 public class Base { } 11 public class Test : Base { } 12 #endregion 13 14 #region 定义委托 15 public delegate T MyDelegate<out T>(); 16 #endregion 17 18 class Program 19 { 20 static void Main(string[] args) 21 { 22 MyDelegate<Test> d1 = delegate() 23 { 24 return new Test(); 25 }; //使用匿名方法 26 27 // 再定义一个MyDelegate委托,类型为Base 28 MyDelegate<Base> d2 = d1; 29 // 测试调用委托变量d2 30 Base b = d2(); 31 // 输出变量b的真实类型 32 Console.Write("变量b的类型:{0}", b.GetType().Name); 33 34 Console.Read(); 35 } 36 } 37 }
例子中,类型的隐式转换与委托变量赋值方向一致,因此本实例的变体属于协变。
上面给出了输出类型参数,接着我们看下输入类型参数,代码如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace My 8 { 9 #region 定义类型 10 public abstract class WorkBase 11 { 12 public abstract void Run(); 13 } 14 public class FinanceMng : WorkBase 15 { 16 public override void Run() 17 { 18 Console.WriteLine("财务管理"); 19 } 20 } 21 public class PaymentMng : WorkBase 22 { 23 public override void Run() 24 { 25 Console.WriteLine("薪酬管理"); 26 } 27 } 28 public class JobanalyMng : WorkBase 29 { 30 public override void Run() 31 { 32 Console.WriteLine("工作分析"); 33 } 34 } 35 #endregion 36 37 #region 泛型接口 38 public interface IWork<in W> 39 { 40 void DoWork(W w); 41 } 42 43 // 实现泛型接口 44 public class MyWork<T> : IWork<T> where T : WorkBase 45 { 46 public void DoWork(T w) 47 { 48 w.Run(); 49 } 50 } 51 #endregion 52 53 class Program 54 { 55 static void Main(string[] args) 56 { 57 IWork<WorkBase> everyWork = new MyWork<WorkBase>(); 58 59 // 调用一 60 IWork<FinanceMng> work1 = everyWork; 61 work1.DoWork(new FinanceMng()); 62 63 // 调用二 64 IWork<PaymentMng> work2 = everyWork; 65 work2.DoWork(new PaymentMng()); 66 67 // 调用三 68 IWork<JobanalyMng> work3 = everyWork; 69 work3.DoWork(new JobanalyMng()); 70 71 Console.Read(); 72 } 73 } 74 }
本例中赋值的方向与类型隐式转换的方向相反,属于逆变。
通过前面的例子,可以得出一条规律:输入类型参数(使用in修饰符)都是逆变,输出类型参数(使用out修饰符)都是协变。。。。。