协变 和 逆变

转载至 https://cloud.tencent.com/developer/article/1388161 

本文简述了 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# 泛型接口中的协变与逆变

posted on 2020-02-29 11:48  jshchg  阅读(488)  评论(0编辑  收藏  举报

导航