# Chapter 18 Custom Attributes

# Using Custom Attributes

Attributes, such as public, private, static, and so on, can be applied to types and members. I think we’d all agree on the usefulness of applying attributes, but wouldn’t it be even more useful if we could define our own attributes? For example, what if I could define a type and somehow indicate that the type can be remoted via serialization? Or maybe I could apply an attribute to a method to indicate that certain security permissions must be granted before the method can execute.

Of course, creating and applying user-defined attributes to types and methods would be great and convenient, but it would require the compiler to be aware of these attributes so it would emit the attribute information into the resulting metadata. Because compiler vendors usually prefer not to release the source code for their compiler, Microsoft came up with another way to allow user-defined attributes. This mechanism, called custom attributes, is an incredibly powerful mechanism that’s useful at both application design time and run time. Anyone can define and use custom attributes, and all compilers that target the common language runtime (CLR) must be designed to recognize custom attributes and emit them into the resulting metadata.

The first thing you should realize about custom attributes is that they’re just a way to associate additional information with a target. The compiler emits this additional information into the managed module’s metadata. Most attributes have no meaning for the compiler; the compiler simply detects the attributes in the source code and emits the corresponding metadata.

The .NET Framework Class Library (FCL) defines literally hundreds of custom attributes that can be applied to items in your own source code. Here are some examples:

  • Applying the DllImport attribute to a method informs the CLR that the implementation of the method is actually in unmanaged code contained in the specified DLL.

  • Applying the Serializable attribute to a type informs the serialization formatters that an instance’s fields may be serialized and deserialized.

  • Applying the AssemblyVersion attribute to an assembly sets the version number of the assembly.

  • Applying the Flags attribute to an enumerated type causes the enumerated type to act as a set of bit flags.

Following is some C# code with many attributes applied to it. In C#, you apply a custom attribute to a target by placing the attribute in square brackets immediately before the target. It’s not important to understand what this code does. I just want you to see what attributes look like.

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)] 
 public static extern Boolean GetVersionEx([In, Out] OSVERSIONINFO ver); 
}

In this case, the StructLayout attribute is applied to the OSVERSIONINFO class, the MarshalAs attribute is applied to the CSDVersion field, the DllImport attribute is applied to the GetVersionEx method, and the In and Out attributes are applied to GetVersionEx’s ver parameter. Every programming language defines the syntax a developer must use in order to apply a custom attribute to a target. Microsoft Visual Basic .NET, for example, requires angle brackets (<, >) instead of square brackets.

The CLR allows attributes to be applied to just about anything that can be represented in a file’s metadata. Most commonly, attributes are applied to entries in the following definition tables: TypeDef (classes, structures, enumerations, interfaces, and delegates), MethodDef (including constructors), ParamDef, FieldDef, PropertyDef, EventDef, AssemblyDef, and ModuleDef. Specifically, C# allows you to apply an attribute only to source code that defines any of the following targets: assembly, module, type (class, struct, enum, interface, delegate), field, method (including constructors), method parameter, method return value, property, event, and generic type parameter.

When you’re applying an attribute, C# allows you to specify a prefix specifically indicating the target the attribute applies to. The following code shows all of the possible prefixes. In many cases, if you leave out the prefix, the compiler can still determine the target an attribute applies to, as shown in the previous example. In some cases, the prefix must be specified to make your intentions clear to the compiler. The prefixes shown in italics in the following code are mandatory.

using System; 
[assembly: SomeAttr] // Applied to assembly 
[module: SomeAttr] // Applied to module 
[type: SomeAttr] // Applied to type 
internal sealed class SomeType<[typevar: SomeAttr] T> { // Applied to generic type variable
 [field: SomeAttr] // Applied to field 
 public Int32 SomeField = 0; 
 [return: SomeAttr] // Applied to return value 
 [method: SomeAttr] // Applied to method 
 public Int32 SomeMethod( 
 [param: SomeAttr] // Applied to parameter 
 Int32 SomeParam) { return SomeParam; } 
 [property: SomeAttr] // Applied to property 
 public String SomeProp { 
 [method: SomeAttr] // Applied to get accessor method 
 get { return null; } 
 } 
 [event: SomeAttr] // Applied to event 
 [field: SomeAttr] // Applied to compiler-generated field 
 [method: SomeAttr] // Applied to compiler-generated add & remove methods 
 public event EventHandler SomeEvent; 
}

Now that you know how to apply a custom attribute, let’s find out what an attribute really is. A custom attribute is simply an instance of a type. For Common Language Specification (CLS) compliance, custom attribute classes must be derived, directly or indirectly, from the public abstract System.Attribute class. C# allows only CLS-compliant attributes. By examining the .NET Framework SDK documentation, you’ll see that the following classes (from the earlier example) are defined: StructLayoutAttribute, MarshalAsAttribute, DllImportAttribute, InAttribute, and OutAttribute. All of these classes happen to be defined in the System.Runtime.InteropServices namespace, but attribute classes can be defined in any namespace. Upon further examination, you’ll notice that all of these classes are derived from System.Attribute, as all CLS-compliant attribute classes must be.

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

