C#泛型的“变形术“:协变逆变的5个致命实战,99%人踩过坑!

🔥关注墨瑾轩,带你探索编程的奥秘!🚀
🔥超萌技术攻略,轻松晋级编程高手🚀
🔥技术宝库已备好,就等你来挖掘🚀
🔥订阅墨瑾轩,智趣学习不孤单🚀
🔥即刻启航,编程之旅更有趣🚀

硬核拆解协变逆变的"变形术"(附血泪案例)

1. 协变:从子类到父类的"优雅下跪"(IEnumerable的真相)

// 你写的方法:接收Animal集合publicstaticvoidSave(IEnumerable<Animal>animals){// 你只想把动物们喂饱foreach(varanimalinanimals)animal.Feed();}// 你得意地调用:vardogList=newList<Dog>();// Dog是Animal的子类Save(dogList);// 编译器居然没报错?!

关键注释:
IEnumerable<out T>中的out是协变的"通行证",它告诉编译器:“T只会被输出,不会被输入”
所以IEnumerable<Dog>能安全地"降级"为IEnumerable<Animal>——因为Dog是Animal的子类,喂狗和喂动物本质没区别

墨氏冷幽默:
协变就像"儿子给老子发红包"——儿子的钱是老子的钱的子集,所以发红包没问题。
逆变呢?就像"老子给儿子发红包"——这得看儿子有没有本事接住,不然就是"打脸"!

为啥能这样?

