定制特性

.Net Framework最具有创意的功能之一: 定制特性.

可以宣告式地自己的代码构造添加注解来实现特殊功能. 允许为几乎每一个元数据记录项定义和应用信息. 这种可扩展的元数据信息能在运行时查询,从而动态改变代码的执行方式.

使用定制特性

关于自定义特性, 首先要知道它们只是将一些附加信息与某个目标元素关联起来的方式. 编译器在托管模块的元数据中生成(嵌入)这些额外的信息.

FCL定义了几百个定制特性,例如:

  • DllImport应用于方法, 告诉CLR此方法的实现位于指定DLL的非托管代码中.
  • Serializable应用于类型, 告诉序列格式化器一个实例的字段可以序列化和反序列化.
    • 格式化器是实现了IFormatter接口的类型,它知道如何序列化和反序列化一个对象组.
  • AssemblyVersion应用于程序集, 设置程序集的版本号
  • Flags应用于枚举类型, 枚举类型就成了位标志(bit flags)集合

CLR允许将特性应用于可在文件的元数据中表示的几乎任何东西, 最常应用特性的还是以下定义表中的记录项:TypeDef(类,结构,枚举,接口和委托),MethodDef(含构造器),ParamDef,FieldDef,Property,EventDef,AssemblyDef,ModuleDef.

C# 值允许将特性应用于定义以下任何目标元素的源代码: 程序集,模块,类型(类,结构,枚举,接口,委托),字段,方法(含构造器),方法参数,方法返回值,属性,事件和泛型参数.

// 不能省略前缀,必须向编译器清楚的表明我们的意图
[assembly: MyAttr(1)] // 应用于程序集
[module: MyAttr(2)]   // 应用于模块

// 可省略前缀, 编译器可以判断此特性要应用于什么目标元素
// [MyAttr(3)]
[type: MyAttr(3)]     // 应用于类型
internal sealed class SomeType <[typevar: MyAttr(4)] T> // 应用于泛型类型变量
{

    [field: MyAttr(5)] // 应用于字段
    public Int32 SomeField = 0;

    [return: MyAttr(6)] // 应用于返回值   不能省略前缀.
    [method: MyAttr(7)] // 应用于方法
    public Int32 SomeMethod(
        [param: MyAttr(8)] // 应用于方法参数
        Int32 SomeParam)
    {
        return SomeParam;
    }

    [property: MyAttr(9)] // 应用于属性
    public String SomeProp
    {
        [method: MyAttr(10)] // 应用于get访问器方法
        get { return null; }
    }

    [event: MyAttr(11)]  // 应用于事件
    [field: MyAttr(12)]  // 应用于编译器生成的字段  不能省略前缀
    [method: MyAttr(13)] // 应用于编译器生成的add&&remove方法  不能省略前缀
    public event EventHandler SomeEvent;
}

[AttributeUsage(AttributeTargets.All)]
public class MyAttr : Attribute
{
    public MyAttr(Int32 x)
    {
    }
}

定制特性其实是一个类型的实例. 符合CLS的要求,定制特性必须直接或者间接从公共抽象类型Attribute派生.

C# 编译器允许省略Attribute后缀,减少打字量. 例如 : [DllImport(…)] 而不是 [DllImortAttribute(….)]

特性是类的实例. 类必须有公共构造器才能创建它的实例. 所以将特性应用于目标元素时,语法类似于调用类的某个实例构造器. C#语言还支持特殊的语法, 允许设置与特性类关联的公共字段或属性.

[DllImport("kernel32",CharSet = CharSet.Auto, SetLastError = true)]

这个特性传递了一些东西, 调用构造器时永远不会出现这样的语法. 在这个例子中,”Kernel32”这个String类型的参数已经传给它了。构造器的参数称为”定位参数”,而且是强制性的。也就是说,应用attribute时,必须指定参数。 那么,另外两个”参数”是什么?这种特殊的语法允许在DllImportAttbute对象构造好之后,设置对象的任何公共字段和属性

在这个例子中,当DllImportAttbute对象构造好,而且将"Kernel32"传给构造器之后,对象的公共实例字段CharSetSetListError被分别设置为CharSet.Autotrue用于设置字段或属性的”参数”被称为”命名参数”。 这种参数是可选的,因为在应用attribute的一个实例时,不一定要指定命名参数。

