泛型的协变(Covariance)和逆变(Contravariance)

一、什么是协变和逆变

  • 如果存在一个泛型接口IFoo<T>
    它的泛型参数的子类型IFoo<Dog>可以安全的转换成泛型父类型的IFoo<Animal>
    这个过程就称为协变

  • 如果存在一个泛型接口IFoo<T>
    它的泛型参数的父类型IFoo<Animal>可以安全的转换成泛型子类型的IFoo<Dog>
    这个过程就成为逆变

    我们先明确两个术语:

  • 协变(Covariance):保持类型顺序。如果 DogAnimal 的子类,那么 IEnumerable<Dog> 可以被视为 IEnumerable<Animal>

  • 逆变(Contravariance):反转类型顺序。如果 DogAnimal 的子类,那么 Action<Animal> 可以被视为 Action<Dog>

二、协变与逆变解决的问题

  • 假设你有如下类层次:
    class Animal { }
    class Dog : Animal { }
    
    在没有协变的情况下,以下代码会编译失败:
    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> : IEnumerable
    
    因为泛型默认是 不变(Invariant) 的,即 List<Dog>List<Animal> 没有继承关系。
    但直觉上,狗的列表“应该是”动物列表的一种。协变就是为了解决这种合理的类型转换需求,同时保证类型安全。

三、实际应用场景

  • 协变: 你有一个 IReadOnlyList<IDocument>,但实际传入的是 IReadOnlyList<PdfDocument>,协变让它可以接受。
  • 逆变: 你有一个排序器IComparer<object>,它可以用于比较任何对象,包括 stringPerson,所以可以赋值给 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>,它的 _animalAnimal 类型。
    GetAnimal() 返回的是 Animal,但 catHandler.GetAnimal() 声称返回的是 Cat
    如果内部返回的是普通 Animal(不是 Cat),强转成 Cat 会抛出 InvalidCastException

🛑 2、为什么 in T 必须禁止 T 作为返回值?

  • 因为:
    逆变允许:IContravariant<Animal>IContravariant<Cat>
    IContravariant<Animal>.GetAnimal() 可能返回 DogCat等任何动物
    如果你把它当作 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/awaitawait 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
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值