🔥关注墨瑾轩,带你探索编程的奥秘!🚀
🔥超萌技术攻略,轻松晋级编程高手🚀
🔥技术宝库已备好,就等你来挖掘🚀
🔥订阅墨瑾轩,智趣学习不孤单🚀
🔥即刻启航,编程之旅更有趣🚀
硬核拆解协变逆变的"变形术"(附血泪案例)
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保证了子类实现能安全升级为父类接口AnimalService用IRepository<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)——“父子互相发红包,但不能乱发”
墨氏总结:
- 协变(out):当T只被读(如
IEnumerable<T>、Func<T>) - 逆变(in):当T只被写(如
IComparer<T>、Action<T>) - 不可变:当T既读又写(如
List<T>、Dictionary<T>)