可以将多个attribute应用于一个目标元素。 将多个attribute应用一个目标元素时,attribute的顺序是无关紧要的。在C#中,可将每个attribute都封闭到一对方括号中,也可以在一对方括号中封闭多个以逗号分隔的attribute。

如果特性类的构造器不获取参数,那么圆括号也可以省略.

[Serializable][Flags]
[Serializable, Flags]

定义自己的特性类

  • 定义一个FlagsAttribute类(Attribute后缀可以省略),继承于Attribute.
  • 非抽象特性至少要包含一个公共构造器.

应将特性想象成逻辑状态容器. 也就是说,虽然特性类型是一个类, 但这个类应该很简单.

  • 只提供一个公共构造器来接受特性的强制性(或定位器)状态信息(例如 构造器的参数),
  • 而且这个类可以提供公共字段和属性, 来接受特性的可选状态信息(例如CharSet = CharSet.Auto, SetLastError = true).
  • 特性类不应该提供任何公共方法,事件或其他成员.

一般不鼓励使用公共字段. 特性也不例外, 使用属性要好的多,属性能提供更大的灵活性.

FlagsAttribute类的实例现在能应用于任何目标元素, 事实上应该只能应用于枚举,应用在其他类型上没有意义, 所以需要给特性类应用System.AttributeUsageAttribute类的实例.

namespace System
{
    // 特性类型本质还是类,所以可以应用特性
    [AttributeUsage(AttributeTargets.Enum, Inherited = false)]
    public class FlagsAttribute : System.Attribute
    {
        public FlagsAttribute()
        {
        }
    }
}

AttributeUsage是一个简单的类,告诉编译器定制的特性的合法应用范围, 所有编译器都内建了对该特性的支持. 源码如下:

如果没有向自己的类应用AttributeUsage特性, 编译器会有默认设定: 默认可以应用于所有元素,只能向每个目标应用一次, 可继承.

namespace System
{
  // 可应用于类
  [AttributeUsage(AttributeTargets.Class, Inherited = true)]
  public sealed class AttributeUsageAttribute : Attribute
  {
    internal static AttributeUsageAttribute Default = new AttributeUsageAttribute(AttributeTargets.All);
    // 默认是支持全类型
    private AttributeTargets _attributeTarget = AttributeTargets.All;
    // 默认是可继承
    private bool _inherited = true;
    // 默认只能应用一次
    private bool _allowMultiple;

    // 公共构造器,需要一个枚举参数(位标志)
    public AttributeUsageAttribute(AttributeTargets validOn)
    {
      this._attributeTarget = validOn;
    }

    internal AttributeUsageAttribute(AttributeTargets validOn, bool allowMultiple, bool inherited)
    {
      this._attributeTarget = validOn;
      this._allowMultiple = allowMultiple;
      this._inherited = inherited;
    }

    public AttributeTargets ValidOn
    {
      get
      {
        return this._attributeTarget;
      }
    }
    // 是否允许多次应用于同一个目标
    // 大部分特性这样做没有意义
    public bool AllowMultiple
    {
      get
      {
        return this._allowMultiple;
      }
      set
      {
        this._allowMultiple = value;
      }
    }
    // 特性应用于基类时,是否同时应用于派生类和重写的方法
    public bool Inherited
    {
      get
      {
        return this._inherited;
      }
      set
      {
        this._inherited = value;
      }
    }
  }
}
  • AllowMultiple
    • 有少数特性有必要多次应用于同一目标, FCL特性类ConditionalAttribute允许将它的多个实例应用于同一个目标元素, 不将AllowMultiple明确设为true,特性就只能想选定的目标元素应用一次.
  • Inherited
    • 特性应用于基类时,是否同时应用于派生类和重写的方法
// 可将此特性应用于类和方法,并且该特性会被子类继承
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method , Inherited = true)]
internal class TastyAttribute : Attribute
{}

[Tasty] [Serializable]
internal class BaseType
{
   [Tasty] protected virtual void Do(){}
}

// 无法序列化, 因为Serializable被标记为不可继承
internal class DerivedType : BaseType
{
   // 此方法会被视为[Tasty]
   protected override void Do()
   { }
}

注意: .NetFramework只认为 , 方法, 属性, 事件, 字段, 方法返回值和参数等目标元素可继承. 只有在该特性应用于上述某个目标的前提下, 才将Inherited设为true, 可继承特性不会造成在托管模块中为派生类型生成额外的元数据.

