协变 和 逆变
摘要
● 协变和逆变的定义是什么?给我们带来了什么便利?如何应用?
● 对于可变的泛型接口,为什么要区分成协变的和逆变的两种?只要一种不是更方便吗?
● 为什么还有不可变的泛型接口,为什么有的泛型接口要故意声明成不可变的?
● 复合的可变泛型接口遵循哪些规则?
● 协变和逆变的数学定义是什么?如何利用数学模型解释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、不可变
如果一个接口的泛型参数没有in或out修饰符,它就是不可变的。比如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、为什么一个泛型参数不可以即是协变的又是逆变的?
简单来说是为了在编译期进行类型安全检查。