反射和特性

思考并回答以下问题:

  • 什么是元数据?meta这个词翻译成汉语是什么意思?保存在哪?.meta文件在Unity里是干嘛用的?
  • 反射是什么?有什么作用?
  • 使用反射需要哪个命名空间?
  • Type类型的对象是干嘛用的?有哪两种方法可以获取Type对象?
  • 可以使用特性来给类型添加元数据怎么理解?
  • 对于程序中用到的每一个类型,CLR都会创建一个包含这个类型信息的Type类型的对象。这个过程是自动的吗?
  • 不管创建的类型有多少个实例,只有一个Type对象会关联到所有这些实例吗?
  • 特性只是某个特殊类型的类。特殊在哪里?
  • 安全起见,通常建议你声明一个sealed的特性类。为什么sealed更安全?
  • 如果你不声明构造函数,编译器会为我们产生一个隐式、公共且无参的构造函数。编译器为什么这么做?

本章涵盖:

  • 元数据和反射
  • Type类
  • 获取Type对象
  • 什么是特性
  • 应用特性
  • 有关应用特性的更多内容
  • 自定义特性
  • 访问特性

元数据和反射

大多数程序都要处理数据,包括读、写、操作和显示数据。(图形也是一种数据的形式。)然而,对于某些程序来说,它们操作的数据不是数字、文本或图形,而是程序和程序类型本身的信息

  • 有关程序及其类型的数据被称为元数据(metadata),它们保存在程序的程序集中。
  • 程序在运行时,可以查看其他程序集或其本身的元数据。一个运行的程序查看本身的元数据或其他程序的元数据的行为叫做反射(reflection)。

对象浏览器是显示元数据的程序的一个示例。它可以读取程序集,然后显示所包含的类型以及类型的所有特性和成员。

本章将介绍程序如何使用Type类来反射数据,以及程序员如何使用特性来给类型添加元数据

说明

要使用反射,我们必须使用System.Reflection命名空间。

Type类

之前已经介绍了如何声明和使用C#中的类型。包括预定义类型(int、long和string等),BCL中的类型(Console、IEnumerable等)以及用户自定义类型(MyClass、MyDel等)。每一种类型都有自己的成员和特性。

BCL声明了一个叫做Type的抽象类,它被设计用来包含类型的特性。使用这个类的对象能让我们获取程序使用的类型的信息。

由于Type是抽象类,因此它不能有实例。而是在运行时,CLR创建从Type(RuntimeType)派生的类的实例,Type包含了类型信息。当我们要访问这些实例时,CLR不会返回派生类的引用而是Type基类的引用。但是,为了简单起见,在本章剩余的篇幅中,我会把引用所指向的对象称为Type类型的对象(虽然从技术角度来说是一个BCL内部的派生类型的对象)。

需要了解的有关Type的重要事项如下。

  • 对于程序中用到的每一个类型,CLR都会创建一个包含这个类型信息的Type类型的对象。
  • 程序中用到的每一个类型都会关联到独立的Type类的对象。
  • 不管创建的类型有多少个实例,只有一个Type对象会关联到所有这些实例。

图1显示了一个运行的程序,它有两个MyClass对象和一个OtherClass对象。注意,尽管有两个MyClass的实例,只会有一个Type对象来表示它。

图1 对于程序中使用的每一个类型CLR都会实例化Type类型的对象

我们可以从Type对象中获取需要了解的有关类型的几乎所有信息。表1列出了类中更有用的成员。

表1 System.Type类部分成员

成 员
成员类型
描 述
Name 属性 返回类型的名字
Namespace 属性 返回包含类型声明的命名空间
Assembly 属性 返回声明类型的程序集。如果类型是泛型的,返回定义这个类型的程序集
GetFields 方法 返回类型的字段列表
GetProperties 方法 返回类型的属性列表
GetMethods 方法 返回类型的方法列表

获取Type对象

本节学习使用GetType方法和typeof运算符来获取Type对象。object类型包含了一个叫做GetType的方法,它返回对实例的Type对象的引用。由于每一个类型最终都是从object继承的,所以我们可以在任何类型对象上使用GetType方法来获取它的Type对象,如下所示:

1
Type t = myInstance.GetType();

下面的代码演示了如何声明一个基类以及从它派生的子类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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();
}
Console.ReadKey();
}
}