特性构造器和字段/属性数据类型

定制特性类可定义构造器来获取参数. 使用特性时必须指定这些参数,可以在类中定义非静态公共字段属性. 在定义特性类的实例构造器,字段和属性时, 可供选择的数据类型并不多. 具体地说,只允许Boolean,Char,Byte,SByte,Int16,UInt16,Int32,UInt32,Int64,UInt64,Single,Double,String,Type,Object或枚举类型. 也可以是使用上述任意类型的一维0基数组, 但应尽量避免使用数组.因为定制特性如果它的构造器要获取数组作为参数,就会失去与CLS的相容性.

应用特性时必须传递一个编译时常量表达式, 它与特性类定义的类型匹配.

  • 在特性类定义了一个Type参数,字段或者属性的任何地方,都必须使用C# typeof操作符.
  • 在特性类定义了一个Object参数,字段或者属性的任何地方,都可以传递一个Int32,String或其他任何常量表达式(包括null)
    • 如果常量表达式代表值类型,那么在运行时构造特性的实例时会对值类型进行 装箱.
public enum Color
{
    Red
}

[AttributeUsage(AttributeTargets.All)]
internal sealed class SomeAttribute : Attribute
{
    // 构造器中的参数 称为定位参数
    public SomeAttribute(String name, Object o, Type[] types)
    {
        // 'name'  引用一个String
        // 'o'     引用一个合法的类型,如果有必要就进行装箱
        // 'types' 引用一个一维0基Type数组
    }

    // 字段,属性称为 命名参数(可选)
}

// 应用特性时, 传入对应的参数
// 定位参数必须要填, 命名参数可选
[Some("Jeff", Color.Red, new Type[] {typeof(Math), typeof(Console)})]
internal sealed class SomeType
{
}

编译器检测到目标元素应用了定制特性时, 会调用特性类的构造器,向它传递任何指定的参数, 从而构造特性类型的实例. 编辑器采用增强型构造器语法所指定的值,对任何公共字段和属性进行初始化. 构造并初始化好定制特性类的对象之后, 编译器将它的状态序列化到目标元素的元数据表记录项中.

重要提示: 为方便理解,可以这么想象定制特性, 是类的实例, 被序列化成驻留在元数据中的字节流. 运行时可对元数据中的字节进行反序列化, 从而构造出类的实例.

实际发生的事: 编译器在元数据中生成创建特性类的实例所需的信息,每个构造器参数都是1字节的类型ID,后跟具体的值. 对构造器参数进行序列化时, 编译器先写入字段/属性名称,在跟上1字节的类型ID,最后是具体的值. 如果是数组, 则会先保存数组元素的个数, 再跟上每个单独的元素.

检测定制特性

如果只是定义, 应用自己想要的所有实例,这样除了在程序集中生成额外的数据,没有其他任何意义, 在15章描述了如何将Flags特性应用于枚举类型, 从而改变System.EnumToStringFormat方法的行为.

方法的行为之所以改变,是因为它们会在 运行时检查自己操作的枚举类型是否关联了Flags特性元数据. 代码利用反射的技术检测特性的存在.

public override String ToString()
{
  // IsDefined 要求系统查看枚举类型的元数据, 是否关联了FlagsAttribute类的实例
  if (this.GetType().IsDefined(typeof(FlagsAttribute), false))
  {
      // 如果是, 就将值视为一个位标志枚举类型
      ....
  }else
  {
      // 如果不是,就将值视为一个普通枚举类型
      ...
  }
}

所以在定义定制特性时,也必须实现一些代码来检测目标上是否存在该特性类的实例.

FCL提供了多种那个方式来检测特性的存在. System.Reflection.CustomAttributeExtensions类定义的扩展方法:

  • IsDefined
  • GetCustomAttributes
    • 通常用于将AllowMultiple设为true的特性. 列出所有特性.
  • GetCustomAttribute
    • 如果

以上每个方法都有几个重载版本. 如果只想判断目标是否应用了一个特性,应该使用IsDefined,比另外2个方法更高效. 但是不会构造特性对象,不会调用构造器,也不会设置字段和属性.

要构造特性对象, 必须调用GetCustomAttributesGetCustomAttribute方法, 每次调用,都会构造指定特性类的新实例, 并根据源代码中指定的值来设置每个实例的字段和属性. 两个方法都返回对完全构造好的特性类实例的引用.

