专栏首页tkokof 的技术,小趣及杂念编程小知识之协变和逆变

编程小知识之协变和逆变

本文简述了 C# 中协变和逆变的一些知识

在 C# 中, 协变逆变 能够实现 数组类型委托类型 的隐式引用转换, .NET Framework 4 (包括)以后, C# 也开始支持在 泛型接口泛型委托 中使用协变和逆变,下面的内容也主要围绕泛型类型参数的协变和逆变来进行讲解.

  • 什么是协变?

所谓协变(Covariance),是指能够使用比原始指定的类型派生程度更大的类型,简单理解就是 子类转为父类 这种变化.

C# 中协变对应的关键字为 out,我们一起来看个例子:

// generics covariance delegate
public delegate T CovarianceDelegate<out T>();

public static string Func()
{
	return string.Empty;
}

// ...

// CovarianceDelegate<string> can assign to CovarianceDelegate<object>
CovarianceDelegate<string> d1 = Func;
CovarianceDelegate<object> d2 = d1;
object o = d2();

上面代码中的函数 Func, 正常应该对应于委托 CovarianceDelegate<string>,但是因为我们使用了协变(<out T>),所以类型参数间只要构成 子类(示例中是 string)转父类(示例中是 object) 关系时便可以正确进行隐式引用转换,所以示例中将 d1(CovarianceDelegate<string>) 赋值于 d2(CovarianceDelegate<object>) 是合法的.

另外注意一点的就是,协变(out)的泛型类型参数只能作为输出参数,不能作为输入参数,关键字 out 的字面意思也很好的说明了这一点,下面的代码便是一个误用的例子:

// error, T just can be output param ...
public delegate T CovarianceDelegate<out T>(T input);

我们可以拿之前的示例来加深一下理解:

d1 是委托 CovarianceDelegate<string>,其返回一个 string 类型,
d2 的委托 CovarianceDelegate<object>,其返回一个 object 类型,
我们将 d1 赋值给 d2, 并调用 d2 的话(object o = d2()),实际上而言,
内部返回的应该是一个 string 类型(d2 -> d1 -> Func, Func 的返回类型是 string),
但是由于 string 类型可以正确的转换为 object 类型,
所以通过调用 d2 返回一个 object 类型是安全的(由内部的 string 类型转换而来)

上面的说明也解释了为何协变类型参数只能作为输出参数的原因,因为只有这样才能保证类型安全,如果不加这个限制,将其用于输入参数,我们将面对需要将父类转为子类的尴尬境地,类型安全自然难以保证.

  • 什么是逆变?

所谓逆变(Contravariance),是指能够使用比原始指定的类型派生程度更小的类型,简单理解就是 父类转为子类 这种变化.

C# 中逆变对应的关键字为 in, 我们同样先来看个示例:

// generics contravariance delegate
public delegate void ContravarianceDelegate<in T>(T val);

public static void Func(object val)
{
}

// ...

// ContravarianceDelegate<object> can assign to ContravarianceDelegate<string>
ContravarianceDelegate<object> d1 = Func;
ContravarianceDelegate<string> d2 = d1;
d2(string.Empty);

与协变(out)相对的,逆变(in)的泛型类型参数只能用于输入参数,不能用于输出参数,我们同样用上面的示例来讲解一下:

d1 是委托 ContravarianceDelegate<object>,其接受一个 object 类型参数,
d2 是委托 ContravarianceDelegate<string>,其接受一个 stirng 类型参数,
我们将 d1 赋值给 d2,并调用 d2 的话(d2(string.Empty)),
实际传入的参数是 string 类型,
但期望的参数是 object 类型(d2 -> d1 -> Func, Func 接受的参数类型是 object 类型),
但是由于 string 类型可以正确的转换为 object 类型,
所以通过调用 d2 传入一个 string 类型参数是安全的(string 类型内部会转换为 object 类型)

可以看到,虽然逆变是指 父类转为子类 这种看似不安全的类型变化(一般认为,子类转为父类总是安全的,父类转为子类则是不安全的),但这只是形式上的(ContravarianceDelegate<object> 转为 ContravarianceDelegate<string>,形式上看是进行了 object 类型到 string 类型的转换),内部而言,因为限制了输入参数的关系,实际进行的仍然是 子类转为父类 的过程,这也是保证逆变类型安全的前提,这点上逆变和协变其实是一致的.

小结:

  • 协变和逆变用于隐式引用转换
  • 协变的关键字为 out,被其修饰的参数类型只能用于输出参数
  • 逆变的关键字为 in,被其修饰的参数类型只能用于输入参数
  • 子类总是可以安全的转为父类是保证协变和逆变类型安全的统一前提

