正如上篇C#核心-反射揭秘1所讲述的那样,C#反射是写框架代码非常重要的一个技术点,要成为优秀的开发者,必须掌握它。接下来我会讲解一下反射的全部技术点,然后一一举例具体的应用。
上篇说了依赖注入DI的用法,最终是通过以下方式使用,请看图。
图1
就是通过接口的方式使用服务,上篇也大概说了一下原理,没有看的朋友请先看一下。
我还是大致说一下过程,图1其实是我写的api接口,api接口调用会有一个路由,框架会解析路由然后通过反射实例化图1这个GoodsController对象,反射实例化GoodsController首先需要实例化构造函数里面的形参,这些形参都是接口形式,但是实际上会实例化他们的派生类,然后派生类实例化对象赋值给这些接口,这里用到的是里氏替换原则,派生类放在别的地方我这里就不展示了。实例化派生类的过程中,也会遇到派生类里面构造函数里面也会有一些形参,那么也得实例化那些形参才行。所以这里会有一个递归实例化的过程,最终实例化最源头的GoodsController对象,这个过程说的比较抽象,大家知道大概概念就行,后面会通过代码详细讲解。
这样的做法有什么好处呢,好处就是从依赖派生类转为依赖接口,这样就减少了依赖,因为接口只是定义一些方法名,具体实现在派生类,如果派生类名称需要修改,或者换一个派生类的话,通过接口的方式不需要改动,这里就说到了一个叫开闭原则,开闭原则就是对修改封闭,对拓展开放,也就是写过的代码少改,通过拓展的方式添加代码,这里接口定义的话就是少改了代码了。
这里关键是需要知道接口和派生类的关系,不然DI框架也不知道怎么实例化对象赋值给接口了。接口和派生类的实现我们可能放在很多dll里面,所以这里就要提到加载程序集的功能。我们都知道类型和接口是放在程序集中的,所以DI框架会收集接口和对应派生类的关系放在一个集合中以便后续使用,所以我们先说怎么加载程序集。请看下图
图2
请看图2,我们加载一个dll的方式是这样的,当然这个dll需要在我们的运行目录里面。
var assembly = Assembly.Load(new AssemblyName(){Name="xxx"});
通过assembly就可以就可以解析出来里面的接口以及派生类关系了,这个我们后面再说。
如果你的接口和派生类都在一个dll里面,你就可以这么干,当然还可以这么使用,请看下图
图3
图3,可以通过程序集的字符串进行查找。你还可以通过路径查找。
图4
如图4这样 Assembly.LoadFile(FilePath)。还可以通过下图
图5
Assembly.LoadFrom(dll文件名)。
还有获取当前使用的程序集合,如下面这句,意思就是拿到当前程序集。
Assembly.GetExecutingAssembly();
以上都是加载一个程序集。
当然很多情况下我们的接口和派生类是放在不同的程序集里面的,比如你有一个service层,也有一个repository层,一个放逻辑,一个放仓储,不通的程序集。
这个时候我们可以可以这么做,请看下图。
图6
首先通过Assembly.GetEntryAssembly().Location)找到当前应用程序集exe所在的路径
图7
请看图7,这里是具体文件
然后找到这个文件所在目录,也就是运行目录,接口找到这个运行目录下的所有的dll程序集地址。var addInAssemblies = Directory.EnumerateFiles(addInDir, "*.dll");这个返回的是一个序列。这样我们可以通过linq循环拿到每一个的程序集。
还可以通过下图。
图8
图8这样拿到程序集AppDomain.CurrentDomain.GetAssemblies()。这里是通过应用程序域,应用程序域是进程下clr的功能,它支持多个应用程序域,彼此隔离内存无法直接相互引用,这相当于支持动态加载dll,一个进程可以支持多个应用程序域然后彼此隔离。每个进程首先会分配一个默认的应用程序域,这个应用程序域我们后面另外开篇再讲。这个拿到的程序集合就会包含了一些系统程序集,虽然后面我们在获取接口和类的时候会做过滤,但是这样拿到的程序集合比较多,效率会差一点。
还可以通过下图。
图9
先获取当前应用程序域的地址,也就是运行目录,然后通过
Directory.GetFiles(baseDirectory, "*.dll")
模糊查到我们想要的dll的路径地址。这样后面我们可以通过Assembly.LoadFrom拿到程序集了。
我们这里的AppDomain.CurrentDomain,可以使用Thread.GetDomain()替换,他们都指向同一个应用程序域。
现在我们得到了Assembly或者Assembly集合,然后我们就可以通过反射得到接口和派生类的集合,请看下图。
图10
图10代码的目的就是收集接口和派生类的关系,我们通过
Dictionary<Type,Type> 结构来收集接口和派生类的关系,当然实际DI框架里面没有这么简单的设计,这里只是说清楚概念而已。
这里通过
Assembly.GetExportedTypes().Where(t=>t.IsInterface == true)
拿到所有接口定义
assembly.GetExportedTypes().Where(t => interfaceInfo.IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract).FirstOrDefault();
通过这句代码拿到接口对应的派生类
GetExportedTypes方法是指获取公共类型,这个包括类,接口,枚举。上面代码获取接口的时候用t.IsInterface == true进行过滤拿到接口序列,后面这句代码通过
interfaceInfo.IsAssignableFrom(t) 这句话的意思是interfaceInfo是否可以被t替换,意思就是里氏替换原则,派生类可以赋值基类或者接口,返回bool,进行过滤,意思就是拿到接口的实现派生类,并且不能是接口和抽象类。拿到的结果可能会有多个,我们这里取第一个。
然后我们就可以收集接口和派生类的关系了。接下来就是使用的时候从这个集合拿到类,然后通过
Activator.CreateInstance(Type)
方式进行实例化对象。
上面提的例子,就是api接口的方式,就是这样创建控制层实例,然后调用方法,调用过程也是反射方式,我们接着讲。
上面讲述了获取程序集和类型的技术,接下来讲一下实例化对象。实例化对象包括实例化无参构造函数类对象,实例化有参构造函数类对象,实例化数组对象,实例化泛型类对象。这里每一个反射实例化对象都很重要。
图11
图12
请看图11和图12,这里定义了4个类,Person,Body,Head,Foot,其中Person类的构造函数里
有Body类型形参,Body类构造函数里面有Head和Foot类型形参。我们的目的是通过反射实例化Person类。
图13
图13是反射实例化方式之一
再看图11,首先Person类只定义了一个有两个参数的构造函数,没有无参构造函数,所以必须先实例化构造函数里面的参数,然后才能实例化Person类。
如果直接调用Activator.CreateInstance(Type type)不传参数的话,实例化Person会直接报错。所以先得找到构造函数,然后实例化每个构造函数对象,然后拼成数组,再调用
Activator.CreateInstance(Type type ,object?[]? args)。
type.GetConstructors().FirstOrDefault();
这句话就是找到第一个构造函数,因为是反射创建实例化对象,到底用哪个构造函数,是需要有一个策略的,但是最终都需要决定其中一个构造函数,我们这里就默认第一个就行了。
ParameterInfo[] ps = ci.GetParameters();
这句话是拿到构造函数里面的参数,通过
ParameterInfo.ParameterType可以知道参数的类型Type
然后我们无法得知Type是否有有参构造函数,所以这里我们需要用一个递归拿到最终的实例
constructorParams.Add(Create(pi.ParameterType));
constructorParams是实例化对象数组。
图14
我们执行一下图14,得到结果。
图15
可以看到我们通过反射获取到了Person对象。
Activator.CreateInstance是一种反射创建实例化方式之一,当然也有很多重载方法,还有一个就是
图16
这个是通过构造函数对象执行实例化对象。效果是一样的。
上述代码几个类的耦合太厉害,接下来我们结合DI的思路来创建Person引入接口
图17
图18
请看图18,新增了IBody,IHead,IFoot三个接口,其中请看Body里面构造函数里面,已经变量IHead,IFoot接口类型参数了,再看图17 Person类里面的构造函数参数也变成IBody接口类型参数了。看我们是怎么实现的。
我们定义了一个
static Dictionary<Type, Type> registerDic = new Dictionary<Type, Type>();
全局静态字典,存储接口和实现类的关系。
看Main方法里面,首先通过反射加载程序集,拿到接口和派生类的关系存入刚才的registerDic里面。这块我们开篇已经讲述过了。
接口看Create方法里面
registerDic.TryGetValue(pi.ParameterType, out var type1);
constructorParams.Add(Create(type1));
这两条代码,第一个是通过类型从字典里面找到派生类,然后实例化派生类。
图19
我们看图19 结果,我们已经实例化了Person对象,而且可以通过接口执行方法。
这里最简单的实现了一下DI的原理,当然这里我想表达的是反射在其中的作用。
今日头条文章有字数限制,请接着看
C#核心-反射揭秘3
,