定制特性(Attribute)

作者:追风剑情 发布于:2021-3-10 10:28 分类:C#

本章内容:
● 使用定制特性
● 定义自己的特性类
● 特性构造器和字段/属性数据类型
● 检测定制特性
● 两个特性实例的相互匹配
● 检测定制特性时不创建从Attribute派生的对象
● 条件特性类

本章讨论 Microsoft .NET Framework 提供的最具创意的功能之一:定制特性(custom attribute)。利用定制特性,可宣告式地为自己的代码构造添加注解来实现特殊功能。定制特性允许为几乎每一个元数据表记录项定义和应用信息。这种可扩展的元数据信息能在运行时查询,从而动态改变代码的执行方式。使用各种.NET Framework 技术(Windows 窗体,WPF 和 WCF 等),会发现它们都利用了定制特性,目的是方便开发者在代码中表达他们的意图。任何.NET Framework 开发人员都有必要完全掌握定制特性。

示例——定制特性
using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
internal sealed class OSVERSIONINFO {
	public OSVERSIONINFO() {
		OSversionInfoSize = (UInt32) Marshal.SizeOf(this);
	}
	
	public UInt32 OSversionInfoSize = 0;
	public UInt32 MajorVersion = 0;
	public UInt32 MinorVersion = 0;
	public UInt32 BuildNumber = 0;
	public UInt32 PlatformId = 0;
	
	[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
	public String CSDVersion = null;
}

internal sealed class MyClass {
	[DllImport("Kernel32", CharSet = CharSet.Auto, SetLastError = true)]
	//将In和Out特性应用于参数ver
	public static extern Boolean GetVersionEx([In, Out] OSVERSIONINFO ver);
}

CLR 允许将特性应用于可在文件的元数据中表示的几乎任何东西。不过最常应用特性的还是以下定义表中的记录项:TypeDef(类、结构、枚举、接口和委托),MethodDef(含构造器),ParamDef, FieldDef, PropertyDef, EventDef, AssemblyDef 和 ModuleDef。更具体地说,C#只允许将特性应用于定义以下任何目标元素的源代码:程序集、模块、类型(类、结构、枚举、接口、委托)、字段、方法(含构造器)、方法参数、方法返回值、属性、事件和泛型类型参数

应用特性时,C#允许用一个前缀明确指定特性要应用于的目标元素。以下代码展示了所有可能的前缀。许多时候,即使省略前缀,编译器也能判断特性要应用于什么目标元素(就像上例展示的那样)。但在其他时候,必须指定前缀向编译器清楚表明我们的意图。下面倾斜显示的前缀是必须的。

示例——特性前缀
using System;

[assembly: SomeAttr] //应用于程序集
[module: SomeAttr]   //应用于模块

[type: SomeAttr]	 //应用于类型
internal sealed class SomeType<[typevar: SomeAttr] T> { //应用于泛型类型变量
	[field: SomeAttr] //应用于字段
	public Int32 SomeAttrField = 0;
	
	[return: SomeAttr] //应用于返回值
	[method: SomeAttr] //应用于方法
	public Int32 SomeMethod (
		[param: SomeAttr]	//应用于参数
		Int32 SomeParam) {
		return SomeParam;
	}
	
	[property: SomeAttr] //应用于属性
	public String SomeProp {
		[method: SomeAttr] //应用于get访问器方法
		get { return null; }
	}
	