As I mentioned earlier, an attribute is an instance of a class. The class must have a public constructor so that instances of it can be created. So when you apply an attribute to a target, the syntax is similar to that for calling one of the class’s instance constructors. In addition, a language might permit some special syntax to allow you to set any public fields or properties associated with the attribute class. Let’s look at an example. Recall the application of the DllImport attribute as it was applied to the GetVersionEx method earlier.

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

The syntax of this line should look pretty strange to you because you could never use syntax like this when calling a constructor. If you examine the DllImportAttribute class in the documentation, you’ll see that its constructor requires a single String parameter. In this example, "Kernel32" is being passed for this parameter. A constructor’s parameters are called positional parameters and are mandatory; the parameter must be specified when the attribute is applied.

What are the other two “parameters”? This special syntax allows you to set any public fields or properties of the DllImportAttribute object after the object is constructed. In this example, when the DllImportAttribute object is constructed and "Kernel32" is passed to the constructor, the object’s public instance fields, CharSet and SetLastError, are set to CharSet.Auto and true, respectively. The “parameters” that set fields or properties are called named parameters and are optional because the parameters don’t have to be specified when you’re applying an instance of the attribute. A little later on, I’ll explain what causes an instance of the DllImportAttribute class to actually be constructed.

Also note that it’s possible to apply multiple attributes to a single target. For example, in this chapter’s first program listing, the GetVersionEx method’s ver parameter has both the In and Out attributes applied to it. When applying multiple attributes to a single target, be aware that the order of attributes has no significance. Also, in C#, each attribute can be enclosed in square brackets, or multiple attributes can be comma-separated within a single set of square brackets. If the attribute class’s constructor takes no parameters, the parentheses are optional. Finally, as mentioned earlier, the Attribute suffix is also optional. The following lines behave identically and demonstrate all of the possible ways of applying multiple attributes.

[Serializable][Flags] 
[Serializable, Flags] 
[FlagsAttribute, SerializableAttribute] 
[FlagsAttribute()][Serializable()]

💡小结:利用定制特性(custom attribute),可宣告式地为自己的代码构造添加注解来实现特殊功能。定制特性允许为几乎每一个元数据记录项定义和应用信息。这种可扩展的元数据信息能在运行时查询,从而动态改变代码的执行方式。关于自定义特性,首先要知道它们只是将一些附加信息与某个目标元素关联起来的方式。编译器在托管模块的元数据中生成(嵌入)这些额外的信息。大多数特性对编译器来说没有意义:编译器只是机械地检测源代码中的特性,并生成对应的元数据。每种编程语言都定义了将定制特性应用于目标元素所采用的语法。例如,Microsoft Visual Basic .NET 要求使用一对尖括号(<>)而不是方括号。CLR 允许将特性应用于可在文件的元数据中表示的几乎任何东西。更具体地说,C# 只允许将特性应用于定义以下任何目标元素的源代码:程序集、模块、类型(类、结构、接口、委托)、字段、方法(含构造器)、方法参数、方法返回值、属性、事件和泛型类型参数。应用特性时,C# 允许用一个前缀明确指定特性要应用于的目标元素。许多时候,即使省略前缀,编译器也能判断特性要应用于什么目标元素。但在其他时候,必须指定前缀向编译器清楚表明我们的意图。定制特性其实是一个类型的实例。为了符合 “公共语言规范”(CLS)的要求,定制特性类必须直接或间接从公共抽象类 System.Attribute 派生。C# 只允许符合 CLS 规范的特性。如前所述,特性是类的实例。类必须有公共构造器才能创建它的实例。所以,将特性应用于目标元素时,语法类似于调用类的某个实例构造器。构造器的参数称为定位参数(positional parameter),而且是强制性的;也就是说,应用特性时必须指定参数。用于设置字段或属性的 “参数” 称为命名参数(named parameter)。这种参数是可选的,因为在应用特性的实例时不一定要指定参数。将多个特性应用于单个目标元素时,注意特性的顺序无关紧要。另外,在 C# 中,既可将每个特性都封闭到一对方括号中,也可在一对方括号中封闭多个以逗号分隔的特性。如果特性类的构造器不获取参数,那么圆括号可以省略。最后, Attribute 后缀也是可选的。

# Defining Your Own Attribute Class

You know that an attribute is an instance of a class derived from System.Attribute, and you also know how to apply an attribute. Let’s now look at how to define your own custom attribute classes. Say you’re the Microsoft employee responsible for adding the bit flag support to enumerated types. To accomplish this, the first thing you have to do is define a FlagsAttribute class.

namespace System { 
 public class FlagsAttribute : System.Attribute { 
 public FlagsAttribute() { 
 } 
 } 
}

