目 录CONTENT

文章目录

CSharp(四十八) 协变与逆变详解

C# 协变与逆变详解


一、先从一个生活中的困惑说起

假设你养了一只狗(Dog 继承自 Animal),现在有人问你要一只动物(Animal),你把狗给他——完全没问题,因为狗本来就是动物

class Animal { }
class Dog : Animal { }

// 日常认知:狗可以当作动物
Dog dog = new Dog();
Animal animal = dog;  // ✅ 没问题,狗是动物

但到了泛型和委托里,这个逻辑就"卡住"了:

List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs;  // ❌ 编译错误!为什么不行?

Func<Dog> getDog = () => new Dog();
Func<Animal> getAnimal = getDog;  // ❌ 也不让!

问题来了DogAnimal,但 List<Dog> 不是 List<Animal>Func<Dog> 也不是 Func<Animal>

协变和逆变就是用来解决"泛型之间能不能互相转换"这个问题的。


二、核心概念——用"箱子"来理解

2.1 类比:水果和苹果

🍎 Apple 继承自 🍏 Fruit

常识:
  Apple → Fruit         ✅ 苹果就是水果

困惑:
  List<Apple> → List<Fruit>    ❌ 一筐苹果不是一筐水果??
  Func<Apple> → Func<Fruit>    ❌ 这个怎么判断?

原因:筐是一个"容器",容器里的东西和容器本身是两码事。

2.2 一张图看懂协变和逆变

                    协变(out)
        用"更具体"代替"更宽泛"——"输出"位置
        ─────────────────────────────→

基类型                         派生类型
Animal    ←────────────────     Dog
Fruit     ←────────────────     Apple
IEnumerable<Animal>  ←───────  IEnumerable<Dog>
Func<Animal>         ←───────  Func<Dog>

        ←─────────────────────────────
                    逆变(in)
        用"更宽泛"代替"更具体"——"输入"位置



关键词记忆:
  协变 =  out  =  输出  =  派生 → 基类  =  "能出得去"
  逆变 =  in   =  输入  =  基类 → 派生  =  "能进得来"

三、协变(Covariance)—— out 关键字

3.1 什么是协变?

协变允许你使用"更具体"的类型代替"更宽泛"的类型,前提是类型参数只出现在"输出"位置。

用生活场景理解:

一个"生产水果"的机器,你给它换成"生产苹果"的机器,完全没问题。因为苹果也是水果,生产出来的东西仍然是水果。

// 协变 = 派生类型 → 基类型(在输出位置)

IEnumerable<Dog> dogs = new List<Dog>();
IEnumerable<Animal> animals = dogs;  // ✅ 一堆狗可以当一堆动物来看

Func<Dog> getDog = () => new Dog();
Func<Animal> getAnimal = getDog;     // ✅ 返回狗的委托可以当返回动物的委托用

3.2 定义支持协变的泛型接口

out 关键字标记类型参数:

// 定义一个支持协变的接口——"只出不进"
interface IProducer<out T>
{
    T Produce();           // ✅ T 在"输出"位置 → 可以用 out
    // void Consume(T item);  // ❌ 如果加这个,T 也出现在"输入"位置,out 就失效了
}

// 实现
class DogProducer : IProducer<Dog>
{
    public Dog Produce()
    {
        return new Dog();
    }
}

// 使用——协变让转换合法
IProducer<Dog> dogProducer = new DogProducer();
IProducer<Animal> animalProducer = dogProducer;  // ✅ 协变!

Animal result = animalProducer.Produce();  // 实际返回的是 Dog
Console.WriteLine(result.GetType().Name);  // Dog

3.3 C# 内置的支持协变的类型(已经帮你写好了 out)

// 这些接口都定义了 out T,所以支持协变:

IEnumerable<out T>       // ✅ 只能从中取数据
IEnumerator<out T>       // ✅ 只能从中取数据
IReadOnlyList<out T>     // ✅ 只读列表
IReadOnlyCollection<out T>
Func<out TResult>        // ✅ 只返回值,不接收参数

