C#之反射、元数据详解
前言
在本节中主要讲述自定义特性、反射。自定义特性允许把自定义元数据与程序元素关联起来。这些元数据是在编译过程中创建的,并嵌入程序集中。反射是一个普通的术语,它描述了在运行过程中检查和处理程序元素的功能。例如,反射运行完成以下任务:
- 枚举类型的成员
- 实例化新对象
- 执行对象的成员
- 查找类型的信息
- 查找程序集的信息
- 检查应用于某个类型的自定义特性
- 创建和编译新程序集
这个列表列出了许多功能,本章中主要介绍部分常用的功能。
自定义特性
一、编写自定义特性
1. 理解自定义特性
[LastModified("Test","Test")] public class TestNumber { }
这个例子首先会发现LastModified这个特性,首先把字符串Attribute追加到这个名称后面,形成一个组合LastModifiedAttribute,然后在其搜多路径的所有名称空间去搜索这个名称的类。注意如果本来就以Attribute结尾了,那么也就不会组合在一起了。编译器会找到含有改名称的类,且这个类直接或间接派生自System.Attribute。编译器很认为这个类包含控制特性用法的信息。特别是属性类需要指定:
- 特性可以应用到那些类型的程序元素上(类、结构、属性和方法等)
- 是否可以多次应用到同一个应用程序元素上
- 在应用到类和接口上时,是否由派生类和接口继承
- 这个特性有那些必选和可选参数
如果哦编译器找不到对应的特性类,或者找到了但是使用方式或者信息不对,编译器就会产生一个编译错误。
下面我们看看自定义特性其中的各个元素如何定义吧
2. 指定AttributeUsage特性
第一个要注意的就是AttributeUsage特性,它是特性类的标记。AttributeUsage主要用于标识自定义特性可以应用到那些类型的程序元素上。 这些信息都是由第一个参数提供的,该参数输入必选参数,其类型是枚举类型AttributeTargets。其成员如下:
All |
32767 |
可以对任何应用程序元素应用属性。 |
Assembly |
1 |
可以对程序集应用属性。 |
Class |
4 |
可以对类应用属性。 |
Constructor |
32 |
可以对构造函数应用属性。 |
Delegate |
4096 |
可以对委托应用属性。 |
Enum |
16 |
可以对枚举应用属性。 |
Event |
512 |
可以对事件应用属性。 |
Field |
256 |
可以对字段应用属性。 |
GenericParameter |
16384 |
可以对泛型参数应用属性。 目前,此属性仅可应用于 C#、Microsoft 中间语言 (MSIL) 和已发出的代码中。 |
Interface |
1024 |
可以对接口应用属性。 |
Method |
64 |
可以对方法应用属性。 |
Module |
2 |
可以对模块应用属性。 Module 引用的是可移植可执行文件(.dll 或 .exe),而不是 Visual Basic 标准模块。 |
Parameter |
2048 |
可以对参数应用属性。 |
Property |
128 |
可以对属性 (Property) 应用属性 (Attribute)。 |
ReturnValue |
8192 |
可以对返回值应用属性。 |
Struct |
8 |
可以对结构应用属性,即值类型。 |
在上面列表中,有两个值不对应于任何程序元素:Assembly和Module。特性可以应用到整个程序集或模块中,而不是应用到代码中的一个元素上,在这种情况下,这个特性可以放在源代码的任何地方,但需要关键字Assembly和Module作为前缀
[assembly:SupportsWhatsNew]
[module: SupportsWhatsNew]
下面我们再介绍几个参数AllowMultiple表示一个特性是否可以多次应用到同一项,Inherited表示应用到类或接口上的特性是否可以自动应用到所以的派生的类或接口上。如果特性应用到方法或者属性上,就表示是否可以自动应用到该方法或属性等的重新版本上。
二、自定义特性示例
经过上面的介绍,下面我们开始定义自定义特性示例。这里我们将创建两个类库,第一个WhatsNewAttributes库程序集,其中定义了两个特性,LastModifiedAttribute和SupportsWhatsNewAttribute。
LastModifiedAttribute特性可以用于标记最后一次修改数据项的时间,它有两个必选参数:修改的日期和包含描述修改的信息。还有一个可选参数issues,它可以用来描述该数据项的任何重要问题。
SupportsWhatsNewAttribute是一个较小的类,不带有任何参数的特性。这个特性是一个程序集的特性,用于把程序集标记为通过SupportsWhatsNewAttribute维护的文档。
/// <summary> /// 用于标记最后一次修改数据项的时间和信息。 /// </summary> [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Constructor, AllowMultiple = true, Inherited = false)] public class LastModifiedAttribute : Attribute { private readonly DateTime _dateModified; private readonly string _changes; public LastModifiedAttribute(string dateModified, string changes) { _dateModified = DateTime.Parse(dateModified); _changes = changes; } public DateTime DateModified => _dateModified; public string Changes => _changes; public string Issues { get; set; } } /// <summary> /// 用于把程序集标记为通过LastModifiedAttribute维护的文档 /// </summary> [AttributeUsage(AttributeTargets.Assembly)] public class SupportsWhatsNewAttribute : Attribute { }
接下来我们介绍第二个库VectorClass。VectorClass库引用了WhatsNewAttributes库,添加声明后我们使用全局程序集特性标记程序集。
[assembly:SupportsWhatsNew] namespace VectorClass { [LastModified("2017-7-19", "更新C#7,.NET Core 2")] [LastModified("2015-6-6", "更新C#6,.NET Core")] [LastModified("2010-2-14", "修改第一步")] public class Vector : IFormattable, IEnumerable<double> { public Vector(double x, double y, double z) { X = x; Y = y; Z = z; } public Vector(Vector vector) : this(vector.X, vector.Y, vector.Z) { } public double X { get; } public double Y { get; } public double Z { get; } public IEnumerator<double> GetEnumerator() { throw new NotImplementedException(); } [LastModified("2017-7-19", "将ijk格式从StringBuilder更改为格式字符串")] public string ToString(string format, IFormatProvider formatProvider) { if (format == null) { return ToString(); } switch (format.ToUpper()) { case "N": return "|| " + Norm().ToString() + " ||"; case "VE": return $"( {X:E}, {Y:E}, {Z:E} )"; case "IJK": return $"{X} i + {Y} j + {Z} k"; default: return ToString(); } } public double Norm() => X * X + Y * Y + Z * Z; IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); } [LastModified("2015-6-6", "修改")] [LastModified("2010-2-14", " 类创建")] public class VectorEnumerator : IEnumerator<double> { public double Current => throw new NotImplementedException(); object IEnumerator.Current => throw new NotImplementedException(); public void Dispose() { throw new NotImplementedException(); } public bool MoveNext() { throw new NotImplementedException(); } public void Reset() { throw new NotImplementedException(); } } } }
这里我们还需要设置下csproj项目文件添加
<version>2.1.0</version>
到这里我们介绍了自定义特性相关。接下来我们介绍反射,然后根据反射示例加上自定义特性示例去完成一个小的demo。
反射
反射是.NET中的重要机制,通过反射,可以在运行时获得程序或程序集中每一个类型(包括类、结构、委托、接口和枚举等)的成员和成员的信息。有了反射,即可对每一个类型了如指掌。另外我还可以直接创建对象,即使这个对象的类型在编译时还不知道。
一、System.Type类
Type t=typeof(double);
这里使用Type类只为了存储类型的引用,以前把Type看做一个类,实际上时一个抽象的基类。实例化一个Type对象,实际上就实例化了Type的一个派生类。尽管一般情况下派生类只提供各种Type方法和属性的不同重载,但是这些方法和属性返回对应数据类型的正确数据。通常,获取指定任何给定类型的Type引用有3中常用的方式:
- 使用typeof运算符,就想上面的例子一样
- 使用GetType()方法,所有的类都会从System.Object继承这个方法。
double d = 10; Type t = d.GetType();
- 调用Type类的静态方法GetType()
Type t = Type.GetType("System.Double");
Type是实现许多反射功能的入口,它实现了许多方法和属性,这里我们将介绍如何使用这个类。
属性 |
返回值 |
Name |
数据类型名称 |
FullName |
数据类型的完全限定名(包括名称空间名) |
Namespace |
在其中定义数据类型的名称空间名 |
其次,属性还可以进一步获取Type对象的引用,这些引用表示相关的类
属性 |
返回对应的Type引用 |
BaseType |
该Type的直接基本类型 |
UnderlyingSystemType |
该Type在.NET运行库中映射的类型。这个成员只能在完整的框架中使用 |
其中还有许多布尔属性表示这种类型是否是一个类。还是一个枚举等等。这些特性包括IsAbstract、IsArray、IsClass、IsEnum、IsInterface、IsPointer、IsPrimitive(一种预定义的基元数据类型)、IsPublic、IsSealed以及IsValueType。例如判断类型是否是数组:
Type t = typeof(double); if (t.IsArray)//返回布尔值 { }
二、方法
System.Type的大多数方法都用于获取对应数据类型的成员信息:构造函数、属性、方法和事件等。下面我们看看Type的成员方法,这里遵循一个模式。注意名称为复数形式的方法返回一个数组。
返回的对象类型 |
方法 |
ConstructorInfo |
GetConstructor(),GetConstructors() |
EventInfo |
GetEvent(),GetEvents() |
FieldInfo |
GetField(),GetFields() |
MemberInfo |
GetMember(),GetMembers(),GetDefaultMembers() |
MethodInfo |
GetMethod(),GetMethods() |
PropertyInfo |
GetProperty(),GetProperties() |
GetMember()和GetMembers()方法返回的数据类型的任何成员或所有成员的详细信息,不管这些成员是构造函数、属性、方法等
三、Assembly类
Assembly类在System.Reflection名称空间定义,它允许访问给定程序集的元数据,它也可以包含可以加载和执行程序集的方法。
我们可以先看第一个方法Assembly.Load()或者Assembly.LoadFrom()。这两个方法的区别在于Load方法的参数时程序集的名称,运行库会在各个位置搜索该程序集,试图找到该程序集,这些位置包括本地目录和群居程序集缓存。
1、获取在程序集好难过定义的类型的详细信息
这里我跟根据Assembly类的一个功能来获取程序集中定义的所有类型的详细信息,只要调用Assembly.GetTypes()方法,他就可以返回一个包含所有类型的详细信息的System.Type引用数组。
Assembly theAssembly = Assembly.Load(new AssemblyName("VectorClass")); Type[] types = theAssembly.GetTypes();
2、获取自定义特性的详细信息
用于查找在程序集或类型中定义了什么自定义特性的方法取决于与该特性相关的对象类型。如果要确定程序集从整体上关联了什么自定义特性,就需要调用Assembly类的一个静态方法
Attribute[] attributes = Attribute.GetCustomAttributes(theAssembly);
完成示例
到这里我们就简单的介绍了自定义特性以及反射,我们就接着完成我们的示例,刚刚以及定义了两个程序集以及自定义特性。现在我们要做的就是配合反射来获取相关程序集的信息。主要实现效果是:说明公司如何定期升级软件,自动记录升级的信息。
class Program { /// <summary> /// 输出的消息 /// </summary> private static readonly StringBuilder outputText = new StringBuilder(1000); /// <summary> /// 存储的时间 /// </summary> private static DateTime backDateTo = new DateTime(2017,2,1); static void Main(string[] args) { //获取访问的程序集 Assembly theAssembly = Assembly.Load(new AssemblyName("VectorClass")); //获取自定义特性的详细信息 Attribute supportsAttribute = theAssembly.GetCustomAttribute(typeof(SupportsWhatsNewAttribute)); AddToOutput($"assembly:{theAssembly.FullName}"); if (supportsAttribute==null) { AddToOutput("这个程序集不支持"); return; } else { AddToOutput("定义的类型是:"); } //获取程序集中定义的公共类型集合 IEnumerable<Type> types = theAssembly.ExportedTypes; foreach ( Type definedType in types) { DisplayTypeInfo(definedType); } Console.WriteLine(backDateTo); Console.WriteLine(outputText.ToString()); Console.ReadLine(); } public static void DisplayTypeInfo(Type type) { if (!type.GetTypeInfo().IsClass) { return; } AddToOutput($"{Environment.NewLine}类 {type.Name}"); //获取类型的详细信息然后获取其自定义详细信息选择自定义特性再筛选时间 IEnumerable<LastModifiedAttribute> lastModifiedAttributes = type.GetTypeInfo().GetCustomAttributes() .OfType<LastModifiedAttribute>().Where(a => a.DateModified >= backDateTo).ToArray(); if (lastModifiedAttributes.Count()==0) { AddToOutput($"\t这个{type.Name}没有改变{Environment.NewLine}"); } else { foreach (LastModifiedAttribute item in lastModifiedAttributes) { WriteAttributeInfo(item); } AddToOutput("这些类的修改方法:"); //获取类的信息中的方法 foreach (MethodInfo methond in type.GetTypeInfo().DeclaredMembers.OfType<MethodInfo>()) { //获取这些方法的自定义特性信息筛选时间 IEnumerable<LastModifiedAttribute> attributesToMethods = methond.GetCustomAttributes().OfType<LastModifiedAttribute>() .Where(a => a.DateModified >= backDateTo).ToArray(); if (attributesToMethods.Count()>0) { AddToOutput($"{methond.ReturnType}{methond.Name}()"); foreach (Attribute attribute in attributesToMethods) { WriteAttributeInfo(attribute); } } } } } static void AddToOutput(string Text) => outputText.Append("\n" + Text); private static void WriteAttributeInfo(Attribute attribute) { if (attribute is LastModifiedAttribute lastModifiedAttribute) { AddToOutput($"\tmodified:{lastModifiedAttribute.DateModified:D}:{lastModifiedAttribute.Changes}"); if (lastModifiedAttribute.Issues!=null) { AddToOutput($"\tOutstanding issues:{lastModifiedAttribute.Issues}"); } } } }
上面都打上了详细备注,完整的项目示例已存放在Github上。有兴趣的可以Download下来看看。
总结
本篇文章主要介绍了Type和Assembly类,它们是访问反射所提供的扩展功能的主要入口点。反射是.NET中的重要机制,通过反射,可以在运行时获得程序或程序集中每一个类型(包括类、结构、委托、接口和枚举等)的成员和成员的信息。
不是井里没有水,而是你挖的不够深。不是成功来得慢,而是你努力的不够多。
欢迎大家扫描下方二维码,和我一起学习更多的C#知识