调用上述任何方法,内部都必须扫描托管模块的元数据. 执行字符串比较来定位指定的定制特性类. 这些操作会耗费一定的时间, 可以考虑缓存这些方法调用的结果,而不是反复调用来请求相同的信息.

System.Reflection命名空间定义了几个类允许检查模块的元数据:

  • Assembly
  • Module
  • ParameterInfo
  • MemberInfo
  • Type
  • MethodInfo
  • ConstructorInfo
  • FieldInfo
  • EventInfo
  • PropertyInfo
  • 各自的*Builder类

所有类都提供了IsDefinedGetCustomAttributes方法.

反射类提供的GetCustomAttributes方法返回的是Object数组(Object[]),而不是由Attribute实例构成的数组(Attribute[]),不过可以不用关心此不一致性.

只有Attribute,Type,MethodInfo类才实现了支持Boolean inherit参数的反射方法. 其他检查特性的所有反射方法都会忽略inherit参数, 而且不会检查继承层次结构. 所有要检查事件,属性,字段,构造器或参数是否应用了继承的特性,只能调用Attribute的某个方法.

还要注意: 将一个类传给IsDefined,GetCustomAttributes,GetCustomAttribute方法时, 会检测是否应用了指定的特性类或者指定特性类的派生类, 如果只是想搜索一个具体的特性类, 应针对返回值执行一次额外的检查, 确保方法返回的正是想搜索的类. 还可以将自己的特性类定义为sealed,减少可能存在的混淆,并避免执行这个额外的检查.

演示如何列出一个类型中定义的所有方法,并显示应用于每个方法的特性:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;

public static class CustomAttributes
{
    public static void Main()
    {
        DetectingAttributes.Go();
    }

    [Serializable]
    [DefaultMember("Main")]
    [DebuggerDisplay("Richter", Name = "Jeff", Target = typeof(DetectingAttributes))]
    public sealed class DetectingAttributes
    {
        [Conditional("Debug")]
        [Conditional("Release")]
        public void DoSomething()
        {
        }

        public DetectingAttributes()
        {
        }


        [MethodImpl(MethodImplOptions.NoInlining)]
        [STAThread]
        public static void Go()
        {
            Go(ShowAttributes);
        }

        // 传入一个委托对象(方法的包装)
        private static void Go(Action<MemberInfo> showAttributes)
        {
            // 显示应用于这个类型的特性集
            showAttributes(typeof(DetectingAttributes));

            // 获取与类型关联的方法集
            var members =
                from m in typeof(DetectingAttributes).GetTypeInfo().DeclaredMembers.OfType<MethodBase>()
                where m.IsPublic
                select m;

            foreach (MemberInfo member in members)
            {
                // 显示应用于这个成员的特性集
                showAttributes(member);
            }
        }

        private static void ShowAttributes(MemberInfo attributeTarget)
        {
            var attributes = attributeTarget.GetCustomAttributes<Attribute>();

            Console.WriteLine("特性应用于 {0}: {1}",
                attributeTarget.Name, (attributes.Count() == 0 ? "None" : String.Empty));

            foreach (Attribute attribute in attributes)
            {
                // 显示应用的每个特性的类型
                Console.WriteLine("  {0}", attribute.GetType().ToString());

                if (attribute is DefaultMemberAttribute)
                    Console.WriteLine("    MemberName={0}",
                        ((DefaultMemberAttribute) attribute).MemberName);

                if (attribute is ConditionalAttribute)
                    Console.WriteLine("    ConditionString={0}",
                        ((ConditionalAttribute) attribute).ConditionString);

                if (attribute is CLSCompliantAttribute)
                    Console.WriteLine("    IsCompliant={0}",
                        ((CLSCompliantAttribute) attribute).IsCompliant);

                DebuggerDisplayAttribute dda = attribute as DebuggerDisplayAttribute;
                if (dda != null)
                {
                    Console.WriteLine("    Value={0}, Name={1}, Target={2}",
                        dda.Value, dda.Name, dda.Target);
                }
            }

            Console.WriteLine();
        }
    }
}

