泛型协变抗变
一、协变和逆变是什么?
先从字面上理解 协变(Covariance)、逆变(Contravariance)。
co- 是英文中表示“协同”、“合作”的前缀。协变 的字面意思就是 “与变化的方向相同”。
contra- 是英文中表示“相反”的前缀,逆变 的字面意思就是是 “与变化方向相反”。
那么问题来了,这里的 变化方向 指的是什么?
C# 中对于对象(即对象引用),仅存在一种隐式类型转换,即 子类型的对象引用到父类型的对象引用的转换。这里的变化指的就是这种 子->父 的类型转换。
object o = "hello";
//string (子类)类型的引用转换为 object (父类)类型的引用
协变与逆变虽然从名字上看是两个完全相反的转换,但其实只是“子类型引用到父类型引用”这一过程在函数中使用的 两个不同阶段 而已,接下来将详细说明这点。
二、使用函数的不同阶段发生的类型转换
假设有一函数,接收 object 类型的参数,输出 string 类型的返回值:
string Method(object o)
{
return "abc";
}
那么在Main函数中我们可以这样调用它:
string s = "abc";
object o = Method(s);
注意,这里发生了两次隐式类型转换:
在向函数输入时,参数 s 由 string 类型转换为 object 类型
在函数输出(返回)时,返回值 由 string 类型转换为 object 类型
我们这里可以看作是函数签名可发生变换(不论函数的内容,不影响结果):
string Method(object o) 可变换为 string Method(string o)
string Method(string o) 可变换为 object Method(string o)
也就是说,在函数输入时,函数的 输入类型 可由 object 变换为 string,父->子
在函数输出时,函数的 输出类型 可由string变换为object,子->父
三、理解泛型接口中的 in、out参数
没有指定in、out的情况
假设有一泛型接口,并且有一个类实现了此接口:
interface IDemo<T>
{
T Method(T value);
}
public class Demo : IDemo<string>
{
//实现接口 IDemo<string>
public string Method(string value)
{
return value;
}
}
在Main函数中这样写:
IDemo<string> demoStr = new Demo();
IDemo<object> demoObj = demoStr;
上面的这段代码中的第二行包含了一个假设:
IDemo<string> 类型能够隐式转换为 IDemo<object> 类型
这乍看上去就像“子类型引用转换为父类型引用” 一样,然而很遗憾,他们并不相同。假如可以进行隐式类型转换,那就意味着:
string Method(string value) 能转换为 object Method(object value)
从上一节中我们知道,在函数这输入和输出阶段,其类型可变化方向是不同的。
所以在C#中,要想应用泛型接口类型的隐式转换,需要讨论“输入”和“输出”两种情况。
接口仅用于输出的情况,协变
interface IDemo<out T>
{
//仅将类型 T 用于输出
T Method(object value);
}
public class Demo : IDemo<string>
{
//实现接口
public string Method (object value)
{
//别忘了类型转换!
return value.ToString();
}
}
在Main函数中这样写:
IDemo<string> demoStr = new Demo();
IDemo<object> demoObj = demoStr;
可将 string Method (object value) 转换为 object Method (object value)
即可将 IDemo<string> 类型转换为 IDemo<object> 类型。
仅从泛型的类型上看,这是 “子->父” 的转换,与第一节中提到的转换方向相同,称之为“协变”。
接口仅用于输入的情况,逆变
同理我们可以给 T 加上 in 参数:
interface IDemo<in T>
{
//仅将类型 T 用于输入
string Method(T value);
}
public class Demo : IDemo<object>
{
//实现接口
public string Method (object value)
{
return value.ToString();
}
}
在Main函数中这样写:
IDemo<object> demoObj = new Demo();
IDemo<string> demoStr = demoObj;
这里可将 string Method (object value) 转换为 string Method (string value)
即可将 IDemo<object> 类型转换为 IDemo<string> 类型。
仅从泛型的类型上看,这是 “父->子” 的转换,与第一节中提到的转换方向相反,称之为“逆变”,有时也译作“抗变”或“反变”。
四、总结
以上只讨论了协变与逆变在方法中的情况,其实在属性中情况也相类似,不再说明。
可能大家也发现了,所谓“协”与“逆”都是只是一种表象,其内在本质为同一过程。
协变代码实例
static void Main(string[] args) { IIndex<Rectangle> rectangles = RectangleCollection.GetRectangles(); IIndex<Shape> shapes = rectangles; Console.ReadKey(); } public interface IIndex<out T> { T this[int index] { get; } int Count { get; } } public class RectangleCollection : IIndex<Rectangle> { private Rectangle[] data = new Rectangle[3] { new Rectangle{Width=2,Height=3}, new Rectangle{Width=3,Height=7}, new Rectangle{Width=4.5,Height=2.9} }; private static RectangleCollection coll; public static RectangleCollection GetRectangles() { return coll ?? (coll = new RectangleCollection()); } public Rectangle this[int index] { get { if (index < 0 || index > data.Length) { throw new ArgumentOutOfRangeException("index"); } return data[index]; } } public int Count { get { return data.Length; } } } public class Shape { public double Width { get; set; } public double Height { get; set; } public override string ToString() { return String.Format("width:{0},height:{1}", Width, Height); } } public class Rectangle : Shape { }
抗变代码实例:
static void Main(string[] args) { IIndex<Rectangle> rectangles = RectangleCollection.GetRectangles(); IDisplay<Shape> shapeDisplay = new ShapeDisplay(); IDisplay<Rectangle> rectangleDisplay = shapeDisplay; rectangleDisplay.Show(rectangles[0]); Console.ReadKey(); } public interface IDisplay<in T> { void Show(T item); } public class ShapeDisplay : IDisplay<Shape> { public void Show(Shape item) { Console.WriteLine("{0} width:{1},height:{2}", item.GetType().Name, item.Width, item.Height); } }
浙公网安备 33010602011771号