这段代码产生了如下的输出:

1
2
3
4
5
6
Object type: BaseClass
Field: BaseField

Object type: DerivedClass
Field: DerivedField
Field: BaseField

Main方法创建了每一个类的实例并且把这些引用放在了一个叫做bca的数组中以方便使用。在外层的foreach循环中,代码得到了Type对象并且输出类的名字,然后获取类的字段并输出。图2演示了内存中的对象。

图2 基类和派生类对象以及它们的Type对象

我们还可以使用typeof运算符来获取Type对象。只需要提供类型名作为操作数,它就会返回Type对象的引用,如下所示:

1
2
Type t = typeof( DerivedClass );
// ↑ 运算符 ↑ 希望的Type对象的类型

下面的代码给出了一个使用typeof运算符的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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);

Console.ReadKey();
}
}
}

这段代码产生了如下的输出:

1
2
3
4
Result is DerivedClass.
It has the following fields:
MyFieldDerived
MyFieldBase

什么是特性

特性(attribute)是一种允许我们向程序的程序集增加元数据的语言结构。它是用于保存程序结构信息的某种特殊类型的类。

  • 将应用了特性的程序结构(program construct)叫做目标(target)。
  • 设计用来获取和使用元数据的程序(比如对象浏览器)叫做特性的消费者(consumer)。
  • .NET预定了很多特性,我们也可以声明自定义特性。

图3是使用特性中相关组件的概览,并且也演示了如下有关特性的要点。

  • 我们在源代码中将特性应用于程序结构。
  • 编译器获取源代码并且从特性产生元数据,然后把元数据放到程序集中。
  • 消费者程序可以获取特性的元数据以及程序中其他组件的元数据。注意,编译器同时生产和消费特性。

图3 创建和使用特性的相关组件

根据惯例,特性名使用Pascal命名法并且以Attribute后缀结尾。当为目标应用特性时,我们可以不使用后缀。例如,对于SerializableAttribute和MyAttributeAttribute这两个特性,我们在把它们应用到结构时可以使用Serializable和MyAttribute短名称。

应用特性

我们先不讲解如何创建特性,而是看看如何使用已定义的特性。这样,你会对它们的使用情况有个大致了解。

特性的目的是告诉编译器把程序结构的某组元数据嵌入程序集。我们可以通过把特性应用到结构来实现。

  • 在结构前放置特性片段来应用特性。
  • 特性片段被方括号包围,其中是特性名和特性的参数列表。

例如,下面的代码演示了两个类的开始部分。最初的几行代码演示了把一个叫做Serializable的特性应用到MyClass。注意,Serializable没有参数列表。第二个类的声明有一个叫做MyAttribute的特性,它有一个带有两个string参数的参数列表。

1
2


有关特性需要了解的重要事项如下:

  • 大多数特性只针对直接跟随在一个或多个特性片段后的结构;
  • 应用了特性的结构称为被特性装饰(decorated或adorned,两者都应用得很普遍)。

预定义特性

特 性
意 义
Serializable 声明结构可以被序列化
NonSerialized 声明结构不能被序列化
DllImport 声明是非托管代码实现的
AttributeUsage 声明特性能应用到什么类型的程序结构。将这个特性应用到特性声明上

自定义特性

你或许已经注意到了,应用特性的语法和之前见过的其他语法很不相同。你可能会觉得特性是和结构完全不同的类型,其实不是,特性只是某个特殊类型的类

有关特性类的一些要点如下。

  • 用户自定义的特性类叫做自定义特性。
  • 所有特性类都派生自System.Attribute。

声明自定义特性

总体来说,声明一个特性类和声明其他类一样。然而,有一些事项值得注意,如下所示。

  • 要声明一个自定义特性,需要做如下工作。
    • 声明一个派生自System.Attribute的类。
    • 给它起一个以后缀Attribute结尾的名字。
  • 安全起见,通常建议你声明一个sealed的特性类。

例如,下面的代码显示了MyAttributeAttribute特性的声明的开始部分:

1
2
3
4
5
6
7
// MyAttributeAttribute是特性名
// MyAttributeAttribute后面的Attribute是后缀
// System.Attribute是基类
public sealed class MyAttributeAttribute : System.Attribute
{
...
}

由于特性持有目标的信息,所有特性类的公共成员只能是:

  • 字段
  • 属性
  • 构造函数