Notice that the FlagsAttribute class inherits from Attribute; this is what makes the FlagsAttribute class a CLS-compliant custom attribute. In addition, the class’s name has a suffix of Attribute; this follows the standard convention but is not mandatory. Finally, all non-abstract attributes must contain at least one public constructor. The simple FlagsAttribute constructor takes no parameters and does absolutely nothing.

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

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

So far, instances of the FlagsAttribute class can be applied to any target, but this attribute should really be applied to enumerated types only. It doesn’t make sense to apply the attribute to a property or a method. To tell the compiler where this attribute can legally be applied, you apply an instance of the System.AttributeUsageAttribute class to the attribute class. Here’s the new code.

namespace System { 
 [AttributeUsage(AttributeTargets.Enum, Inherited = false)] 
 public class FlagsAttribute : System.Attribute { 
 public FlagsAttribute() { 
 } 
 } 
}

In this new version, I’ve applied an instance of AttributeUsageAttribute to the attribute. After all, the attribute type is just a class, and a class can have attributes applied to it. The AttributeUsage attribute is a simple class that allows you to specify to a compiler where your custom attribute can legally be applied. All compilers have built-in support for this attribute and generate errors when a user-defined custom attribute is applied to an invalid target. In this example, the AttributeUsage attribute specifies that instances of the Flags attribute can be applied only to enumerated type targets.

Because all attributes are just types, you can easily understand the AttributeUsageAttribute class. Here’s what the FCL source code for the class looks like.

[Serializable] 
[AttributeUsage(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; 
 // This is the one public constructor 
 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 AttributeTargets ValidOn { 
 get { return m_attributeTarget; } 
 } 
}

As you can see, the AttributeUsageAttribute class has a public constructor that allows you to pass bit flags that indicate where your attribute can legally be applied. The System.AttributeTargets enumerated type is defined in the FCL as follows.

[Flags, Serializable] 
public enum AttributeTargets { 
 Assembly = 0x0001, 
 Module = 0x0002, 
 Class = 0x0004, 
 Struct = 0x0008, 
 Enum = 0x0010, 
 Constructor = 0x0020, 
 Method = 0x0040, 
 Property = 0x0080, 
 Field = 0x0100, 
 Event = 0x0200, 
 Interface = 0x0400, 
 Parameter = 0x0800, 
 Delegate = 0x1000, 
 ReturnValue = 0x2000, 
 GenericParameter = 0x4000, 
 All = Assembly | Module | Class | Struct | Enum | 
 Constructor | Method | Property | Field | Event | 
 Interface | Parameter | Delegate | ReturnValue | 
 GenericParameter 
}

The AttributeUsageAttribute class offers two additional public properties that can optionally be set when the attribute is applied to an attribute class: AllowMultiple and Inherited .

For most attributes, it makes no sense to apply them to a single target more than once. For example, nothing is gained by applying the Flags or Serializable attributes more than once to a single target. In fact, if you tried to compile the following code, the compiler would report the following message: error CS0579: Duplicate 'Flags' attribute.

[Flags][Flags] 
internal enum Color { 
 Red 
}

For a few attributes, however, it does make sense to apply the attribute multiple times to a single target. In the FCL, the ConditionalAttribute attribute class allows multiple instances of itself to be applied to a single target. If you don’t explicitly set AllowMultiple to true, your attribute can be applied no more than once to a selected target.

AttributeUsageAttribute ’s other property, Inherited, indicates if the attribute should be applied to derived classes and overriding methods when applied on the base class. The following code demonstrates what it means for an attribute to be inherited.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited=true)] 
internal class TastyAttribute : Attribute { 
} 
[Tasty][Serializable] 
internal class BaseType { 
 [Tasty] protected virtual void DoSomething() { } 
} 
internal class DerivedType : BaseType { 
 protected override void DoSomething() { } 
}

In this code, DerivedType and its DoSomething method are both considered Tasty because the TastyAttribute class is marked as inherited. However, DerivedType is not serializable because the FCL’s SerializableAttribute class is marked as a noninherited attribute.

Be aware that the .NET Framework considers targets only of classes, methods, properties, events, fields, method return values, and parameters to be inheritable. So when you’re defining an attribute type, you should set Inherited to true only if your targets include any of these targets. Note that inherited attributes do not cause additional metadata to be emitted for the derived types into the managed module. I’ll say more about this a little later in the “Detecting the Use of a Custom Attribute” section.

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

