C# - 逆变的具体应用场景
前言
早期在学习泛型的协变与逆变时,网上的文章讲解、例子算是能看懂,但关于逆变的具体应用场景这方面的知识,我并没有深刻的认识。
本文将在具体的场景下,从泛型接口设计的角度出发,逐步探讨逆变的作用,以及它能帮助我们解决哪方面的问题?
这篇文章算是协变、逆变知识的感悟和分享,开始之前,你应该先了解协变、逆变的基本概念,这类文章很多,这里就不再赘述。
协变的应用场景
虽然协变不是今天的主要内容,但在此之前,我还是想提一下关于协变的应用场景。
其中最常见的应用场景就是——如果方法的某个参数是一个集合时,我习惯将这个集合参数定义为IEnumerable<T>
类型。
class Program
{
public static void Save(IEnumerable<Animal> animals)
{
// TODO
}
}
public class Animal { }
IEnumerable<T>
中的T
就是标记了代表协变的关键字out
namespace System.Collections.Generic
{
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
}
假如泛型T
为父类Animal
类型,Dog
为Animal
的子类,其他人在调用这个方法时,
不仅可以传入IEnumerable<Animal>
、List<Animal>
、Animal[]
类型的参数,
还可以传入IEnumerable<Dog>
、List<Dog>
、Dog[]
等其他继承自IEnumerable<Animal>
类型的参数。
这样,方法的兼容性会更强。
class Program
{
public static void Save(IEnumerable<Animal> animals)
{
// TODO
}
static void Main(string[] args)
{
var animalList = new List<Animal>();
var animalArray = new Animal[] { };
var dogList = new List<Dog>();
var dogArray = new Dog[] { };
Save(animalList);
Save(animalArray);
Save(dogList);
Save(dogArray);
}
}
public class Animal { }
public class Dog : Animal { }
逆变的应用场景
提起逆变,可能大家见过类似下面这段代码:
class Program
{
static void Main(string[] args)
{
IComparer<Animal> animalComparer = new AnimalComparer();
IComparer<Dog> dogComparer = animalComparer;// 将 IComparer<Animal> 赋值给 IComparer<Dog>
}
}
public class AnimalComparer : IComparer<Animal>
{
// 省略具体实现
}
IComparer<T>
中的T
就是标记了代表逆变的关键字in
namespace System.Collections.Generic
{
public interface IComparer<in T>
{
int Compare(T? x, T? y);
}
}
在看完这段代码后,不知道你们是否跟我有一样的想法:道理都懂,可是具体的应用场景呢?
要探索逆变可以帮助我们解决哪些问题,我们试着从另一个角度出发——在某个场景下,不使用逆变,是否会遇到某些问题。
假设我们需要保存各种基础资料,根据需求我们定义了对应的接口,以及完成了对应接口的实现。这里假设Animal
与Human
就是其中的两种基础资料类型。
public interface IAnimalService
{
void Save(Animal entity);
}
public interface IHumanService
{
void Save(Human entity);
}
public class AnimalService : IAnimalService
{
public void Save(Animal entity)
{
// TODO
}
}
public class HumanService : IHumanService
{
public void Save(Human entity)
{
// TODO
}
}
public class Animal { }
public class Human { }
现在增加一个批量保存基础资料的功能,并且实时返回保存进度。
public class BatchSaveService
{
private static readonly IAnimalService _animalSvc;
private static readonly IHumanService _humanSvc;
// 省略依赖注入代码
public void BatchSaveAnimal(IEnumerable<Animal> entities)
{
foreach (var animal in entities)
{
_animalSvc.Save(animal);
// 省略监听进度代码
}
}
public void BatchSaveHuman(IEnumerable<Human> entities)
{
foreach (var human in entities)
{
_humanSvc.Save(human);
// 省略监听进度代码
}
}
}
完成上面代码后,我们可以发现,监听进度的代码写了两次,如果像这样的基础资料类型很多,想要修改监听进度的代码,则会牵一发而动全身,这样的代码就不便于维护。
为了使代码能够复用,我们需要抽象出一个保存基础资料的接口ISave<T>
。
使IAnimalService
、IHumanService
继承ISave<T>
,将泛型T
分别定义为Animal
、Human
public interface ISave<T>
{
void Save(T entity);
}
public interface IAnimalService : ISave<Animal> { }
public interface IHumanService : ISave<Human> { }
这样,就可以将BatchSaveAnimal()
和BatchSaveHuman()
合并为一个BatchSave<T>()
public class BatchSaveService
{
private static readonly IServiceProvider _svcProvider;
// 省略依赖注入代码
public void BatchSave<T>(IEnumerable<T> animals)
{
ISave<T> service = _svcProvider.GetRequiredService<ISave<T>>();// GetRequiredService()会在无对应接口实现时抛出错误
foreach (T animal in animals)
{
service.Save(animal);
// 省略监听进度代码
}
}
}
重构后的代码达到了可复用、易维护的目的,但很快你会发现新的问题。
在调用重构后的BatchSave<T>()
时,传入Human
类型的集合参数,或Animal
类型的集合参数,代码能够正常运行,
但在传入Dog
类型的集合参数时,代码在运行到第8行时会报错,因为我们并没有实现ISave<Dog>
接口。
虽然Dog
是Animal
的子类,但却不能使用保存Animal
的方法,这肯定会被接口调用者吐槽,因为它不符合里氏替换原则。
static void Main(string[] args)
{
List<Human> humans = new() { new Human() };
List<Animal> animals = new() { new Animal() };
List<Dog> dogs = new() { new Dog() };
var saveSvc = new BatchSaveService();
saveSvc.BatchSave(humans);
saveSvc.BatchSave(animals);
saveSvc.BatchSave(dogs);// 由于没有实现ISave<Dog>接口,因此代码运行时会报错
}
在T
为Dog
时,要想获取ISave<Animal>
这个不相关的服务,我们可以从IServiceCollection
服务集合中去找。
虽然我们拿到了注册的所有服务,但如何才能在T
为Dog
类型时,拿到对应ISave<Animal>
服务呢?
这时,逆变就派上用场了,
我们将接口ISave<T>
加上关键字in
后,就可以将ISave<Animal>
分配给ISave<Dog>
public class BatchSaveService
{
private static readonly IServiceProvider _svcProvider;
private static readonly IServiceCollection _svcCollection;
// 省略依赖注入代码
public void BatchSave<T>(IEnumerable<T> entities)
{
// 假设T为Dog,只有在ISave<T>接口标记为逆变时,
// typeof(ISave<Animal>).IsAssignableTo(typeof(ISave<Dog>)),才会是true
Type serviceType = _svcCollection.Single(x => x.ServiceType.IsAssignableTo(typeof(ISave<T>))).ServiceType;
ISave<T> service = _svcProvider.GetRequiredService(serviceType) as ISave<T>;// ISave<Animal> as ISave<Dog>
foreach (T entity in entities)
{
service.Save(entity);
// 省略监听进度代码
}
}
}
现在BatchSave<T>()
算是符合里氏替换原则,但这样的写法也有缺点
优点:调用时,写法干净简洁,不需要设置过多的泛型参数,只需要将传入对应的参数变量即可。
缺点:如果传入的参数没有对应的接口实现,编译仍然会通过,只有在代码运行时才会报错,提示不够积极、友好。
并且如果我们实现了ISave<Dog>
接口,那代码运行到第11行时会得到ISave<Dog>
和ISave<Animal>
两个结果,不具有唯一性。
要想在错误使用接口时,编译器及时提示错误,可以将接口重构成下面这样
public class BatchSaveService
{
private static readonly IServiceProvider _svcProvider;
// 省略依赖注入代码
public void BatchSave<TService, T>(IEnumerable<T> entities) where TService : ISave<T>
{
ISave<T> service = _svcProvider.GetService<TService>();
foreach (T entity in entities)
{
service.Save(entity);
// 省略监听进度代码
}
}
}
class Program
{
static void Main(string[] args)
{
List<Human> humans = new() { new Human() };
List<Animal> animals = new() { new Animal() };
List<Dog> dogs = new() { new Dog() };
var saveSvc = new BatchSaveService();
saveSvc.BatchSave<IHumanService, Human>(humans);
saveSvc.BatchSave<IAnimalService, Animal>(animals);
saveSvc.BatchSave<IAnimalService, Dog>(dogs);
// 假如实现了继承ISave<Dog>的接口IDogService,可以改为
// saveSvc.BatchSave<IDogService, Dog>(dogs);
}
}
这样在错误使用接口时,编译器就会及时报错,但由于需要设置多个泛型参数,使用起来会有些麻烦。
讨论
以上是我遇见的比较常见的关于逆变的应用场景,上述两种方式你觉得哪种更好?是否有更好的设计方式?或者大家在写代码时遇见过哪些逆变的应用场景?
欢迎大家留言讨论和分享。
C# - 逆变的具体应用场景的更多相关文章
- 协变(covariance),逆变(contravariance)与不变(invariance)
协变,逆变与不变 能在使用父类型的场景中改用子类型的被称为协变. 能在使用子类型的场景中改用父类型的被称为逆变. 不能做到以上两点的被称为不变. 以上的场景通常包括数组,继承和泛型. 协变逆变与泛型( ...
- 不变(Invariant), 协变(Covarinat), 逆变(Contravariant) : 一个程序猿进化的故事
阿袁工作的第1天: 不变(Invariant), 协变(Covarinat), 逆变(Contravariant)的初次约 阿袁,早!开始工作吧. 阿袁在笔记上写下今天工作清单: 实现一个scala类 ...
- 编写高质量代码改善C#程序的157个建议[协变和逆变]
前言 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议42.使用泛型参数兼容泛型接口的不可变性 建议43.让接口 ...
- .NET 4.0中的泛型的协变和逆变
转自:http://www.cnblogs.com/jingzhongliumei/archive/2012/07/02/2573149.html 先做点准备工作,定义两个类:Animal类和其子类D ...
- 转载.NET 4.0中的泛型的协变和逆变
先做点准备工作,定义两个类:Animal类和其子类Dog类,一个泛型接口IMyInterface<T>, 他们的定义如下: public class Animal { } public ...
- 【温故而知新-万花筒】C# 异步编程 逆变 协变 委托 事件 事件参数 迭代 线程、多线程、线程池、后台线程
额基本脱离了2.0 3.5的时代了.在.net 4.0+ 时代.一切都是辣么简单! 参考文档: http://www.cnblogs.com/linzheng/archive/2012/04/11/2 ...
- Java中的逆变与协变(转)
看下面一段代码 Number num = new Integer(1); ArrayList<Number> list = new ArrayList<Integer>(); ...
- 解读经典《C#高级编程》最全泛型协变逆变解读 页127-131.章4
前言 本篇继续讲解泛型.上一篇讲解了泛型类的定义细节.本篇继续讲解泛型接口. 泛型接口 使用泛型可定义接口,即在接口中定义的方法可以带泛型参数.然后由继承接口的类实现泛型方法.用法和继承泛型类基本没有 ...
- 一个简单例子理解C#的协变和逆变
关于协变逆变,SolidMango的解释是比较可取的.有了协变,比如,在需要返回IEnumerable<object>类型的时候,可以使用IEnmerable<string>来 ...
随机推荐
- iterator 前++ 后++区别
for(iterator it = begin(); it != end(); ++it) 此处的 begin()<==>this->begin() 或者for(ite ...
- HMS Core版本发布公告
新增动作捕捉能力.通过简单拍摄即可获得人体3D骨骼关键点数据,广泛应用于虚拟形象.体育运动和医学分析等场景: 3D物体建模能力iOS版本上线. 查看详情>> 新增道路吸附能力.可根据坐标点 ...
- 尚硅谷SSM-CRUD实战Demo
SSM-CRUD实战项目 1. 项目总览 SpringMVC + Spring + MyBatis CRUD:增删改查 功能: 分页 数据校验 jquery前端校验+JSR303后端校验 ajax R ...
- CF1077A Frog Jumping 题解
Content 在一个数轴上有一个动点,初始时在 \(0\) 这个位置上,接下来有若干次操作,对于第 \(i\) 次操作: 如果 \(i\) 是奇数,那么动点往右移 \(a\) 个单位. 如果 \(i ...
- 以太网/ IPV4/IPV6包头,TCP包头格式回顾
问题:以太网数据包,承载的数据内容大小46~1500字节,是如何来的? 以太网数据包结构 以太网协议规定最小链路层数据包(帧)为64字节,其中以太网首部+尾部共计18字节(源/目的MAC12字节:上 ...
- 雨课堂自动切换PPT代码
浏览器运行js步骤 原仓库 Podium = {}; Podium.keydown = function(k) { var oEvent = document.createEvent('Keyboar ...
- 【LeetCode】1181. Before and After Puzzle 解题报告(C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 保存首尾字符串 日期 题目地址:https://lee ...
- 【LeetCode】940. Distinct Subsequences II 解题报告(Python)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 动态规划 日期 题目地址:https://leetc ...
- Java语言程序设计复习提纲
这是我在准备Java考试时整理的提纲,如果是通过搜索引擎搜索到这篇博客的师弟师妹,建议还是先参照PPT和课本,这个大纲也不是很准确,自己总结会更有收获,多去理解含义,不要死记硬背,否则遇到概念辨析题 ...
- Capstone CS5267|CS5267参数|CS5267规格书
CS5267 USB Type-C to HDMI2.0b 4k@60Hz Converter with PD3.0 Support 1.CS5267概述 Capstone CS5267是一款高性能T ...