番外

考虑以下代码:

public delegate void Delegate1<in T>();
public delegate void Delegate2<in T>(Delegate1<T> d1);

按照之前逆变(in)仅能作为输入参数的说明,"似乎"上面的代码没有什么问题,但实际上这两行代码并不能通过编译,原因我们可以通过下面的代码来进行理解(示例代码的前提是 Delegate2 支持逆变):

public static void Func1(Delegate1<object> d1)
{
}

public static void Func2()
{
}

Delegate2<object> d1 = Func1;
Delegate2<string> d2 = d1;
d2(Func2);
d1 是委托 Delegate2<object>, 其接受一个 Delegate1<object> 类型的参数,
d2 是委托 Delegate2<string>, 其接受一个 Delegate1<string> 类型的参数,
将 d1 赋值给 d2, 并调用 d2 的话(d2(Func2)),
实际传入的参数是 Delegate1<string> 类型,
但期望的参数是 Delegate1<object> 类型(d2 -> d1 -> Func1, Func1 接受的参数类型是 Delegate1<object> 类型),
所以 Delegate2 支持逆变(in)的前提就是 Delegate1<string> 可以正确的转换为 Delegate1<object>,
即 Delegate1 应该支持协变(out)!

通过将 Delegate1 改为支持协变,代码就可以编译通过了:

public delegate void Delegate1<out T>();
public delegate void Delegate2<in T>(Delegate1<T> d1);

进一步的小结:

  • 协变逆变需要根据具体情况分析,不能简单的参照输入参数及输出参数原则
  • 输入参数及输出参数原则是依据参数本身而言的,不适用于参数的包装类型

参考资料

  1. 协变和逆变 (C#)
  2. 泛型中的协变和逆变
  3. 深入理解 C# 协变和逆变
  4. 理解 C# 泛型接口中的协变与逆变

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 随便再聊一点点Coroutine(确实只是一点点~)

      之前写过一点Coroutine相关的东西(这里和这里),大致讲了些自己关于Unity协程的理解,自己在平日的工作中也确实用到了不少相关的知识,遂而引发了一个...

    用户2615200
  • 一个游戏程序员的代码书写观(一)

    游戏中基本都有MessageBox的需求,虽然可以使用OS层面的MessageBox,但是一般而言都不能满足游戏的需求,有鉴于此,我们实现了第一版的定制Mess...

    用户2615200
  • [译]Unity 实用技巧

    TextArea 特性可以让我们更加方便的在 Inspector 中编辑字符串文本.

    用户2615200
  • 你绝没用过的一款高级空间可视化工具

    说起 Python 中的可视化,我们一般用的最多的是 Matplotlib,绘制一般的图效果都很好。有时候也会用风格比较好看的 Pyecharts 库,尤其是在...

    AI科技大本营
  • 【Python基础系列】常见的数据预处理方法(附代码)

    本文简单介绍python中一些常见的数据预处理,包括数据加载、缺失值处理、异常值处理、描述性变量转换为数值型、训练集测试集划分、数据规范化。

    统计学家
  • C#3.0新增功能02 匿名类型

    匿名类型提供了一种方便的方法,可用来将一组只读属性封装到单个对象中,而无需首先显式定义一个类型。 类型名由编译器生成,并且不能在源代码级使用。 每个属性的类型由...

    张传宁老师
  • 为什么vue中的data必须是一个函数?

    object是引用类型,如果不用function返回,每个组件的data都是内存的同一个地址,一个数据改变了其他也改变了。

    用户3258338
  • 大数据24小时 | 数据调研公司IHS拟 59亿美元收购Markit,人人网“复出”拟千万元投资体育大数据

    美国数据调研公司IHS将拟59亿美元收购伦敦金融数据服务商Markit ? 美国IHS宣布,将收购伦敦金融数据供应商Markit,打造价值130亿美元的数据和商...

    数据猿
  • 谷歌之后,亚马逊也开源了自家的深度学习工具

    GAIR 今年夏天,雷锋网将在深圳举办一场盛况空前的“全球人工智能与机器人创新大会”(简称GAIR)。大会现场,雷锋网将发布“人工智能&机器人Top25创新企...

    AI科技评论
  • [Oracle ASM全解析] asmcmd管理ASM文件

    -l 代表显示详细信息 -s代表显示空间使用 -t代表按时间排序而不是按名称 –permission表示显示权限信息

    bsbforever

扫码关注云+社区

领取腾讯云代金券