// 示例
IEnumerable<string> strings = new List<string> { "a", "b", "c" };
IEnumerable<object> objects = strings;  // ✅ 协变!

foreach (object obj in objects)
{
    Console.WriteLine(obj);  // 一个个取出来,都是 object
}

3.4 委托中的协变(Func)

class Animal { }
class Dog : Animal { }
class Cat : Animal { }

// Func<out TResult>:返回值位置是 out
Func<Dog> makeDog = () => new Dog();
Func<Animal> makeAnimal = makeDog;  // ✅ 协变

Animal animal = makeAnimal();
Console.WriteLine(animal.GetType().Name);  // Dog


// 更复杂的例子
Func<string, Dog> parseDog = s => new Dog();
Func<string, Animal> parseAnimal = parseDog;  // ✅ 返回值是 Dog,可以当 Animal

四、逆变(Contravariance)—— in 关键字

4.1 什么是逆变?

逆变允许你使用"更宽泛"的类型代替"更具体"的类型,前提是类型参数只出现在"输入"位置。

用生活场景理解:

一个"给狗喂食"的机器,你给它换成"给动物喂食"的机器——更宽泛了。但因为狗也是动物,给动物喂食的机器同样能给狗喂食。完全 OK!

// 逆变 = 基类型 → 派生类型(在输入位置)

Action<Animal> feedAnimal = a => Console.WriteLine($"喂了一只{a.GetType().Name}");
Action<Dog> feedDog = feedAnimal;  // ✅ 能喂动物的,肯定能喂狗

feedDog(new Dog());  // 输出: 喂了一只Dog

4.2 定义支持逆变的泛型接口

in 关键字标记类型参数:

// 定义一个支持逆变的接口——"只进不出"
interface IConsumer<in T>
{
    void Consume(T item);    // ✅ T 在"输入"位置 → 可以用 in
    // T Produce();             // ❌ 如果加这个,T 也出现在"输出"位置,in 就失效了
}

// 实现
class AnimalFeeder : IConsumer<Animal>
{
    public void Consume(Animal animal)
    {
        Console.WriteLine($"喂了一只 {animal.GetType().Name}");
    }
}

// 使用——逆变让转换合法
IConsumer<Animal> animalFeeder = new AnimalFeeder();
IConsumer<Dog> dogFeeder = animalFeeder;  // ✅ 逆变!能喂动物的也能喂狗

dogFeeder.Consume(new Dog());  // 输出: 喂了一只 Dog

4.3 C# 内置的支持逆变的类型

// 这些接口都定义了 in T,所以支持逆变:

IComparer<in T>         // ✅ 比较器——接收参数,不返回 T
IEqualityComparer<in T> // ✅ 相等比较器
Action<in T>            // ✅ 接收参数,不返回值

// 示例
class AnimalComparer : IComparer<Animal>
{
    public int Compare(Animal x, Animal y)
    {
        return x.GetType().Name.CompareTo(y.GetType().Name);
    }
}

IComparer<Animal> animalComparer = new AnimalComparer();
IComparer<Dog> dogComparer = animalComparer;  // ✅ 逆变!能比较动物的也能比较狗

4.4 委托中的逆变(Action)

// Action<in T>:参数位置是 in
Action<Animal> handleAnimal = a => Console.WriteLine($"处理动物: {a.GetType().Name}");
Action<Dog> handleDog = handleAnimal;  // ✅ 逆变

handleDog(new Dog());  // 输出: 处理动物: Dog
// 能处理 Animal 的方法,用来处理 Dog 完全没问题

4.5 Func 中同时有协变和逆变

// Func<in T, out TResult>
//    参数 T 是 in(逆变),返回值 TResult 是 out(协变)

Func<Animal, Dog> transform = a => new Dog();

// 逆变:参数可以用更宽泛的
// 协变:返回值可以用更具体的
// 所以组合起来:
Func<Animal, Dog>   原始:    输入 Animal → 输出 Dog
Func<Dog, Animal>   结果:    输入 Dog    → 输出 Animal

