反射和特性
- 元数据和反射
- 有些程序及其类型的数据称为元数据,它们保存在程序的程序集中
- 程序在运行时,可以查看其他程序集或其本身的元数据,这个行为叫做反射
- 对象浏览器是显示元数据的程序的一个实例
- 要使用反射,必须使用
System.Reflection
命名空间
- Type类
- Type类概述
- BCL中提供的抽象类,被设计用来包含类型的特性
- 在运行时,CLR创建Type(RuntimeType)派生的类的实例,包含了类型信息
- 当我们访问这些实例时,CLR不会返回派生类的引用而是基类Type的引用
- 简单起见,可以把引用指向的对象成为Type类型的对象,但实际上是BCL内部派生类型的对象
- 有关Type的重要事项
- 对于程序中用到的每一个类型,CLR都会创建一个包含这个类型信息的Type类型的对象
- 程序中用到的每一个类型,都会关联到独立的Type类的对象
- 不管创建的类型有多少个实例,只有一个Type对象会关联到所有这些实例
- 貌似类似于static
- 可以从Type对象中获取类型的信息
- Name:属性,返回类型的名字
- Namespace:属性,返回类型的生命的命名空间
- Assembly:属性,返回声明类型的程序集,如果类型是泛型的,则返回定义这个类型的程序集
- GetFields:方法,返回类型的字段列表
- GetProperties:方法,返回类型的属性列表
- Getthonds:方法,返回类型的方法列表
- Type类概述
- 获取Type对象
GetType
方法- object类型包含了一个叫做
GetType
的方法,返回对实例的Type
对象的引用 - 通过对象去调用方法
- object类型包含了一个叫做
typeof
运算符- 在括号内传入类型
- 特性
- 特性是一种允许我们向程序集增加元数据的语言结构,它是用于保存程序结构信息的某种特殊类型的类
- 将应用了特性的程序结构叫做目标
- 设计用来获取和使用元数据的程序的特性叫做消费者
- .NET预定义了很多特性,也可以声明自定义特性
- 特性的要点
- 在源代码中将特性应用于程序结构
- 编译器获取源代码并且从特性产生元数据,然后把元数据放到程序集中
- 消费者程序可以获取特性的元数据以及程序中其他组件的元数据
- 编译器同时产生和消费特性
- 特性名以Attribute后缀结尾,使用目标时省略后缀
- 特性是一种允许我们向程序集增加元数据的语言结构,它是用于保存程序结构信息的某种特殊类型的类
- 应用特性
- 这里所说的结构是是指程序结构,包含结构体、类、方法等,不是单单指结构体struct
- 特性的目的是告诉编译器把程序结构的某组元数据嵌入程序集,可以通过把特性应用到结构来实现
- 在结构前防止特性片段来应用特性
- 特性片段被方括号包围,其中是特性名和特性的参数列表
- 大多数特性只针对直接跟随着一个或多个特性后的结构
- 引用了特性的结构成为被特性装饰
- 预定义的保留的特性
Obsolete
特性- 可以将结构标注为过期的,在使用时编译器会给出警告,但仍然可以调用
- 两个可选参数
message
:string?
,表示警告的提示信息error
:bool
类型,如果为true,则会标记错误而不是警告
Conditional
特性- 需要使用
System.Diagnostics
命名空间 - 允许包括或排斥特定方法的所有调用,为方法声明应用Conditional特性并把编译符号(宏定义的内容)作为参数来使用
- 如果定义了编译符号,和普通方法没有区别
- 如果没有定义编译符号,那么编译器会忽略代码中这个方法的所有调用
-
#define DEBUG // ... void Test() { ... } // ... #if DEBUG Test(); #endif
-
#define DEBUG // ... [Conditional("DEBUG")] void Test() { ... } // ... Test();
- 定义方法的CIL代码本身总会包含在程序集中,只是调用代码会被插入或忽略
- 编译这段代码时,会检查是否有编译符号的定义
- 如果被定义,编译器会像往常一样包含所有对方法的调用
- 如果没有定义,编译器就不会输出任何对方法的调用代码
- 编译这段代码时,会检查是否有编译符号的定义
- 需要使用
- 调用者信息特性
- 可以访问文件路径、代码行数、调用成员的名称等源代码信息,需要使用
System.Runtime.CompilerServices
命名空间 - 三个特性
CallerFilePath
:调用者的文件路径CallerLineNumber
:调用方法的行号CallerMemberName
:调用方法的成员名
- 只能用于方法的参数,且是给定默认值的可选参数
- 可以访问文件路径、代码行数、调用成员的名称等源代码信息,需要使用
DebuggerStepThrough
特性- 位于
System.Diagnostics
命名空间 - 在单步调试时可以不进入方法
- 可以用于类、结构、构造函数、方法和访问器
- 位于
- 其他预定义特性
CLSCompliant
:声明可公开的成员应该被编译器检测是否符合CLS,兼容的程序集可以被任何.NET兼容的语言使用Serializable
:声明结构可以被序列化NonSerialized
:声明结构不能被序列化DLLImport
:声明是非托管代码实现的WebMethod
:声明方法应该被作为XML Web服务的一部分暴露AttributeUsage
:声明特性能应用到什么类型的程序结构,将这个特性引用到结构声明上
- 有关应用特性的更多内容
- 多个特性
- 可以为单个结构应用多个特性
- 多层结构:用多个方括号应用多个特性
- 逗号分隔:多个特性写在一个方括号内,用逗号隔开
- 其他类型的目标
- 除了类,特性还可以应用到字段、属性等其他程序结构
- 还可以显式的标注特性,如
[return : MyAttribute ]
- C#提供了10个标准的特性名不,大多数目标名可以自明,而
type
覆盖了类、结构、委托、枚举、接口;typevar
目标名称指定使用泛型结构的类型参数event
、field
、method
、param
、property
、return
、type
、typevar
、assembly
、module
- 全局特性
- 可以使用
assembly
和module
目标名称来使用显式目标说明符把特性设置在程序集或模块级别 - 程序集级别的特性必须放置在任何命名空间之外,并且通常放在AssemblyInfo.cs文件中
- AssemblyInfo.sc文件通常包含有关公司、产品、以及版权信息的元数据
- 可以使用
- 多个特性
- 自定义特性
- 用户自定义的特性类叫做自定义特性,所有的特性类都派生自
System.Attribute
- 声明自定义特性
- 派生自
System.Attribute
- 以Attribute结尾
- 安全起见,通常建议声明为sealed
- 所有特性的公共成员只能是
- 字段
- 属性
- 构造函数
- 派生自
- 使用特性的构造函数
- 每个特性至少必须有一个公共的构造函数
- 如果不显式提供构造函数,编译器会提供一个隐式的、公共的、无参的构造函数
- 可以被重载
- 必须使用全名,不能省略后缀,只有在应用特性的时候才能省略后缀
- 指定构造函数
- 为目标应用特性时,其实是是在指定构造函数来创建特性的实例
- 列在特性应用中的参数其实就是构造函数的参数
- 应用特性时,构造函数的实参必须是在编译期能确定值的常量表达式
- 如果使用无参的构造函数,圆括号可以省略
- 使用构造函数
- 不能显式调用构造函数,特性的实例创建后,只有特性的消费者访问特性时才能调用构造函数
- 应用一个特性是一条声明语句,不会决定什么时候构造特性类的对象
- 构造函数中的位置参数和命名参数
- 在特性类声明时,可以提供多于形参个数的实参,多出来的实参被称为命名参数,给特性类的其他成员进行赋值,需要显式写出成员名称和等号
- 注意这里的命名参数和前面学习的命名参数不同
- 前面讲的命名参数是指定的仍然是形参,只不过可以改变传参的位置和顺序,这里的命名参数可以在形参之外给成员赋值
- 前面学习的命名参数使用的是冒号,这里使用登号
- 限制特性的使用
AttributeUsage
特性是一个很重要的预定义特性,可以应用到自定义特性,用来限制特性使用在某个目标类型上- 有三个重要的公共属性
ValidOn
:保存特性能应用到的目标类型的列表,构造函数的第一个参数必须是AttributeTarget
类型的枚举值,无指定默认值Inherited
:指示特性是否会装饰类型的派生类修饰,默认为true,可以使用命名参数指定AllowMutiple
:指示目标是否被应用到多个特性的实例的布尔值,默认为false,可以使用命名参数指定
- 构造函数
- 接受单个位置参数,指定了特性允许的目标类型(
AttributeTarge
枚举类型),用来设置ValidOn
属性 AttributeTarge
枚举的成员All
、Assembly
、Class
、Constructor
、Delegate
、Enum
、Event
、Field
、GenericParameter
、Interface
、Method
、Module
、Parameter
、Property
、ReturnValue
、Struct
- 接受单个位置参数,指定了特性允许的目标类型(
- 自定义特性的最佳实践
- 特性类应该表示目标结构的一些状态
- 如果特性需要某些字段,可以通过包含具有位置参数的构造函数来收集数据,可选字段可以采用命名参数按需初始化
- 除了属性之外,不要实现公共方法或其他函数成员
- 为了更安全,把特性类声明为sealed
- 在特性声明中使用
AttributeUsage
来显式指定特性目标组
- 用户自定义的特性类叫做自定义特性,所有的特性类都派生自
- 访问特性
- 对于自定义特性也可以使用Type对象来获取信息,Type的两个方法
IsDefined
和GetCustomAttributes
在这里非常有用 - 使用
IsDefined
方法- 可以使用该方法来检测某个特性是否应用到某个类上
- 第一个参数接受需要检测的特性的Type对象
- 第二个参数指示是否搜索类的继承树来查找这个特性
- 使用
GetCustomAttributes
方法- 返回应用到结构的特性的数组
- 实际返回的对象是object类型的数组,因此我们必须将它强制转换为相应的特性类型
- 参数指示是否搜索类的继承树来查找特性
- 调用方法后,每一个与目标相关联的特性的实例就会被创建
- 返回应用到结构的特性的数组
- 对于自定义特性也可以使用Type对象来获取信息,Type的两个方法