• 协变和逆变


    首先, 假设有一下三个类的继承关系

        class Person
        {
            public string Name { get; set; }
        }
    
        class Student: Person
        {
            public string Class { get; set; }  //班级
        }
    
        class HighSchoolStudent:Student
        {
            public string DormNo { get; set; } //假设HighSchool才住宿舍
        }

    然后编码

        static void Main(string[] args)
        {
            IEnumerable<Student> studentList = new List<Student>();
            IEnumerable<Person> personList = studentList;
        }

    我们很自然的认为, 既然studentList里真实保存的对象是Student, 那么转换为IEnumerable<Person>应该是自然而然的事情
    但是在.Net Framework 4之前, 这却是不允许的, 编译时会提示

    无法将类型“System.Collections.Generic.IEnumerable<Test.Student>”隐式转换为“System.Collections.Generic.IEnumerable<Test.Person>”。存在一个显式转换(是否缺少强制转换?)

    即使你变成这样也不可以

    IEnumerable<Person> personList = (IEnumerable<Person>)studentList;

    好在.Net Framework 4有了泛型协变和逆变后, 开始支持这种转换了, 下面我们开始学习一下协变和逆变

    首先要了解 协变 和 逆变的定义: 对于泛型方法、泛型类
    如果某个返回的类型可以由其基类替换,那么这个类型就是支持协变

    IEnumerable<Student> d = new List<Student>();
    IEnumerable<Person> b = d;

    如果某个输入参数类型可以由其派生类型替换,那么这个类型就是支持逆变

    Action<Person> b = (target) => { Console.WriteLine(target.Name); };
    Action<Student> d = b;

    可以看出, 不管是协变还是逆变, 都遵从规则: 子类可以向父类隐式转换, 但是父类不能向子类隐式转换
    需要注意的是, 协变、逆变都是针对泛型变量(这里的b和d), 而不是后面List<Student> 可不可以Add(new Person()) 或Add(new HighSchoolStudent())

    下面这个泛型方法func1: 输入一个Person, 处理后, 输出一个新Student
    func2可以安全的由func1转换过来: func1需要输入输入一个 Person的时候, func2给的是Student, 可以安全的隐式转换; func1输出Student的时候, func2只要Person就可以了, 可以安全的隐式转换

    func1=Func<in Person, out Student>()
    func2=Func<Student, Person >()
    func1=func2;

    in :  Student作为输入参数, 转换(逆变)成 in Person
    out:  out Student转换(协变)成Person 作为输出结果

    我们平常用到的IEnumerable<out T>就是带 out 的协变(但List却不是)
    不对啊, 对于IEnumerable<Student>, 通常理解是, 要创建一个Student的列表, 赋值给它的肯定是一个Student列表, Student就是它的输入类型, 按理说 T 前面要加in 才对啊!?
    其实呢, 是我们对IEnumerable<T>理解不够, IEnumerable<Student> list=new List<Student>(), 其实表达的意思是, 你从IEnumerable<Student> list取出(输出)的对象默认就是Student, 后面的List可以是new List<HighSchoolStudent>(), 但你从IEnumerable<Student> list 取得的对象仍然是Student; 如果你用IEnumerable<Person> list指向这个列表, 那么取出来的对象就是Person
    同时编译器为确保类型安全, 总是强制要求赋值给list的必须是Student对象, 久而久之, 让人错觉赋值( 输入)给list的必须Student, 然后你搞混了, 这里的隐式转换其实是列表的里的对象, 但我们现在讨论的是泛型变量

    我们平时用到的Action<in T>, 就是带in 的逆变
    输入T参数, 最终输出void 的方法, 输入参数可以用派生类替代, 还是很好理解的

    这时候你可以理解为啥List<T> 不带out了, 因为List<T> 同时具有读取和写入功能;
    如果只是读取功能, List<Student> 转换成 List<Person> 当然没问题
    但是写入的时候, List<Student>即使能转换为List<Person>, 却不可能Add(new Person())
    IEnumerable<Student> 只有读功能,将里面的对象转换为IEnumerable<Person> 没有任何问题

    总结: 对于只需要输入或者只需要输出的泛型接口来说, 尽量加上 in 或者 out, 以支持泛型转换
    public delegate TResult Func<in T1, in T2, in T3, out TResult>( (T1 arg1, T2 arg2,…) 就是很好的例子

    平时我们撸码的时候, 一般都用不到这些协变、逆变, 但是编写高度抽象的泛型方法时候, 却要知道这些知识

    end

  • 相关阅读:
    ATS(App Transport Security)对HTTP协议屏蔽引起的问题
    后台子线程(非主线程)更新UI引起的警告
    Xcode无法启动ios模拟器的问题
    UIButton修改文字大小问题
    imageNamed和imageWithContentsOfFile-无法加载图片的问题
    storyboard在ios模拟器无法显示的问题
    返回一个数组的连续子数组和的最大值
    第二周学习进度总结
    软件工程开学第一节课课堂测试
    第一周学习进度总结
  • 原文地址:https://www.cnblogs.com/zhouandke/p/5831079.html
Copyright © 2020-2023  润新知