	[event: SomeAttr] //应用于事件
	[field: SomeAttr] //应用于编译器生成的字段
	[method: SomeAttr] //应用于编译器生成的 add & remove 方法
	public event EventHandler SomeEvent;
}

前面介绍了如何应用定制特性,现在看看特性到底是什么。定制特性其实是一个类型的实例。为了符合“公共语规范”(CLS)的要求,定制特性类必须直接或间接从公共抽象类System.Attribute 派生。C#只允许符合 CLS 规范的特性。查看文档会发现定义了以下类(参见前面的例子):StructLayoutAttribute,MarshalAsAttribute,DllImportAttribute,InAttributeOutAttribute。所有这些类碰巧都在 System.Runtime.InteropServices 命名空间中定义。但特性类可以在任何命名空间中定义。进一步查看,会发现所有这些类都从System.Attribute 派生,所有符合 CLS 规范的特性类都肯定从这个类派生。

注意   将特性应用于源代码中的目标元素时,C#编译器允许省略 Attribute后缀以减少打字量,并提升源代码的可读性。本章许多示例代码都利用了C#提供的这一便利。例如,许多源代码用的都是[DllImport(...)],而不是[DllImportAttribute(...)]。

如前所述,特性是类的实例。类必须有公共构造器才能创建它的实例。所以,将特性应用于目标元素时,语法类似于调用类的某个实例构造器。除此之外,语言可能支持一些特殊的语法,允许设置与特性类关联的公共字段或属性。前面的例子将 DllImport 特性应用于 GetVersionEx方法:
[DllImport("Kerne132", Charset = Charset.Auto, SetLastError = true)]
这一行代码的语法表面上看很奇怪,因为调用构造器时永远不会使用这样的语法。查阅DllImportAttribute 类的文档,会发现它的构造器要求接受一个 String 参数。在这个例子中,"Kernel32"将传给这个参数。构造器的参数称为定位参数(positional parameter),而且是强制性的;也就是说,应用特性时必须指定参数。

那么,另外两个“参数”是什么呢?这种特殊的语法允许在构造好 DllImportAttribute 对象后设置对象的任何公共字段或属性。在这个例子中,将"Kernel32"传给构造器并构造好DllImportAttribute 对象之后,对象的公共实例字段 CharSet 和 SetLastError 分别设为CharSet.Auto 和 true。用于设置字段或属性的“参数”称为命名参数(named parameter)。这种参数是可选的,因为在应用特性的实例时不一定要指定参数。稍后会解释是什么导致了实际地构造 DllImportAttribute 类的实例。

还要注意,可将多个特性应用于一个目标元素。例如,在本章的第一个示例程序中,GeVersionEx方法的 ver 参数同时应用了 In 和 Out 这两个特性。将多个特性应用于单个目标元素时,注意特性的顺序无关紧要。另外,在 C#中,既可将每个特性都封闭到一对方括号中,也可在一对方括号中封闭多个以逗号分隔的特性。如果特性类的构造器不获取参数,那么圆括号可以省略。最后,就像前面说到的那样,Attribute 后缀也是可选的。下面代码行具有相同的行为,它们演示了应用多个特性时所有可能的方式:
[Serializable][Flags]
[Serializable, Flags]
[FlagsAttribute, SerializableAttribute]
[FlagsAttribute()][Serializable()]

定义自己的特性类

示例——定义自己的特性类
namespace System {
	public class FlagsAttribute : System.Attribute {
		public FlagsAttribute() {
		
		}
	}
}

注意,FlagsAttribute 类从 Attribute 继承。这使 FlagsAttribute 类成为符合 CLS 规范的定制特性。此外,注意类名有 Attrtibute 后缀:这是为了保持与标准的相容性,但这并不是必须的。最后,所有非抽象特性至少要包含一个公共构造器。上述代码中的 FlagsAttribute构造器非常简单,不获取任何参数,也不做任何事情。

重要提示   应将特性想像成逻辑状态容器。也就是说,虽然特性类型是一个类,但这个类应该很简单。应该只提供一个公共构造器来接受特性的强制性(或定位性)状态信息,而且这个类可以提供公共字段和属性,以接受特性的可选(或命名)状态信息。类不应提供任何公共方法、事件或其他成员。

我通常不鼓励使用公共字段。特性也不例外,我同样不鼓励在这种类型中使用公共字段。使用属性要好得多。因为在更改特性类的实现方式时,属性能提供更大的灵活性。

现在的情况是 FlagsAttribute 类的实例能应用于任何目标元素。但事实上,这个特性应该只能应用于枚举类型,应用于属性或方法是没有意义的。为了告诉编译器这个特性的合法应用范围,需要向特性类应用 System.AttributeUsageAttribute 类的实例。下面是新的代码:

示例——定义自己的特性类
namespace System {
	//AttributeUsage告诉编译器此特性只能用于枚举
	//Inherited 该值确定指示的属性是否由派生类和重写成员继承
	[AttributeUsage(AttributeTargets.Enum, Inherited = false)]
	public class FlagsAttribute : System.Attribute {
		public FlagsAttribute() {
		
		}
	}
}

新版本将 AttributeUsageAttribute 的实例应用于特性。毕竟,特性类型本质上还是类,而类是可以应用特性的。AttributeUsage 特性是一个简单的类,可利用它告诉编译器定制特性的合法应用范围。所有编译器都内建了对该特性的支持,并会在定制特性应用于无效目标时报错。在这个例子中,AttributeUsage 特性指出 Flags 特性的实例只能应用于枚举类型的目标。

示例——AttributeUsageAttribute
[Serializable]
[AttributeUsageAttribute(AttributeTargets.Class, Inherited=true)]
public sealed class AttributeUsageAttribute : Attribute {
	internal static AttributeUsageAttribute Default =
		new AttributeUsageAttribute(AttributeTargets.All);
	