// 逆变:参数从 Animal 变 Dog(更具体)—— 能处理 Animal 就能处理 Dog
// 协变:返回值从 Dog 变 Animal(更宽泛)—— 返回 Dog 就是返回 Animal

五、实战对比——能转和不能转

5.1 完整对比示例

using System;
using System.Collections.Generic;

class Animal
{
    public string Name { get; set; }
}
class Dog : Animal
{
    public void Bark() => Console.WriteLine($"{Name}: 汪汪!");
}

class Program
{
    static void Main()
    {
        // ===== 1. 协变:IEnumerable<out T> =====
        List<Dog> dogs = new List<Dog>
        {
            new Dog { Name = "旺财" },
            new Dog { Name = "大黄" }
        };

        // ✅ 协变:IEnumerable<Dog> → IEnumerable<Animal>
        IEnumerable<Animal> animals = dogs;
        foreach (Animal a in animals)
        {
            Console.WriteLine(a.Name);  // 只能看到 Animal 的属性
        }

        // ===== 2. 逆变:Action<in T> =====
        Action<Animal> printAnimal = a => Console.WriteLine($"动物的名字: {a.Name}");

        // ✅ 逆变:Action<Animal> → Action<Dog>
        Action<Dog> printDog = printAnimal;
        printDog(new Dog { Name = "小黑" });  // 输出: 动物的名字: 小黑


        // ===== 3. List<T> 不支持协变/逆变!=====
        List<Dog> dogList = new List<Dog>();
        // List<Animal> animalList = dogList;  // ❌ 编译错误!

        // 为什么?因为如果允许,就会出这种问题:
        // animalList.Add(new Cat());  // Cat 是 Animal,但不是 Dog!
        // 那 dogList 里就有了 Cat,类型不安全!


        // ===== 4. Func<out T> 协变 =====
        Func<Dog> createDog = () => new Dog { Name = "新狗" };

        // ✅ 协变:Func<Dog> → Func<Animal>
        Func<Animal> createAnimal = createDog;
        Animal result = createAnimal();
        Console.WriteLine(result.Name);  // 新狗


        // ===== 5. 逆变:IComparer<in T> =====
        class AnimalAgeComparer : IComparer<Animal>
        {
            public int Compare(Animal x, Animal y)
                => x.Name.CompareTo(y.Name);
        }

        IComparer<Animal> comparer = new AnimalAgeComparer();
        // ✅ 逆变:IComparer<Animal> → IComparer<Dog>
        IComparer<Dog> dogComparer = comparer;
    }
}

5.2 关键规则——为什么 List<T> 不支持?

// IEnumerable<T>(只读接口)定义:
// public interface IEnumerable<out T>   ← 有 out,支持协变

// 原因是:你只能从里面"取"数据,不能"塞"数据。
// 取出来的 Dog 当 Animal 看待,安全!


// List<T> 定义(简化):
// public class List<T>
// 没有 out/in 标记 ← 不支持协变/逆变

// 原因是:List 既能"取"又能"塞",如果支持协变就危险了!
List<Dog> dogs = new List<Dog>();
// List<Animal> animals = dogs;  // 假设允许(实际不允许)
// animals.Add(new Cat());       // Cat 是 Animal,可以加进来
// Dog dog = dogs[0];            // 但实际上拿到的是 Cat!类型不安全!


// 对比:数组居然支持协变!(C# 历史遗留的坑)
Dog[] dogArray = new Dog[3];
Animal[] animalArray = dogArray;  // ⚠️ 编译通过,但危险!
// animalArray[0] = new Cat();    // 运行时报错!ArrayTypeMismatchException

六、完整实战示例:消息处理系统

用一个完整的例子,把协变和逆变串起来:

using System;
using System.Collections.Generic;

// ===== 消息类型体系 =====
class Message
{
    public string From { get; set; }
    public DateTime Time { get; set; }
    public override string ToString() => $"[{Time:HH:mm}] 来自 {From}";
}

class TextMessage : Message
{
    public string Content { get; set; }
    public override string ToString() => base.ToString() + $": {Content}";
}

