反射常常与接口、依赖反转原则一起使用。反射事实上是.Net框架的内容,不是C#语言的内容。
对于托管类语言,反射很重要。单元测试、依赖注入、泛型编程,都基于反射机制。
反射的实质其实就是:给我一个对象,我能在不知道它是什么静态类型,且不使用new操作符的情况下,再创建出一个与它同类型的新对象,并且能够访问其方法。
从定义来看,反射有两方面好处。
一方面,我们知道,使用new操作符,即是创造了一个紧耦合,直接地将所在类与new后的静态类型紧耦合在了一起。而使用反射,直接避免紧耦合的发生,降低了耦合度。
另一方面,在具体的工程中,在编写程序阶段,因为用户的请求很多变,你很难预测用户需求,不可能写成百上千的if-else语句使用静态的类型枚举出所有可能结果。这就体现了反射的动态效果。反射是在编写程序阶段,不确定具体逻辑的时候存在的。程序需要以不变应万变的能力,这个能力就是反射。
但是注意,不要过多使用反射,以影响程序的性能。
static void Main()
{
ITank tank = new HeavyTank();
//----------------------------
var t = tank.GetType();
object o = Activator.CreateInstance(t);
MethodInfo fireMe = t.GetMethod("Fire");
MethodInfo runMe = t.GetMethod("Run");
fireMe.Invoke(o, null);
runMe.Invoke(o, null);
}
反射长相如此。
但是一般不会直接使用反射,而是会使用封装好了的反射。而封装好了的反射最重要的功能就是依赖注入。
依赖注入最重要的是容器container,即sevice provider。其中装着很多类型和对应的接口,要实例的时候就向他要就行。
using Microsoft.Extensions.DependencyInjection;
static void Main()
{
var serviceCollection = new ServiceCollection();//容器
serviceCollection.AddScoped(typeof(ITank),typeof(HeavyTank));
//typeof拿到动态类型描述;第一个参数是接口类型,第二个是实现接口类型的类型。
var serviceProvider = serviceCollection.BuildServiceProvider();
//-----------------一次性的注册-------------------------------接下来不再有new
ITank tank = serviceProvider.GetService<ITank>();
tank.Fire();
tank.Run();
}
这样做的好处是什么呢?比如说程序有一个地方要改,不要heavytank了,全部变成lighttank。这时候如果用new操作符,就得改多处,而且还不能用一键替换,因为你不知道哪里要改哪里不用改。如果使用这样的封装反射,就只用改第二行就行。
但是反射的强大不仅仅在此。
class Program
{
static void Main()
{
var serviceCollection = new ServiceCollection();//容器
serviceCollection.AddScoped(typeof(ITank), typeof(HeavyTank));
serviceCollection.AddScoped(typeof(IVehicle), typeof(Car));
serviceCollection.AddScoped<Driver>();
//typeof拿到动态类型描述;第一个参数是接口类型,第二个是实现接口类型的类型。
var serviceProvider = serviceCollection.BuildServiceProvider();
//-----------------一次性的注册-------------------------------
var driver = serviceProvider.GetService<Driver>();
driver.Drive();
}
}
class Driver
{
private IVehicle _vehicle;
public Driver(IVehicle vehicle)
{
_vehicle = vehicle;
}
public void Drive()
{
Console.WriteLine("I am driving a {0}.",_vehicle.GetType().Name);
}
}
Driver这个类本来要求产生实例时需要构造传入一个IVehicle,现在没传。容器会自动帮你在它已有的类型里面找到一个IVehicle接口的类,然后帮你自动创造一个实例传进去。比如这里就传进去了一个Car。
注意此处似乎类型还挺严格的。
Driver要求一个IVehicle类,虽然IVehicle->ITank,但是不允许呢。
同样的,如果有一个TankDriver类,要求一个ITank,给一个IVehicle
也是不允许的。
再注意一个点,如果有多个IVehicle,会取最新的那个。这个新体现在代码先后上,比如说:
这时候司机开的就是Truck了。
于是我在想,这个“新版本”会不会跟子类父类有关呢?因而此处,我创建一个Car的子类Rolls_Royce:
这样依然是后面的被引用,没啥好说。
这样是Car被引用,依然是后面的被引用。
这说明所谓的“最新的”,其实只是添加到容器的先后顺序而已,不是这种子父类的版本问题。
于是我在想,是不是容器是采用类似栈什么的结构?遍历下来,自然是遍历得少省事,所以优先选取先放进去的。
这里,依赖注入体现在,把容器创建的实例注入到构造器。
依赖注入在别的语言也是有的,比如在java中,这玩意叫作自动牵线。
看完了依赖注入这个例子,我们来看看反射是如何进行解耦合,产生更松的耦合的,即,反射是如何让程序有“以不变应万变”的能力的。
这常常用在插件式编程上。
什么是插件式编程呢?就是一个玩意,由一个主体程序,和若干个插件组成。插件不与主体程序一起编译,但是会与其一起工作。插件往往由第三方提供,而且可能在主体程序完成后再插。这样的编程模式,有利于形成以主体程序为中心的生态圈。
这类似于游戏mod、创意工坊之类的吧。
因而,一般的主体程序都会发布包含程序接口(API)的开发包(SDK)。
在这样的模式中,主体程序就是所谓“不变”,插件就是所谓“万变”。因为开发主体程序时显然不能预测出未来可能有的插件,并枚举出来。
而为了避免第三方开发者犯小错误【如把Run方法写成run方法,导致找不到这个方法】,需要用SDK里的API【就是接口,写程序时也有留意到吧,接口对方法很严格的】来约束开发者开发,同时也能帮助开发者减轻劳动。
下面,我们通过实例来理解这个思想。
这次例子是那种婴儿车,上面配置有小动物和数字按钮。按一下小动物的头像,再按一下数字,就能让小动物叫数字次的声音。而动物数量可能会有其他人想多加一些,这时候就需要有插件了。
下面,我们先来编写主体程序。
首先,我们获取程序所在文件夹:
在此处建立一个Animal文件夹:
接下来,我们编写主体程序,包括导入Animal里面的动物和编写使用逻辑两个方面。
首先是导入Animal里的动物的代码逻辑:
namespace AnimalBaby
{
class Program
{
static void Main(string[] args)
{
//load
var folder = Path.Combine(Environment.CurrentDirectory, "Animal");
var files = Directory.GetFiles(folder);
//创建用于存储动物类型的数据组
var animalTypes = new List<Type>();
//两个循环。第一层遍历所有文件,第二层遍历一个文件中的所有类。
foreach (var file in files)
{
//得到每个小dll文件里的所有类
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
var types = assembly.GetTypes();
foreach (var t in types)
{
if (t.GetMethod("Voice")!=null)//找到dll文件中的动物类
{
animalTypes.Add(t);
}
}
}
//load end
}
}
}
接下来就是激动人心的使用反射了,相当于模仿一个插u盘的过程:
注意一下反射在这里的不可替代性。t是动态的,反射可以根据t自由选取所需小动物,并且前面已经对没有“Voice”方法的t过滤了,故而可以放心调用。真是完美啊,太牛逼了,反射!
写完了主体程序,接下来让我们写各种各样的小动物吧。【略】
最终主体程序代码如下:
namespace AnimalBaby
{
class Program
{
static void Main(string[] args)
{
//load
var folder = Path.Combine(Environment.CurrentDirectory, "Animal");
var files = Directory.GetFiles(folder);
var animalTypes = new List<Type>();
foreach (var file in files)
{
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
var types = assembly.GetTypes();
foreach (var t in types)
{
if (t.GetMethod("Voice")!=null)
{
animalTypes.Add(t);
}
}
}
//load end
//具体逻辑
while (true)
{
for (int i = 0; i < animalTypes.Count; i++)
{
Console.WriteLine("{0} : {1}", i + 1, animalTypes[i].Name);
}
Console.WriteLine("=====================");
Console.WriteLine("Please choose the animal.");
int choice = int.Parse(Console.ReadLine());
if (choice > animalTypes.Count || choice < 1)
{
Console.WriteLine("You have made an ilegal choice!");
continue;
}
Console.WriteLine("How many times you want it to cry?");
int times = int.Parse(Console.ReadLine());
var t = animalTypes[choice - 1];//小动物类型,Type
var m = t.GetMethod("Voice");//保存叫的方法
object o = Activator.CreateInstance(t);//使用反射创建实例
m.Invoke(o, new object[] { times });
}
}
}
}
运行效果:
事到如今,尚感到一些不足,就是可能开发者会把Voice这个方法写错。为了约束这一点,且提供便利,我们可以使用SDK。
下面就来看看怎么弄出一个SDK来。
感觉学完这节课,跟着做了个小项目啊~总结一下吧。
这个项目的核心难点,首先一个就是使用反射,动态根据插件里有什么东西,我就创建什么样的实例来执行那个东西所具有的方法。这里的方法应该是各个东西之间的共性,可变的是种类,不变的是共同的方法。
注意反射的基本过程。得到type t-得到方法m-声明object o,以t为type-o为参数调用方法m
第二个,就是装载程序的方法。我们可以看到整体思路是,先找到文件夹所在地址,再筛选出其中的所有dll文件,再创建一个装这些类型的Type数据组,然后把所有dll文件、所有符合要求的类都筛选出来,存入到Type数据组之中。
关于这些类型的使用方法,这里用到了两个。一个是得到他的名字,一个是用它来声明实例(反射)。由此可见,这个方法十分具有通用性,对那些需要读取一大堆类的项目来说。