// 特性应用于 DoSomething:
//   System.Diagnostics.ConditionalAttribute
//     ConditionString=Debug
//   System.Diagnostics.ConditionalAttribute
//     ConditionString=Release
//
// 特性应用于 Go:
//   System.STAThreadAttribute
//
// 特性应用于 .ctor: None

两个特性实例的相互匹配

判断是否向目标应用了一个特性实例,可能还需要检查特性的字段来确定它们的值.

  • 一个办法是 写代码检查特性类的字段值. System.Attribute重写了Object的Equals方法,会在内部比较两个对象的类型,类型一致的话会利用反射来比较两个特性对象中的字段值, 每个字段都调用Equals方法, 所有字段匹配才返回true.
    • 可以在自己的定制特性类中重写Equals来移除反射的使用,从而提升性能.
  • Attribute类还公开的虚方法Match,可重写它来提供更丰富的语义. Match的默认实现只是调用Equal方法并返回它的结果.

下例演示了如何重写Equals和Match,后者在一个特性代表另一个特性的子集的前提下返回true.

using System;
using System.Reflection;

internal sealed class MatchingAttributes
{
    public static void Main()
    {
        // ChildAccount应用了此特性[Accounts(Accounts.Savings)]
        CanWriteCheck(new ChildAccount());
        // AdultAccount应用了此特性[Accounts(Accounts.Savings | Accounts.Checking | Accounts.Brokerage)]
        CanWriteCheck(new AdultAccount());

        // 只是为了演示在一个没有应用AccountsAttribute的类型上,
        // 方法也能正常工作
        CanWriteCheck(new MatchingAttributes());

//      MatchingAttributes+ChildAccount 类型不可以开支票.
//      MatchingAttributes+AdultAccount 类型可以开支票.
//      MatchingAttributes              类型不可以开支票.
    }

    private static void CanWriteCheck(Object obj)
    {
        // 构造attribute类型的一个实例,并把它初始化成我们要显式查找的内容
        Attribute checking = new AccountsAttribute(Accounts.Checking);

        // 构造应用于类型的特性实例
        // 获取传入类型是否有AccountsAttribute实例,并构造特性实例
        Attribute validAccounts = obj.GetType().GetCustomAttribute<AccountsAttribute>(false);

        // 如果将特性应用于类型 并且特性指定了Accounts.Checking账户
        // 表示该类型可以开支票
        // 利用Attribute的.Match方法比较特性实例是否一致
        if ((validAccounts != null) && checking.Match(validAccounts))
        {
            Console.WriteLine("{0} 类型可以开支票.", obj.GetType());
        }
        else
        {
            Console.WriteLine("{0} 类型不可以开支票.", obj.GetType());
        }
    }

    [Flags]
    private enum Accounts
    {
        Savings   = 0x0001,
        Checking  = 0x0002,
        Brokerage = 0x0004
    }


    // 重写特性的Equals方法和Match方法
    [AttributeUsage(AttributeTargets.Class)]
    private sealed class AccountsAttribute : Attribute
    {
        private Accounts m_accounts;

        public AccountsAttribute(Accounts accounts)
        {
            m_accounts = accounts;
        }

        public override Boolean Match(Object obj)
        {
            // 如果基类实现了Match,而基类不是Attribute
            // 就取消下面这两行注释
            // 调用基类实现的Match, obj和base不相等(说明他们不是从同一个基类派生的),
            // 表示肯定不相等,返回false
            // if (!base.Match(obj))
            //     return false;

            // 由于this不为null,obj为null的话, 对象肯定不相等
            // 如果基类正确实现了Match, 下面这行可以删除
            if (obj == null) return false;

            // 如果对象属于不同的类型,肯定不匹配
            // 如果基类正确实现了Match, 下面这行可以删除
            if (this.GetType() != obj.GetType()) return false;

            // 将obj转型为我们的类型,以访问字段
            // 注意:装备不可能失败,因为我们知道两个对象是相同的类型
            AccountsAttribute other = (AccountsAttribute) obj;

            // 比较字段,判断他们是否有相同的值
            // 这里判断this账户的Flags是否是other的Flags的子集
            if ((other.m_accounts & m_accounts) != m_accounts)
                return false;

            return true; // 对象匹配
        }

