Types, members, modules, and assemblies all have associated metadata that is used by all the major CLR services, is considered an indivisible part of an application, and can be accessed via reflection (see the earlier Section 3.10).
A key characteristic of metadata is that it can be extended. You extend the metadata with custom attributes, which allow you to “decorate” a code element with additional information stored in the metadata associated with the element.
This additional information can then be retrieved at runtime and used to build services that work declaratively, which is the way that the CLR implements core features such as serialization and interception.
Decorating an element with a custom attribute is
known as specifying the custom attribute and is
done by writing the name of the attribute enclosed in
brackets ([]
) immediately before the element declaration as follows:
[Serializable] public class Foo {...}
In this example, the Foo
class is specified as
serializable. This information is saved in the metadata for
Foo
, and affects the way the CLR treats an
instance of this class.
A useful way to think about custom attributes is that they expand the
built-in set of declarative constructs in the C# language such as
public
, private
, and
sealed
.
In
reality, custom attributes are
simply types derived from
System.Attribute
with language constructs for
specifying them on an element (see Section 2.16 in Chapter 2).
These language constructs are recognized by the compiler, which emits a small chunk of data into the metadata. This custom data includes a serialized call to the constructor of the custom attribute type (containing the values for the positional parameters), and a collection of property set operations (containing the values for the named parameters).
The compiler also recognizes a small number of pseudo-custom attributes. These are special attributes that have direct representation in metadata and are stored natively (i.e., not as chunks of custom data). This is primarily a runtime performance optimization, although it has some implications for retrieving attributes via reflection, as discussed later.
To understand this, consider the following class with two specified attributes:
[Serializable, Obsolete] class Foo {...}
When compiled, the metadata for the class Foo
looks like this in MSIL:
.class private auto ansi serializable
Foo {
.custom instance void
System.ObsoleteAttribute::.ctor( ) = ( 01 00 00 00 )
...
}
}
Compare the different treatment by the compiler of the
Obsolete
attribute, which is a custom
attribute and is stored as a serialized constructor call to the
System.ObsoleteAttribute
type, to the
treatment of the
Serializable
attribute, which is a pseudo-custom attribute represented directly in
the metadata with the serializable
token.
At runtime the core CLR services such as serialization and remoting inspect the custom and pseudo-custom attributes to determine how to handle an instance of a type.
In the case of custom attributes, this is done by creating an instance of the attribute (invoking the relevant constructor call and property-set operations), and then performing whatever steps are needed to determine how to handle an instance of the type.
In the case of pseudo-custom attributes, this is done by simply inspecting the metadata directly and determining how to handle an instance of the type. Consequently, handling pseudo-custom attributes is more efficient than handling custom attributes.
Note that none of these steps is initiated until a service or user program actually tries to access the attributes, so there is little runtime overhead unless required.
The .NET Framework makes extensive use of attributes for purposes ranging from simple documentation to advanced support for threading, remoting, serialization, and COM interop. These attributes are all defined in the BCL, and can be used, extended, and retrieved by your own code.
However, certain attributes are treated specially by the compiler and
the runtime. Three attributes considered general enough to be defined
in the C# specification are AttributeUsage
,
Conditional
, and Obsolete
.
Other attributes such as CLSCompliant
,
Serializable
, and NonSerialized
are also treated specially.
[AttributeUsage(
target-enum
|
[, AllowMultiple= [true |false ]]? |
[, Inherited= [true |false ]]? |
] (for classes) |
The
AttributeUsage
attribute is applied to a new
attribute class declaration. It controls how the new attribute should
be treated by the compiler, specifically, what set of targets
(classes, interfaces, properties, methods, parameters, etc.) the new
attribute can be specified on, whether multiple instances of this
attribute may be applied to the same target, and whether this
attribute propagates to subtypes of the target.
target-enum
is a bitwise
mask of values from the System. AttributeTargets
enum, which
looks like this:
namespace System { [Flags] 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, All = 0x3fff, ClassMembers = 0x17fc, } }
[Conditional
(symbol)
]
(for methods) |
The Conditional
attribute can
be applied to any method with a void
return type.
The presence of this attribute tells the compiler to conditionally
omit calls to the method unless
symbol
is defined in
the calling code. This is similar to wrapping every call to the
method with
#if
and #endif
preprocessor directives, but
Conditional
has the advantage of needing to be
specified only in one place.
[Obsolete( [Message= ]?
message
|
IsError= [true |false ]] |
] (for all attribute targets) |
Applied
to any valid attribute target, the Obsolete
attribute indicates that the target is obsolete.
Obsolete
can include a message that explains which
alternative types or members to use and a flag that tells the
compiler to treat the use of this type or member as either a warning
or an error.
For example, referencing type Bar
in the following
example causes the compiler to display an error message and halts
compilation:
[Obsolete("Don't try this at home", IsError=true)] class Bar { ... }
[CLSCompliant(true |false)]
|
(for all attribute targets) |
Applied to an assembly, the
CLSCompliant
attribute tells the compiler whether
to validate CLS compliance for all the exported types in the
assembly. Applied to any other attribute target, this attribute
allows the target to declare if it should be considered
CLS-compliant. In order to mark a target as CLS-compliant, the entire
assembly needs to be considered as such.
In the following example, the CLSCompliant
attribute is used to specify an assembly as CLS-compliant and a class
within it as not CLS-compliant:
[assembly:CLSCompliant(true)] [CLSCompliant(false)] public class Bar { public ushort Answer { get {return 42;} } }
In addition to using the predefined attributes supplied by the .NET Framework, you can also create your own.
To create a custom attribute:
Derive a class from
System.Attribute
or from a descendent of
System.Attribute
. By convention the class name
should end with the word “Attribute,” although this
isn’t required.
Provide the class with a public constructor. The parameters to the constructor define the positional parameters of the attribute and are mandatory when specifying the attribute on an element.
Declare public-instance fields, public-instance read/write properties, or public-instance write-only properties to specify the named parameters of the attribute. Unlike positional parameters, these are optional when specifying the attribute on an element.
The types that can be used for attribute constructor parameters and
properties are bool
, byte
,
char
, double
,
float
, int
,
long
, short
,
string
, object
, the
Type
type, enum
, or a
one-dimensional array of the aforementioned types.
Finally, define what the attribute, may be specified on using the
AttributeUsage
attribute, as described in the
preceding section.
Consider the following example of a custom attribute,
CrossRef-Attribute
,
which removes the limitation that the CLR metadata contains
information about statically linked types but not dynamically linked
ones.
using System; [AttributeUsage(AttributeTargets.ClassMembers, AllowMultiple=true)] class CrossRefAttribute : Attribute { Type xref; string desc = ""; public string Description { set { desc=value; } } public CrossRefAttribute(Type xref) { this.xref=xref; } public override string ToString( ) { string tmp = (desc.Length>0) ? " ("+desc+")" : ""; return "CrossRef to "+xref.ToString( )+tmp; } }
From the attribute user’s perspective, this attribute can be
applied to any class member multiple times (note the use of the
AttributeUsage
attribute to control this).
CrossRefAttribute
takes one mandatory positional
parameter (namely the type to cross reference) and one optional named
parameter (the description), and is used as follows:
[CrossRef(typeof(Bar), Description="Foos often hang around Bars")] class Foo {...}
Essentially, this attribute embeds cross references to dynamically linked types (with optional descriptions) in the metadata. This information can then be retrieved at runtime by a class browser to present a more complete view of a type’s dependencies.
Retrieving attributes at runtime is done using reflection
via one of the
GetCustomAttribute
overloads on the object’s
Type
instance. This is one of the few
circumstances where the difference between custom
attributes and pseudo-custom attributes becomes apparent,
since pseudo-custom attributes can’t be retrieved with
GetCustomAttribute
.
Here is an example that uses reflection to determine what attributes are on a specific type:
using System; [Serializable, Obsolete] class Test { static void Main( ) { Type t = typeof(Test); object[] caarr = t.GetCustomAttributes( ); Console.WriteLine("{0} has {1} custom attribute(s)", t, caarr.Length); foreach (object ca in caarr) Console.WriteLine(ca); } }
Although the Test
class
of the preceding example has two attributes specified, the sample
produces the following output:
Test has 1 custom attribute(s) System.ObsoleteAttribute
This demonstrates how the
Serializable
attribute (a pseudo-custom attribute)
isn’t accessible via reflection, while the
Obsolete
attribute (a custom attribute)
still
is.
3.146.221.149