C#图解教程 第二十四章 反射和特性
2021-05-13 20:29
标签:关联 eth bool 设置 caller 默认 数字 强制 包含 Note PS:理解有待加强 大多数程序都要处理数据,包括读、写、操作和显示数据。(图形也是一种数据的形式。)然而,对于某些程序来说,它们操作的数据不是数字、文本或图形,而是程序和程序类型本身的信息。 对象浏览器是显式元数据的程序的一个示例。它可以读取程序集,然后显示所包含的类型以及类型的所有特性和成员。 要使用反射,我们必须使用System.Reflection命名空间。 之前已经介绍了如何声明和使用C#中的类型。包括预定义类型(int、long和string等)、BCL中的类型(Console、IEnumerable等)以及用户自定义类型(MyClass、Mydel等)。每一种类型都有自己的成员和特性。 下图显示了一个运行的程序,它有两个MyClass对象和一个OtherClass对象。注意,尽管有两个MyClass的实例,只会有一个Type对象来表示它。 本节学习使用GetType方法和typeof运算符来获取Type对象。object类型包含了一个叫做GetType的方法,它返回对实例的Type对象的引用。由于每一个类型最终都是从object继承的,所以我们可以在任何类型对象上使用GetType方法来获取它的Type对象,如下所示: 下面的代码给出了一个使用typeof运算符的简单示例: 特性(attribute)是一种允许我们向程序的程序集增加元数据的语言结构。它是用于保存程序结构信息的某种特殊类型的类。 下图是使用特性中相关组件的概览,并且也演示了如下有关特性的要点。 根据惯例,特性名使用Pascal命名法并且以Attribute后缀结尾。当为目标应用特性时,我们可以不使用后缀。例如,对于SerializableAttribute和MyAttributeAttribute这两个特性,我们在把它们应用到结构时可以使用Serializable和MyAttribute短名称。 我们先不讲解如何创建特性,而是看看如何使用已定义的特性。这样,你会对它们的使用情况有个大致了解。 例如,下面的代码演示了两个类的开始部分。最初的几行代码演示了把一个叫做Serializable的特性应用到MyClass。注意,Serializable没有参数列表。第二个类的声明有一个叫做MyAttribute的特性,它有一个带有两个string参数的参数列表。 有关特性需要了解的重要事项如下: 在学习如何定义自己的特性之前,本小节会先介绍几个.NET预定义特性。 一个程序可能在其生命周期中经历多次发布,而且很可能延续多年。在程序生命周期的后半部分,程序员经常需要编写类似功能的新方法替换老方法。出于多种原因,你可能不想再使用那些调用过时的旧方法的老代码,而只想用新编写的代码调用新方法。 注意,即使PrintOut被标注为过期,Main方法还是调用了它。代码编译也运行得很好并且产生了如下的输出: Note Conditional特性允许我们包括或排斥特定方法的所有调用。为方法声明应用Conditional特性并把编译符作为参数来使用。 定义方法的CIL代码本身总是会包含在程序集中。只是调用代码会被插入或忽略。 Conditional特性的示例 调用者信息特性可以访问文件路径、代码行数、调用成员的名称等源代码信息。 下面的代码声明了一个名为MyTrace的方法,它在三个可选参数上使用了这三个调用者信息特性。如果调用方法时显式指定了这些参数,则会使用真正的参数值。但在下面所示的Main方法中调用时,没有显式提供这些值,因此系统将会提供源代码的文件路径、调用该方法的代码行数和调用该方法的成员名称。 我们在单步调试代码时,常常希望调试器不要进入某些方法。我们只想执行该方法,然后继续调试下一行。DebuggerStepThrough特性告诉调试器在执行目标代码时不要进入该方法调试。 下面这段随手编造的代码在一个访问器和一个方法上使用了该特性。你会发现,调试器调试这段代码时不会进入IncrementFields方法或X属性的set访问器。 .NET框架预定义了很多编译器和CLR能理解和解释的特性,下表列出了一些。在表中使用了不带Attribute后缀的短名称。例如,CLSCompliant的全名是CLSCompliantAttribute。 至此,我们演示了特性的简单使用,都是为方法应用单个特性。这部分内容将会讲述其他特性的使用方式。 我们可以为单个结构应用多个特性。 例如,下面的两个代码片段显示了应用多个特性的两种方式。两个片段的代码是等价的。 除了类,我们还可以将特性应用到诸如字段和属性等其他程序结构。以下的声明显示了字段上的特性以及方法上的多个特性: 我们还可以显式地标注特性,从而将它应用到特殊的目标结构。要使用显式目标,在特性片段的开始处放置目标类型,后面跟冒号。例如,如下的代码用特性装饰方法,并且还把特性应用到返回值上。 如下表所列,C#语言定义了10个标准的特性目标。大多数目标名可以自明(self-explanatory),而type覆盖了类、结构、委托、枚举和接口。 typevar目标名称指定使用泛型结构的类型参数。 我们还可以通过使用assembly和module目标名称来使用显式目标说明符把特性设置在程序集或模块级別。(程序集和模块在第21章中解释过。)一些有关程序集级别的特性的要点如下: 如下的代码行摘自AssemblyInfo.cs文件: 你或许已经注意到了,应用特性的语法和之前见过的其他语法很不相同。你可能会觉得特性是和结构完全不同的类型,其实不是,特性只是某个特殊类型的类。 总体来说,声明一个特性类和声明其他类一样。然而,有一些事项值得注意,如下所示。 例如,下面的代码显示了MyAttributeAttribute特性的声明的开始部分: 由于特性持有目标的信息,所有特性类的公共成员只能是: 特性和其他类一样,都有构造函数。每一个特性至少必须有一个公共构造函数。 例如,如果有如下的构造函数(名字没有包含后缀),编译器会产生一个错误消息: 当我们为目标应用特性时,其实是在指定应该使用哪个构造函数来创建特性的实例。列在特性应用中的参数其实就是构造函数的参数。 其他有关特性构造函数的要点如下。 和其他类一样,我们不能显式调用构造函数。特性的实例创建后,只有特性的消费者访问特性时才能调用构造函数。这一点与其他类的实例很不相同,这些实例都创建在使用对象创建表达式的位置。应用一个特性是一条声明语句,它不会决定什么时候构造特性类的对象。 和普通类的方法与构造方法相似,特性的构造方法同样可以使用位置参数和命名参数。如下代码显示了使用一个位置参数和两个命名参数来应用一个特性: 下面的代码演示了特性类的声明以及为MyClass类应用特性。注意,构造函数的声明只列出了一个形参,但我们可通过命名参数给构造函数3个实参。两个命名参数设置了字段Ver和Reviewer的值。 构造函教需要的任何位置参数都必须放在命名参数之前。 我们已经看到了可以为类应用特性。而特性本身就是类,有一个很重要的预定义特性可以用来应用到自定义特性上,那就是AttributeUsage特性。我们可以使用它来限制特性使用在某个目标类型上。 AttributeUsage有三个重要的公共属性,如下表所示。表中显示了属性名和属性的含义。对于后两个属性,还显示了它们的默认值。 强烈推荐编写自定义特性时参考如下实践。 如下代码演示了这些准则: 在本章开始处,我们已经看到了可以使用Type对象来获取类型信息。对于访问自定义特性来说,我们也可以这么做。Type的两个方法(IsDefined和GetCustomAttributes)在这里非常有用。 我们可以使用Type对象的IsDefined方法来检测某个特性是否应用到了某个类上。 GetCustomAttributes方法返回应用到结构的特性的数组。 下面的代码使用了前面的示例中相同的特性和类声明。但是,在这种情况下,它不检测特性是否应用到了类,而是获取应用到类的特性的数组,然后遍历它们,输出它们的成员的值。 C#图解教程 第二十四章 反射和特性 标签:关联 eth bool 设置 caller 默认 数字 强制 包含 原文地址:http://www.cnblogs.com/moonache/p/7532006.html
元数据和反射
Type 类
获取Type对象
什么是特性
应用特性
预定义的保留的特性
Obsolete(废弃)特性
Conditional特性
调用者信息特性
DebuggerStepThrough 特性
其他预定义特性
有关应用特性的更多内容
多个特性
其他类型的目标
全局特性
自定义特性
声明自定义特性
使用特性的构造函数
指定构造函数
使用构造函数
构造函数中的位置参数和命名参数
限制特性的使用
自定义特性的最佳实践
访问特性
使用IsDefined方法
使用GetCustomAttributes方法
反射和特性
元数据和反射
本章将介绍程序如何使用Type类来反射数据,以及程序员如何使用特性来给类型添加元数据。
Type 类
BCL声明了一个叫做Type的抽象类,它被设计用来包含类型的特性。使用这个类的对象能让我们获取程序使用的类型的信息。
由于Type是抽象类,因此它不能有实例。而是在运行时,CLR创建从Type(RuntimeType)派生的类的实例,Type包含了类型信息。当我们要访问这些实例时,CLR不会返回派生类的引用而是Type基类的引用。但是,为了简单起见,在本章剩余的篇幅中,我会把引用所指向的对象称为Type类型的对象(虽然从技术角度来说是一个BCL内部的派生类型的对象)。
需要了解的有关Type的重要事项如下:
我们可以从Type对象中获取需要了解的有关类型的几乎所有信息。下表列出了类中更有用的成员。 获取Type对象
Type t = myInstance.GetType();
下面的代码演示了如何声明一个基类以及从它派生的子类。Main方法创建了每一个类的实例并且把这些引用放在了一个叫做bca的数组中以方便使用。在外层的foreach循环中,代码得到了Type对象并且输出类的名字,然后获取类的字段并输出。下图演示了内存中的对象。 using System;
using System.Reflection;
class BaseClass
{
public int BaseField=0;
}
class DerivedClass:BaseClass
{
public int DerivedField=0;
}
class Program
{
static void Main()
{
var bc=new BaseClass();
var dc=new DerivedClass();
BaseClass[] bca=new BaseClass[]{bc,dc};
foreach(var v in bca)
{
Type t=v.GetType();
Console.WriteLine("Object type : {0}",t.Name);
FieldInfo[] fi=t.GetFields();
foreach(var f in fi)
{
Console.WriteLine(" Field : {0}",f.Name);
}
Console.WriteLine();
}
}
}
我们还可以使用typeof运算符来获取Type对象。只需要提供类型名作为操作数,它就会返回Type对象的引用,如下所示:Type t = typeof(DerivedClass);
↑ ↑
运算符 希望的Type对象的类型
using System;
using System.Reflection;
namespace SimpleReflection
{
class BaseClass
{
public int MyFieldBase;
}
class DerivedClass:BaseClass
{
public int MyFieldDerived;
}
class Program
{
static void Main()
{
Type tbc=typeof(DerivedClass);
Console.WriteLine("Result is {0}.",tbc.Name);
Console.WriteLine("It has the following fields:");
FieldInfo[] fi=tbc.GetFields();
foreach(var f in fi)
{
Console.WriteLine(" {0}",f.Name);
}
}
}
}
什么是特性
应用特性
特性的目的是告诉编译器把程序结构的某组元数据嵌入程序集。我们可以通过把特性应用到结构来实现。
[Serializable]
public class MyClass
{
…
}
[MyAttribute("Simple class","Version 3.57")]
public class MyOtherClass
{
…
}
预定义的保留的特性
Obsolete(废弃)特性
如果出现这种情况,你肯定希望稍后操作代码的团队成员或程序员也只使用新代码。要警告他们不要使用旧方法,可以使用Obsolete特性将程序结构标注为过期的,并且在代码编译时显式有用的警告消息。以下代码给出了一个使用的示例:class Program
{
//应用特性
[Obsolete("User method SuperPrintOut")]
static void PrintOut(string str)
{
Console.WriteLine(str);
}
static void Main(string[] args)
{
PrintOut("Start of Main");
}
}
不过,在编译的过程中,编译器产生了下面的CS0618警告消息来通知我们正在使用一个过期的结构:
另外一个Obsolete特性的重载接受了bool类型的第二个参数。这个参数指定目标是否应该被标记为错误而不仅仅是瞥告。以下代码指定了它需要被标记为错误: 标记为错误
↓
[Obsolete("User method SuperPrintOut",true)]
static void PrintOut(string str)
{
…
}
Conditional特性
Conditional特性类似于C语言的条件编译
例如,在如下的代码中,把Conditional特性应用到对一个叫做TraceMessage的方法的声明上。特性只有一个参数,在这里是字符串DoTrace。
[Conditional("DoTrace")]
static void TraceMessage(string str)
{
Console.WriteLine(str);
}
以下代码演示了一个使用Conditional特性的完整示例。
#define DoTrace
using System;
using System.Diagnostics;
namespace AttributeConditional
{
class Program
{
[Conditional("DoTrace")]
static void TraceMessage(string str)
{
Console.WriteLine(str);
}
static void Main()
{
TraceMessage("Start of Main");
Console.WriteLine("Doing work in Main.");
TraceMessage("End of Main");
}
}
}
如果注释掉第一行来取消DoTrace的定义,编译器就不再会插人两次对TraceMessage的调用代码。这次,如果我们运行程序,就会产生如下输出: 调用者信息特性
CallerFilePath
、CallerLineNumber
和CallerMemberName
using System;
using System.Runtime.CompilerServices;
public static class Program
{
public static void MyTrace(string message,
[CallerFilePath] string fileName="",
[CallerLineNumber] int lineNumber=0,
[CallerMemberName] string callingMember="")
{
Console.WriteLine("File: {0}",fileName);
Console.WriteLine("Line: {0}",lineNumber);
Console.WriteLine("Called From: {0}",callingMember);
Console.WriteLine("Message: {0}",message);
}
public static void Main()
{
MyTrace("Simple message");
}
}
DebuggerStepThrough 特性
在我自己的代码中,这是最常使用的特性。有些方法很小并且毫无疑问是正确的,在调试时对其反复单步调试只能徒增烦恼。但使用该特性时要十分小心,因为你并不想排除那些可能含有bug的代码。
关于DebuggerStepThrough要注意以下两点:
using System;
using System.Diagnostics;
class Program
{
int _x=1;
int X
{
get{return _x;}
[DebuggerStepThrough]
set
{
_x=_x*2;
_x+=value;
}
}
public int Y{get;set;}
public static void Main()
{
var p=new Program();
p.IncrementFields();
p.X=5;
Console.WriteLine("X = {0}, Y = {1}",p.X,p.Y);
}
[DebuggerStepThrough]
void IncrementFields()
{
X++;
Y++;
}
}
其他预定义特性
有关应用特性的更多内容
多个特性
[Serializable ] //多层结构
[MyAttribute("Simple class", "Version 3.57")]
[MyAttribute("Simple class", "Version 3.57"),Serializable] //逗号分隔
其他类型的目标
[MyAttribute("Holds a value", "Version 3.2")] //字段上的特性
public int MyField;
[Obsolete] //方法上的特性
[MyAttribute("Prints out a message.", "Version 3.6")]
public void Printout()
{
…
}
显式目标说明符
↓
[method: MyAttribute("Prints out a message.", "Version 3.6")]
[return: MyAttribute("This value represents …", "Version 2.3")]
public long ReturnSetting()
{
…
}
全局特性
[assembly: AssemblyTitle("SuperWidget")]
[assembly: AssemblyDescription("Implements the SuperWidget product.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("McArthur Widgets, Inc.")]
[assembly: AssemblyProduct("Super Widget Deluxe")]
[assembly: AssemblyCopyright("Copyright ? McArthur Widgets 2012")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture(")]
自定义特性
有关特性类的一些要点如下。
声明自定义特性
特性名 基类
↓ ↓
public sealed class MyAttributeAttribute : System.Attribute
{
…
}
使用特性的构造函数
public MyAttributeAttribute(string desc,string ver)
{
Description=desc;
VersionNumber=ver;
}
指定构造函数
例如,在下面的代码中,MyAttribute被应用到一个字段和一个方法上。对于字段,声明指定了使用单个字符串的构造函数。对于方法,声明指定了使用两个字符串的构造函数。[MyAttribute("Holds a value")] //使用一个字符串的构造函数
public int MyField;
[MyAttribute("version 1.3", "Sal Martin")] //使用两个字符串的构造函数
public void MyMethod()
{
…
}
[MyAttr]
class SomeClass …
[MyAttr()]
class OtherClass …
使用构造函数
下图比较了普通类构造函数的使用和特性的构造函数的使用。
构造函数中的位置参数和命名参数
位置参数 命名参数 命名参数
↓ ↓ ↓
[MyAttribute("An excellent class",Reviewer="Amy McArthur",Ver="0.7.15.33")]
public sealed class MyAttributeAttribute : System.Attribute
{
public string Description;
public string Ver;
public string Reviewer;
public MyAttributeAttribute(string desc) //一个形参
{
Description = desc;
}
}
//三个实参
[MyAttribute("An excellent class”, Reviewer="Amy McArthur", Ver="7.15.33")]
class MyClass
{
…
}
限制特性的使用
例如,如果我们希望自定义特性MyAttribute只能应用到方法上,那么可以以如下形式使用AttributeUsage: 只针对方法
↓
[AttributeUsage( AttributeTarget.Method )]
public sealed class MyAttributeAttribute : System.Attribute
{
…
}
AttributeUsage的构造函数
AttributeUsage的构造函数接受单个位置参数,该参数指定了特性允许的目标类型。它用这个参数来设置ValidOn属件,可接受目标类型是AttributeTarget枚举的成员。AttributeTarget枚举的完整成员列表如下表所示。
我们可以通过使用按位或运算符来组合使用类型。例如,在下面的代码中,被装饰的特性只能应用到方法和构造函数上。 目标
↓
[AttributeUsage( AttributeTarget.Method| AttributeTarget.Constructor )]
public sealed class MyAttributeAttribute : System.Attribute
当我们为特性声明应用AttributeUsage时,构造函数至少需要一个参数,参数包含的目标类型会保存在ValidOn中。我们还可以通过使用命名参数有选择性地设置Inherited和AllowMultiple属性。如果我们不设置,它们会保持如表24-4所示的默认值。
作为示例,下面一段代码指定了MyAttribute的如下方面。
[AttributeUsage( AttributeTarget.Class, //必需的,位置参数
Inherited = false, //可选的,命名参数
AllowMultiple = false )] //可选的,命名参数
public sealed class MyAttributeAttribute : System.Attribute
{
…
}
自定义特性的最佳实践
[AttributeUsage( AttributeTargets.Class )]
public sealed class ReviewCommentAttribute : System.Attribute
{
public string Description {get;set;}
public string VersionNumber {get;set;}
public string ReviewerID {get;set;}
public ReviewCommentAttribute(string desc, string ver)
{
Description = desc;
VersionNumber = ver;
}
}
访问特性
使用IsDefined方法
例如,以下的代码声明了一个有特性的类MyClass,并且作为自己特性的消费者在程序中访问声明和被应用的特性。代码的开始处是MyAttribute特性和应用特性的MyClass类的声明。这段代码做了下面的事情。
[AttributeUsage(AttributeTargets.Class)]
public sealed class ReviewCommentAttribute:System.Attribute
{…}
[ReviewComment("Check it out","2.4")]
class MyClass{}
class Program
{
static void Main()
{
var mc=new MyClass();
Type t=mc.GetType();
bool isDefined=
t.IsDefined(typeof(ReviewCommentAttribute),false);
if(isDefined)
Console.WriteLine("ReviewComment is applied to type {0}",t.Name);
}
}
使用GetCustomAttributes方法
object[] AttArr = t.GetCustomAttributes(false);
using System;
[AttributeUsage(AttributeTargets.Class)]
public sealed class MyAttributeAttribute:System.Attribute
{
public string Description {get;set;}
public string VersionNumber{get;set;}
public string ReviewerID {get;set;}
public MyAttributeAttribute(string desc,string ver)
{
Description=desc;
VersionNumber=ver;
}
}
[MyAttribute("Check it out","2.4")]
class MyClass
{
}
class Program
{
static void Main()
{
Type t=typeof(MyClass);
object[] AttArr=t.GetCustomAttributes(false);
foreach(Attribute a in AttArr)
{
var attr=a as MyAttributeAttribute;
if(null!=attr)
{
Console.WriteLine("Description :{0}",attr.Description);
Console.WriteLine("Version Number :{0}",attr.VersionNumber);
Console.WriteLine("Reviewer ID :{0}",attr.ReviewerID);
}
}
}
}