使用特性的构造函数

特性和其他类一样,都有构造函数。每一个特性至少必须有一个公共构造函数。

  • 和其他类一样,如果你不声明构造函数,编译器会为我们产生一个隐式、公共且无参的构造函数。
  • 特性的构造函数和其他构造函数一样,可以被重载。
  • 声明构造函数时必须使用类全名,包括后缀。我们只可以在应用特性时使用短名称。
    例如,如果有如下的构造函数(名字没有包含后缀),编译器会产生一个错误消息:
    1
    2
    3
    4
    5
    6
    7
    //                 后缀
    // ↓
    public MyAttributeAttribute (string desc, string ver)
    {
    Description = desc;
    VersionNumber = ver;
    }

指定构造函数

当我们为目标应用特性时,其实是在指定应该使用哪个构造函数来创建特性的实例。列在特性应用中的参数其实就是构造函数的参数。

例如,在下面的代码中,MyAttribute被应用到一个字段和一个方法上。对于字段,声明指定了使用单个字符串的构造函数。对于方法,声明指定了使用两个字符串的构造函数。

1
2
3
4
5
6
7
8
[MyAttribute("Holds a value")]  // 使用一个字符串的构造函数
public int MyField;

[MyAttribute("Version 1.3", "Sal Martin")] // 使用两个字符串的构造函数
public void MyMethod()
{
...
}

其他有关特性构造函数的要点如下。

  • 在应用特性时,构造函数的实参必须是在编译期能确定值的常量表达式。
  • 如果应用的特性构造函数没有参数,可以省略圆括号。例如,如下代码的两个类都使用MyAttr特性的无参构造函数。两种形式的意义是相同的。
    1
    2
    3
    4
    5
    [MyAttr]
    class SomeClass ...

    [MyAttr]
    class OtherClass ...

使用构造函数

和其他类一样,我们不能显式调用构造函数。特性的实例创建后,只有特性的消费者访问特性时才能调用构造函数。这一点与其他类的实例很不相同,这些实例都创建在使用对象创建表达式的位置。应用一个特性是一条声明语句,它不会决定什么时候构造特性类的对象。

图4比较了普通类构造函数的使用和特性的构造函数的使用。

  • 命令语句的实际意义是:“在这里创建新的类。”
  • 声明语句的意义是:“这个特性和这个目标相关联,如果需要构造特性,使用这个构造函数。”

构造函数中的位置参数和命名参数

和普通类的方法与构造方法相似,特性的构造方法同样可以使用位置参数和命名参数。如下代码显示了使用一个位置参数和两个命名参数来应用一个特性:

下面的代码演示了特性类的声明以及为MyClass类应用特性。注意,构造函数的声明只列出了一个形参,但我们可通过命名参数给构造函数3个实参。两个命名参数设置了字段Ver和Reviewer的值。

说明

构造函数需要的任何位置参数都必须放在命名参数之前。

限制特性的使用

我们已经看到了可以为类应用特性。而特性本身就是类,有一个很重要的预定义特性可以用来应用到自定义特性上,那就是AttributeUsage特性。我们可以使用它来限制特性使用在某个目标类型上。

例如,如果我们希望自定义特性MyAttribute只能应用到方法上,那么可以以如下形式使用AttributeUsage:

1
2
3
4
5
6
// 只针对方法
[AttributeUsage(AttributeTarget.Method)]
public sealed class MyAttributeAttribute : System.Attribute
{
...
}

AttributeUsage有三个重要的公共属性,如表4所示。表中显示了属性名和属性的含义。对于后两个属性,还显示了它们的默认值。

表4 AttributeUsage的公共属性

名 字
意 义
默认值
ValidOn 保存特性能应用到的目标类型的列表。构造函数的第一个参数必须是AttributeTarget类型的枚举值 true
Inherited 一个布尔值,它指示特性是否会被装饰类型的派生类所继承
AllowMutiple 一个指示目标是否被应用多个特性的实例的布尔值 false

AttributeUsage的构造函数

AttributeUsage的构造函数接受单个位置参数,该参数指定了特性允许的目标类型。它用这个参数来设置ValidOn属性,可接受目标类型是AttributeTarget枚举的成员。Attributerarget枚举的完整成员列表如表5所示。

