Attributes are a .NET feature with no direct Java equivalent. For a Java developer new to C#, attributes may well be one of the hardest language features to appreciate. However, knowledge of attributes is essential to use the .NET Framework for anything but the most trivial programs.
Attributes are a generic mechanism for associating declarative information (metadata) with program elements. This metadata is contained in the compiled assembly, allowing programs to retrieve it through reflection at run time. Other programs, particularly the common language runtime (CLR), use this information to determine how they should interact with and manage program elements.
There are many predefined attributes in the .NET class libraries, but the power and flexibility of attributes stem from the fact that they are classes. Custom attributes can be created by deriving a class from the abstract base class System.Attribute. This provides a metadata mechanism that is extensible beyond the scope of the default .NET class libraries.
This section looks first at how to assign existing attributes to elements of code and then at how to define custom attributes. We don’t discuss all the attributes included in the .NET class libraries; these will be discussed throughout the rest of the book in sections relevant to their purpose.
The mechanisms used to access attribute metadata at run time are covered in Chapter 12.
The names of all attribute classes defined in the .NET class libraries end with the word Attribute. However, attribute classes are normally referenced using short names without the word Attribute appended. When using attributes, the programmer has the choice of using either form; use of the short name improves readability, and the C# compiler will automatically generate the appropriate full name. For example, the Serializable and SerializableAttribute attribute names represent the same attribute within a C# program and can be used interchangeably.
Applying previously defined attributes to program elements is called attribute specification. Attributes can be specified on the following target elements: assembly, class, constructor, delegate, enum, event, field, interface, method, module, parameter, property, return value, and struct. There is also a meta-attribute specifically applicable to attribute declarations. Valid target elements are defined as part of an attribute declaration.
Attribute specification involves placing the attribute name and any necessary parameters in square brackets immediately preceding the target element. If you apply more than one attribute to a target, place each attribute in its own set of square brackets. Alternatively, use a comma-separated list of attributes in a single set of square brackets. The ordering of attributes is not important. The following code fragment demonstrates the syntax of attribute specification:
// Single attribute specified on a class definition [Serializable] public class MyClass { // Two attributes specified separately on a method [CLSCompliant(true)] [WebMethod(false, Description="My Web Method")] public int MyMethod(int someParam) { // implementation } // Two attributes specified in a single line on a method [WebMethod(true, Description="My Other Web Method"), CLSCompliant(true)] public int MyOtherMethod(string someParam) { // implementation } }
In this example, we’ve used three different attributes. The Serializable attribute was placed on the same line as the target class declaration. The CLSCompliant and WebMethod attributes are both specified on two method declarations. For the first method, each attribute is on a separate line; for the second method, they are placed together in a single set of square brackets on a single line. Both approaches have the same effect.
The foregoing examples also demonstrate how parameters are passed to attributes. The Serializable attribute takes no parameters. The CLSCompliant attribute takes one positional parameter, and the WebMethod attribute takes one positional and one named parameter. The way the compiler handles attribute parameters is one of the key behaviors that differentiate attributes from other classes.
Positional parameters are the same as parameters in any normal instance constructor. They are expected in a predefined order, must be of the correct type, and must appear before any named parameters.
Named parameters are specified by providing a comma-separated list of name/value pairs. Named parameters must be placed after all positional parameters; their order is not important. Providing named parameters that are not supported by an attribute or values that cannot be implicitly converted to the expected type will cause a compile-time error.
The target of an attribute is normally apparent by its position preceding the target element. However, there are circumstances in which the target is a module or an assembly, or in which the target is ambiguous. Ambiguity occurs most frequently when specifying an attribute on a member that returns a value; both the method and the return value are valid attribute targets.
In these situations, the programmer must clarify the target by prefixing the attribute name with the appropriate attribute target specifier from the following list: assembly, field, event, method, module, param, property, return, or type. The most common use of the attribute target specifier is to identify an assembly or module as the target of an attribute; these are called global attributes. For example:
[assembly:CLSCompliant(true)] [module:DynamicLoad]
For global attributes, these statements must occur after any top level using statements but before any namespace or type declarations.
An attribute is a class that derives from System.Attribute. The following is an example of a simple attribute used to identify the developer who created a class or assembly:
using System ; // Needed for Attributes [AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple=true, Inherited = false)] public class CreatorAttribute: System.Attribute { private String name; // creator's name private String email; // creator's email // Declare public constructor public CreatorAttribute (String creator) { name = creator; email = ""; } // Declare a property to get/set the creator's email address public string Email { get { return email; } set { email = value; } } // Declare a property to get the creator's name public string Name { get { return name;} } }
The following examples demonstrate the use of this attribute:
[assembly:Creator("Bob")] [assembly:Creator("Jim" , Email = "[email protected]")] [Creator ("Judy", Email = "[email protected]")] public class MyClass { // class implementation }
The AttributeUsage attribute defines how a new attribute can be used. AttributeUsage takes one positional and two named parameters, which are described in Table 6-2. The table also specifies the default value applied to an attribute declaration if the AttributeUsage attribute is omitted or a parameter isn’t specified.
A compiler error occurs if an attempt is made to use the AttributeUsage attribute on a class that is not derived from System.Attribute.
Table 6-2. AttributeUsage Parameter Description
Parameter | Type | Description | Default |
---|---|---|---|
validOn | positional | Identifies which program elements the attribute is valid against. Valid values are any member of the System.AttributeTargets enumeration. | AttributeTargets.All |
AllowMultiple | named | Whether the attribute can be specified more than once for a single element. | false |
Inherited | named | Whether the attribute is inherited by derived classes or overridden members. | true |
The remainder of the attribute declaration is the same as for any other class; however, all attribute classes must be declared public.
The documentation for the first release of .NET states that all user-defined attributes will implicitly have the Attribute string appended to their class name if not done so manually. However, our experience shows that this is not the case. Custom attributes should always be given a name ending in the word Attribute to maintain consistency, enabling the C# compiler to support the dual-name behavior described earlier.
A custom attribute must have at least one public constructor. The constructor parameters become the attribute’s positional parameters. As with any other class, more than one constructor can be specified; providing overloaded constructors gives the users of the attribute the option of using different sets of positional parameters when specifying the attribute.
While the majority of attributes are used for specifying run-time-accessible metadata, a number of them are evaluated at compile time. The AttributeUsage attribute discussed earlier is evaluated at compile time. Two other important compile-time attributes are discussed in the following sections.
The Conditional attribute marks a method whose execution depends on the definition of a preprocessor symbol. (See the #define and #undef section within the Preprocessor Directives section later in this chapter.) If a method is marked with the Conditional attribute, any attempts to execute the method will be removed by the compiler if the specified preprocessor symbol is not defined at the calling point.
The Conditional attribute is a more elegant but less flexible alternative to using #ifdef preprocessor directives. It centralizes the conditional logic rather than having many #ifdef statements throughout the code. To be a valid target of the Conditional attribute, a method must return void; otherwise, it won’t be possible to remove references without breaking the calling code.
For example, to define a method whose execution is conditional on the declaration of the symbol DEBUG, use the following syntax:
//The following method will execute if the DEBUG symbol is defined [Conditional("DEBUG")] public void SomeMethod() { // implementation here}
Multiple instances of the Conditional attribute can be specified for a method producing a logical OR behavior. For example:
// The following method will execute if WIN95 OR WIN2000 // symbols are defined [Conditional("WIN95"), Conditional("WIN2000")] public void SomeMethod() { // implementation here}
Achieving logical AND behavior is messy, involving the unpleasant use of intermediate Conditional methods. For example:
// The following method will execute if the DEBUG symbol is defined [Conditional("DEBUG")] public void SomeMethod() { SomeOtherMethod() } // The following method will only execute if the WIN95 symbol is defined. // When called from SomeMethod above, this has the effect of only executing // SomeOtherMethod if both the DEBUG AND WIN95 symbols are defined [Conditional("WIN95")] private void SomeOtherMethod() { // implementation here}
The Obsolete attribute marks a program element as obsolete and forces the compiler to raise a warning or an error when other code attempts to use the element. This is similar to marking an element as deprecated in Java but places more control in the hands of the code’s author.
The two positional parameters of the Obsolete attribute specify the message to be displayed by the compiler and whether the compiler raises a warning or an error. For example:
// Specifying true as the second argument means that code referencing // the following method will generate a compiler error about the // method being deprecated. [Obsolete("This method has been deprecated.", true)] public string MyMethod() { // implementation here} // Specifying false as the second argument means that code referencing // the following method will generate a compiler warning that the // method is in the process of being phased out. [Obsolete("This method is being phased out, use SomeMethod().", false)] public string MyOtherMethod() { // implementation here}
The Obsolete attribute is not limited to specification on methods. Valid targets are class, struct, enum, constructor, method, property, field, event, interface, and delegate.