class ImageMessage : Message
{
    public string ImageUrl { get; set; }
    public override string ToString() => base.ToString() + $" [图片: {ImageUrl}]";
}

class VoiceMessage : Message
{
    public int Duration { get; set; }
    public override string ToString() => base.ToString() + $" [语音: {Duration}秒]";
}


// ===== 协变接口:消息读取器(只出不进) =====
interface IMessageReader<out T>
{
    T GetLatest();
    IEnumerable<T> GetAll();
}

class TextMessageReader : IMessageReader<TextMessage>
{
    private List<TextMessage> _messages = new List<TextMessage>();

    public void Add(TextMessage msg) => _messages.Add(msg);
    public TextMessage GetLatest() => _messages[^1];
    public IEnumerable<TextMessage> GetAll() => _messages;
}


// ===== 逆变接口:消息处理器(只进不出) =====
interface IMessageHandler<in T>
{
    void Handle(T message);
}

class LogMessageHandler : IMessageHandler<Message>
{
    public void Handle(Message message)
    {
        Console.WriteLine($"📝 [日志] {message}");
    }
}

class NotificationHandler : IMessageHandler<Message>
{
    public void Handle(Message message)
    {
        Console.WriteLine($"🔔 [通知] 收到新消息!");
    }
}


// ===== 应用 =====
class Program
{
    static void Main()
    {
        // 准备数据
        var reader = new TextMessageReader();
        reader.Add(new TextMessage { From = "张三", Time = DateTime.Now, Content = "你好!" });
        reader.Add(new TextMessage { From = "李四", Time = DateTime.Now, Content = "在吗?" });

        // ✅ 协变:IMessageReader<TextMessage> → IMessageReader<Message>
        IMessageReader<TextMessage> textReader = reader;
        IMessageReader<Message> messageReader = textReader;  // 协变!

        Console.WriteLine("===== 读取消息(协变) =====");
        foreach (Message msg in messageReader.GetAll())
        {
            Console.WriteLine(msg);
        }


        // ✅ 逆变:IMessageHandler<Message> → IMessageHandler<TextMessage>
        IMessageHandler<Message> logHandler = new LogMessageHandler();
        IMessageHandler<TextMessage> textLogHandler = logHandler;  // 逆变!

        Console.WriteLine("\n===== 处理消息(逆变) =====");
        TextMessage latest = textReader.GetLatest();
        textLogHandler.Handle(latest);  // 能处理 Message 的,就能处理 TextMessage


        // ✅ 逆变链:给一个类型注册多个处理器
        var handlers = new List<IMessageHandler<TextMessage>>();
        handlers.Add(new LogMessageHandler());        // 逆变!
        handlers.Add(new NotificationHandler());      // 逆变!

        Console.WriteLine("\n===== 多处理器(逆变链) =====");
        foreach (var handler in handlers)
        {
            handler.Handle(latest);
        }
    }
}

输出:

===== 读取消息(协变) =====
[14:30] 来自 张三: 你好!
[14:31] 来自 李四: 在吗?

===== 处理消息(逆变) =====
📝 [日志] [14:31] 来自 李四: 在吗?

===== 多处理器(逆变链) =====
📝 [日志] [14:31] 来自 李四: 在吗?
🔔 [通知] 收到新消息!

七、委托中的协变与逆变——完整示例

using System;

class Animal
{
    public string Name { get; set; }
    public override string ToString() => Name;
}

class Dog : Animal
{
    public void Bark() => Console.WriteLine($"{Name}: 汪汪");
}

class Cat : Animal
{
    public void Meow() => Console.WriteLine($"{Name}: 喵喵");
}

class Program
{
    // 委托定义(用 Func 和 Action,已经内置了 in/out)
    // Func<in T, out TResult>  —— 参数是逆变,返回值是协变
    // Action<in T>             —— 参数是逆变