💡小结:为了告诉编译器自定义特性的合法应用范围,需要向特性类应用 System.AttributeUsageAttribute 类的实例。 AttributeUsage 特性是一个简单的类,可利用它告诉编译器定制特性的合法应用范围。所有编译器都内建了对该特性的支持,并会在定制特性应用于无效目标时报错。大多数特性多次应用于同一个目标是没有意义的。但少数几个特性确实有必要多次应用于同一个目标。FCL 特性类 ConditionalAttribute 允许将它的多个实例应用于同一个目标元素。不将 AllowMultiple 明确设为 true,特性就只能向选定的目标元素应用一次。 AttributeUsageAttribute 的另一个属性是 Inherited ,它指出特性在应用于基类时,是否同时应用于派生类和重写的方法。.NET Framework 只认为类、方法、属性、事件、字段、方法返回值和参数等目标元素是可继承的。所以,定义特性类型时,只有在该特性应用于上述某个目标的前提下,才应该将 Inherited 设为 true。注意,可继承特性不会造成在托管模块中为派生类型生成额外的元数据。

# Attribute Constructor and Field/Property Data Types

When defining your own custom attribute class, you can define its constructor to take parameters that must be specified by developers when they apply an instance of your attribute type. In addition, you can define nonstatic public fields and properties in your type that identify settings that a developer can optionally choose for an instance of your attribute class.

When defining an attribute class’s instance constructor, fields, and properties, you must restrict yourself to a small subset of data types. Specifically, the legal set of data types is limited to the following: Boolean, Char, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double, String, Type, Object, or an enumerated type. In addition, you can use a single-dimensional, zero-based array of any of these types. However, you should avoid using arrays because a custom attribute class whose constructor takes an array is not CLS-compliant.

When applying an attribute, you must pass a compile-time constant expression that matches the type defined by the attribute class. Wherever the attribute class defines a Type parameter, Type field, or Type property, you must use C#’s typeof operator, as shown in the following code. Wherever the attribute class defines an Object parameter, Object field, or Object property, you can pass an Int32, String, or any other constant expression (including null). If the constant expression represents a value type, the value type will be boxed at run time when an instance of the attribute is constructed.

Here’s an example of an attribute and its usage.

using System; 
internal enum Color { Red } 
[AttributeUsage(AttributeTargets.All)] 
internal sealed class SomeAttribute : Attribute { 
 public SomeAttribute(String name, Object o, Type[] types) { 
 // 'name' refers to a String 
 // 'o' refers to one of the legal types (boxing if necessary) 
 // 'types' refers to a 1-dimension, 0-based array of Types 
 } 
} 
[Some("Jeff", Color.Red, new Type[] { typeof(Math), typeof(Console) })] 
internal sealed class SomeType { 
}

Logically, when a compiler detects a custom attribute applied to a target, the compiler constructs an instance of the attribute class by calling its constructor, passing it any specified parameters. Then the compiler initializes any public fields and properties by using the values specified via the enhanced constructor syntax. Now that the custom attribute object is initialized, the compiler serializes the attribute object’s state out to the target’s metadata table entry.

💡重要提示:为方便理解,可以这样想象定制特性:它是类的实例,被序列化成驻留在元数据中的字节流。运行时可对元数据中的字节进行反序列化,从而构造出类的实例。实际发生的事情是:编译器在元数据中生成创建特性类的实例所需的信息。每个构造器参数都会 1 字节的类型 ID,后跟具体的值。对构造器的参数进行 “序列化” 时,编译器先写入字段 / 属性名称,再跟上 1 字节的类型 ID,最后是具体的值。如果是数组,则会先保存数组元素的个数,再跟上每个单独的元素。

💡小结:定义特性类的实例构造器、字段和属性时,可供选择的数据类型并不多。具体地说,只允许 BooleanCharByteSByteInt16UInt16Int32UInt32Int64UInt64SingleDoubleStringTypeObject 或枚举类型。此外,可使用上述任意类型的一维 0 基数组。但应尽量避免使用数组,因为对于定制特性,如果它的构造器要获取数组作为参数,就会失去与 CLS 的相容性。构造并初始化好定制特性类的对象之后,编译器将它的状态序列化到目标元素的元数据表记录项中。

# Detecting the Use of a Custom Attribute

Defining an attribute class is useless by itself. Sure, you could define attribute classes all you want and apply instances of them all you want, but this would just cause additional metadata to be written out to the assembly—the behavior of your application code wouldn’t change.

In Chapter 15, “Enumerated Types and Bit Flags,” you saw that applying the Flags attribute to an enumerated type altered the behavior of System.Enum ’s ToString and Format methods. The reason that these methods behave differently is that they check at run time if the enumerated type that they’re operating on has the Flags attribute metadata associated with it. Code can look for the presence of attributes by using a technology called reflection. I’ll give some brief demonstrations of reflection here, but I’ll discuss it fully in Chapter 23, “Assembly Loading and Reflection.”

If you were the Microsoft employee responsible for implementing Enum ’s Format method, you would implement it like the following.

public override String ToString() { 
 // Does the enumerated type have an instance of 
 // the FlagsAttribute type applied to it? 
 if (this.GetType().IsDefined(typeof(FlagsAttribute), false)) { 
 // Yes; execute code treating value as a bit flag enumerated type. 
 ... 
 } else { 
 // No; execute code treating value as a normal enumerated type. 
 ... 
 } 
 ... 
}

