协变 和 逆变

摘要

    ● 协变和逆变的定义是什么?给我们带来了什么便利?如何应用?
  ● 对于可变的泛型接口,为什么要区分成协变的和逆变的两种?只要一种不是更方便吗?
  ● 为什么还有不可变的泛型接口,为什么有的泛型接口要故意声明成不可变的?
  ● 复合的可变泛型接口遵循哪些规则?
  ● 协变和逆变的数学定义是什么?如何利用数学模型解释C#4里的协变和逆变的规则?

前言

    协变和逆变是c#4.0引入的新概念,主要是针对于泛型而言的。有了它们,我们可以更准确的定义泛型委托和接口。

    首先举个栗子,比如IEnumerable<T> 接口是协变的,我们实现了一个这样的函数:

static void PrintPersonName(IEnumerable<Person> persons)
{
    foreach (Person person in persons)
    {
        Console.WriteLine(person.Name);
    }
}

    那么PrintPersonName这个方法就可以接受任何Person类的子类型列表作为它的参数。比如,若Student是Person的子类,那么可以这样调用:

IList<Student> students = new List<Student>();
PrintPersonName(students);

    在C#4.0之前,上面的语句是无法通过编译的,因为IEnumerable接口是不可变(invariant的。PrintPersonName方法只能接受Person列表作为其参数。如果Person的子类想实现同样的功能就必须自己PrintName方法,或者将PrintPersonName方法定义为泛型方法:

static void PrintPersonName<T>(IEnumerable<T> persons) where T : Person
{
    foreach (Person person in persons)
    {
        Console.WriteLine(person.Name);
    }
}

    上述方法可以运行的很好,但是不如直接协变接口这样简单明了。

协变和逆变的定义

  1、不可变

    如果一个接口的泛型参数没有inout修饰符,它就是不可变的。比如IList<T>。我们既不能这样:

IList<Person> personList1 = null;
IList<Student> stuList = null;
personList1 = stuList; // 编译错误:无法将IList<Student>隐式转换为IList<Person>

    也不能这样:

IList<Person> personList1 = null;
IList<Student> stuList = null;
stuList = personList1; // 编译错误:无法将IList<Person>隐式转换为IList<Student>

    只能这样:

IList<Person> personList1 = null;
IList<Person> personList2 = null;
personList1 = personList2;

  2、协变

    如果一个接口的泛型参数有out修饰符,它就是协变的。比如IEnumerable<out T>。我们既可以这样:

IEnumerable<Person> persons1 = null;
IEnumerable<Person> persons2 = null;
persons1 = persons2;

    也可以这样:

IEnumerable<Person> persons = null;
IEnumerable<Student> students = null;
persons = students; // 可以将IEnumerable<Student>隐式转换为IEnumerable<Person>

    但不能这样:

IEnumerable<Person> persons = null;
IEnumerable<Student> students = null;
students = persons; // 无法将IList<Person>隐式转换为IList<Student>

  3、逆变

    如果一个接口的泛型参数有in修饰符,它就是逆变的。比如IComparer<in T>。我们既可以这样:

IComparer<Person> personComparer1 = null;
IComparer<Person> personComparer2 = null;
personComparer1 = personComparer2;

    也可以这样:

IComparer<Person> personComparer = null;
IComparer<Student> studentComparer = null;
studentComparer = personComparer; // 可以把IComparer<Person>隐式转换为IComparer<Student>

    但不能这样:

IComparer<Person> personComparer = null;
IComparer<Student> studentComparer = null;
personComparer = studentComparer; // 无法将IComparer<Student>隐式转换为IComparer<Person>

    4、小结

  • 协变和逆变是一对互斥的概念
  • 只有接口和委托的泛型参数可以是协变或逆变的
  • 协变的泛型参数只能作为方法的返回值的类型
  • 逆变的泛型参数只能作为方法的参数的类型

C#中协变和逆变的设计

    在C#4.0的基础类库中,一些接口的泛型参数分别用了in或out修饰,比如:

public interface IEnumerator<out T> : IDisposable, IEnumerator
{
    T Current { get; }
}
public interface IComparable<in T> { int CompareTo(T other); }

    而另一些却没有:

public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
{
    T this[int index] { get; set; }
int IndexOf(T item); void Insert(int index, T item); void RemoveAt(int index); }
public interface IEquatable<T> { bool Equals(T other); }

    那么问题来了:

    1、为什么 IComparable<in T> 被声明成逆变的而 IEquatable<T> 却被声明成不可变的?

    2、为什么 IList<T> 被声明为不可变的?

    简单来说,既然协变的接口的泛型参数只能作为函数的返回值,而逆变的接口的泛型参数只能作为函数的参数,那么像 IList<T> 这种 T 既要做为返回值又要作为参数的情况,自然只能声明为不可变的了。

    3、为什么一个泛型参数不可以即是协变的又是逆变的?

    简单来说是为了在编译期进行类型安全检查。

 

本文转自:https://www.cnblogs.com/wangwust/p/6823431.html

posted @ 2020-04-24 15:13  NiKaFace  阅读(285)  评论(0编辑  收藏  举报