【c#基础】反射、元数据和动态编程
一:在运行期间检查代码和动态编程
介绍自定义特性、反射和动态编程。自定义特性允许把自定义元数据与程序元素关联起来。
这些元素局是在编译过程中创建的,并嵌入到程序集中。
反射是一个普通术语,它描述了运行过程中检查和处理程序元素的功能。
反射允许完成一下任务
1):枚举类型的成员
2):实例化新对象 --CreateInstance()
3):执行对象的成员
4):查找类型的信息
5):查找程序集的信息
6):检查应用于某种类型的自定义特性
7):创建和编译新程序集。
二:自定义特性
对于特殊的特性,编译器可以以特俗的方法定制编译过程。例如:可以根据StructLayout特性中的信息在内存中布置结构。
自定义特性不会影响编译过程,因为编译器不能识别他们,但这些特性在应用于程序元素时,可以在编译好的程序集中用作元数据。
这些元数据在文档说明中非常有用。但是,使自定义特性非常强大的因素是使用反射,代码可以读取这些元数据,使用它们在运行期间做出决策。自定义特性可以直接影响代码运行的方式。
用处:自定义特性可以用于支持对自定义许可类进行声明性的代码访问安全检查,把信息与程序元素关联起来,程序元素由测试工具使用,或者在开发可扩展的架构时,允许加载插件或模块。
2.1:编写自定义特性
编译器遇到代码中某个应用了自定义特性的元素时,该如何处理。
[FieldName("SocialSecurityNumber")] public string SocialSecurityNumber { get;set; }
说明:当c#编译器发现这个属性(property)应用了一个FieldName特性时,首先会把字符串Attribute追加到这个名称后面,形成一个组合名称FieldNameAttribute,然后在其搜索路径的所有名称空间(即在using语句中提及的名称空间)中搜索有指定名称的类。
如果该特性的名称以Attribute结尾。编译器就不会把该字符串加到组合名称中,而是不修改该特性名。上面代码等价于
[FieldNameAttribute("SocialSecurityNumber")] public string SocialSecurityNumber { get;set; }
编译器会找到含有该名称的类,且这个类直接或间接派生自System.Attribute。编译器还认为这个类包含控制特性用法的信息。特别是属性类需要指定:
1:特性可以应用到哪些类型的程序元素上(类、结构、属性和方法)
2:它是否可以多次应用到同一个程序元素上
3:特性在应用到类或接口上时,是否由派生类和接口继承
4:这些特性由那些必选和可选参数
如果编译器找不到对应的特性类,或者找到一个特性类,但使用特性的方式与特性类中的信息不匹配,编译器就会产生一个编译错误。例如:特性类指定只能用于类,但我们把它引用到结构上,这样编译器就会产生一个编译错误。
[AttributeUsage(AttributeTargets.Property,AllowMultiple=false,Inherited=false)] public class FieldNameAttribute:Attribute { private string _name; public FieldNameAttribute(string name) { _name=name; } }
2.2:指定AttributeUsage特性
AttributeUsage主要用于标识自定义特性可以应用到哪些类型的程序元素上。这些信息由它第一个参数给出,该参数是必选的。该类型是枚举类型AttributeTargets。指定特性可以应用于哪些类型上。
AttributeTargets枚举成员如下:
注意:Assembly和Module。特性可以应用到真个程序集或模块中 ,而不是引用到代码中的一个元素上,在这种情况下,这个特性可以放在源代码任何地方,但需要用关键字Assembly或Module作为前缀
[assembly:SomeAssemblyAttribute(Parameters)]
[moudule:SomeAttribute(parameters)]
也可以使用Or运算符把这些值组合起来。
[AttributeUsage(AttributeTargets.Property|AttributeTargets.Field,AllowMultiple=false,Inherited=false)] public class FileNameAttribute:Attribute
AttributeUsage特性还包含另外两个参数:AllowMultiple和Inherited。他们用不同的语法来指定:
<ParameterName>=<ParameterValue>,而不是只给出这些参数的值。这些参数是可选的,根据需要,可以忽略他们。
AllowMultiple参数表示一个特性是否可以多次应用到同一项上,这里把它设置为false,表示如果编译器遇到下述代码,就会产生一个错误:
[FieldName("SocialSecutiryNumber")] [FieldName("NationInsuranceNumer")] public string SocialSecutiryNumber { }
把Inhertited参数设置为true,就表示应用到类或接口上的特性也可以自动应用到所有派生类或接口上。如果特性应用到方法或属性上,它就可以自动应用到该方法或属性等重写版本上。
2.3:指定特性参数
如何指定自定义特性接受的参数。
反射会从程序集中读取元数据(特性),并实例化他们表示的特性类。因此编译器需要确保存在自定义特性指定的要求特性,才能在运行期间实例化指定特性。
三:反射
通过System.Type类,可以访问关于 任何数据类型的信息。
System.Reflection.Assembly类,它可以用于访问给定程序集的相关信息,或者把这个程序集加载到程序中。
3.1:System.Type类
Type t=typeof(double);//这个做的目的只为了存储类型的引用
Type类实际上时一个抽象的基类,只要实例化一个Type对象,实际上就实例化了Type一个派生类。尽管派生类只提供各种Type方法和属性的不同重载,这些方法和属性返回对应数据类型的正确数据。
Type有与每种数据类型对应的派生类。他们一般不添加新的方法或属性。
通常,获取指向任何给定类型的Type引用有3种常用方式。
1:使用typeof运算符
Type t=type0f(double)
2:使用GetType()方法,所有类都会从System.Object继承这个方法。
double d=10; Type t=d.GetType();
调用GetType()方法,而不是把类型的名称作为其参数。但是,返回的Type对象仍只与该数据类型相关:它不包含与该类型的实例相关的任何信息。如果引用了一个对象,但不能确保该对象上是哪个类的实例,GetType()方法就很有用。
3:调用Type类的静态方法GetType()
Type是许多反射功能的入口。它实现许多方法和属性,注意:可用的属性都是只读的:可以使用Type去顶数据类型,但不能使用它修改该类型。
3.1.1Type的属性
由Type实现的属性可以分为下述三类。许多属性都可以获取包含与类相关的各种名称的字符串。
属性还可以进一步获取Type对象的引用,这些引用表示相关的类
许多布尔属性表示这种类型时一个类,还是一个枚举等。
这些特性包括 IsAbstract、IsArray、IsClass、IsEnum、IsInterface、IsPointer、IsPrimitive(一种预定义的基本元数据类型)、IsPublic、IsSealed、以及IsValueType
参考官方文档:https://docs.microsoft.com/zh-cn/dotnet/api/system.type?view=netframework-4.8
IsAbstract:获取一个值,通过该值指示 Type 是否为抽象或者是接口。当类型定义为abstract和Interface是,返回true,其他都返回false。
如果当前Type表示泛型类型或泛型方法的定义中的类型参数,则此属性始终返回false。
IsArray:是否数组、IsClass 否是一个类或委托,即。不是值类型或接口
IsInterface:是否是接口,即:不是类或值类型
IsByRef:是否是引用传递
IsPrimitive:是否是基元类型 就是clr自带的基本类型如int,double 等。
IsPointer:是否为在指针
IsPublic:是否声明公共类型
IsSealed:是否声明为封装类
IsValueType:是否为值类型
IsVisible:是否可由程序集之外的代码访问的值,true
是公共类型或公共嵌套类型从而使所有封闭类型都是公共类型 ,否则就是false
IsSerializable:是否可序列化 ,要在相应的元素设置 [Serializable] 特性 ,如果有这个就是true。
还有一些其他的,可以去上面给出的连接地址里查找。
也可以获取在其中定义该类型的程序集的引用
Type t=typeof(Vector); Assamebly containingAssembly=new Assembly(t);
3.1.2 方法
System.Type的大多数方法都用于获取对应数据类型的成员信息“构造函数、属性、方法和事件。
有很多方法,但是模式都差不多。
如:GetMethod()和GetMethods()。GetMethod()方法返回System.Reflection.MethodInfo对象的一个引用,其中包含一个方法的细节信息。GetMethods()返回这种引用的一个数组,区别是这个返回所有方法的细节信息。该方法包含特定的参数列表。这两个方法都有重载方法,重载方法有一个附加的参数,即BindingFlags枚举值,该值表示应返回那些值成员,例如:返回公有成员、实例成员和静态成员。
如果不带参数就是返回数据类型的所有公共方法。
图表中,从上到下分别为:构造函数、事件、字段、成员、方法、属性。
GetMember()和GetMembers()方法返回数据类型的任何成员或所有成员的详细信息,不管这些成员是构造函数、属性和方法。
1 private static StringBuilder _outputText = new StringBuilder(); 2 static void Main(string[] args) 3 { 4 Type t = typeof(double); 5 AnalyzeType(t); 6 Console.WriteLine($"Analysis of type{t.Name}"); 7 Console.WriteLine(_outputText.ToString()); 8 9 } 10 11 static void AnalyzeType(Type type) 12 { 13 TypeInfo typeInfo = type.GetTypeInfo(); 14 AddToOutput($"Type Name:{type.Name}"); 15 AddToOutput($"Full Name:{type.FullName}"); 16 AddToOutput($"Namespace :{type.Namespace}"); 17 Type tBase = type.BaseType; 18 if (tBase != null) 19 { 20 AddToOutput($"Base Type:{tBase.Name}"); 21 } 22 23 AddToOutput($"\n public number:"); 24 foreach (var memberInfo in type.GetMembers()) 25 { 26 27 28 #if DNXCORE 29 AddToOutput($"{memberInfo.DeclaringType} {memberInfo.Name}"); 30 #else 31 AddToOutput($"{memberInfo.DeclaringType} {memberInfo.MemberType} {memberInfo.Name}"); 32 #endif 33 } 34 } 35 36 static void AddToOutput(string text) 37 { 38 _outputText.Append("\n" + text); 39 } 40 }
3.1.3 Assembly
Assembly类在System.Reflection 命名空间下,它允许访问访问给定程序的元数据,
它也包含可以加载和执行程序集的方法。与Type类一样,Assembly类包含非常多的方法和属性。
如何使用Assembly实例?
1:将相应的程序集加载到正在运行的进程中。代码如下(二选一就可以了),也有其他重载版本。
//Load()方法参数是程序集的名称,运行库会在各个位置上搜索该程序集。 //这些位置包括本地目录和全局程序集缓存 Assembly assembly1=Assembly.Load("程序集的名称"); //而LoadFrom()方法的参数是程序集的完整路径名,它不会在其他位置搜索该程序集 Assembly assembly2=Assembly.LoadFrom(@"c:\My Projects\....");
2:上面步骤完成后就可以进行获取在程序集中定义的类型的详细信息
加载一个称心股集后,就可以使用它的各种属性进行查询。
//查询全名 string name=assembly1.FullName;
//获取包含所有类型的详细信息的数组
Type[] type=assembly1.GetTypes(); foreach(Type definedType in types) { //代码 }
2.1:获取自定义特性的详细信息
Attribut[] definedAttributes=Attribute.GetCustomAttributes(assembly1);
GetCustomAttributes()方法用于获取程序集的特性,有重载方法。
重载方法中可以根据需要我们传入的Type对象,返回我们需要的对象。
4:为反射使用动态语言扩展
4.1使用反射API调用成员
object result=calc.GetType().GetMethod("方法名").Invoke(calc,new objec[]{x,y});
4.2使用动态类型调用成员
dynaminc calc=GetCalculator(); double result=calc.Add(x,y);
5:dynamic类型
dynamic类型允许编写忽略编译期间的类型检查的的代码。编译器假定,给dynamic类型的对象定义的任何操作都是有效的。
dynamic dynamicPerson=new Person();
dynamicPerson.GetFullName("John","Smith");
与var关键字不同,定义为dynamic的对象可以在运行期间改变其类型。
使用var关键字时,对象类型的确会延迟。类型一旦确定,就不能改变,动态对象类型可以改变,而且可以改变多次。
Dynamic类型有两个限制,动态对象不支持扩展方案,匿名函数(lambda表达式)也不能用作动态方法调用的参数。因此Linq不能用于动态对象。
6 DynamicObject 和ExpandoObject
如果要创建自己的动态对象,有两种方法,
从DynamicObject中派生,或者使用ExpandoObject。使用DynamicObject需要的工作较多,
因为必须重写几个方法。ExpandoObject时一个可立即使用的密封类。
6.1:DynamicObject
假定要在运行期间构件这个对象,且系统事先不知道该对象有什么属性或该对象可能支持什么方法。此时就可以使用基于DynamicObject的对象。
private Dictionary<string, object> _dynamicData = new Dictionary<string, object>(); public override bool TryGetMember(GetMemberBinder binder, out object result) { bool success; result = null; if (_dynamicData.ContainsKey(binder.Name)) { result = _dynamicData[binder.Name]; success = true; } else { result = "Property Not Found"; success = false; } return success; } public override bool TrySetMember(SetMemberBinder binder, object value) { _dynamicData[binder.Name] = value; return true; } public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { dynamic method = _dynamicData[binder.Name]; result = method((DateTime) args[0]); return result != null; }
dynamic wroxDyn = new WroxDynamicObject(); wroxDyn.FirstName = "Bugs"; wroxDyn.LastName = "Bunny"; Console.WriteLine(wroxDyn.GetType()); Console.WriteLine($"{wroxDyn.FirstName} {wroxDyn.LastName}");
上面代码:没有调用重写的方法,是.Net Framework帮助完成了调用。
DynamicObject处理了绑定,我们只需要引用FirstName和LastName属性即可。
就好像它们一直存在一样。
上面是添加属性,下面介绍添加方法。
dynamic wroxDyn = new WroxDynamicObject(); Func<DateTime, string> GetTomorrow = today => today.AddDays(1).ToShortDateString(); wroxDyn.GetTomorrowDate=GetTomorrow; Console.WriteLine($"Tomorrow is {wroxDyn.GetTomorrowDate(DateTime.Now)}");
这段代码使用创建了委托GetTomorrow 。把委托设置为wroxDyn对象上的
GetTomorrowDate()方法。最后代用新方法,并传递今天的日期 19年10月5号 ,加一天输出10月六号。
动态功能在次发挥了作用。
6.2:ExpandoObject
ExpandoObject的工作方式类似创建WroxDynamicObject,区别是不必重写方法。
static void DoExpando() { dynamic expObj = new ExpandoObject(); expObj.FirstName = "Daffy"; expObj.LastName = "Duck"; Console.WriteLine($"{expObj.FirstName}{expObj.LastName }"); Func<DateTime, string> GetTomorrow = today => today.AddDays(1).ToShortDateString(); expObj.GetTomorrowDate = GetTomorrow; Console.WriteLine($"Tomorrow is{expObj.GetTomorrowDate(DateTime.Now)}"); expObj.Friends = new List<Person>(); expObj.Friends.Add(new Person() {FirstName = "Bob", LastName = "Jones"}); expObj.Friends.Add(new Person() {FirstName = "Robert", LastName = "Jones"}); expObj.Friends.Add(new Person() {FirstName = "Bobbt", LastName = "Jones"}); foreach (var friend in expObj.Friends) { Console.WriteLine($"{friend.FirstName} {friend.LastName}"); } }
初看起来,这似乎与使用dynamic类型没有区别。但其中有两个微妙的区别非常重要。
第一:不能仅创建dynamic类型的空对象,必须把dynamic类型赋予某个对象。
下面代码时无效的
dynamic dynObj; dynObj.FirstName="Joe";
但是这段代码用ExpandoObjec;可以使用
第二:因为dynamic类型必须赋予某个对象。所以,如果执行GetType调用,它就会报告赋予了dynamic类型的对象类型。所以如果把它赋予int,GetType就报告它是一个int.这个不适用于ExpandoObject或派生自DynamicObject的对象。
如果需要控制动态对象中属性的添加和访问,则使用该对象派生自DynamicObject是最佳选择。
使用DynamicObject,可以重写几个方法,准确地控制对象与运行库的交互方式。
而对于其他情况,就应使用dynamic类型或ExpandoObject。
总结:
Type和Assembly类是访问反射所提供的扩展功能的主要入口点
反射一个常用的方面:自定义特性。
通过使用ExpandoObject代替多个对象。代码量会显著减少。