        public override Boolean Equals(Object obj)
        {
            // 如果基类实现了Equals,而基类不是Object
            // 就取消下面这两行注释
            // 调用基类实现的Equals, obj和base不相等(说明他们不是从同一个基类派生的),
            // 表示肯定不相等,返回false
            // if (!base.Equals(obj))
            //     return false;

            // 由于this不为null,obj为null的话, 对象肯定不相等
            // 如果基类正确实现了Equals, 下面这行可以删除
            if (obj == null) return false;

            // 如果对象属于不同的类型,肯定不匹配
            // 如果基类正确实现了Match, 下面这行可以删除
            if (this.GetType() != obj.GetType()) return false;

            // 将obj转型为我们的类型,以访问字段
            // 注意:装备不可能失败,因为我们知道两个对象是相同的类型
            AccountsAttribute other = (AccountsAttribute) obj;

            // 比较字段,判断他们是否有相同的值
            if (other.m_accounts != m_accounts)
                return false;

            return true; // 对象相等
        }

        // 重写HashCode,因为重写了Equlas
        // 如果只重写了equals方法而没有重写hashCode方法的话
        // 则会违反约定的第二条:相等的对象必须具有相等的散列码(hashCode)
        public override Int32 GetHashCode()
        {
            return (Int32) m_accounts;
        }
    }

    [Accounts(Accounts.Savings)]
    private sealed class ChildAccount
    {
    }

    [Accounts(Accounts.Savings | Accounts.Checking | Accounts.Brokerage)]
    private sealed class AdultAccount
    {
    }
}

检测定制特性时不创建从Attribute派生的对象

另一种方法检测应用于元数据记录项的特性. 在某些安全性要求严格的场合,这个技术能保证不执行从Attribute派生类中的代码. 毕竟,调用AttributeGetCustomAttributeGetCustomAttributes方法时, 会在内部调用特性类的构造器,而且可能会调用属性的set访问器方法, 首次访问类型会造成CLR调用类型的类型构造器(如果有的话). 这些方法中可能有一些执行方法,存在安全隐患.

使用System.Reflection.CustomAttributeData类,可以在查找attribute时同时禁止执行attribute类中的代码。这个类定义了一个静态方法GetCustomAttributes来获取与一个目标关联的attribute。该方法有4个重载版本:一个接受一个Assembly,一个接受一个Module,一个接受一个ParameterInfo,还有一个接受一个Memberinfo。这个类是在System.Reflection命名空间定义的。通常,是先用Assembly的静态方法ReflectionOnlyLoad加载一个程序集,在用CustomAttributeData类分析这个程序集的元数据中的attribute。简单的说,ReflectionOnlyLoad以一种特方式加载程序集,期间会禁止CLR执行程序集中的任何代码,包括类型构造器。

CustomAttributeDataGetCustomAttributes方法相当于一个工厂方法。也就是说,调用它会返回IList<CustomAttributeData>类型的对象,其中包括了一个有CustomAttributeData构成的一个集合。在集合中,应用于指定目标的每个定制attribute都有一个对象的元素。针对每个CustomAttribute对象都可以查询一些只读属性,判断attribute对象是如何构造和初始化的。具体的说,Customctor属性指出构造器方法”要”如何调用。ComstructorArguments属性以一个IList<CustomAttributeTypedArgument>实例的形式返回”将”传给这个构造器的实参。NamedArguments属性以一个IList<CustomAttributeNamedArgument>实例的形式,返回”要”设置的字段或属性。注意,这里之所以说”将”,是因为不会实际地调用构造器和set访问器方法。通过禁止执行attribute类的任何方法,我们获得了增强的安全性。

[Serializable]
[DefaultMemberAttribute("Main")]
[DebuggerDisplayAttribute("Richter", Name = "Jeff", Target = typeof(DetectingAttributes))]
public sealed class DetectingAttributes
{
    [Conditional("Debug")]
    [Conditional("Release")]
    public void DoSomething()
    {
    }