This code calls Type’s IsDefined method, effectively asking the system to look up the metadata for the enumerated type and see whether an instance of the FlagsAttribute class is associated with it. If IsDefined returns true, an instance of FlagsAttribute is associated with the enumerated type, and the Format method knows to treat the value as though it contained a set of bit flags. If IsDefined returns false, Format treats the value as a normal enumerated type.

So if you define your own attribute classes, you must also implement some code that checks for the existence of an instance of your attribute class (on some target) and then execute some alternate code path. This is what makes custom attributes so useful!

The FCL offers many ways to check for the existence of an attribute. If you’re checking for the existence of an attribute via a System.Type object, you can use the IsDefined method as shown earlier. However, sometimes you want to check for an attribute on a target other than a type, such as an assembly, a module, or a method. For this discussion, let’s concentrate on the extension methods defined by the System.Reflection.CustomAttributeExtensions class. This class defines three static methods for retrieving the attributes associated with a target: IsDefined , GetCustomAttributes , and GetCustomAttribute . Each of these functions has several overloaded versions. For example, each method has a version that works on type members (classes, structs, enums, interfaces, delegates, constructors, methods, properties, fields, events, and return types), parameters, and assemblies. There are also versions that allow you to tell the system to walk up the derivation hierarchy to include inherited attributes in the results. Table 18-1 briefly describes what each method does.

image-20221122113657675

If you just want to see if an attribute has been applied to a target, you should call IsDefined because it’s more efficient than the other two methods. However, you know that when an attribute is applied to a target, you can specify parameters to the attribute’s constructor and optionally set fields and properties. Using IsDefined won’t construct an attribute object, call its constructor, or set its fields and properties.

If you want to construct an attribute object, you must call either GetCustomAttributes or GetCustomAttribute. Every time one of these methods is called, it constructs new instances of the specified attribute type and sets each of the instance’s fields and properties based on the values specified in the source code. These methods return references to fully constructed instances of the applied attribute classes.

When you call any of these methods, internally, they must scan the managed module’s metadata, performing string comparisons to locate the specified custom attribute class. Obviously, these operations take time. If you’re performance conscious, you should consider caching the result of calling these methods rather than calling them repeatedly asking for the same information.

The System.Reflection namespace defines several classes that allow you to examine the contents of a module’s metadata: Assembly, Module, ParameterInfo, MemberInfo, Type, MethodInfo, ConstructorInfo, FieldInfo, EventInfo, PropertyInfo, and their respective *Builder classes. All of these classes also offer IsDefined and GetCustomAttributes methods.

The version of GetCustomAttributes defined by the reflection classes returns an array of Object instances (Object[]) instead of an array of Attribute instances (Attribute[]). This is because the reflection classes are able to return objects of non–CLS-compliant attribute classes. You shouldn’t be concerned about this inconsistency because non–CLS-compliant attributes are incredibly rare. In fact, in all of the time I’ve been working with the .NET Framework, I’ve never even seen one.

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

There’s one more thing you should be aware of: When you pass a class to IsDefined, GetCustomAttribute, or GetCustomAttributes, these methods search for the application of the attribute class you specify or any attribute class derived from the specified class. If your code is looking for a specific attribute class, you should perform an additional check on the returned value to ensure that what these methods returned is the exact class you’re looking for. You might also want to consider defining your attribute class to be sealed to reduce potential confusion and eliminate this extra check.

Here’s some sample code that lists all of the methods defined within a type and displays the attributes applied to each method. The code is for demonstration purposes; normally, you wouldn’t apply these particular custom attributes to these targets as I’ve done here.

using System; 
using System.Diagnostics; 
using System.Reflection; 
[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] 
 public static void Main() { 
 // Show the set of attributes applied to this type 
 ShowAttributes(typeof(Program));
 // Get the set of methods associated with the type 
 var members = 
 from m in typeof(Program).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(); 
 } 
}

Building and running this application yields the following output.

Attributes applied to Program: 
 System.SerializableAttribute 
 System.Diagnostics.DebuggerDisplayAttribute 
 Value=Richter, Name=Jeff, Target=Program 
 System.Reflection.DefaultMemberAttribute 
 MemberName=Main 
Attributes applied to DoSomething: 
 System.Diagnostics.ConditionalAttribute 
 ConditionString=Release 
 System.Diagnostics.ConditionalAttribute 
 ConditionString=Debug 
Attributes applied to Main: 
 System.CLSCompliantAttribute 
 IsCompliant=True 
 System.STAThreadAttribute 
Attributes applied to .ctor: None

