前言
编程其实就是写代码, 而写代码目的就是实现业务, 所以, 语法和框架也是为了实现业务而存在的. 因此, 不管多么高大上的目标, 实质上都是业务.
所以, 我认为不要把写代码上升到科学的高度. 上升到艺术就可以了, 因为艺术本身也没有高度....
软件设计存在过度设计, 语法和框架的理解, 也存在过度理解. 比如, 反编译下, 看看反射是怎么实现的...
有兴趣是好事, 但就算知道了反射的本质, 了解了反射是如何设计的, 你技术也没什么质的改变. 因为, 技术水平最终还是要落实到应用上.
在比如, 过度的追求代码性能, 也不见得是一件好事, 因为,[大多数]情况下, 硬件比程序员便宜多了...(注意这里指的是代码不是算法和数据库性能)
所以, 不论什么事, 过度了, 总不是好事.
----------------------------------------------------------------------------------------------------
本篇文章主要介绍 C# 反射[用法] .
反射是架构师必会的基础, 因为任何一个被设计出来的框架, 都要使用反射.
反射也是最隐蔽的语法, 因为反射写出来后, 通常它会被直接封装, 然后调用者就只负责使用, 不再关注他的具体实现.
这与它的特性有关, 因为反射就是为了减少代码冗余而存在的, 所以, 看不见很正常.
反射的定义
官方定义: 反射提供了封装程序集, 模块和类型的对象(Type 类型). 可以使用反射动态创建类型的实例, 将类型绑定到现有对象, 或从现有对象获取类型并调用其方法或访问其字段和属性. 如果代码中使用了属性, 可以利用反射对它们进行访问.
看不懂? 没关系, 我们把它翻译成人类可理解的语言.
C# 编程语言中, 最常使用的是类和类中的函数和属性. 正向调用的方法是, 创建类, 然后用类创建一个对象. 接下来就可以用这个对象调用类中的方法和属性了.
而反射, 就是相对于这种正向调用的存在. 即, 它是反向调用.
反射可以通过类名的字符串来创建类, 可以通过函数名的字符串和属性名的字符串, 来调用类下的函数和属性.
有同学会问了, 既然正向可以调用, 那么反向调用干什么呢?
会有这种问题的同学, 先别着急, 继续往下看, 反射既然存在, 就必然有存在的道理.
反射的基础应用
1, 类反射
先看下面代码; 代码为通过类名称的字符, 反射出类的对象.
- public class ReflectionSyntax
- {
- public static void Excute()
- {
- Type type = GetType("Syntax.Kiba");
- Kiba kiba = (Kiba)Activator.CreateInstance(type);
- Type type2 = GetType2("Syntax.Kiba");
- Kiba kiba2 = (Kiba)Activator.CreateInstance(type2);
- }
- public static Type GetType(string fullName)
- {
- Assembly assembly = Assembly.Load("Syntax");
- Type type = assembly.GetType(fullName, true, false);
- return type;
- }
- public static Type GetType2(string fullName)
- {
- Type t = Type.GetType(fullName);
- return t;
- }
- }
- public class Kiba
- {
- public void PrintName()
- {
- Console.WriteLine("Kiba518");
- }
- }
在代码中我们看到, 反射时传递了字符串 "Syntax.Kiba", 然后通过解析字符串, 获取到了该字符串对应的类的类型, 最后再借助 Activator 来辅助创建类的实例.
其中字符串 "Syntax.Kiba" 是一个完全限定名. 什么是完全限定名? 完全限定名就是命名空间 + 类名. 在反射的时候, 需要我们传递完全限定名来确定到底要去哪个命名空间, 找哪个类.
在代码中我们还可以看到, 获取类型的方式有两种, 一种是较复杂的, 一种是简单的.
GetType2 方法是简单的获取类别, 通过 Type 直接就解析了字符串. 而 GetType 则先进行了加载 Assembly(组件), 然后再由组件获取类型.
两者有什么区别呢?
区别是, 用 Type 直接解析, 只能解析当前命名空间下的类. 如果该类存在于引用的 DLL 中, 就解析不了.
而 GetType 方法中的[Assembly.Load 指定了程序集名], 所以, 在反射时, 就会去指定的命名空间里找对应的类. 这样就能找到非本程序集下的类了.
[Assembly.Load 指定了程序集名]这句话不好理解?
没关系, 换个表达, Assembly.Load 指定了命名空间的名称, 所以反射时, 会去这个命名空间里找类, 这样是不是就好理解了.
Assembly
Assembly 的存在让反射变得特别灵活, 其中 Assembly.Load 不止可以导入我们引入的程序集(或命名空间).
也可以导入我们未引入程序集的 dll. 调用模式如下:
System.Reflection.Assembly o = System.Reflection.Assembly.Load("mscorlib.dll");
Assembly 导入了程序集后, 还可以不借助 Activator 来辅助, 自己就可以创建类. 如下:
- Assembly assembly = Assembly.Load("Syntax");
- Kiba kiba = (Kiba)assembly.CreateInstance("Syntax.Kiba");
有的同学可能会担心性能, 会觉得这样反射, 会使程序变慢.
有这种想法的同学, 其实你已经是在过度理解语法了. 这种地方的代码性能其实是可以不用关心的.
那么, 到底会不会变慢呢?
答案是这样的, 如果你是使用完全限定名来反射, 速度就是一样的. 如果是反射时, 只写了一个类名, 那么速度就会变慢. 因为它要遍历所有的命名空间, 去找这个类.
即, 只要反射时把类的命名空间写全, 那么速度就不会慢.
2, 函数反射
函数的反射应用主要是使用类 MethodInfo 类反射, 下面先看下基础应用.
- public static void ExcuteMethod()
- {
- Assembly assembly = Assembly.Load("Syntax");
- Type type = assembly.GetType("Syntax.Kiba", true, false);
- MethodInfo method = type.GetMethod("PrintName");
- object kiba = assembly.CreateInstance("Syntax.Kiba");
- object[] pmts = new object[] { "Kiba518" };
- method.Invoke(kiba, pmts);// 执行方法
- }
- public class Kiba
- {
- public string Name { get; set; }
- public void PrintName(string name)
- {
- Console.WriteLine(name);
- }
- }
一些同学第一眼看上去可能会有点不适应, 因为好像很多类都是大家不经常用的. 这也没办法, 因为这是一个进阶的过程, 必须经历从陌生到熟悉. 当你熟悉了这样的代码后, 就代表你的技术水平又进步了一个台阶.
下面讲解一些这些代码.
首先我们导入了命名空间, 接着我们获取了该命名空间下 Kiba 这个类的类型; 接下来我们通过这个类型来获取指定名称的函数.
然后我们通过 Assembly 创建了一个 Kiba 的实例, 接着定义了一个参数的 Object 数组, 因为 Kiba 类下的函数 PrintName 只有一个参数, 所以, 我们只为这个 Object 数组添加一个对象[Kiba518].
最后, 我们通过 method.Invoke 来调用这个函数, 由于是反射, 所以调用时, 需要指定 Kiba 类的实例对象和入参.
这样, 函数的反射就实现了.
3, 属性反射
属性反射是用 PropertyInfo 类来实现, 下面看基础的属性反射.
- public static void ExcuteProperty()
- {
- Kiba kiba = new Kiba();
- kiba.Name = "Kiba518";
- object name = ReflectionSyntax.GetPropertyValue(kiba, "Name");
- Console.WriteLine(name);
- }
- public static object GetPropertyValue(object obj, string name)
- {
- PropertyInfo property = obj.GetType().GetProperty(name);
- if (property != null)
- {
- object drv1 = property.GetValue(obj, null);
- return drv1;
- }
- else
- {
- return null;
- }
- }
如代码所示, 首先我们定义了一个 Kiba 的对象, 并为 Name 赋值, 然后我们通过 GetPropertyValue 方法, 传递了 Kiba 对象和要获取值的属性名称.
GetPropertyValue 函数里通过使用 PropertyInfo 完成了反射.
有的同学可能会觉得, 这个很鸡肋, 既然已经得到对象, 还反射做什么, 直接获取就可以了呀.
别着急, 我们接下来一起看反射的架构应用.
反射的架构应用
框架编写的核心目的之一, 是统一系统秩序. 那么什么是系统秩序呢?
首先我们看下系统的构成, 系统个通常是由子系统, 程序集, 类, 函数这四部分构成. 如下图所示.
既然系统由子系统, 程序集, 类, 函数这四个基础元素构成, 那么系统秩序, 自然指的就是这四个元素的秩序. 而这四个元素最难形成秩序的就是函数了.
很显然, 任何的项目都存在重复的函数, 或者功能相近的函数. 而彻底杜绝这种情况, 显然是不可能的. 那么我们只好尽量是设计会避免重复元素的框架了. 而反射, 正是为此而存在的.
反射的架构应用
现实中的框架因为这样那样的原因, 会有千奇百怪的设计, 所以拘泥于一种设计模式是愚蠢的, 实战中要多种设计模式一起应用, 局部设计有时候只取设计模式中一部分也可以. 这样才能实现项目的量身定制.
所以, 这里只介绍一种实战的架构应用, 一种使用反射的框架基础结构. 下面请框架基础代码.
- public class Client
- {
- public void ExcuteGetNameCommand()
- {
- Proxy proxy = new Proxy();
- GetNameCommand cmd = new GetNameCommand();
- ResultBase rb = proxy.ExcuteCommand(cmd);
- }
- }
- public class Proxy
- {
- public ResultBase ExcuteCommand(CommandBase command)
- {
- var result = HandlerSwitcher.Excute(command);
- return result as ResultBase;
- }
- }
- public class HandlerSwitcher
- {
- private const string methodName = "Excute";// 约定的方法名
- private const string classNamePostfix = "Handler";// 约定的处理 Command 的类的名称的后缀
- // 获取命名空间的名称
- public static string GetNameSpace(CommandBase command)
- {
- Type commandType = command.GetType();// 获取完全限定名
- string[] CommandTypeNames = commandType.ToString().Split('.');
- string nameSpace = "";
- for (int i = 0; i <CommandTypeNames.Length - 1; i++)
- {
- nameSpace += CommandTypeNames[i];
- if (i < CommandTypeNames.Length - 2)
- {
- nameSpace += ".";
- }
- }
- return nameSpace;
- }
- public static object Excute(CommandBase command)
- {
- string fullName = command.GetType().FullName;// 完全限定名
- string nameSpace = GetNameSpace(command);// 命名空间
- Assembly assembly = Assembly.Load(nameSpace);
- Type handlerType = assembly.GetType(fullName + classNamePostfix, true, false);
- object obj = assembly.CreateInstance(fullName + classNamePostfix);
- MethodInfo handleMethod = handlerType.GetMethod(methodName);// 获取函数基本信息
- object[] pmts = new object[] { command }; // 传递一个参数 command
- try
- {
- return handleMethod.Invoke(obj, pmts);
- }
- catch (TargetInvocationException tie)
- {
- throw tie.InnerException;
- }
- }
- }
- public class GetNameCommandHandler
- {
- public ResultBase Excute(CommandBase cmd)
- {
- GetNameCommand command = (GetNameCommand)cmd;
- ResultBase result = new ResultBase();
- result.Message = "I'm Kiba518";
- return result;
- }
- }
- public class GetNameCommand: CommandBase
- {
- }
- public class CommandBase
- {
- public int UserId { get; set; }
- public string UserName { get; set; }
- public string ArgIP { get; set; }
- }
- public class ResultBase
- {
- public string Message { get; set; }
- }
代码中框架很简单, 主要目的是实现一个代理, 用于处理继承了 CommandBase 的类的代理.
即, 客户端, 不论传来什么样的 Command, 只要它是继承自 CommandBase 的, 这个代理都会找到对应的处理类, 并执行处理, 且返回结果.
为了更清晰的理解这段代码, 我们可以参考下面这个流程图. 结合了图片在来看代码, 框架结构就会更清晰.
这个简单的框架中, 使用了一个概念, 叫做约定优先原则, 也叫做约定优于配置; 喜欢概念的小伙伴可以自行百度.
框架中使用的两个约定如下:
第一个是, 处理 Command 的类必须后缀名是 Command 的类名 + Handler 结尾.
第二个是, 处理 Command 的类中的处理函数名必须为 Excute.
其实概念就是供大家使用的, 会用即可; 学习的过程中, 概念之类的术语, 有个印象即可.
PS: 为了阅读方便, 这里面的类都集中写在了一个命名空间之下了, 如果有想使用这种设计模式的同学, 请按照自己项目所需进行扩展.
----------------------------------------------------------------------------------------------------
这样, 我们就通过反射实现了一个非常简约的框架, 通过使用这个框架, 会让代码变的更加简洁.
而为了实现每个模块的简洁, 反射也将会被封装在各个模块的底层, 所以, 反射毫无疑问, 就是框架设计的基础.
反射与特性
反射在系统中另一个重要应用就是与特性的结合使用.
在一些相对复杂的系统中, 难免会遇到一些场景, 要讲对象中的一部分属性清空, 或者要获取对象中的某些属性赋值. 通常我们的实现方式就是手写, 一个一个的赋值.
而利用反射并结合特性, 完全可以简化这种复杂操作的代码量.
- public partial class ReflectionSyntax
- {
- public void ExcuteKibaAttribute()
- {
- Kiba kiba = new Kiba();
- kiba.ClearName = "Kiba518";
- kiba.NoClearName = "Kiba518";
- kiba.NormalName = "Kiba518";
- ClearKibaAttribute(kiba);
- Console.WriteLine(kiba.ClearName);
- Console.WriteLine(kiba.NoClearName);
- Console.WriteLine(kiba.NormalName);
- }
- public void ClearKibaAttribute(Kiba kiba)
- {
- List<PropertyInfo> plist = typeof(Kiba).GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public).ToList();// 只获取 Public 的属性
- foreach (PropertyInfo pinfo in plist)
- {
- var attrs = pinfo.GetCustomAttributes(typeof(KibaAttribute), false);
- if (null != attrs && attrs.Length> 0)
- {
- var des = ((KibaAttribute)attrs[0]).Description;
- if (des == "Clear")
- {
- pinfo.SetValue(kiba, null);
- }
- }
- }
- }
- }
- public class Kiba
- {
- [KibaAttribute("Clear")]
- public string ClearName { get; set; }
- [KibaAttribute("NoClear")]
- public string NoClearName { get; set; }
- public string NormalName { get; set; }
- }
- [System.AttributeUsage(System.AttributeTargets.All)]
- public class KibaAttribute : System.Attribute
- {
- public string Description { get; set; }
- public KibaAttribute(string description)
- {
- this.Description = description;
- }
- }
如上述代码所示, 我们通过反射, 将拥有 KibaAttribute 特性的, 且描述为 Clear 的属性, 清空了.
当然为了一个属性这么做不值得, 但如果一个对象有 70 个属性的时候, 这么做就值得了.
既然能清除属性的数据, 那么自然就可以为属性赋值. 至于如何实现反射赋值, 相信大家可以举一反三.
反射 + 特性最常见的场景
反射 + 特性一起应用, 最常见的场景就是用 ADO.NET 从数据库查询出 DataTable 的数据, 然后将 DataTable 的数据转换成 Model 实体类型.
我们在开发中, 为了让实体更加充血, 往往会对数据实体增加一些属性和方法.(什么是充血? 充血就是充血模型, 有兴趣的同学可以自行百度了解下, 简单说就是为实体加属性和方法.)
那么, 在用反射, 将 DataTable 转存到 Model 实体的时候, 遍历属性并赋值的时候, 就会多遍历那么几次.
如果只是一个实体, 那么, 多遍历几次也没影响. 但, 如果是数十万的数据, 那这多几次的遍历影响就大了.
而用反射 + 特性, 就可以减少这些额外遍历次数.
讲了这么多为什么不给代码呢?
因为我觉得, 将上面的内容全理解的同学, 应该可以说, 已经框架启蒙了. 那么, 这个反射 + 特性的 DataTable 转数据实体, 如果能自己写出来, 就算是框架入门了. 所以, 这里给大家留下了一个练习的空间.
注意, 我这里说的是框架, 而不是架构.
框架与架构的区别是这样的, 框架是个名词, 而架构是个动词. 框架即便很熟练了, 也不见得可以架构的很好. 这个大家还是要注意区别.
结语
看完了整篇文章, 有的同学可能会有疑问, 这么生疏的 PropertyInfo 和 MethodInfo 真的有人会用吗? 都是 Copy 代码, 然后使用吧.
答案是, 当然有人可以熟练应用. 反射是架构师的入门基础, 任何一个 [可以实战] 的架构师, 都需要随时随地的可以手写出反射, 因为优化框架是他们的责任.
所以, 对此有所怀疑的小伙伴, 可以努力练习了, 将委托融入血液, 是高级软件工程师的基础, 而将反射融入血液, 就是架构师的基础了.
- C# 语法 -- 元组类型
- C# 语法 -- 泛型的多种应用
- C# 语法 --await 与 async 的正确打开方式
- C# 语法 -- 委托, 架构的血液
- C# 语法 -- 事件, 逐渐边缘化的大哥.
- C# 语法 -- 消息, MVVM 的核心技术.
我对 C# 的认知.
来源: https://www.cnblogs.com/kiba/p/9446905.html