    public DetectingAttributes()
    {
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    [STAThread]
    public static void Go()
    {
        Go(ShowAttributes);
        Go(ShowAttributesReflectionOnly);
    }

    private static void Go(Action<MemberInfo> showAttributes)
    {
        // Show the set of attributes applied to this type
        showAttributes(typeof(DetectingAttributes));

        // Get the set of methods associated with the type
        var members =
            from m in typeof(DetectingAttributes).GetTypeInfo().DeclaredMembers.OfType<MethodBase>()
            where m.IsPublic
            select m;

        foreach (MemberInfo member in members)
        {
            // Show the set of attributes applied to this member
            showAttributes(member);
        }
    }

    private static void ShowAttributes(MemberInfo attributeTarget)
    {
        var attributes = attributeTarget.GetCustomAttributes<Attribute>();

        Console.WriteLine("Attributes applied to {0}: {1}",
            attributeTarget.Name, (attributes.Count() == 0 ? "None" : String.Empty));

        foreach (Attribute attribute in attributes)
        {
            // Display the type of each applied attribute
            Console.WriteLine("  {0}", attribute.GetType().ToString());

            if (attribute is DefaultMemberAttribute)
                Console.WriteLine("    MemberName={0}",
                    ((DefaultMemberAttribute) attribute).MemberName);

            if (attribute is ConditionalAttribute)
                Console.WriteLine("    ConditionString={0}",
                    ((ConditionalAttribute) attribute).ConditionString);

            if (attribute is CLSCompliantAttribute)
                Console.WriteLine("    IsCompliant={0}",
                    ((CLSCompliantAttribute) attribute).IsCompliant);

            DebuggerDisplayAttribute dda = attribute as DebuggerDisplayAttribute;
            if (dda != null)
            {
                Console.WriteLine("    Value={0}, Name={1}, Target={2}",
                    dda.Value, dda.Name, dda.Target);
            }
        }

        Console.WriteLine();
    }

    private static void ShowAttributesReflectionOnly(MemberInfo attributeTarget)
    {
        IList<CustomAttributeData> attributes =
            CustomAttributeData.GetCustomAttributes(attributeTarget);

        Console.WriteLine("Attributes applied to {0}: {1}",
            attributeTarget.Name, (attributes.Count == 0 ? "None" : String.Empty));

        foreach (CustomAttributeData attribute in attributes)
        {
            // Display the type of each applied attribute
            Type t = attribute.Constructor.DeclaringType;
            Console.WriteLine("  {0}", t.ToString());
            Console.WriteLine("    Constructor called={0}", attribute.Constructor);

            IList<CustomAttributeTypedArgument> posArgs = attribute.ConstructorArguments;
            Console.WriteLine("    Positional arguments passed to constructor:" +
                              ((posArgs.Count == 0) ? " None" : String.Empty));
            foreach (CustomAttributeTypedArgument pa in posArgs)
            {
                Console.WriteLine("      Type={0}, Value={1}", pa.ArgumentType, pa.Value);
            }


            IList<CustomAttributeNamedArgument> namedArgs = attribute.NamedArguments;
            Console.WriteLine("    Named arguments set after construction:" +
                              ((namedArgs.Count == 0) ? " None" : String.Empty));
            foreach (CustomAttributeNamedArgument na in namedArgs)
            {
                Console.WriteLine("     Name={0}, Type={1}, Value={2}",
                    na.MemberInfo.Name, na.TypedValue.ArgumentType, na.TypedValue.Value);
            }

            Console.WriteLine();
        }

        Console.WriteLine();
    }
}

条件特性类

定义、应用和反射特性能带来许多便利,所以开发人员越来越频繁地使用这些技术。特性简化了对代码的注释,还能实现丰富的功能。近来,开发人员越来越喜欢在设计和调试期间利用特性来辅助开发。

//#define TEST
//#define VERIFY

using System;
using System.Diagnostics;

[Cond]
public static class ConditionalAttributeDemo
{
    // 只有在定义了VERIFY或TEST的前提下,编译器才会在元数据中生成特性信息
    // 但是特性类的定义元数据和实现仍然存在于程序集中
    [Conditional("TEST")]
    [Conditional("VERIFY")]
    public sealed class CondAttribute : Attribute
    {
    }

    public static void Main()
    {
        // 判断当前类ConditionalAttributeDemo, 是否应用了CondAttribute特性
        Console.WriteLine("CondAttribute {0} 应用于程序类型.",
            Attribute.IsDefined(typeof(ConditionalAttributeDemo), typeof(CondAttribute)) ?
                "" : "没有");
    }
}

// CondAttribute 没有 应用于程序类型.

只有在定义了VERIFY或TEST的前提下,编译器发现目标元素应用了CondAttribute的实例,并且满足这个CondAttribute条件 才会在元数据中生成特性信息,但是特性类的定义元数据和实现仍然存在于程序集中

条件特性类: [Conditional(“TEST”)]