💡小结:仅仅定义特性类没有用。确实可以定义自己想要的所有特性类,并应用自己想要的所有实例。但这样除了在程序集中生成额外的元数据,没有其他任何意义。应用程序代码的行为不会有任何改变。因此,在定义定制特性时,也必须实现一些代码来检测某些目标上是否存在该特性类的实例,然后执行一些逻辑分支代码。这样定制特性才能真正发挥作用。FCL 提供了多种方式来检测特性的存在。如果通过 System.Type 对象来检测特性,可以像前面展示的那样使用 IsDefined 方法。但有时需要检测除了类型之外的其他目标(比如程序集、模块或方法)上的特性。 System.Reflection.CustomAttributeExtensions 类定义了三个静态方法来获取与目标关联的特性: IsDefinedGetCustomAttributesGetCustomAttributes 。每个方法都有几个重载版本。例如,每个方法都有一个版本能操作类型成员 (类、结构、枚举、接口、委托、构造器、方法、属性、字段、事件和返回类型)、参数、模块和程序集。还有一些版本能指示系统遍历继承层次结构,在结果中包含继承的特性。它们在元数据上反射以查找与 CLS 相容的定制特性类型的实例。调用上述任何方法,内部都必须扫描托管模块的元数据,执行字符串比较来定位指定的定制特性类。显然,这些操作会耗费一定时间。假如对性能的要求比较高,可考虑缓存这些方法的调用结果,而不是反复调用来请求相同的信息。反射类提供的 GetCustomAttribute 方法返回的是由 Object 实例构成的数组 ( Object[] ),而不是由 Attribute 实例构成的数组 ( Attribute[] )。这是由于反射类能返回不相容于 CLS 规范的特性类的对象。不过,大可不必关心这种不一致性,因为非 CLS 相容的特性是很稀少的。还要注意,将一个类传给 IsDefinedGetCustomAttribute 或者 GetCustomAttribute 方法时,这些方法会检测是否应用了指定的特性类或者它的派生类。如果只是想搜索一个具体的特性类,应针对返回值执行一次额外的检查,确保方法返回的正是想搜索的类。还可考虑将自己的特性类定义成 sealed ,减少可能存在的混淆,并避免执行这个额外的检查。

# Matching Two Attribute Instances Against Each Other

Now that your code knows how to check whether an instance of an attribute is applied to a target, it might want to check the fields of the attribute to see what values they have. One way to do this is to write code that explicitly checks the values of the attribute class’s fields. However, System.Attribute overrides Object’s Equals method, and internally, this method compares the types of the two objects. If they are not identical, Equals returns false. If the types are identical, then Equals uses reflection to compare the values of the two attribute objects’ fields (by calling Equals for each field). If all the fields match, then true is returned; otherwise, false is returned. You might override Equals in your own attribute class to remove the use of reflection, improving performance.

System.Attribute also exposes a virtual Match method that you can override to provide richer semantics. The default implementation of Match simply calls Equals and returns its result. The following code demonstrates how to override Equals and Match (which returns true if one attribute represents a subset of the other) and then shows how Match is used.

using System; 
[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 Boolean Match(Object obj) { 
 // If the base class implements Match and the base class 
 // is not Attribute, then uncomment the following line. 
 // if (!base.Match(obj)) return false; 
 // Since 'this' isn't null, if obj is null, 
 // then the objects can't match 
 // NOTE: This line may be deleted if you trust 
 // that the base type implemented Match correctly. 
 if (obj == null) return false; 
 // If the objects are of different types, they can't match 
 // NOTE: This line may be deleted if you trust 
 // that the base type implemented Match correctly. 
 if (this.GetType() != obj.GetType()) return false; 
 // Cast obj to our type to access fields. NOTE: This cast 
 // can't fail since we know objects are of the same type 
 AccountsAttribute other = (AccountsAttribute) obj; 
 // Compare the fields as you see fit 
 // This example checks if 'this' accounts is a subset 
 // of others' accounts 
 if ((other.m_accounts & m_accounts) != m_accounts) 
 return false; 
 return true; // Objects match 
 } 
 public override Boolean Equals(Object obj) { 
 // If the base class implements Equals, and the base class 
 // is not Object, then uncomment the following line. 
 // if (!base.Equals(obj)) return false; 
 // Since 'this' isn't null, if obj is null, 
 // then the objects can't be equal 
 // NOTE: This line may be deleted if you trust 
 // that the base type implemented Equals correctly. 
 if (obj == null) return false; 
 // If the objects are of different types, they can't be equal 
 // NOTE: This line may be deleted if you trust 
 // that the base type implemented Equals correctly. 
 if (this.GetType() != obj.GetType()) return false; 
 // Cast obj to our type to access fields. NOTE: This cast 
 // can't fail since we know objects are of the same type 
 AccountsAttribute other = (AccountsAttribute) obj; 
 // Compare the fields to see if they have the same value 
 // This example checks if 'this' accounts is the same 
 // as other's accounts 
 if (other.m_accounts != m_accounts) 
 return false; 
 return true; // Objects are equal 
 } 
 // Override GetHashCode since we override Equals 
 public override Int32 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() { 
 CanWriteCheck(new ChildAccount()); 
 CanWriteCheck(new AdultAccount()); 
 // This just demonstrates that the method works correctly on a 
 // type that doesn't have the AccountsAttribute applied to it. 
 CanWriteCheck(new Program()); 
 } 
 private static void CanWriteCheck(Object obj) { 
 // Construct an instance of the attribute type and initialize it 
 // to what we are explicitly looking for. 
 Attribute checking = new AccountsAttribute(Accounts.Checking); 
 // Construct the attribute instance that was applied to the type 
 Attribute validAccounts =
 obj.GetType().GetCustomAttribute<AccountsAttribute>(false); 
 // If the attribute was applied to the type AND the 
 // attribute specifies the "Checking" account, then the 
 // type can write a check 
 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()); 
 } 
 } 
}