    static void Main()
    {
        // ===== 1. Func 中的协变(返回值) =====
        Func<Dog> createDog = () => new Dog { Name = "旺财" };
        Func<Animal> createAnimal = createDog;  // ✅ 协变

        Animal a = createAnimal();
        Console.WriteLine($"创建了: {a}");  // 创建了: 旺财

        // ===== 2. Action 中的逆变(参数) =====
        Action<Animal> printAnimalName = animal =>
            Console.WriteLine($"动物名叫: {animal.Name}");

        Action<Dog> printDogName = printAnimalName;  // ✅ 逆变
        printDogName(new Dog { Name = "大黄" });     // 动物名叫: 大黄

        // ===== 3. Func 中参数逆变 + 返回值协变 =====
        Func<Animal, Dog> original = animal => new Dog { Name = "变换来的" };

        // 参数逆变:Animal → Dog(能处理 Animal 的就能处理 Dog)
        // 返回值协变:Dog → Animal(返回 Dog 就是返回 Animal)
        Func<Dog, Animal> converted = original;  // ✅ 双向转换!

        Animal result = converted(new Dog { Name = "输入" });
        Console.WriteLine(result);  // 变换来的
    }
}

八、快速判断表——能不能转?

8.1 常用的类型

类型 支持协变? 支持逆变? 原因
IEnumerable<T> 只读(只出不进),定义有 out
IEnumerator<T> 只读,定义有 out
IReadOnlyList<T> 只读,定义有 out
Func<TResult> 只有返回值,定义有 out
IComparer<T> 只接收参数(只进不出),定义有 in
IEqualityComparer<T> 同上,定义有 in
Action<T> 只接收参数,定义有 in
List<T> 又能读又能写,不安全
Dictionary<TKey,TValue> 又能读又能写
T[] (数组) ⚠️ 危险 历史遗留,运行时检查

8.2 判断口诀

类型参数用在哪?问自己两个问题:

1. 只出现在返回值位置?  → 用 out(协变)
   例:T GetItem()  →  IEnumerable<T>, Func<T>

2. 只出现在参数位置?    → 用 in(逆变)
   例:void SetItem(T item)  →  IComparer<T>, Action<T>

3. 两个位置都出现?      → 不能变!
   例:List<T> 的 Add(T item) 和 T this[int i]

8.3 一张图总结

                      只输出(返回值)
                    ← out 协变可行 →
  派生类型 ────────────────────────────→ 基类型

    Dog          IEnumerable<Dog>         IEnumerable<Animal>
    Cat          Func<Cat>                Func<Animal>
    string       IReadOnlyList<string>    IReadOnlyList<object>


                      只输入(参数)
                    ← in 逆变可行 →
  基类型   ────────────────────────────→ 派生类型

    Animal        IComparer<Animal>       IComparer<Dog>
    object        Action<object>          Action<string>
    Message       IMessageHandler<Message>  IMessageHandler<TextMessage>

九、自定义泛型时的注意事项

9.1 什么时候加 out?

// ✅ 正确:T 只出现在返回位置
interface IReader<out T>
{
    T Read();               // ✅ 返回 T —— 输出位置
    IEnumerable<T> ReadAll(); // ✅ 返回 T —— 输出位置
}

// ❌ 错误:T 还出现在参数位置
// interface IRepository<out T>
// {
//     T GetById(int id);           // ✅ 返回,没问题
//     void Save(T entity);         // ❌ T 出现在参数位置了!
// }

9.2 什么时候加 in?

// ✅ 正确:T 只出现在参数位置
interface IWriter<in T>
{
    void Write(T data);      // ✅ 参数 —— 输入位置
    void WriteAll(T[] data); // ✅ 参数 —— 输入位置
}

// ❌ 错误:T 还出现在返回位置
// interface IProcessor<in T>
// {
//     void Process(T data);         // ✅ 输入,没问题
//     T GetResult();                // ❌ T 出现在返回值位置了!
// }

9.3 同时需要进和出?拆成两个接口

// 如果一个类型既需要读又需要写,拆开来:

interface IReader<out T>
{
    T Read();
}

interface IWriter<in T>
{
    void Write(T item);
}