namespaceSystem.Collections.Generic{// 重点!out关键字让T可以"向下兼容"publicinterfaceIEnumerable<outT>:IEnumerable{IEnumerator<T>GetEnumerator();}}

血泪教训:
去年我写了个IEnumerable<Dog>的API,结果调用方传了个IEnumerable<Animal>进来,编译器直接给我报错了
原因?我忘了在接口里加out——协变不是魔法,是编译器的"安全锁"!


2. 逆变:从父类到子类的"危险上位"(IComparer的真相)

// 你设计的比较器:比较AnimalpublicclassAnimalComparer:IComparer<Animal>{publicintCompare(Animalx,Animaly)=>x.Weight.CompareTo(y.Weight);}// 你想用这个比较器比较DogvardogComparer=newAnimalComparer();// 但Dog是Animal的子类vardogs=newList<Dog>();dogs.Sort(dogComparer);// 编译器:你疯了?!

关键注释:
IComparer<in T>中的in是逆变的"通行证",它告诉编译器:“T只会被输入,不会被输出”
所以IComparer<Animal>能安全地"升级"为IComparer<Dog>——因为比较Dog时,Animal的比较逻辑依然适用。

墨氏吐槽:
逆变就像"爸爸让儿子去接客"——儿子是爸爸的子类,所以接客能力不比爸爸差。
但如果你让"儿子接客"去"比较爸爸",那就是"祖传代码"的翻车现场!

为啥能这样?

namespaceSystem.Collections.Generic{// 重点!in关键字让T可以"向上兼容"publicinterfaceIComparer<inT>{intCompare(Tx,Ty);}}

血泪教训:
我曾把IComparer<Animal>直接赋值给IComparer<Dog>,结果线上跑着跑着数组越界
原因?Dog可能有Bark()方法,而Animal没有,逆变不是万能的,是编译器的"安全裤"!


3. 协变 vs 逆变:程序员的"左右手互搏术"

特性协变 (out)逆变 (in)
T的流向只能输出(Read)只能输入(Write)
安全场景IEnumerable<T>IComparer<T>
类比儿子给老子发红包老爷子给儿子发红包
常见错误漏写out漏写in

墨氏排比:
协变不是万能的,没协变是万万不能的,乱用协变是自寻死路的!
逆变不是万能的,没逆变是万万不能的,乱用逆变是祖传代码的!

实战案例:依赖注入的"变形术"

// 你设计的接口(带协变)publicinterfaceIRepository<outT>{TGetById(intid);}// 你实现的接口(Dog是Animal的子类)publicclassDogRepository:IRepository<Dog>{publicDogGetById(intid)=>newDog();// 严格返回Dog}// 你在服务中用AnimalRepositorypublicclassAnimalService{privatereadonlyIRepository<Animal>_animalRepo;// 协变:IRepository<Dog> → IRepository<Animal>publicAnimalService(IRepository<Animal>repo){_animalRepo=repo;}publicvoidFeedAnimals(){varanimals=_animalRepo.GetAll();// 能安全返回Animal集合}}

关键注释:

  • IRepository<out T>out保证了子类实现能安全升级为父类接口
  • AnimalServiceIRepository<Animal>接口,却能注入DogRepository实现——这就是协变的真谛!
  • 不加out?编译器直接给你"啪"一巴掌:“你这T是只能输出的,为啥还往里塞Dog?”

墨氏自黑:
我当年在依赖注入里漏了out,结果线上一炸——“喂,为啥AnimalService喂出来的全是Dog?”
产品经理:“这不就是我想要的吗?”
我:“不!这是bug!”
(后来发现是协变没加,当场把咖啡泼在键盘上)


4. 逆变的"危险区":为什么Action<T>不能协变?

// 你写的方法:接收AnimalpublicstaticvoidFeedAnimal(Animalanimal)=>animal.Feed();// 你想用Dog的ActionvardogAction=newAction<Dog>(d=>d.Bark());// Dog会叫varanimalAction=dogAction;// 编译器:你疯了?!

关键注释:
Action<T>不能协变,因为T是输入(Write),不是输出(Read)。
如果允许Action<Dog> → Action<Animal>,那么调用animalAction(new Cat())时,会把Cat当Dog处理,结果就是程序崩溃

墨氏比喻:
协变是"儿子给老子发红包"——儿子的钱是老子的子集,安全。
逆变是"老子给儿子发红包"——老子的钱是儿子的父集,安全。
Action<T>是"老子让儿子发红包"——儿子的钱是老子的子集,不安全!

正确用法:

// 用逆变:Action<in T>(但C#不支持,因为Action<T>是输入)publicstaticvoidFeedAnimal(Animalanimal)=>animal.Feed();// 正确:用逆变接口publicinterfaceIFeedAction<inT>{voidFeed(Tanimal);}// 你能把Dog的FeedAction升级为Animal的FeedActionIFeedAction<Dog>dogAction=d=>d.Bark();IFeedAction<Animal>animalAction=dogAction;// 安全!

墨氏扎心:
为啥C#不给Action<T>in
“因为程序员太懒,不想写in,结果线上崩了。”
—— 一个被Action<T>坑到凌晨三点的程序员的内心独白


5. 协变逆变的"终极禁忌":为什么List<T>不能协变?

vardogList=newList<Dog>();varanimalList=(List<Animal>)dogList;// 编译器:你疯了?!animalList.Add(newCat());// 这里会崩溃!

关键注释:
List<T>不能协变,因为它是可写集合T是输入,不是输出)。
如果允许List<Dog> → List<Animal>,那么你就能往List<Animal>里塞Cat结果就是List<Dog>里混进了Cat——数据污染!

墨氏灵魂拷问:
为啥IEnumerable<T>能协变,List<T>不能?
“因为IEnumerable是只读的,List是可写的。”
(别问,问就是编译器的"安全锁")

正确做法:

// 用IEnumerable<T>(只读)实现协变IEnumerable<Animal>animalEnumerable=dogList;// 安全!animalEnumerable.ToList();// 安全转换,不会污染原始List

墨氏血泪:
我曾把List<Dog>直接转成List<Animal>,结果线上一炸
“用户说:我养的狗突然变成猫了!”
产品经理:“这功能真酷!”
我:“不!这是bug!”
(后来发现是List<T>不能协变,当场把键盘砸了)


协变逆变的"墨氏心法"——别让编译器当"爹"

协变:输出型(out)——“儿子给老子发红包”
逆变:输入型(in)——“老子给儿子发红包”
不可变:输入输出型(T)——“父子互相发红包,但不能乱发”

墨氏总结:

  1. 协变(out):当T只被读(如IEnumerable<T>Func<T>
  2. 逆变(in):当T只被写(如IComparer<T>Action<T>
  3. 不可变:当T既读又写(如List<T>Dictionary<T>