Building and running this application yields the following output.

ChildAccount types can NOT write checks. 
AdultAccount types can write checks. 
Program types can NOT write checks.

💡小结: System.Attribute 重写了 ObjectEquals 方法,会在内部比较两个对象的类型。不一致会返回 false。如果一致, Equals 会利用反射来比较两个特性对象中的字段值(为每个字段都调用 Equals )。所有字段都匹配就返回 true;否则返回 false。可在自己的定制特性中重写 Equals 来移除反射的使用,从而提升性能。 System.Attribute 还公开了虚方法 Match ,可重写它来提供更丰富的语义。 Match 的默认实现只是调用 Equals 方法来返回它的结果。

# Detecting the Use of a Custom Attribute Without Creating Attribute-Derived Objects

In this section, I discuss an alternate technique for detecting custom attributes applied to a metadata entry. In some security-conscious scenarios, this alternate technique ensures that no code in an Attribute-derived class will execute. After all, when you call Attribute’s GetCustomAttribute(s) methods, internally, these methods call the attribute class’s constructor and can also call property set accessor methods. In addition, the first access to a type causes the CLR to invoke the type’s type constructor (if it exists). The constructor, set accessor, and type constructor methods could contain code that will execute whenever code is just looking for an attribute. This allows unknown code to run in the AppDomain, and this is a potential security vulnerability.

To discover attributes without allowing attribute class code to execute, you use the System. Reflection.CustomAttributeData class. This class defines one static method for retrieving the attributes associated with a target: GetCustomAttributes. This method has four overloads: one that takes an Assembly, one that takes a Module, one that takes a ParameterInfo, and one that takes a MemberInfo. This class is defined in the System.Reflection namespace, which is discussed in Chapter 23. Typically, you’ll use the CustomAttributeData class to analyze attributes in metadata for an assembly that is loaded via Assembly’s static ReflectionOnlyLoad method (also discussed in Chapter 23). Briefly, ReflectionOnlyLoad loads an assembly in such a way that prevents the CLR from executing any code in it; this includes type constructors.

CustomAttributeData’s GetCustomAttributes method acts as a factory. That is, when you call it, it returns a collection of CustomAttributeData objects in an object of type IList. The collection contains one element per custom attribute applied to the specified target. For each CustomAttributeData object, you can query some read-only properties to determine how the attribute object would be constructed and initialized. Specifically, the Constructor property indicates which constructor method would be called, the ConstructorArguments property returns the arguments that would be passed to this constructor as an instance of IList, and the NamedArguments property returns the fields/properties that would be set as an instance of IList. Notice that I say “would be” in the previous sentences because the constructor and set accessor methods will not actually be called—we get the added security by preventing any attribute class methods from executing.

Here’s a modified version of a previous code sample that uses the CustomAttributeData class to securely obtain the attributes applied to various targets.

using System; 
using System.Diagnostics; 
using System.Reflection; 
using System.Collections.Generic; 
[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] 
 public static void Main() { 
 // Show the set of attributes applied to this type 
 ShowAttributes(typeof(Program)); 
 // Get the set of methods associated with the type 
 MemberInfo[] members = typeof(Program).FindMembers( 
 MemberTypes.Constructor | MemberTypes.Method, 
 BindingFlags.DeclaredOnly | BindingFlags.Instance | 
 BindingFlags.Public | BindingFlags.Static, 
 Type.FilterName, "*"); 
 foreach (MemberInfo member in members) { 
 // Show the set of attributes applied to this member 
 ShowAttributes(member); 
 } 
 } 
 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) { 
 // 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(); 
 } 
}

Building and running this application yields the following output.

Attributes applied to Program: 
 System.SerializableAttribute 
 Constructor called=Void .ctor() 
 Positional arguments passed to constructor: None 
 Named arguments set after construction: None 
 
 System.Diagnostics.DebuggerDisplayAttribute 
 Constructor called=Void .ctor(System.String) 
 Positional arguments passed to constructor: 
 Type=System.String, Value=Richter 
 Named arguments set after construction: 
 Name=Name, Type=System.String, Value=Jeff 
 Name=Target, Type=System.Type, Value=Program 
 
 System.Reflection.DefaultMemberAttribute 
 Constructor called=Void .ctor(System.String) 
 Positional arguments passed to constructor: 
 Type=System.String, Value=Main 
 Named arguments set after construction: None 