我们可以通过使用按位或运算符来组合使用类型。例如,在下面的代码中,被装饰的特性只能应用到方法和构造函数上。

1
2
3
4
5
6
// 目标
[AttributeUsage( AttributeTarget.Method | AttributeTarget.Constructor )
public sealed class MyAttributeAttribute : System.Attribute
{
...
}

表5 AttributeTarget枚举的成员

1
2
3
4
All Delegate GenericParameter Parameter
Assembly Enum Interface Property
Class Event Method ReturnValue
Constructor Field Module Struct

当我们为特性声明应用AttributeUsage时,构造函数至少需要一个参数,参数包含的目标类型会保存在ValidOn中。我们还可以通过使用命名参数有选择性地设置Inherited和AllowMultiple属性。如果我们不设置,它们会保持如表4所示的默认值。作为示例,下面一段代码指定了MyAttribute的如下方面。

  • MyAttribute能且只能应用到类上。
  • MyAttribute不会被应用它的派生类所继承。
  • 不能有MyAttribute的多个实例应用到同一个目标上。
1
2
3
4
5
6
7
8
[AttributeUsage( AttributeTarget.Class, // 必需的,位置参数  
Inherited = false, // 可选的,命名参数
AllowMultiple = false )] // 可选的,命名参数

public sealed class MyAttributeAttribute : System.Attribute
{
...
}

自定义特性的最佳实践

强烈推荐编写自定义特性时参考如下实践。

  • 特性类应该表示目标结构的一些状态。
  • 如果特性需要某些字段,可以通过包含具有位置参数的构造函数来收集数据,可选字段可以采用命名参数按需初始化。
  • 除了属性之外,不要实现公共方法或其他函数成员。
  • 为了更安全,把特性类声明为sealed。
  • 在特性声明中使用AttributeUsage来显式指定特性目标组。

如下代码演示了这些准则:

1
2
3
4
5
6
7
8
9
10
11
12
13
[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;
}
}

访问特性

可以使用Type对象来获取类型信息。对于访问自定义特性来说,也可以这么做。Type的两个方法(IsDefined和GetCustomAttributes)在这里非常有用。

使用IsDefined方法

我们可以使用Type对象的IsDefined方法来检测某个特性是否应用到了某个类上。

例如,以下的代码声明了一个有特性的类MyClass,并且作为自己特性的消费者在程序中访问声明和被应用的特性。代码的开始处是MyAttribute特性和应用特性的MyClass类的声明。这段代码做了下面的事情。

  • 首先,Main创建了类的一个对象。然后通过使用从object基类继承的GetType方法获取了Type对象的一个引用。
  • 有了Type对象的引用,就可以调用IsDefined方法来判断ReviewComment特性是否应用到了这个类。
    • 第一个参数接受需要检查的特性的Type对象。
    • 第二个参数是bool类型的,它指示是否搜索MyClass的继承树来查找这个特性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[AttributeUsage(AttributeTargets.Class)]
public sealed class ReviewCommentAttribute : System.Attribute
{...}

[ReviewComment("Check it out", "2.4")]
class MyClass{ }

class Program
{
static void Main()
{
MyClass mc = new MyClass(); // 创建类实例
Type t = mc.GetType(); // 从实例中获取类型对象

bool isDefined = t.IsDefiner(typeof(ReviewCommentAttribute), false); // 创建特性的类型

if(isDefined)
Console.WriteLine("ReviewComment is applied to type {0}", t.Name);
}
}

这段代码产生了如下的输出:

1
ReviewComment is applied to type MyClass

使用GetCustomAttributes方法

GetCustomAttributes方法返回应用到结构的特性的数组。

  • 实际返回的对象是object的数组,因此我们必须将它强制转换为相应的特性类型。
  • 布尔参数指定是否搜索继承树来查找特性。

    1
    object[] AttArr = t.GetCustomAttributes (false);
  • 调用GetCustomAttributes方法后,每一个与目标相关联的特性的实例就会被创建。

下面的代码使用了前面的示例中相同的特性和类声明。但是,在这种情况下,它不检测特性是否应用到了类,而是获取应用到类的特性的数组,然后遍历它们,输出它们的成员的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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)
{
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);
}
}
}
}

这段代码产生了如下的输出:

1
2
3
Description : Check it out
Version Number : 2.4
Reviewer ID :

0%