	internal Boolean m_allowMultiple = false;
	internal AttributeTargets m_attributeTarget = AttributeTargets.All;
	internal Boolean m_inherited = true;
	
	// 这是一个公共构造器
	public AttributeUsageAttribute(AttributeTargets validOn) {
		m_attributeTarget = validOn;
	}
	
	internal AttributeUsageAttribute(AttributeTargets validOn, 
		Boolean allowMultiple, Boolean inherited) {
		m_attributeTarget = validOn;
		m_allowMultiple = allowMultiple;
		m_inherited = inherited;
	}
	
	//是否允许特性向目标元素应用多次
	public Boolean AllowMultiple {
		get { return m_allowMultiple; }
		set { m_allowMultiple = value; }
	}
	
	//派生类是否可继承特性
	public Boolean Inherited {
		get { return m_inherited; }
		set { m_inherited = value; }
	}
	
	public Boolean ValidOn {
		get { return m_attributeTarget; }
	}
}

示例——AttributeTargets
[ComVisible(true)]
[Flags]
public enum AttributeTargets
{
	//可以对程序集应用特性。
	Assembly = 1,
	//可以对模块应用特性。
	Module = 2,
	//可以对类应用特性。
	Class = 4,
	//可以对结构应用性,即值类型。
	Struct = 8,
	//可以对枚举应用特性。
	Enum = 16,
	//可以对构造函数应用特性。
	Constructor = 32,
	//可以对方法应用特性。
	Method = 64,
	//可以对属性 (Property) 应用特性。
	Property = 128,
	//可以对字段应用特性。
	Field = 256,
	//可以对事件应用特性。
	Event = 512,
	//可以对接口应用特性。
	Interface = 1024,
	//可以对参数应用特性。
	Parameter = 2048,
	//可以对委托应用特性。
	Delegate = 4096,
	//可以对返回值应用特性。
	ReturnValue = 8192,
	//可以对泛型参数应用特性。
	GenericParameter = 16384,
	//可以对任何应用程序元素应用特性。
	All = 32767
}

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

注意   定义自己的特性类时,如果忘记向自己的类应用 AttributeUsage 特性,编译器和 CLR 将假定该特性能应用于所有目标元素,向每个目标元素都只能应用一次,而且可继承,这些假定模仿了 AttributeUsageAttribute 类中的默认字段值。

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

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

应用特性时必须传递一个编译时常量表达式,它与特性类定义的类型匹配。在特性类定义了一个Type参数、Type 字段或者 Type 属性的任何地方,都必须使用 C# typeof操作符(如下例所示)。在特性类定义了一个 Object 参数、Object 字段或者 Object 属性的任何地方,都可传递一个 Int32、String 或其他任何常量表达式(包括 null)如果常量表达式代表值类型,那么在运行时构造特性的实例时会对值类型进行装箱。以下是一个示例特性及其用法:

示例
using System;

internal 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,最后是具体的值。如果是数组,则会先保存数组元素的个数,再跟上每个单独的元素。

检测定制特性

仅仅定义特性类没有用。确实可以定义自己想要的所有特性类,并应用自己想要的所有实例。但这样除了在程序集中生成额外的元数据,没有其他任何意义。应用程序代码的行为不会有任何改变。

System.Reflection.CustomAttributeExtensions定义的三个静态方法
方法名称 说明
IsDefined 如果至少有一个指定的 Attribute 派生类的实例与目标关联,就返回true。这个方法效率很高,因为它不构造(反序列化)特性类的任何实例
GetCustomAttributes 返回应用于目标的指定特性对象的集合。每个实例都使用编译时指定的参数、字段和属性来构造(反序列化)。如果目标没有应用指定特性类的实例,就返回一个空集合。该方法通常用于已将 AllowMultiple 设为 true 的特性,或者用于列出已应用的所有特性
GetCustomAttribute 返回应用于目标的指定特性类的实例。实例使用编译时指定的参数、字段和属性来构造(反序列化)。如果目标没有应用指定特性类的实例,就返回 null。如果目标应用了指定特性的多个实例,就抛出 System.Reflection.AmbiguousMatchException 异常。该方法通常用于已将 AllowMultiple 设为 false 的特性

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

System.Reflection 命名空间定义了几个类允许检查模块的元数据。这些类包括 Assembly,Module, ParameterInfo, MemberInfo, Type, MethodInfo, ConstructorInfo, FieldInfo, EventInfo, PropertyInfo 及其各自的 *Builder 类。所有类都提供了 IsDefinedGetCustomAttributes 方法。

反射类提供的 GetCustomAttributes 方法返回的是由 Object 实例构成的数组(Object[]),而不是由 Attribute 实例构成的数组(Attribute[])。这是由于反射类能返回不相容于 CLS 规范的特性类的对象。不过,大可不必关心这种不一致性,因为非 CLS 相容的特性是很稀少的。事实上,我与.NET Framework 打交道至今,还没有见过一例。

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

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

以下示例代码列出了一个类型中定义的所有方法,并显示应用于每个方法的特性。代码仅供演示,平时不会像这样将这些定制特性应用于这些目标元素。

using System;
using System.Diagnostics;
using System.Reflection;
using System.Linq;

[assembly: CLSCompliant(true)]

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

