C#语法——反射

2020年01月12日 11:25 0 点赞 0 评论 更新于 2025-11-21 21:29
C#语法——反射

本篇文章主要介绍C#反射的用法。

反射概述

反射是架构师必须掌握的基础知识,因为任何设计出来的框架都要用到反射。同时,反射也是一种较为隐蔽的语法,反射代码通常会被封装起来,调用者只需使用,无需关注其具体实现。这与反射的特性有关,反射的存在是为了减少代码冗余,因此其具体实现不易被看到是正常的。

反射的定义

官方定义:反射提供了封装程序集、模块和类型的对象(Type 类型)。可以使用反射动态创建类型的实例,将类型绑定到现有对象,或从现有对象获取类型并调用其方法或访问其字段和属性。如果代码中使用了属性,可以利用反射对它们进行访问。

为了便于理解,下面进行通俗解释。在C#编程语言中,最常用的是类以及类中的函数和属性。正向调用的方式是先创建类,再用类创建对象,然后通过该对象调用类中的方法和属性。而反射则是相对于正向调用的反向调用方式。反射可以通过类名的字符串来创建类,也可以通过函数名的字符串和属性名的字符串来调用类的函数和属性。

可能有同学会问,既然正向调用可行,为何还需要反向调用呢?别着急,继续往下看,反射的存在必有其合理之处。

反射的基础应用

类反射

下面的代码展示了如何通过类名称的字符串反射出类的对象:

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 的存在让反射变得非常灵活,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");

有些同学可能担心反射会影响程序性能。实际上,如果使用完全限定名进行反射,速度与正常调用相同。但如果只写类名进行反射,速度会变慢,因为需要遍历所有命名空间来查找类。所以,只要在反射时写全类的命名空间,速度就不会受到影响。

函数反射

函数的反射应用主要使用 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 函数只有一个参数,所以数组中只添加一个对象 "Kiba518"。最后,通过 method.Invoke 调用函数,反射调用时需要指定 Kiba 类的实例对象和入参。

属性反射

属性反射使用 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";
/* 约定的处理Command的类的名称的后缀 */
private const string classNamePostfix = "Handler";

/* 获取命名空间的名称 */
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);
/* 传递一个参数command */
object[] pmts = new object[] { 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 的类。客户端传来的任何继承自 CommandBaseCommand,代理都会找到对应的处理类并执行处理,最后返回结果。

为了更好地理解代码,可以参考对应的流程图,结合图片看代码,框架结构会更清晰。

这个简单的框架使用了约定优先原则(约定优于配置),具体约定如下:

  1. 处理 Command 的类的后缀名必须是 Command 的类名加 Handler
  2. 处理 Command 的类中的处理函数名必须为 Excute

概念是为了方便使用,学习过程中对概念有印象即可。需要注意的是,代码中的类都集中在一个命名空间下,实际使用时可根据项目需求进行扩展。

通过反射实现的这个简约框架,能让代码更加简洁。为了实现每个模块的简洁,反射通常会被封装在各个模块的底层,因此反射是框架设计的基础。

反射与特性

反射在系统中的另一个重要应用是与特性结合使用。在复杂系统中,有时需要清空对象的部分属性或获取对象的某些属性值,通常的做法是手动一个一个赋值。而利用反射结合特性,可以简化这种复杂操作的代码量。

以下是示例代码:

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 的属性清空。对于单个属性,这样做可能显得多余,但当对象有大量属性时,这种方法就很有价值。

既然能清除属性数据,自然也能为属性赋值,相信大家可以举一反三实现反射赋值。

反射+特性最常见的场景

反射和特性一起应用最常见的场景是使用 ADO.NET 从数据库查询出 DataTable 数据,然后将 DataTable 数据转换成 Model 实体类型。

在开发中,为了让实体更加充血(即增加实体的属性和方法),在使用反射将 DataTable 数据转存到 Model 实体时,遍历属性并赋值会增加额外的遍历次数。如果只有一个实体,多遍历几次影响不大,但如果有数十万条数据,额外的遍历次数会产生较大影响。

而使用反射结合特性可以减少这些额外的遍历次数。这里没有给出具体代码,因为理解前面内容的同学已经具备框架启蒙的基础,如果能自己实现 DataTable 转数据实体,就算是框架入门了。

需要注意的是,这里说的是框架,而不是架构。框架是名词,架构是动词,熟练掌握框架并不意味着能够进行良好的架构设计,要注意两者的区别。

结语

看完文章,有些同学可能会怀疑 PropertyInfoMethodInfo 是否真的有人会用,认为大家只是复制代码使用。实际上,反射是架构师的入门基础,任何实战型架构师都需要能够随时手写反射代码,因为优化框架是他们的职责。

所以,对此有怀疑的同学可以努力练习,将委托融入血液是高级软件工程师的基础,而将反射融入血液则是架构师的基础。

本文来源: C#语法——反射