反射、特性、接口、属性综合例子
- 例子说明
现需给一个婴儿车编写主体程序,这个婴儿车的功能主要有:
- 上面有按键,输入数值,然后发出数值对应动物的声音。比如按下1,婴儿车就发出狗叫的声音
- 声音文件或声音逻辑程序(Dll文件)需要放在一个文件夹Animals下,这个逻辑程序需要放在函数名称为Voice的函数下
- 主体程序需要支持第三方插件,第三方只要把相关的声音文件或声音逻辑程序(Dll文件)放在Voice文件夹下,婴儿车也可以发出第三方指定的声音
- 主体程序实现
下面是主体程序,主要原理就是遍历Animals文件夹里的所有dll的所有类,找到有Voice方法的类。然后通过反射技术创建类对象并调用其Voice函数
//获取声音文件的路径
var folder = Path.Combine(Environment.CurrentDirectory, "Animals");
//获取所有文件
var files = Directory.GetFiles(folder);
var AnimalType = new List<Type>();
foreach(var mfile in files)
{
//加载文件程序集
var assembly = Assembly.LoadFile(mfile);
//获取程序集的所有类
var types = assembly.GetTypes();
//遍历所有类,把类中有Voice方法的类添加到数组
foreach (var t in types)
{
if (t.GetMethod("Voice") != null)
{
AnimalType.Add(t);
}
}
}
//下面是操作面板逻辑
while (true)
{
for(int i = 0; i < AnimalType.Count; i++)
{
Console.WriteLine($"{i+1}.{AnimalType[i].Name}");
}
Console.WriteLine("========================");
Console.WriteLine("请选择动物!");
int index = int.Parse(Console.ReadLine());
if (index > AnimalType.Count || index < 1)
{
Console.WriteLine("输入有误,请重新输入!");
continue;
}
Console.WriteLine("请输入次数!");
int times = int.Parse(Console.ReadLine());
//通过反射根据输入的数字获得对应的类
var t = AnimalType[index - 1];
//通过反射获得这个类下的Voice方法
var m = t.GetMethod("Voice");
//通过反射创建对象
var o = Activator.CreateInstance(t);
//通过反射传入参数调用方法
m.Invoke(o, new object[] {times});
}
- 第三方程序开发
上面已经往常主体程序开发,婴儿车出厂就自带有这个主体程序。现在新建一个VS项目,模拟第三方已经拿到主体程序,需要给他开发各种动物声音。
- 新建第一个动物Cat类库项目,并新建类
public class Cat
{
public void Voice(int times)
{
for (int i = 0; i < times; i++)
{
Console.WriteLine("miaomiaomiao喵");
}
}
}
- 新建第二个动物Dog类库项目,并新建类
public void Voice(int times)
{
for (int i = 0; i < times; i++)
{
Console.WriteLine("wanwanwan汪");
}
}
- 结构如下:
- 编译后会在对应目录下生成两个dll文件,分别是:Cat.dll、Dog.dll
- 现在第三方已经生产了两种动物的声音,现在把它拷贝到主体程序的Animals文件夹下
- 运行主体程序,可以正常运行
- 改进1
站在开发主体程序的第一方来考虑问题,此项目已经基本完成,可以正常运行起来。但是主体程序是通过反射找到Voice方法来实现调用的,假如第三方假如由于粗心,把Voice写成了voice或者vioce等错误,那么主体程序是无法识别的。为了解决这个问题,开发主体程序的第一行就需要引用接口,然后写成一个SDK给第三方,第三方必须按照这个接口规则来写,就可以杜绝以上问题。下面是第一方程序的改进
- 新建一个单独的项目,生成类库,写一个interface
public interface IAnimal
{
void Voice(int times);
}
- 结构:
- 它会生成一个AnimalVoiceSDK.dll,后面把这个dll发给第三方,让第三方开发时必须继承这个接口,按照这个接口的规则生成类。
- 主体程序由于引入了接口,也可以做一些优化
- 优化1:遍历第三方的类时,通过反射获取到类的继承类,假如该继承类是我们发给第三方的Ianimal接口才继续。
if (t.GetInterfaces().Contains(typeof(IAnimal)) ==true &&
t.GetMethod("Voice") != null)
{
AnimalType.Add(t);
}
优化前:
if (t.GetMethod("Voice") != null)
{
AnimalType.Add(t);
}
- 优化2:反射创建对象时,是用的Object对象类型,假如引用了接口,则不需要强类型了
//通过反射创建对象
IAnimal o = (IAnimal)Activator.CreateInstance(t);
//直接调用方法
o.Voice(times);
优化前:
//通过反射创建对象
var o = Activator.CreateInstance(t);
//通过反射传入参数调用方法
m.Invoke(o, new object[] {times});
- 第三方拿到这个SDK后,需要引用到他们的项目里面去,下面的改进是由第三方完成:
- 第三方项目中引用AnimalVoiceSDK.dll,并继承接口下的Ianimal
public class Cat: IAnimal
{
public void Voice(int times)
{
for (int i = 0; i < times; i++)
{
Console.WriteLine("miaomiaomiao喵");
}
}
}
- 由于继承了接口,假如把Voice写成了voice或者vioce等错误,软件会直接报错。这样就可以完全杜绝此问题
- 改进2
假如第三方在写Voice函数时,有一些没完成的,或者调试仍有问题的。直接给到婴儿车上会有问题。为了解决这个问题,开发主体程序的第一方需要提供一个属性,这个属性可以标识在Voice函数是,主体程序反射遍历方法时,若函数标识有属性的,则不添加跳过。
- 第一方在SDK中添加一个自定义属性,属性中可以不添加任何内容。因为只是简单的标识作用
namespace AnimalVoiceSDK
{
public class UnfinishAttribute:Attribute
{
}
}
- 第一方修改反射获得Voice方法,标识有上面的属性的跳过
if (t.GetInterfaces().Contains(typeof(IAnimal)) ==true &&
t.GetMethod("Voice") != null)
{
var isUnfinishedAttribute = t.GetMethod("Voice").CustomAttributes.Any(a =>a.AttributeType == typeof(UnfinishAttribute));
if (isUnfinishedAttribute == false)//没标识有未完成属性的才添加
{
AnimalType.Add(t);
}
}
- 然后把SDK发给第三方,假如第三方如下面标识,则婴儿车则不读取
public class Cat: IAnimal
{
[UnfinishAttribute]
public void Voice(int times)
{
for (int i = 0; i < times; i++)
{
Console.WriteLine("miaomiaomiao喵");
}
}
}