反射和特性

  1. 元数据和反射
    • 有些程序及其类型的数据称为元数据,它们保存在程序的程序集中
    • 程序在运行时,可以查看其他程序集或其本身的元数据,这个行为叫做反射
    • 对象浏览器是显示元数据的程序的一个实例
    • 要使用反射,必须使用System.Reflection命名空间
  2. Type类
    • Type类概述
      • BCL中提供的抽象类,被设计用来包含类型的特性
      • 在运行时,CLR创建Type(RuntimeType)派生的类的实例,包含了类型信息
      • 当我们访问这些实例时,CLR不会返回派生类的引用而是基类Type的引用
      • 简单起见,可以把引用指向的对象成为Type类型的对象,但实际上是BCL内部派生类型的对象
    • 有关Type的重要事项
      • 对于程序中用到的每一个类型,CLR都会创建一个包含这个类型信息的Type类型的对象
      • 程序中用到的每一个类型,都会关联到独立的Type类的对象
      • 不管创建的类型有多少个实例,只有一个Type对象会关联到所有这些实例
      • 貌似类似于static
    • 可以从Type对象中获取类型的信息
      • Name:属性,返回类型的名字
      • Namespace:属性,返回类型的生命的命名空间
      • Assembly:属性,返回声明类型的程序集,如果类型是泛型的,则返回定义这个类型的程序集
      • GetFields:方法,返回类型的字段列表
      • GetProperties:方法,返回类型的属性列表
      • Getthonds:方法,返回类型的方法列表
  3. 获取Type对象
    • GetType方法
      • object类型包含了一个叫做GetType的方法,返回对实例的Type对象的引用
      • 通过对象去调用方法
    • typeof运算符
      • 在括号内传入类型
  4. 特性
    • 特性是一种允许我们向程序集增加元数据的语言结构,它是用于保存程序结构信息的某种特殊类型的类
      • 将应用了特性的程序结构叫做目标
      • 设计用来获取和使用元数据的程序的特性叫做消费者
      • .NET预定义了很多特性,也可以声明自定义特性
    • 特性的要点
      • 在源代码中将特性应用于程序结构
      • 编译器获取源代码并且从特性产生元数据,然后把元数据放到程序集中
      • 消费者程序可以获取特性的元数据以及程序中其他组件的元数据
      • 编译器同时产生和消费特性
    • 特性名以Attribute后缀结尾,使用目标时省略后缀
  5. 应用特性
    • 这里所说的结构是是指程序结构,包含结构体、类、方法等,不是单单指结构体struct
    • 特性的目的是告诉编译器把程序结构的某组元数据嵌入程序集,可以通过把特性应用到结构来实现
    • 在结构前防止特性片段来应用特性
    • 特性片段被方括号包围,其中是特性名和特性的参数列表
    • 大多数特性只针对直接跟随着一个或多个特性后的结构
    • 引用了特性的结构成为被特性装饰
  6. 预定义的保留的特性
    • Obsolete特性
      • 可以将结构标注为过期的,在使用时编译器会给出警告,但仍然可以调用
      • 两个可选参数
        • messagestring?,表示警告的提示信息
        • errorbool类型,如果为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:声明特性能应用到什么类型的程序结构,将这个特性引用到结构声明上
  7. 有关应用特性的更多内容
    • 多个特性
      • 可以为单个结构应用多个特性
      • 多层结构:用多个方括号应用多个特性
      • 逗号分隔:多个特性写在一个方括号内,用逗号隔开
    • 其他类型的目标
      • 除了类,特性还可以应用到字段、属性等其他程序结构
      • 还可以显式的标注特性,如[return : MyAttribute ]
      • C#提供了10个标准的特性名不,大多数目标名可以自明,而type覆盖了类、结构、委托、枚举、接口;typevar目标名称指定使用泛型结构的类型参数
        • eventfieldmethodparampropertyreturntypetypevarassemblymodule
    • 全局特性
      • 可以使用assemblymodule目标名称来使用显式目标说明符把特性设置在程序集或模块级别
      • 程序集级别的特性必须放置在任何命名空间之外,并且通常放在AssemblyInfo.cs文件中
      • AssemblyInfo.sc文件通常包含有关公司、产品、以及版权信息的元数据
  8. 自定义特性
    • 用户自定义的特性类叫做自定义特性,所有的特性类都派生自System.Attribute
    • 声明自定义特性
      • 派生自System.Attribute
      • 以Attribute结尾
      • 安全起见,通常建议声明为sealed
      • 所有特性的公共成员只能是
        • 字段
        • 属性
        • 构造函数
    • 使用特性的构造函数
      • 每个特性至少必须有一个公共的构造函数
      • 如果不显式提供构造函数,编译器会提供一个隐式的、公共的、无参的构造函数
      • 可以被重载
      • 必须使用全名,不能省略后缀,只有在应用特性的时候才能省略后缀
    • 指定构造函数
      • 为目标应用特性时,其实是是在指定构造函数来创建特性的实例
      • 列在特性应用中的参数其实就是构造函数的参数
      • 应用特性时,构造函数的实参必须是在编译期能确定值的常量表达式
      • 如果使用无参的构造函数,圆括号可以省略
    • 使用构造函数
      • 不能显式调用构造函数,特性的实例创建后,只有特性的消费者访问特性时才能调用构造函数
      • 应用一个特性是一条声明语句,不会决定什么时候构造特性类的对象
    • 构造函数中的位置参数和命名参数
      • 在特性类声明时,可以提供多于形参个数的实参,多出来的实参被称为命名参数,给特性类的其他成员进行赋值,需要显式写出成员名称和等号
      • 注意这里的命名参数和前面学习的命名参数不同
        • 前面讲的命名参数是指定的仍然是形参,只不过可以改变传参的位置和顺序,这里的命名参数可以在形参之外给成员赋值
        • 前面学习的命名参数使用的是冒号,这里使用登号
    • 限制特性的使用
      • AttributeUsage特性是一个很重要的预定义特性,可以应用到自定义特性,用来限制特性使用在某个目标类型上
      • 有三个重要的公共属性
        • ValidOn:保存特性能应用到的目标类型的列表,构造函数的第一个参数必须是AttributeTarget类型的枚举值,无指定默认值
        • Inherited:指示特性是否会装饰类型的派生类修饰,默认为true,可以使用命名参数指定
        • AllowMutiple:指示目标是否被应用到多个特性的实例的布尔值,默认为false,可以使用命名参数指定
      • 构造函数
        • 接受单个位置参数,指定了特性允许的目标类型(AttributeTarge枚举类型),用来设置ValidOn属性
        • AttributeTarge枚举的成员
          • AllAssemblyClassConstructorDelegateEnumEventFieldGenericParameterInterfaceMethodModuleParameterPropertyReturnValueStruct
    • 自定义特性的最佳实践
      • 特性类应该表示目标结构的一些状态
      • 如果特性需要某些字段,可以通过包含具有位置参数的构造函数来收集数据,可选字段可以采用命名参数按需初始化
      • 除了属性之外,不要实现公共方法或其他函数成员
      • 为了更安全,把特性类声明为sealed
      • 在特性声明中使用AttributeUsage来显式指定特性目标组
  9. 访问特性
    • 对于自定义特性也可以使用Type对象来获取信息,Type的两个方法IsDefinedGetCustomAttributes在这里非常有用
    • 使用IsDefined方法
      • 可以使用该方法来检测某个特性是否应用到某个类上
      • 第一个参数接受需要检测的特性的Type对象
      • 第二个参数指示是否搜索类的继承树来查找这个特性
    • 使用GetCustomAttributes方法
      • 返回应用到结构的特性的数组
        • 实际返回的对象是object类型的数组,因此我们必须将它强制转换为相应的特性类型
        • 参数指示是否搜索类的继承树来查找特性
        • 调用方法后,每一个与目标相关联的特性的实例就会被创建