关注墨瑾轩带你探索编程的奥秘超萌技术攻略轻松晋级编程高手技术宝库已备好就等你来挖掘订阅墨瑾轩智趣学习不孤单即刻启航编程之旅更有趣硬核拆解协变逆变的变形术附血泪案例1. 协变从子类到父类的优雅下跪IEnumerable的真相// 你写的方法接收Animal集合publicstaticvoidSave(IEnumerableAnimalanimals){// 你只想把动物们喂饱foreach(varanimalinanimals)animal.Feed();}// 你得意地调用vardogListnewListDog();// Dog是Animal的子类Save(dogList);// 编译器居然没报错关键注释IEnumerableout T中的out是协变的通行证它告诉编译器“T只会被输出不会被输入”。所以IEnumerableDog能安全地降级为IEnumerableAnimal——因为Dog是Animal的子类喂狗和喂动物本质没区别。墨氏冷幽默协变就像儿子给老子发红包——儿子的钱是老子的钱的子集所以发红包没问题。逆变呢就像老子给儿子发红包——这得看儿子有没有本事接住不然就是打脸为啥能这样namespaceSystem.Collections.Generic{// 重点out关键字让T可以向下兼容publicinterfaceIEnumerableoutT:IEnumerable{IEnumeratorTGetEnumerator();}}血泪教训去年我写了个IEnumerableDog的API结果调用方传了个IEnumerableAnimal进来编译器直接给我报错了原因我忘了在接口里加out——协变不是魔法是编译器的安全锁2. 逆变从父类到子类的危险上位IComparer的真相// 你设计的比较器比较AnimalpublicclassAnimalComparer:IComparerAnimal{publicintCompare(Animalx,Animaly)x.Weight.CompareTo(y.Weight);}// 你想用这个比较器比较DogvardogComparernewAnimalComparer();// 但Dog是Animal的子类vardogsnewListDog();dogs.Sort(dogComparer);// 编译器你疯了关键注释IComparerin T中的in是逆变的通行证它告诉编译器“T只会被输入不会被输出”。所以IComparerAnimal能安全地升级为IComparerDog——因为比较Dog时Animal的比较逻辑依然适用。墨氏吐槽逆变就像爸爸让儿子去接客——儿子是爸爸的子类所以接客能力不比爸爸差。但如果你让儿子接客去比较爸爸那就是祖传代码的翻车现场为啥能这样namespaceSystem.Collections.Generic{// 重点in关键字让T可以向上兼容publicinterfaceIComparerinT{intCompare(Tx,Ty);}}血泪教训我曾把IComparerAnimal直接赋值给IComparerDog结果线上跑着跑着数组越界原因Dog可能有Bark()方法而Animal没有逆变不是万能的是编译器的安全裤3. 协变 vs 逆变程序员的左右手互搏术特性协变 (out)逆变 (in)T的流向只能输出Read只能输入Write安全场景IEnumerableTIComparerT类比儿子给老子发红包老爷子给儿子发红包常见错误漏写out漏写in墨氏排比协变不是万能的没协变是万万不能的乱用协变是自寻死路的逆变不是万能的没逆变是万万不能的乱用逆变是祖传代码的实战案例依赖注入的变形术// 你设计的接口带协变publicinterfaceIRepositoryoutT{TGetById(intid);}// 你实现的接口Dog是Animal的子类publicclassDogRepository:IRepositoryDog{publicDogGetById(intid)newDog();// 严格返回Dog}// 你在服务中用AnimalRepositorypublicclassAnimalService{privatereadonlyIRepositoryAnimal_animalRepo;// 协变IRepositoryDog → IRepositoryAnimalpublicAnimalService(IRepositoryAnimalrepo){_animalReporepo;}publicvoidFeedAnimals(){varanimals_animalRepo.GetAll();// 能安全返回Animal集合}}关键注释IRepositoryout T的out保证了子类实现能安全升级为父类接口AnimalService用IRepositoryAnimal接口却能注入DogRepository实现——这就是协变的真谛不加out编译器直接给你啪一巴掌“你这T是只能输出的为啥还往里塞Dog”墨氏自黑我当年在依赖注入里漏了out结果线上一炸——“喂为啥AnimalService喂出来的全是Dog”产品经理“这不就是我想要的吗”我“不这是bug”后来发现是协变没加当场把咖啡泼在键盘上4. 逆变的危险区为什么ActionT不能协变// 你写的方法接收AnimalpublicstaticvoidFeedAnimal(Animalanimal)animal.Feed();// 你想用Dog的ActionvardogActionnewActionDog(dd.Bark());// Dog会叫varanimalActiondogAction;// 编译器你疯了关键注释ActionT不能协变因为T是输入Write不是输出Read。如果允许ActionDog → ActionAnimal那么调用animalAction(new Cat())时会把Cat当Dog处理结果就是程序崩溃墨氏比喻协变是儿子给老子发红包——儿子的钱是老子的子集安全。逆变是老子给儿子发红包——老子的钱是儿子的父集安全。但ActionT是老子让儿子发红包——儿子的钱是老子的子集不安全正确用法// 用逆变Actionin T但C#不支持因为ActionT是输入publicstaticvoidFeedAnimal(Animalanimal)animal.Feed();// 正确用逆变接口publicinterfaceIFeedActioninT{voidFeed(Tanimal);}// 你能把Dog的FeedAction升级为Animal的FeedActionIFeedActionDogdogActiondd.Bark();IFeedActionAnimalanimalActiondogAction;// 安全墨氏扎心为啥C#不给ActionT加in“因为程序员太懒不想写in结果线上崩了。”—— 一个被ActionT坑到凌晨三点的程序员的内心独白5. 协变逆变的终极禁忌为什么ListT不能协变vardogListnewListDog();varanimalList(ListAnimal)dogList;// 编译器你疯了animalList.Add(newCat());// 这里会崩溃关键注释ListT不能协变因为它是可写集合T是输入不是输出。如果允许ListDog → ListAnimal那么你就能往ListAnimal里塞Cat结果就是ListDog里混进了Cat——数据污染墨氏灵魂拷问为啥IEnumerableT能协变ListT不能“因为IEnumerable是只读的List是可写的。”别问问就是编译器的安全锁正确做法// 用IEnumerableT只读实现协变IEnumerableAnimalanimalEnumerabledogList;// 安全animalEnumerable.ToList();// 安全转换不会污染原始List墨氏血泪我曾把ListDog直接转成ListAnimal结果线上一炸“用户说我养的狗突然变成猫了”产品经理“这功能真酷”我“不这是bug”后来发现是ListT不能协变当场把键盘砸了协变逆变的墨氏心法——别让编译器当爹协变输出型out——“儿子给老子发红包”逆变输入型in——“老子给儿子发红包”不可变输入输出型T——“父子互相发红包但不能乱发”墨氏总结协变out当T只被读如IEnumerableT、FuncT逆变in当T只被写如IComparerT、ActionT不可变当T既读又写如ListT、DictionaryT