• [C#]浅谈协变与逆变


    看过几篇说协变与逆变的博客,虽然都是正确无误的,但是感觉都没有说得清晰明了,没有切中要害。
    那么我也试着从我的理解角度来谈一谈协变与逆变吧。


    什么是协变与逆变

    MSDN的解释:
    https://msdn.microsoft.com/zh-cn/library/dd799517.aspx

    协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型。
    泛型类型参数支持协变和逆变,可在分配和使用泛型类型方面提供更大的灵活性。

    一开始我总是分不清协变和逆变,因为MSDN的解释实在是严谨有余而易读不足。
    其实从中文的字面上来理解这两个概念就挺容易的了:

    "协变"即"协调的转变","逆变"即"逆向的转变"。

    为什么说"能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型"是协调的,而"能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型"是逆向的呢,看这两行代码:

    object o = "";
    string s = (string) o;

    string类型到object类型,也就是派生类到基类,是可以隐式转换的,因为任何类型向基类的转换都是类型安全的,所以认为这一转变是协调的。
    object类型到string类型,也就是基类到派生类,就只能是显式转换,因为对象o的实际类型不一定是string,强制转换不是类型安全的,所以认为这一转变是逆向的。

    再看协变与逆变的常见场合:

    IEnumerable<object> o = new List<string>();//协变
    Action<string> s = new Action<object>((arg)=>{...});//逆变

    上例的泛型参数就是分别发生了协调的与逆向的转变。

    协变与逆变的作用对象

    从定义中可以看到,协变与逆变都是针对的泛型参数,而且

    在.NET Framework 4中,Variant类型参数仅限于泛型接口和泛型委托类型。

    为什么是接口和委托?先看IEnumerable<T>和Action<T>的声明:

    public interface IEnumerable<out T> : IEnumerable
    {
        IEnumerator<T> GetEnumerator();
    }
    
    public delegate void Action<in T>(T obj);

    IEnumerable中的out关键字给泛型参数提供了协变的能力,Action中的in关键字给泛型参数提供了逆变的能力。
    这里的out和in是相对于谁的入和出?不是相对于接口和委托,而是相对于方法体
    看它们的实现:

    class MyEnumerable<T> : IEnumerable<T>
    {
        public IEnumerator<T> GetEnumerator()
        {
            yield return default(T);
        }
    
        IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
    }
    
    Action<string> myAction = new Action<object>(
        (o) =>
        {
            Console.WriteLine(o.ToString());
        });

    这样是不是能看出来泛型参数是怎么入和出的了?
    那么接口和委托,它们和方法是什么关系呢,它们两个之间又是什么关系,以下纯属个人理解:

    接口类型定义了一组方法签名,委托类型定义了一个方法结构(方法签名刨除方法名)。
    接口实例和委托实例都包含了一组方法入口

    综上所述,协变与逆变的作用对象是方法体中的泛型参数。

    为什么允许协变与逆变

    协变和逆变都是类型发生了转换,一旦涉及到类型转换当然就要想类型安全的问题。
    协变和逆变之所以可以正常的运转,就是因为这里所涉及到的所有类型转换都是类型安全的!
    回头看最开始的四行代码:

    1 object o1 = "";//类型安全
    2 string s1 = (string) o1;//非类型安全
    3 IEnumerable<object> o2 = new List<string>();//协变
    4 Action<string> s2 = new Action<object>((arg)=>{...});//逆变

    显然第二行的object到string是非类型安全的,那为什么第四行的object到string就是类型安全的呢?
    结合上一个方法体的示例,来看这段代码:

    1 Action<List<int>> myAction = new Action<IList<int>>(
    2     (list) =>
    3     {
    4         Console.WriteLine(list.Count);
    5     });
    6 myAction(new List<int> {1, 2, 3});

    第一行貌似是把IList转换成了List,但是实际上是这样的:
    第六行传入的实参是一个List,进入方法体,List被转换成了IList,然后使用了IList的Count属性。
    所以传参的时候其实发生的是派生类到基类的转换,自然也就是类型安全的了。

    List<string>到IEnumerable<object>的协变其实也是类似的过程:

     1 IEnumerable<Delegate> myEnumerable = new List<Action>
     2 {
     3     new Action(()=>Console.WriteLine(1)),
     4     new Action(()=>Console.WriteLine(2)),
     5     new Action(()=>Console.WriteLine(3)),
     6 };
     7 foreach (Delegate dlgt in myEnumerable)
     8 {
     9     dlgt.DynamicInvoke();
    10 }

    实参是三个Action,调用的是Delegate的DynamicInvoke方法,完全的类型安全转换。

    最后想说的是,所有死记硬背来的知识,都远远不如充分理解的知识来得可靠。

    要是觉得本文还算有点意思就在右下角点个推荐呗~
  • 相关阅读:
    [主席树][学习笔记]
    [bzoj2588][ Count on a tree]
    [bzoj3524][Couriers]
    [luogu3834][可持久化线段树 1(主席树)]
    [luogu3810][bzoj3262][陌上花开]
    [树套树][学习笔记]
    [luogu4556][Vani有约会]
    [线段树合并][学习笔记]
    [hdu6183][Color it]
    [动态开点线段树][学习笔记]
  • 原文地址:https://www.cnblogs.com/vd630/p/4572946.html
Copyright © 2020-2023  润新知