10分钟浅谈泛型协变与逆变

首先声明,本文写的有点粗糙,只让你了解什么是协变和逆变,没有深入研究,根据这些年的工作经验,发现我们在开发过程中,很少会自己去写逆变和协变,因为自从net 4.0 (Framework 3.0) 以后,.net 就为我们提供了 定义好的逆变与协变。我们只要会使用就可以。协变和逆变都是在泛型中使用的。

  • 什么是逆变与协变呢

 

可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用。如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变量。协变和逆变是两个相互对立的概念:

 

  • 如果某个返回的类型可以由其派生类型替换,那么这个类型就是支持协变
  • 如果某个参数类型可以由其基类替换,那么这个类型就是支持逆变的。

 

看起来你有点绕,我们先准备个“”鸟”类,在准备一个“麻雀”类,让麻雀继承鸟类,一起看代码研究

 

  /// <summary>
    ////// </summary>
    public class Bird
    {
        public int Id { get; set; }
    }
    /// <summary>
    /// 麻雀
    /// </summary>
    public class Sparrow : Bird
    {
        public string Name { get; set; }
    }

 

 我们分别取实例化这个类,发现程序是能编译通过的。

Bird bird1 = new Bird();
Bird bird2 = new Sparrow();
Sparrow sparrow1 = new Sparrow();

  //Sparrow sparrow2 = new Bird();//这个是编译不通过的,违反了继承性。

但是我们放在集合中,去实例化,是无法通过的

List<Bird> birdList1 = new List<Bird>();

//List<Bird> birdList2 = new List<Sparrow>();//不是父子关系,没有继承关系
//一群麻雀一定是一群鸟

 那么我们如何去实现在泛型中的继承性呢??这就引入了协变和逆变得概念,为了保证类型的安全,C#编译器对使用了 out 和 in 关键字的泛型参数添加了一些限制:

  • 支持协变(out)的类型参数只能用在输出位置:函数返回值、属性的get访问器以及委托参数的某些位置
  • 支持逆变(in)的类型参数只能用在输入位置:方法参数或委托参数的某些位置中出现。
  • 协变

  我们来看下Net  “System.Collections.Generic”命名空间下的IEnumerable泛型 接口,会发现他的泛型参数使用了out 

现在我们使用下 IEnumerable  接口来进行一下上述实力,会发现,我们的泛型有了继承关系。

IEnumerable<Bird> birdList1 = new List<Bird>();

IEnumerable<Bird> birdList2 = new List<Sparrow>();//协变
//一群麻雀一定是一群鸟

下面我们来自己定义一个协变泛型接口ICustomerListOut<Out T>,让 CustomerListOut 泛型类继承CustomerListOut<Out T> 泛型接口。

代码如下

    /// <summary>
    /// out 协变 只能是返回结果
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface ICustomerListOut<out T>
    {
        T Get();

       // void Show(T t);//T不能作为传入参数
    }

    /// <summary>
    /// 类没有协变逆变
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class CustomerListOut<T> : ICustomerListOut<T>
    {
        public T Get()
        {
            return default(T);
        }

        public void Show(T t)
        {

        }
    }

 我们会发现,在泛型斜变的时候,泛型不能作为方法的参数。我们用自己定义的泛型接口和泛型类进行实例化试试,我们会发现编译通过

ICustomerListOut<Bird> customerList1 = new CustomerListOut<Bird>();//这是能编译的
ICustomerListOut<Bird> customerList2 = new CustomerListOut<Sparrow>();//这也是能编译的,在泛型中,子类指向父类,我们称为协变

到这里协变我们就学完了,协变就是让我们的泛型有了子父级的关系。本文开始的时候,协变和逆变,是在C# 4.0 以后才有的,那C# 4.0以前我们是怎么写的呢,那个时候没有协变?

老版本的写法

  List<Bird> birdList3 = new List<Sparrow>().Select(c => (Bird)c).ToList();//4.0以前的写法

等学完逆变,本文列出C# 4.0 以后的版本 中framework 已经定义好的协变、逆变 泛型接口,泛型类,泛型委托。

  • 逆变

 刚才我们学习了泛型参数用out 去修饰,饺子协变,现在来学习下逆变,逆变是使用in来修饰的

这里就是Net 4.0 给我们提供的逆变写法

我们自己写一个逆变的接口  ICustomerListIn<in T> ,在写一个逆变的 泛型类 CustomerListIn<T>:ICustomerListIn<T> ,代码如下

    /// <summary>
    /// 逆变
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface ICustomerListIn<in T>
    {
        //T Get();//不能作为返回值

        void Show(T t);
    }

    public class CustomerListIn<T> : ICustomerListIn<T>
    {
        public T Get()
        {
            return default(T);
        }

        public void Show(T t)
        {
        }
    }

 

 逆变的泛型参数是不能作为泛型方法的返回值的,我们来看下实例化鸟类,和麻雀类,看好使不好使。

ICustomerListIn<Sparrow> customerList2 = new CustomerListIn<Sparrow>();
ICustomerListIn<Sparrow> customerList1 = new CustomerListIn<Bird>();//父类指向子类,我们称为逆变

ICustomerListIn<Bird> birdList1 = new CustomerListIn<Bird>();
birdList1.Show(new Sparrow());
birdList1.Show(new Bird());

Action<Sparrow> act = new Action<Bird>((Bird i) => { });

 到此我们就完全学完了逆变与协变

 

  • 总结

逆变与协变只能放在泛型接口和泛型委托的泛型参数里面,

在泛型中out修饰泛型称为协变,协变(covariant  修饰返回值 ,协变的原理是把子类指向父类的关系,拿到泛型中。
 在泛型中in 修饰泛型称为逆变, 逆变(contravariant )修饰传入参数,逆变的原理是把父类指向子类的关系,拿到泛型中。

  • NET 中自带的斜变逆变泛型

 序号  类别  名称
 1  接口  IEnumerable<out T> 
 2 委托  Action<in T>
3 委托 Func<out TResult>
 4  接口 IReadOnlyList<out T> 
 5  接口 IReadOnlyCollection<out T>
     

 各位朋友,如果谁还知道,请留言告知

 

posted @ 2018-03-02 15:11  王柏成  阅读(13419)  评论(6编辑  收藏  举报