    public Program() { }

    [CLSCompliant(true)]
    [STAThread]
    static void Main(string[] args)
    {
        // 显示应用于这个类型的特性集
        ShowAttributes(typeof(Program));

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

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

        Console.ReadLine();
    }

    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)
        {
            // 显示所应用的每个特性的类型
            Console.WriteLine(" {0}", attribute.GetType().ToString());

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

            if (attribute is ConditionalAttribute)
                Console.WriteLine(" ConditionalString={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();
    }
}

运行测试
111111.png


两个特性实例的相互匹配

除了判断是否向目标应用了一个特性的实例,可能还需要检查特性的字段来确定它们的值。一个办法是老老实实写代码检查特性类的字段值。但 System.Attribute 重写了 Object 的Equals 方法,会在内部比较两个对象的类型。不一致会返回 false。如果一致,Equals会利用反射来比较两个特性对象中的字段值(为每个字段都调用 Equals)。所有字段都匹配就返回 true:否则返回 false。可在自己的定制特性类中重写 Equals 来移除反射的使用,从而提升性能。

System.Attribute 还公开了虚方法 Match,可重写它来提供更丰富的语义。Match 的默认实现只是调用 Equal 方法并返回它的结果。下例演示了如何重写 Equals 和 Match,后者在一个特性代表另一个特性的子集的前提下返回 true。另外,还演示了如何使用 Match。

using System;

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

    [AttributeUsage(AttributeTargets.Class)]
    internal sealed class AccountsAttribute : Attribute
    {
        private Accounts m_accounts;

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

        public override bool Match(object obj)
        {
            // 如果基类实现了Match,而且基类不是
            // Attribute,就取消对下面这行代码的注释
            // 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'的账户是不是
            // other的账户的一个子集
            if ((other.m_accounts & m_accounts) != m_accounts)
                return false;

            return true; //对象匹配
        }

        public override bool Equals(object obj)
        {
            // 如果基类实现了Equals,而且基类不是
            // Object,就取消对下面这行代码的注释:
            // if (!base.Equals(obj)) return false;

            // 由于'this'不为null,所以假如object为null,
            // 那么对象肯定不相等
            // 注意:如果你信任基类正确实现了Equals,
            // 那么下面这一行可删除
            if (obj == null) return false;

            // 如果对象属于不同的类型,肯定不相等
            // 注意:如果你信任基类正确实现了Equals
            // 那么下面这一行可以删除
            if (this.GetType() != obj.GetType()) return false;

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

            // 比较字段,判断它们是否有相同的值
            // 这个例子判断'this'的账户是不是
            // 与other的账户相同
            if (other.m_accounts != m_accounts)
                return false;

            return true; //对象相等
        }

        // 重写GetHashCode,因为我们重写了Equals
        public override int GetHashCode()
        {
            return (Int32)m_accounts;
        }
    }

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

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

    public sealed class Program
    {
        public static void Main(string[] args)
        {
            CanWriteCheck(new ChildAccount());
            CanWriteCheck(new AdultAccount());

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

            Console.ReadLine();
        }

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

            // 构造应用于类型的特性实例
            Attribute validAccounts = Attribute.GetCustomAttribute(
                obj.GetType(), typeof(AccountsAttribute), false);

            // 如果向精英应用了特性,而且特性指定了
            // 'Checking'账户,表明该类型可以开支票
            if ((validAccounts != null) && checking.Match(validAccounts))
            {
                Console.WriteLine("{0} types can write checks.", obj.GetType());
            }
            else
            {
                Console.WriteLine("{0} types can NOT write checks.", obj.GetType());
            }
        }
    }
}

