元数据和反射
- 元数据:有关程序及其类型的数据,保存在程序的程序集中
- 反射:一个运行的程序查看本身的元数据及类型的所有特性和成员
Type类
Type是个抽象类,用来包含类型的特性,以及获取程序使用类型的信息
System.Type类部分成员
成员 | 成员类型 | 描述 |
---|---|---|
Name | 属性 | 返回类型的名称 |
Namespace | 属性 | 返回包含类型声明的命名空间 |
Assembly | 属性 | 返回声明类型的程序集。如果类型是泛型的,返回定义这个类型的程序集 |
GetFields | 方法 | 返回类型的字段列表 |
GetProperties | 方法 | 返回类型的属性列表 |
GetMethods | 方法 | 返回类型的方法列表 |
获取Tyep对象
通过GetType
和typeof
运算符来获取对象
Type t = myInstance.GetType();
示例
class BaseClass
{
public int BaseField = 0;
}
class DerivedClass:BaseClass
{
public int DerivedField = 0;
}
class Program
{
static void Main(string[] args)
{
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(); //对字段元数据的访问权限 引入命名空间:using System.Reflection;
foreach(var f in fi)
{
Console.WriteLine(" field: {0}", f.Name);
}
}
Console.ReadKey();
}
}
结果
Object type: BaseClass
field: BaseField
Object type: DerivedClass
field: DerivedField
field: BaseField
特性
特性是一种允许我们向程序的程序集增加元数据的语言结构
用于保存程序结构信息的某种特殊类型的类
- 将应用了特性的程序结构叫做目标。
- 设计用来获取和使用元数据的程序(比如对象浏览器)叫做特性的消费者。
过程
- 我们在源代码中将特性应用于程序结构。
- 编译器获取源代码并且从特性产生元数据,然后把元数据放到程序集中。
- 消费者程序可以获取特性的元数据以及程序中其他组件的元数据。注意,编译器同时生产和消费特性。
根据惯例,特性名使用Pascal命名法并且以Attribute
后缀结尾。当为目标应用特性时,我们可以不使用后缀。例如,对于SerializableAttribute
和MyAttributeAttribute
这两个特性,我们在把它们应用到结构时可以使用Serializable
和MyAttribute
短名称。
应用特性
- 在结构前放置特性片段来应用特性。
- 特性片段被方括号包围,其中是特性名和特性的参数列表。
例
例如,下面的代码演示了两个类的开始部分。最初的几行代码演示了把一个叫做Serializable
的特性应用到MyClass。注意,Serializable
没有参数列表。第二个类的声明有一个叫做MyAttribute
的特性,它有一个带有两个string参数的参数列表。
[ Serializable ] //特性
public class MyClass
{
....
}
[MyAtttribute("Simple class", "Version 3.57")] //带参数的特性
public class MyOtherClass
{
...
}
注意
- 大多数特性只针对直接跟随在一个或多个特性片段后的结构;
- 应用了特性的结构称为被特性装饰(decorated或adorned,两者都应用得很普遍)。
预定义的保留特性
Obsolete特性
用于将程序结构(如调用的方法)标注为过期,并且在代码编译时显示有用的警告消息
例
[Obsolete("请使用SuperPrintOut方法")] //将特性应用到方法上
static void PrintOut(string str)
{
Console.WriteLine(str);
}
static void Main(string[] args)
{
PrintOut("Main方法"); //调用Obsolete方法
}
这样调用Obsolete
方法时,编辑器会给出警告,但不会影响运行
另外一个Obsolete
特性的重载接受了bool类型的第二个参数。这个参数指定目标是否应该被标记为错误而不仅仅是警告。以下代码指定了它需要被标记为错误:
[Obsolete("请使用SuperPrintOut方法", true)] //将特性应用到方法上
static void PrintOut(string str)
{
Console.WriteLine(str);
}
static void Main(string[] args)
{
PrintOut("Main方法"); //调用Obsolete方法
}
这样程序就会报错
Conditional特性
Conditional特性允许我们包括或排斥特定方法的所有调用。为方法声明应用Conditional特性并把编译符作为参数来使用。
命名空间:using System.Diagnostics;
注意
- 如果定义了编译符号,那么编译器会包含所有调用这个方法的代码,这和普通方法没有什么区别。
- 如果没有定义编译符号,那么编译器会忽略代码中这个方法的所有调用。
例1
在如下的代码中,把Conditional
特性应用到对一个叫做TraceMessage
的方法的声明上特性只有一个参数,在这里是字符串DoTrace
。
当编译器编译这段代码时,它会检查是否有一个编译符号被定义为DoTrace
。
如果DoTrace
被定义,编译器就会像往常一样包含所有对TraceHessage
方法的调用。
如果没有DoTrace
这样的编译符号被定义,编译器就不会输出任何对TraceMessage
的调用代码。
[Conditional("DoTrace")]
static void TraceMessage(string str)
{
Console.WriteLine(str);
}
static void Main(string[] args)
{
TraceMessage("Hello");
Console.ReadKey();
}
未打印,由于DoTrace未定义,所以不会调用TraceMessage方法
例2
#define DoTrace
[Conditional("DoTrace")]
static void TraceMessage(string str)
{
Console.WriteLine(str);
}
static void Main(string[] args)
{
TraceMessage("Hello");
Console.ReadKey();
}
结果
Hello
分析
1.Main方法包含了两个对TraceMessage方法的调用。
2.TraceMessage方法的声明被用Conditional特性装饰,它带有DoTrace编译符号作为参数。因此,如果DoTrace被定义,那么编译器就会包含所有对TraceNessage的调用代码。
3.由于代码的第一行定义了叫做DoTrace的编译符,编译器会包含两个对TraceMessage的调用。
调用者信息特性
调用者信息特性可以访问文件路径、代码行数、调用成员的名称等源代码信息。
命名空间:using System.Runtime.CompilerServices;
规则
- 这三个特性名称为
CallerFilePath
、CallerLineNumber
和CallerMemberName
。 - 这些特性只能用于方法中的可选参数。
例
下面的代码声明了一个名为MyTrace的方法,它在三个可选参数上使用了这三个调用者信息特性。如果调用方法时显式指定了这些参数,则会使用真正的参数值。但在下面所示的Main方法中调用时,没有显式提供这些值,因此系统将会提供源代码的文件路径、调用该方法的代码行数和调用该方法的成员名称。
public static void MyTrace(string message, [CallerFilePath] string fileName = "",
[CallerLineNumber] int lineNumer = 0,
[CallerMemberName] string callingMember = "")
{
Console.WriteLine("文件路径:{0}", fileName);
Console.WriteLine("行数:{0}", lineNumer);
Console.WriteLine("调用函数:{0}", callingMember);
Console.WriteLine("message:{0}", message);
}
static void Main(string[] args)
{
MyTrace("Simple Message");
Console.ReadKey();
}
结果
文件路径:X:\XXX\XXX\XXXX\Program.cs
行数:42
调用函数:Main
message:Simple Message
DebuggerStepThrough特性
DebuggerstepThrough特性告诉调试器在执行目标代码时不要进人该方法调试。
注意
- 该特性位于
System.Diagnostics
命名空间; - 该特性可用于类、结构、构造函数、方法或访问器。
示例
下列代码展示,调式器(断点)不会进入IncrementFields方或X属性的set访问器
int _x = 1;
int X
{
get { return _x; }
[DebuggerStepThrough] //编辑器调试不进入set访问器
set
{
_x = _x * 2;
_x += value;
}
}
public int Y { get; set; }
[DebuggerStepThrough] //编辑器调试不进入该方法
void IncrementFields()
{
X++;
Y++;
}
static void Main(string[] args)
{
Program p = new Program();
p.IncrementFields();
p.X = 5;
Console.WriteLine("X = {0}, Y = {1}", p.X, p.Y);
Console.ReadKey();
}
其它预定义特性
特性 | 意义 |
---|---|
CLSCompliant | 声明可公开的成员应该被编译器检测是否符合CLS。兼容的程序集可以被任何.NET兼容的语言使用 |
Serializable | 声明结构可以被序列化 |
NonSerialized | 声明结构不能被序列化 |
DLLImport | 声明是非托管代码实现的 |
WebMethod | 声明方法应该被作为XML Web服务的一部分暴露 |
AttributeUsage | 声明特性能应用到什么类型的程序结构。将这个特性应用到特性声明上 |
特性的其它内容
多个特性
- 多个特性可以使用下面列出的任何一种格式:
- 独立的特性片段相互叠在一起;
- 单个特性片段,特性之间使用逗号分隔。
- 我们可以以任何次序列出特性。
[Serializable] //多层结构
[MyAttribute("Simple class", "Version 3.57")]
[MyAttribute("Simple class", "Version 3.57"), Serializable] //逗号分隔
其它类型目标
将特性应用到诸如字段和属性等其他程序结构。
[MyAttribute("Hold 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 represets...", "Version 3.6")]
public long ReturnSetting(){
...
}
特性目标
event | field |
method | param |
property | return |
type | typevar |
assembly | module |
全局特性
通过使用assembly和module目标名称来使用显式目标说明符把特性设置在程序集或模块级别
要点
- 程序级级别的特性必须放置在任何命名空间之外,并且通常放置在AssemblyInfo.cs文件中;
- AssemblyInfo.cs文件通常包含有关公司、产品以及版权信息的元数据。
例
如下的代码行摘自AssemblyInfo.cs文件:
[assembly: AssemblyTitle("Superwidget")]
[assembly: AssemblyDescription("Implements the Superwidget product.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("McArthur widgets,Inc.")]
[assembly: AssemblyProduct( "Super widget Deluxe")]
[assembly: AssemblyCopyright("copyright 0 McArthur widgets 2012")]
[assembly: AssemblyTrademark("")]
[assembly: Assemblyculture("")]
自定义特性
- 用户自定义的特性类叫做自定义特性。
- 所有特性类都派生自System.Attribute。
声明自定义特性
规则
- 要声明一个自定义特性,需要做如下工作。
- 声明一个派生自System.Attribute的类。
- 给它起一个以后缀Attribute结尾的名字。
- 安全起见,通常建议你声明一个sealed的特性类。
例
//MyAttributeAttribute特性名 Attribute后缀
//System.Attribute 基类
public sealed class MyAttributeAttribute: System.Attribute
{
...
}
特性类的公共成员只能是:字段、属性、构造函数
使用特性的构造函数
注意
- 和其他类一样,如果你不声明构造函数,编译器会为我们产生一个隐式、公共且无参的构造函数。
- 特性的构造函数和其他构造函数一样,可以被重载。
- 声明构造函数时必须使用类全名,包括后缀。我们只可以在应用特性时使用短名称。
指定构造函数
当我们为目标应用特性时,其实是在指定应该使用哪个构造函数来创建特性的实例。
列在特性应用中的参数其实就是构造函数的参数。
例
例如,在下面的代码中,MyAttribute被应用到一个字段和一个方法上。对于字段,声明指定了使用单个字符串的构造函数。对于方法,声明指定了使用两个字符串的构造函数。
[MyAttribute("Holds a value")] //使用一个字符串的构造函数
public int MyField;
[MyAttribute("Version 1.3", "Sal Martin")] //使用两个字符串的构造函数
public void MyMethod() {
...
}
要点
- 在应用特性时,构造函数的实参必须是在编译期能确定值的常量表达式。
- 如果应用的特性构造函数没有参数,可以省略圆括号。例如,如下代码的两个类都使用MyAttr特性的无参构造函数。两种形式的意义是相同的。
[MyAttr]
class SomeClass...
[MyAttr()]
class OtherClass...
使用构造函数
//命令语句
MyClass mc = new MyClass("Hello", 15);
//特性声明语句
[MyAttribute("Hello")]
构造函数中的位置参数和命名参数
和普通类的方法与构造方法相似,特性的构造方法同样可以使用位置参数和命名参数。
//位置参数部分是""
//命名参数部分是=
[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")]
static void Main(string[] args)
{
...
}
构造函数需要的任何位置参数都必须放在命名参数之前。
限制特性的使用
特性本身是类,用AttributeUsage特性来限制某个目标类型上
例
例如,如果我们希望自定义特性MyAttribute
只能应用到方法上,那么可以以如下形式使用AttributeUsage
:
[AttributeUsage(AttributeTarget.Method)]
public seald class MyAttributeAttribute: System.Attibute
{...}
AttributeUsage的公共属性
名字 | 意义 | 默认值 |
---|---|---|
ValidOn | 保存特性能应用到的目标类型的列表。构造函数的第一个参数必须是AttributeTarget类型的枚举值 | |
Inherited | 一个布尔值,它指示特性是否会被装饰类型的派生类所继承 | true |
AllowMutiple | 一个指示目标是否被应用多个特性的实例的布尔值 | false |
AttributeUsage的构造函数
AttributeUsage
的构造函数接受单个位置参数,该参数指定了特性允许的目标类型。
它用这个参数来设置ValidOn
属性,可接受目标类型是AttributeTarget
枚举的成员。
例
[AttributeUsage(AttributeTarget.Method | AttributeTarget.Constructor)]
public seald class MyAttributeAttribute: System.Attibute
{...}
AttributeTarget枚举成员
All | Assembly | Class | Constructor |
Delegate | Enum | Event | Field |
GenericParameter | Interface | Method | Module |
Parameter | Property | ReturnValue | Struct |
当我们为特性声明应用AttributeUsage时,构造函数至少需要一个参数,参数包含的目标类型会保存在ValidOn中。我们还可以通过使用命名参数有选择性地设置Inherited和AllowNultiple属性。如果我们不设置,它们会保持默认值。
例
MyAttribute能且只能应用到类上。
MyAttribute不会被应用它的派生类所继承。
不能有MyAttribute的多个实例应用到同一个目标上。
[AttributeUsage(AttributeTarget.class, //必需的,位置参数
Inherited = false, //可选的,命名参数
AllowMultiple = false)] //可选的,命名参数
public sealed class MyAttributeAttribute : System.Attribute{
...
}
自定义特性实践
- 特性类应该表示目标结构的一些状态。
- 如果特性需要某些字段,可以通过包含具有位置参数的构造函数来收集数据,可选字段可以采用命名参数按需初始化。
- 除了属性之外,不要实现公共方法或其他函数成员。
- 为了更安全,把特性类声明为sealed。
- 在特性声明中使用AttributeUsage来显式指定特性目标组。
例
[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方法
使用Type对象的IsDefined方法来检测某个特性是否应用到了某个类上。
例
例如,以下的代码声明了一个有特性的类MyClass
,并且作为自己特性的消费者在程序中访问声明和被应用的特性。代码的开始处是MyAttribute
特性和应用特性的MyClass
类的声明。这段代码做了下面的事情。
- (1)首先,
Main
创建了类的一个对象。然后通过使用从object
基类继承的GetType
方法获取了Type
对象的一个引用。 - (2)有了
Type
对象的引用,就可以调用IsDefined
方法来判断ReviewComment
特性是否应用到了这个类。- 第一个参数接受需要检查的特性的Type对象。
- 第二个参数是
bool
类型的,它指示是否搜索MyClass的继承树来查找这个特性。
[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;
}
}
[ReviewComment("Check it Out", "2.4")]
class MyClass
{
}
class Program
{
static void Main(string[] args)
{
MyClass 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);
Console.ReadKey();
}
}
结果
ReviewComment is applied to type: MyClass
使用GetCustomAttributes方法
GetCustomAttributes方法返回应用到结构的特性的数组。
- 实际返回的对象是
object
的数组,因此我们必须将它强制转换为相应的特性类型。 - 布尔参数指定是否搜索继承树来查找特性。
Object[] AttAr = t.GetCustomAttributes(false);
- 调用
GetCustomAttributes
方法后,每一个与目标相关联的特性的实例就会被创建。
例
下面的代码使用了前面的示例中相同的特性和类声明。但是,在这种情况下,它不检测特性是否应用到了类,而是获取应用到类的特性的数组,然后遍历它们,输出它们的成员的值。
[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(string[] args)
{
Type t = typeof(MyClass);
object[] AttAr = t.GetCustomAttributes(false);
foreach(Attribute a in AttAr)
{
MyAttributeAttribute 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);
}
}
Console.ReadKey();
}
}
结果
Description:Check it Out
Version Number:2.4
Reviewer ID: