一、什么是协变和逆变
-
如果存在一个泛型接口
IFoo<T>
它的泛型参数的子类型IFoo<Dog>可以安全的转换成泛型父类型的IFoo<Animal>
这个过程就称为协变 -
如果存在一个泛型接口
IFoo<T>
它的泛型参数的父类型IFoo<Animal>可以安全的转换成泛型子类型的IFoo<Dog>
这个过程就成为逆变我们先明确两个术语:
-
协变(Covariance):保持类型顺序。如果Dog是Animal的子类,那么IEnumerable<Dog>可以被视为IEnumerable<Animal> -
逆变(Contravariance):反转类型顺序。如果Dog是Animal的子类,那么Action<Animal>可以被视为Action<Dog>
二、协变与逆变解决的问题
- 假设你有如下类层次:
在没有协变的情况下,以下代码会编译失败:class Animal { } class Dog : Animal { }
因为泛型默认是 不变(Invariant) 的,即List<Dog> dogs = new List<Dog>(); List<Animal> animals = new List<Dog>(); //❌编译错误!List<Dog> 不能赋值给 List<Animal> ,泛型不变性,非法。 IEnumerable<Animal> = new List<Dog>(); //✅协变,合法。因为IEnumerable是支持协变的 //IEnumerable泛型的定义为:public interface IEnumerable<out T> : IEnumerableList<Dog>和List<Animal>没有继承关系。
但直觉上,狗的列表“应该是”动物列表的一种。协变就是为了解决这种合理的类型转换需求,同时保证类型安全。
三、实际应用场景
- 协变: 你有一个
IReadOnlyList<IDocument>,但实际传入的是IReadOnlyList<PdfDocument>,协变让它可以接受。 - 逆变: 你有一个排序器
IComparer<object>,它可以用于比较任何对象,包括string或Person,所以可以赋值给IComparer<string>
四、协变与逆变的使用
- 协变(
out)和逆变(in)关键字 只能用于 泛型接口(interface)和泛型委托(delegate) 的类型参数上。
它们不能用于类(class)、结构体(struct)或枚举(enum)的泛型参数。
1、C# 使用 out 和 in 关键字来声明协变和逆变:
-
out T:表示 协变(Covariance),T 只能作为方法的返回值(输出),不能作为方法的参数// 协变:T 只作为返回值(生产者),不能作为接口内部方法的参数类型。 public interface IProducer<out T> { T Produce(); // ✅方法返回 T,T 是“输出” //void Produce(T t); // ❌out T 表示这个类型参数 T 只能作为接口内部方法的返回值,不能作为参数。 } -
in T:表示 逆变(Contravariance),T 只能作为方法的参数(输入),不能作为方法的返回值// 逆变:T 只作为输入参数(消费者),不能作为接口内部方法的返回值 public interface IConsumer<in T> { void Consume(T item); // ✅T 是“输入”,不能作为返回值 //T Produce(); // ❌in T 表示这个类型参数 T 只能接口内部方法的参数,不能作为返回值。 }
2、要这么设计?
-
协变 out T:T 只能作为返回值(输出)
-
含义: 如果 T 是协变的,那么
IEnumerable<Cat>可以当作IEnumerable<Animal>使用,因为“猫是动物”。 -
安全前提: 你只能“读”出 T 类型的对象,不能往里面写。因为写入可能会破坏类型安全。
-
设计原因: 允许更宽松的类型赋值,提升多态性,同时保证类型安全。
-
✅ 安全:从
IEnumerable<猫>读出一个“猫”,当作“动物”用,没问题! -
逆变 in T:T 只能作为参数(输入)
-
含义: 如果 T 是逆变的,那么
IComparer<Animal>可以当作IComparer<Cat>使用。 -
安全前提: 你只会把 T 类型的对象传给方法,而不会从方法中返回 T。
-
设计原因: 一个能比较“任何动物”的比较器,当然也能比较“猫”。
-
✅ 安全:把一只“
猫”传给一个能处理“动物”的比较器,没问题!
3、如果不加限制会怎样?
🛑1、为什么 out T 必须禁止 T 作为参数
-
错误案例1:
// 错误示例:如果允许协变位置传参 IEnumerable<动物> animals = new List<猫>(); // 假设协变允许 animals.Add(new 狗()); // ❌ 危险!List<猫> 里加入了 狗! -
错误案例2:
// ❌ 错误设计:逆变接口却允许返回 T public interface IProblematic<in T> { void SetAnimal(T animal); // ✅ 输入:逆变允许 T GetAnimal(); // ❌ 危险!逆变不允许返回 T,但假设这里允许 }⚠️ 注意:C# 编译器会阻止你在 in T 接口中将 T 作为返回值。但我们现在“假设”它允许,来看后果。
实现这个危险接口:
public class AnimalHandler<T> : IProblematic<T> { private T _animal; public void SetAnimal(T animal) { _animal = animal; } public T GetAnimal() { // 假设我们返回一个“通用的默认动物” if (_animal == null) return (T)(object)new Animal { Name = "Generic Animal" }; return _animal; } }现在开始“类型欺骗”:
// 1. 创建一个处理 Animal 的实例 IProblematic<Animal> animalHandler = new AnimalHandler<Animal>(); // 2. 逆变转换:把它当作 IProblematic<Cat> 使用(因为 Animal 是 Cat 的父类) IProblematic<Cat> catHandler = animalHandler; // ❌ 如果允许,这里就出问题了✅ 逻辑上似乎合理:一个能处理“任何动物”的组件,应该也能处理“猫”。
但危险来了:
// 3. 使用 catHandler 设置一个猫 catHandler.SetAnimal(new Cat { Name = "Mimi" }); // 4. 现在尝试获取一个 Cat Cat myCat = catHandler.GetAnimal(); // ❌ 运行时错误!💥 问题在哪?
catHandler实际是AnimalHandler<Animal>,它的_animal是Animal类型。
GetAnimal()返回的是Animal,但catHandler.GetAnimal()声称返回的是Cat。
如果内部返回的是普通Animal(不是Cat),强转成Cat会抛出InvalidCastException。
🛑 2、为什么 in T 必须禁止 T 作为返回值?
-
因为:
逆变允许:IContravariant<Animal>→IContravariant<Cat>
但IContravariant<Animal>.GetAnimal()可能返回Dog、Cat等任何动物
如果你把它当作IContravariant<Cat>,就期望GetAnimal()返回Cat
但实际上可能返回Dog,导致类型系统崩溃结论:逆变位置如果允许返回 T,就会破坏类型安全。
✅ 正确设计:in T 只允许 T 作为输入
public interface ISafeConsumer<in T> { void Feed(T animal); // ✅ 安全:传入 Cat,实际处理 Animal,没问题 // T GetAnimal(); // ❌ 编译错误!不允许返回 T }使用:
ISafeConsumer<Animal> animalFeeder = new AnimalFeeder(); ISafeConsumer<Cat> catFeeder = animalFeeder; catFeeder.Feed(new Cat()); // ✅ 安全:Cat 传给能处理 Animal 的方法✅ 安全原因:你只传入 Cat,系统把它当作 Animal 处理,没问题。
五、协变与逆变的使用案例
-
1、定义基础类和继承关系
// 动物基类 public class Animal { public string Name { get; set; } public Animal(string name) { Name = name; } public virtual void Speak() { Console.WriteLine($"{Name} 发出了声音。"); } } // 狗类,继承自动物 public class Dog : Animal { public Dog(string name) : base(name) { } public override void Speak() { Console.WriteLine($"{Name} 汪汪叫。"); } } // 猫类,继承自动物 public class Cat : Animal { public Cat(string name) : base(name) { } public override void Speak() { Console.WriteLine($"{Name} 喵喵叫。"); } } -
2、协变(Covariance)示例:out T
// 协变:使用 out T public interface ICovariant<out T> { T GetAnimal(); } public class AnimalProducer : ICovariant<Animal> { public Animal GetAnimal() => new Animal("普通动物"); } public class DogProducer : ICovariant<Dog> { public Dog GetAnimal() => new Dog("小狗旺财"); }使用协变
// 因为 ICovariant<out T> 是协变的,所以 DogProducer 可以当作 AnimalProducer 使用 ICovariant<Animal> producer = new DogProducer(); // 协变:Dog -> Animal Animal animal = producer.GetAnimal(); animal.Speak(); // 输出:小狗旺财 汪汪叫。 -
3、逆变(Contravariance)示例:in T
逆变允许你将 IContravariant 赋值给 IContravariant,适用于消费输入的场景。// 逆变:使用 in T public interface IContravariant<in T> { void Feed(T animal); } public class AnimalFeeder : IContravariant<Animal> { public void Feed(Animal animal) { Console.WriteLine($"喂食 {animal.Name}(通用饲料)"); } } public class DogFeeder : IContravariant<Dog> { public void Feed(Dog dog) { Console.WriteLine($"喂食 {dog.Name}(狗粮)"); } }使用逆变
// 因为 IContravariant<in T> 是逆变的,所以 AnimalFeeder 可以用于 Dog IContravariant<Dog> dogFeeder = new AnimalFeeder(); // 逆变:Animal <- Dog dogFeeder.Feed(new Dog("小狗乐乐")); // 输出:喂食 小狗乐乐(通用饲料)
六、排序器案例(in 逆变):
-
在 .NET 中,IComparer 接口已经声明了逆变(in 关键字)。这意味着你不需要“实现一个支持逆变的 IComparer”,因为这个接口本身就支持逆变。
IComparer<T> 的定义:
public interface IComparer<in T> { int Compare(T x, T y); }注意泛型参数 T 前面的 in 关键字。这表示 IComparer 是逆变的。
逆变意味着什么?
如果 Dog 是 Animal 的子类(Dog : Animal),那么 IComparer 可以被当作 IComparer 来使用using System; using System.Collections.Generic; public class Animal { public string Name { get; set; } public int Age { get; set; } } public class Dog : Animal { public string Breed { get; set; } // Dog 特有的属性 } // 实现一个针对比较所有 Animal 的比较器,可以用于 Dog、Cat 等子类(逆变) // 这种支持逆变的排序器,一般用用于一个父类,比如Animal, public class AnimalComparer : IComparer<Animal> { public int Compare(Animal? x, Animal? y) { if (x is null) return y is null ? 0 : -1; if (y is null) return 1; // 只使用 Animal 类中定义的成员 // 这样做是安全的,并且支持逆变 int nameComparison = string.Compare(x.Name, y.Name, StringComparison.Ordinal); if (nameComparison != 0) return nameComparison; return x.Age.CompareTo(y.Age); } } // 另一个例子:按年龄比较 public class AgeComparer : IComparer<Animal> { public int Compare(Animal? x, Animal? y) { if (x is null) return y is null ? 0 : -1; if (y is null) return 1; return x.Age.CompareTo(y.Age); } }利用逆变
class Program { static void Main() { List<Dog> dogs = new List<Dog> { new Dog { Name = "Buddy", Age = 5, Breed = "Golden Retriever" }, new Dog { Name = "Max", Age = 3, Breed = "German Shepherd" }, new Dog { Name = "Charlie", Age = 7, Breed = "Bulldog" } }; // 创建一个 Animal 的比较器 IComparer<Animal> comparer = new AnimalComparer(); // 利用逆变,直接用于 Dog 列表 dogs.Sort(comparer); // ✅ 因为 IComparer<Animal> 是 IComparer<Dog> 的“超类型” // 或者,直接赋值 IComparer<Dog> dogComparer = comparer; // ✅ 逆变允许这种赋值 dogs.Sort(dogComparer); foreach (var dog in dogs) { Console.WriteLine($"{dog.Name} ({dog.Age}) - {dog.Breed}"); } } }
七、通用排序的封装
- 代码
using System; using System.Collections.Generic; using System.Linq; // 示例类:动物和其子类 public class Animal { public string Name { get; set; } } public class Dog : Animal { public string Breed { get; set; } } public class Cat : Animal { public string Color { get; set; } } // 一个通用的、支持逆变的排序器 public static class SortHelper { /// <summary> /// 对任意类型的集合进行排序,支持逆变比较器 /// </summary> /// <typeparam name="T">集合元素类型</typeparam> /// <param name="items">待排序的集合</param> /// <param name="comparer">比较器,支持逆变(IComparer<in T>)</param> /// <returns>排序后的列表</returns> public static List<T> Sort<T>(IEnumerable<T> items, IComparer<T> comparer) { if (items == null) throw new ArgumentNullException(nameof(items)); var list = items.ToList(); list.Sort(comparer); return list; } /// <summary> /// 使用 Func 创建比较器(方便使用) /// </summary> /// <typeparam name="T">元素类型</typeparam> /// <typeparam name="TKey">排序键类型,需支持比较</typeparam> /// <param name="items">集合</param> /// <param name="keySelector">选择排序键的函数</param> /// <param name="descending">是否降序</param> /// <returns>排序后的列表</returns> public static List<T> SortBy<T, TKey>( IEnumerable<T> items, Func<T, TKey> keySelector, bool descending = false) where TKey : IComparable<TKey> { return descending ? items.OrderByDescending(keySelector).ToList() : items.OrderBy(keySelector).ToList(); } } // 一个针对 Animal 的比较器,可以用于 Dog、Cat 等子类(逆变) public class AnimalNameComparer : IComparer<Animal> { public int Compare(Animal x, Animal y) { if (x == null && y == null) return 0; if (x == null) return -1; if (y == null) return 1; return string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase); } } // 演示程序 class Program { static void Main() { var dogs = new List<Dog> { new Dog { Name = "Buddy" }, new Dog { Name = "Max" }, new Dog { Name = "Charlie" } }; // ✅ 关键:AnimalNameComparer 实现了 IComparer<Animal> // 但由于 IComparer<in T> 是逆变的,它可以赋值给 IComparer<Dog> IComparer<Dog> dogComparer = new AnimalNameComparer(); // 使用通用排序方法 var sortedDogs = SortHelper.Sort(dogs, dogComparer); Console.WriteLine("Sorted Dogs by Name:"); foreach (var dog in sortedDogs) { Console.WriteLine(dog.Name); } // 使用 SortBy 辅助方法(更简洁) var sortedByLambda = SortHelper.SortBy(dogs, d => d.Name); Console.WriteLine("\nSorted by Lambda:"); sortedByLambda.ForEach(d => Console.WriteLine(d.Name)); } }
八、.Net 内置支持协变和逆变的接口和委托
1、内置支持协变的 接口 和 委托
-
IEnumerable<out T>作用: 所有可枚举集合的基础接口。List<string> strings = new List<string> { "hello", "world" }; IEnumerable<object> objects = strings; // 协变:IEnumerable<string> -> IEnumerable<object> -
IEnumerator<out T>作用: 枚举器接口。IEnumerator<string> stringEnumerator = GetStrings().GetEnumerator(); IEnumerator<object> objectEnumerator = stringEnumerator; // 协变 -
IQueryable<out T>作用: LINQ 查询的核心接口。IQueryable<string> stringQuery = context.Strings; IQueryable<object> objectQuery = stringQuery; // 协变 -
IComparable<out T>作用: 定义通用比较方法。IComparable<string> stringComparer = ...; IComparable<object> objectComparer = stringComparer; // 协变 -
IAsyncEnumerable<out T>作用:用于表示一个可以异步枚举的元素序列。它是 IEnumerable 的异步对应物,是async/await和await foreach语法的基础。// 假设有一个返回 IAsyncEnumerable<string> 的异步方法 IAsyncEnumerable<string> GetStringsAsync() { return AsyncEnumerable.FromArray("hello", "world"); } // 因为 IAsyncEnumerable<out T> 是协变的,所以可以将 IAsyncEnumerable<string> 赋值给 IAsyncEnumerable<object> IAsyncEnumerable<object> GetObjectsAsync() { return GetStringsAsync(); // ✅ 这是合法的协变赋值 } // 使用 await foreach await foreach (object obj in GetObjectsAsync()) { Console.WriteLine(obj); } -
IReadOnlyCollection<out T>作用:表示其元素可读但不可修改的元素集合。它继承自 IEnumerable,并添加了 Count 属性。IReadOnlyCollection<string> stringCollection = new List<string> { "a", "b" }; IReadOnlyCollection<object> objectCollection = stringCollection; // ✅ 协变 -
IReadOnlyList<out T>作用:表示其元素可读但不可修改的元素列表。它继承自 IReadOnlyCollection 和 IEnumerable,并提供了基于索引的访问(this[int index])。IReadOnlyList<string> stringList = new List<string> { "first", "second" }; IReadOnlyList<object> objectList = stringList; // ✅ 协变 -
Func<out TResult>作用: 表示一个不带参数、返回 TResult 类型的函数。Func<string> getString = () => "Hello"; Func<object> getObject = getString; // 协变:Func<string> -> Func<object> -
Func<in T, out TResult>作用: 表示一个带一个参数 T 并返回 TResult 的函数。TResult 是协变的。Func<int, string> intToString = x => x.ToString(); Func<int, object> intToObject = intToString; // 协变:Func<int, string> -> Func<int, object>同理,
Func<in T1, in T2, out TResult>,Func<in T1, in T2, in T3, out TResult>等,
所有 Func 委托的最后一个类型参数(返回值)都是协变的。
2、内置支持逆变的 接口 和 委托
IComparer<in T>作用: 定义对象的比较方法。IComparer<object> objectComparer = Comparer<object>.Default; IComparer<string> stringComparer = objectComparer; // 逆变:IComparer<object> -> IComparer<string> // 因为任何能比较 object 的比较器,也一定能比较 string(string 是 object 的子类)IEqualityComparer<in T>作用: 定义对象的相等性比较方法。IEqualityComparer<object> objectEq = EqualityComparer<object>.Default; IEqualityComparer<string> stringEq = objectEq; // 逆变IObserver<in T>作用: 这是响应式扩展(Reactive Extensions, Rx)模式的核心接口,定义了数据“消费者”的契约。它与 IObservable 配对使用,构成了 .NET 中的观察者模式(发布-订阅模式)的基础。// 假设我们有一个可以处理任何 object 事件的通用日志记录观察者 IObserver<object> loggerObserver = new LoggerObserver(); // 实现了 OnNext, OnError, OnCompleted // 因为 IObserver<in T> 是逆变的,所以可以将 IObserver<object> 赋值给 IObserver<string> IObserver<string> stringObserver = loggerObserver; // ✅ 这是合法的 // 现在,一个发布 string 事件的可观察对象,可以安全地使用这个 object 观察者 var stringObservable = Observable.Return("Hello, Rx!"); stringObservable.Subscribe(stringObserver); // 会调用 loggerObserver.OnNext("Hello, Rx!")Action<in T>作用: 表示一个接受 T 类型参数、无返回值的操作。Action<object> printObject = obj => Console.WriteLine(obj); Action<string> printString = printObject; // 逆变:Action<object> -> Action<string> // 因为任何能处理 object 的操作,也一定能处理 string printString("Hello"); // 实际上调用 printObject("Hello")Action<in T1, in T2>作用: 表示一个接受两个参数的操作。所有参数类型都是逆变的。Action<object, object> logObjects = (a, b) => Console.WriteLine($"{a}, {b}"); Action<string, string> logStrings = logObjects; // 逆变Predicate<in T>作用: 表示一个定义一组条件并判断对象是否符合条件的委托。Predicate<object> isNotNull = obj => obj != null; Predicate<string> isStringNotNull = isNotNull; // 逆变Func<in T, out TResult>作用: 如前所述,Func 委托的输入参数 T 是逆变的。Func<object, string> objectToString = obj => obj?.ToString() ?? "null"; Func<string, string> stringToString = objectToString; // 逆变:Func<object, string> -> Func<string, string> // 因为这个函数能处理任何 object,自然也能处理 string
956

被折叠的 条评论
为什么被折叠?