运行测试
11111.png

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

本节将讨论如何利用另一种技术检测应用于元数据记录项的特性。在某些安全性要求严格的场合,这个技术能保证不执行从 Attribute 派生的类中的代码。毕竟,调用 Attribute 的GeCustomAttribute 或者 GetCustomAttributes 方法时,这些方法会在内部调用特性类的构造器,而且可能调用属性的 set 访问器方法。此外,首次访问类型会造成 CLR 调用类型的类型构造器(如果有的话)。在构造器、set 访问器方法以及类型构造器中,可能包含每次查找特性都要执行的代码。这就相当于允许未知代码在 AppDomain中运行,所以存在安全隐患。

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

CustomAttributeDataGetCustomAttributes 方法是一个工厂(factory)方法。也就是说。调用它会返回一个 IList<CustomAttributeData> 类型的对象,其中包含了由 CustomAttributeData对象构成的集合。集合中的每个元素都是应用于指定目标的一个定制特性。可查询每个CustomAttributeData 对象的只读属性,判断特性对象如何构造和初始化。具体地说, Constructor 属性指出构造器方法将如何调用。ConstructorArguments 属性以一个IList<CustomAttributeTypedArgument> 实例的形式返回将传给这个构造器的实参。而NamedArguments 属性以一个 IList<CustomAttributeNamedArgument> 实例的形式,返回将设置的字段/属性。注意,之所以说“将”,是因为不会实际地调用构造器和 set 访问器方法。禁止执行特性类的任何方法增强了安全性。

下面是之前例子的修改版本,它利用 CustomAttributeData 类来安全地获取应用于各个目标的特性:

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

[assembly: CLSCompliant(true)]

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

    public Program() { }

    [CLSCompliant(true)]
    [STAThread]
    static void Main(string[] args)
    {
        // 显示应用于这个类型的特性集
        ShowAttributes(typeof(Program));

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

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

        Console.ReadLine();
    }

    private static void ShowAttributes(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)
        {
            // 显示所应用的每个特性的类型
            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();
    }
}

运行测试
11111.png


条件特性类

定义、应用和反射特性能带来许多便利,所以开发人员越来越频繁地使用这些技术。特性简化了对代码的注释,还能实现丰富的功能。近来,开发人员越来越喜欢在设计和调试期间利用特性来辅助开发。例如,Microsoft Visual Studio 代码分析工具(FxCopCmd.exe)提供了一个 System.Diagnostics.CodeAnalysis.SuppressMessageAttribute,可将它应用于类型和成员,从而阻止报告特定的静态分析工具规则冲突(rule violation)。该特性仅对代码分析工具有用:程序平常运行时不会关注它。没有使用代码分析工具时,将 SuppressMessage 特性留在元数据中会使元数据无谓地膨胀,这会使文件变得更大,增大进程的工作集,损害应用程序的性能。假如有一种简单的方式,使编译器只有在使用代码分析工具时才生成SuppressMessage 特性,结果会好很多。幸好,利用条件特性类真的能做到这一点。

应用了 System.Diagnostics.ConditionalAttribute 的特性类称为条件特性类。下面是一个例子:

示例
//#define TEST
#define VERIFY

using System;
using System.Diagnostics;

[Conditional("TEST")][Conditional("VERIFY")]
public sealed class CondAttribute :  Attribute {
}

[Cond]
public sealed class Program {
	public static void Main() {
		Console.WriteLine("CondAttribute is {0}applied to Program type.",
			Attribute.IsDefined(typeof(Program),
			typeof(CondAttribute)) ? "" : "not ");
	}
}

编译器如果发现向目标元素应用了 CondAttribute 的实例,那么当含有目标元素的代码编译时,只有在定义 TEST 或 VERIFY 符号的前提下,编译器才会在元数据中生成特性信息。不过,特性类的定义元数据和实现仍存在于程序集中。

标签: C#

Powered by emlog  蜀ICP备18021003号-1   sitemap

川公网安备 51019002001593号