Attributes applied to DoSomething: 
 System.Diagnostics.ConditionalAttribute 
 Constructor called=Void .ctor(System.String) 
 Positional arguments passed to constructor: 
 Type=System.String, Value=Release 
 Named arguments set after construction: None 
 
 System.Diagnostics.ConditionalAttribute 
 Constructor called=Void .ctor(System.String) 
 Positional arguments passed to constructor: 
 Type=System.String, Value=Debug 
 Named arguments set after construction: None 

Attributes applied to Main: 
 System.CLSCompliantAttribute 
 Constructor called=Void .ctor(Boolean) 
 Positional arguments passed to constructor: 
 Type=System.Boolean, Value=True 
 Named arguments set after construction: None 
 
 System.STAThreadAttribute 
 Constructor called=Void .ctor() 
 Positional arguments passed to constructor: None 
 Named arguments set after construction: None

Attributes applied to .ctor: None

💡小结:调用 AttributeGetCustomAttribute 或者 GetCustomAttributes 方法时,这些方法会在内部调用特性类的构造器,而且可能调用属性的 set 访问器。此外,首次访问类型会造成 CLR 调用类型的类型构造器 (如果有的话)。在构造器、 set 访问器方法以及类型构造器中,可能包含每次查找特性都要执行的代码。这就相当于允许未知代码在 AppDomain 中运行,所以存在安全隐患。可用 System.Reflection.CustomAttributeData 类在查找特性的同时禁止执行特性类中的代码。通过,先用 Assembly 的静态方法 ReflectionOnlyLoad (也在第 23 章讨论) 加载程序集,再用 CustomAttributeData 类分析这个程序集的元数据中的特性。简单地说, ReflectionOnlyLoad 以特殊方式加载程序集,期间会禁止 CLR 执行程序集中的任何代码;其中包括类型构造器。 CustomAttributeDataGetCustomAttributes 方法是一个工厂 (factory) 方法。也就是说,调用它会返回一个 IList<CustomAttributeData> 类型的对象,其中包含了由 CustomAttributeData 对象构成的集合。集合中的每个元素都是应用于指定目标的一个定制特性。可查询每个 CustomAttributeData 对象的只读属性,判断特性对象如何构造和初始化。具体地说, Constructor 属性指出构造器方法将如何调用。 ConstructorArguments 属性以一个 IList<CustomAttributeTypedArgument> 实例的形式返回将传给这个构造器的实参。而 NamedArguments 属性以一个 IList<CustomAttributeNamedArgument> 实例的形式,返回将设置的字段 / 属性。注意,之所以说 “将”,是因为不会实际地调用构造器和 set 访问器方法。禁止执行特性类的任何方法增强了安全性。

# Conditional Attribute Classes

Over time, the ease of defining, applying, and reflecting over attributes has caused developers to use them more and more. Using attributes is also a very easy way to annotate your code while simultaneously implementing rich features. Lately, developers have been using attributes to assist them with design time and debugging. For example, the Microsoft Visual Studio code analysis tool (FxCopCmd.exe) offers a System.Diagnostics.CodeAnalysis.SuppressMessageAttribute that you can apply to types and members in order to suppress the reporting of a specific static analysis tool rule violation. This attribute is only looked for by the code analysis utility; the attribute is never looked for when the program is running normally. When not using code analysis, having SuppressMessage attributes sitting in the metadata just bloats the metadata, which makes your file bigger, increases your process’s working set, and hurts your application’s performance. It would be great if there were an easy way to have the compiler emit the SuppressMessage attributes only when you intend to use the code analysis tool. Fortunately, there is a way to do this by using conditional attribute classes.

An attribute class that has the System.Diagnostics.ConditionalAttribute applied to it is called a conditional attribute class. Here is an example.

//#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 "); 
 } 
}

When a compiler sees an instance of the CondAttribute being applied to a target, the compiler will emit the attribute information into the metadata only if the TEST or VERIFY symbol is defined when the code containing the target is compiled. However, the attribute class definition metadata and implementation is still present in the assembly.

💡小结:特性简化了对代码的注释,还能实现丰富的功能。但有时一些暂时用不上的特性留在元数据中会使元数据无谓地膨胀,这会使文件变得更大,增大进程的工作集,损害应用程序的性能。假如有一种简单的方式,使编译器只有在使用代码分析工具时才生成 SuppressMessage 特性,结果会好很多。利用条件特性就能做到这一点。应用了 System.Diagnostics.ConditionalAttribute 的特性类称为条件特性类。编译器如果发现向目标元素应用了指定了 Conditional 特性的自定义特性的实例,那么当含有目标元素的代码编译时,只有在定义了指定符号的前提下,编译器才会在元数据中生成特性信息。不过,特性类的定义元数据和实现仍然存在于程序集中。