// 实现时合并
class Repository<T> : IReader<T>, IWriter<T>
{
    private T _data;
    public T Read() => _data;
    public void Write(T item) => _data = item;
}

// 使用——各取所需
Repository<Dog> dogRepo = new Repository<Dog>();
IReader<Animal> reader = dogRepo;     // ✅ 协变(只读)
IWriter<Dog> writer = dogRepo;        // ✅ 直接赋值

// IWriter<Animal> animalWriter = dogRepo;  // ❌ 这不行
// 但可以这样:
Action<Dog> writeDog = d => dogRepo.Write(d);

十、常见易错点(避坑指南)

坑1:泛型类本身不支持协变/逆变

// ❌ 只有接口和委托可以标记 in/out,类不行!
// class MyClass<out T> {}  // 编译错误!

// ✅ 只能用在接口或委托上
interface IMyInterface<out T> { }  // ✅ 接口可以
delegate T MyDelegate<out T>();    // ✅ 委托可以

坑2:值类型不参与协变/逆变

// ❌ 协变/逆变只适用于引用类型!
IEnumerable<int> ints = new List<int> { 1, 2, 3 };
// IEnumerable<object> objs = ints;  // 编译错误!int 是值类型

// ✅ 引用类型可以
IEnumerable<string> strings = new List<string> { "a", "b" };
IEnumerable<object> objs = strings;  // ✅ string 是引用类型

坑3:不能同时标记 out 和 in

// ❌ 一个类型参数不能同时是 out 和 in
// interface IBoth<out in T> {}  // 编译错误!

坑4:数组假协变

// ⚠️ 数组支持协变是历史旧账,很危险!
Dog[] dogs = { new Dog { Name = "A" }, new Dog { Name = "B" } };
Animal[] animals = dogs;  // ⚠️ 编译通过

// animals[0] = new Cat();  // 运行时 ArrayTypeMismatchException!

// 建议:用只读集合代替
IReadOnlyList<Dog> safeDogs = new List<Dog> { new Dog(), new Dog() };
IReadOnlyList<Animal> safeAnimals = safeDogs;  // ✅ 安全,不能写

坑5:逆变方法——参数是"能接收什么"不是"本身就是什么"

// 逆变的理解关键:
Action<Animal> feedAnimal = a => Console.WriteLine($"喂 {a.GetType().Name}");

// 逆变:Action<Animal> → Action<Dog>
// 意思是:一个"能接收 Animal"的方法,可以当作"能接收 Dog"的方法来用
// 因为 Dog 是 Animal 的子集,能处理 Animal 肯定能处理 Dog

Action<Dog> feedDog = feedAnimal;  // ✅
feedDog(new Dog());

// 而不是反过来理解!
// Action<Dog> → Action<Animal> 这个不成立

十一、总结

核心概念速查表

概念 关键字 方向 生活比喻 典型类型
协变 out 派生→基类 苹果箱可以当水果箱读数 IEnumerable<T>, Func<T>
逆变 in 基类→派生 能喂动物的机器就能喂狗 IComparer<T>, Action<T>
不变 不能转 List 既能读又能写 List<T>, Dictionary<K,V>

记忆三句话

协变 out(输出) → 派生代替基 → "返回的狗就是动物" → IEnumerable<T>
逆变 in (输入) → 基代替派生 → "能喂动物的就能喂狗" → Action<T>
不变 无标记    → 都不能代替 →  List 读了又写不安全

什么时候会用到?

场景 用的什么
List<Dog>IEnumerable<Animal> 遍历 协变
DogComparer 用在 List<Animal> 排序 (不需要,反过来是逆变)
Action<Dog> 绑定一个处理 Animal 的方法 逆变
返回 Dog 的方法当作返回 Animal 的方法用 协变
接收 Message 的处理器用来处理 TextMessage 逆变

一句话总结:协变(out)让"输出更具体类型的"可以当成"输出更宽泛类型的"来用;逆变(in)让"输入更宽泛类型的"可以当成"输入更具体类型的"来用。核心原则:只出不进用 out,只进不出用 in,又进又出不能变。

0
博主关闭了当前页面的评论