Scrigroup - Documente si articole

     

HomeDocumenteUploadResurseAlte limbi doc
AccessAdobe photoshopAlgoritmiAutocadBaze de dateCC sharp
CalculatoareCorel drawDot netExcelFox proFrontpageHardware
HtmlInternetJavaLinuxMatlabMs dosPascal
PhpPower pointRetele calculatoareSqlTutorialsWebdesignWindows
WordXml

AspAutocadCDot netExcelFox proHtmlJava
LinuxMathcadPhotoshopPhpSqlVisual studioWindowsXml

Programming with Type: Types at Runtime

dot net



+ Font mai mare | - Font mai mic



Programming with Type

Chapter 3 described the CLR's common type system (CTS). The focus of that chapter was largely on the static definition of types, assuming that the use of type in running programs would be implicit. In this chapter, we will focus on explicit programming techniques that deeply integrate type into applications and supporting infrastructure.



Types at Runtime

Types by themselves are rarely all that useful. What makes types interesting is the ability they give programmers to have instances of types to interact with. An instance of a type is either an object or a value, depending on how the type is defined. Instances of the primitive types are values. Instances of most user-defined types are objects, although one can define types that yield values, as Chapter 5 will illustrate in great detail.

Every object and every value is an instance of exactly one type. The affiliation between an instance and a type is often implicit. For example, declaring a variable or field of type System.Int32 results in a block of memory whose affiliation with its type exists only by virtue of the executable code that manipulates it. The CLR (and CLR-based compilers) will allow operations only on those values that are affiliated with the type definition for System.Int32. No additional overhead is needed to enforce this affiliation because the compiler will do the enforcement at compile time and the CLR's verifier will ensure that the affiliation is maintained after the code is loaded.

Each object is also affiliated with a type. However, because objects are always accessed via object references, it is possible that the actual type of the referenced object may not match the declared type of the reference. This will always be the case when an object reference is of an abstract type. Clearly, some mechanism is needed to explicitly affiliate the object with its type to deal with this situation. Enter the CLR object header.

Every object in the CLR begins with a fixed-size object header, as shown in Figure 4.1. The object header is not directly accessible programmatically, but it nonetheless exists. The exact format of this header is undocumented, and the following description is based on empirical analysis of version 1.0 of the CLR on IA-32 architectures. Other implementations of the CLI are likely to diverge somewhat from this format.

Figure 4.1. Object Headers and Type Handles

The object header has two fields. The first field of the object header is the sync block index. One uses this field to lazily associate additional resources (e.g., locks, COM objects) with the object. The second field of the object header is a handle to an opaque data structure that represents the object's type. Although the location of this handle is undocumented, there is explicit support for it via the System.RuntimeTypeHandle type. As a point of interest, in the current implementation of the CLR, an object reference always points to the type handle field of the object's header. The first user-defined field is always sizeof(void*) bytes beyond where the object reference points to.

Every instance of a given type will have the same type handle value in its object header. The type handle is simply a pointer to an undocumented, opaque data structure that contains a complete description of the type, including a pointer to the in-memory representation of the type's metadata. The contents of this data structure are optimized in a manner that makes various performance-critical operations (e.g., virtual method dispatching, object allocation) as fast as possible. The first application of this data structure to look at is dynamic type coercion.

When converting from one type of object reference to another, one must consider the relationship between the two types. If the type of the initial reference is known to be compatible with the type of the new reference, all the CLR needs to make the conversion is a simple IA-32 mov instruction. This is always the case when one is assigning from a reference of a derived type to a reference of a direct or indirect base type or of a known compatible interface (up-casting). If, on the other hand, the type of the initial reference is not known to be compatible with the type of the new reference, then the CLR must perform a runtime test to ascertain whether or not the object's type is compatible with the desired type. Such tests are always necessary when one is assigning from a base type or interface reference to a reference of a more-derived type (down-casting) or of an orthogonal type (side-casting).

To support down-casts and side-casts, CIL defines two opcodes: isinst and castclass. Both opcodes take two arguments: an object reference and a metadata token representing the type of reference desired. Both opcodes generate code that examines the object's type handle to determine whether or not the object's type is compatible with the requested type. The two opcodes differ in how they report the result of the test. If the test succeeds, both opcodes simply leave the object reference on the evaluation stack. If the test fails, the two opcodes behave differently. If the requested type is not supported, the castclass opcode throws an exception of type System.InvalidCastException. In contrast, the isinst opcode simply puts a null reference onto the evaluation stack if the test fails. Because of the relatively high cost of exceptions, one should use constructs that result in the castclass opcode only when the cast is always expected to succeed. Similarly, you should use constructs that result in the isinst opcode when either result is expected. As a point of interest, the JIT compiler will translate the CIL isinst and castclass instructions into calls to the internal, undocumented functions JIT_IsInstanceOf and JIT_ChkCast, respectively. The remainder of this discussion, however, will attribute the behavior of these undocumented functions to the documented CIL instructions described earlier.

Both isinst and castclass take advantage of the data structure referenced by a RuntimeTypeHandle. Although it's not documented, this data structure (internally called a CORINFO_CLASS_STRUCT) contains several critical pieces of information. As shown in Figure 4.2, each type has an interface table. This table contains one entry for each interface that the type is compatible with. Each entry in a type's interface table contains the type handle for the supported interface. Coercions to interface types will be matched using this table. To support coercions to direct or indirect base types, the data structure also contains a pointer to the in-memory representation of the type's metadata, which includes a pointer to the metadata for the type's base type. Conversions to direct or indirect base types will be matched using this part of the data structure. In both cases, the test for type compatibility is simply a linear search through the interface table, followed by a linear traversal through the linked list of metadata structures. This means that for types that support large numbers of interfaces or have a highly layered base type hierarchy (or both), runtime tests for type compatibility will be slower than for simpler types that support fewer interfaces or have a flatter type hierarchy (or both).

Figure 4.2. Type Hierarchy and Runtime Type Information

Each programming language exposes the isinst and castclass opcodes in its own way. In C#, the isinst opcode is exposed via the as and is keywords. The as keyword is a binary operator that takes a variable and a type name. The C# compiler will then emit the proper isinst instruction, and the reference that is returned becomes the result of the operator. Consider the following example:

static void Operate(IPatient p)

C#'s is operator works similarly, except that the resultant reference is converted to a Boolean based on whether or not it is null. Here is the same code written using the is operator:

static void Operate(IPatient p)

This code is semantically equivalent to the previous example. The difference is that the IBillee reference is not available in the second example.

C# exposes the castclass opcode via its cast operator. A C# cast uses the same syntax as a C cast. Consider the following example:

static void Operate(IPatient p)
catch (System.InvalidCastException ex)

Note that in this example an exception handler is used to deal with the potential failure. Again, given the relative expense of exceptions, if the coercion cannot be guaranteed to succeed a priori, a construct that uses the isinst opcode would be more appropriate.

Although the type handle and the data structure it references are largely opaque to programmers working with the CLR, most of the information that is stored in this data structure (and the type's subordinate metadata) is made accessible to programmers via the System.Type type. System.Type provides an easy-to-use facade over the underlying optimized type information. You can get a System.Type object from a type handle by using the GetTypeFromHandle static method of System.Type. You can recover the type handle via the TypeHandle property of System.Type.

Each programming language provides its own mechanism for converting a symbolic type name to a System.Type object. In C#, one uses the typeof operator. The typeof operator takes a symbolic type name and results in a reference to the type's System.Type object. The following demonstrates the typeof operator:

using System;

public sealed class Util

The CLR guarantees that exactly one System.Type object will exist in memory for a given type. That means that in this example, type and t2 are guaranteed to refer to the same System.Type object.

The previous example assumes that the name of the desired type is available at the time of compilation. Additionally, the requested type's assembly will become a static dependency of this module and assembly. To support loading types dynamically without a static dependency, you first need to load the type's assembly dynamically using Assembly.Load or Assembly.LoadFrom. After the assembly is loaded, you can then call the Assembly object's GetType method to extract the desired type. The following code is semantically equivalent to the previous example:

using System;
using System.Reflection;
public sealed class Util

This UseType method differs from the previous example in that this version explicitly loads the assembly containing the type rather than assuming that the assembly dependencies will be resolved automatically.

The previous two examples show how to get a System.Type object based on the name of the type. One can also get the System.Type object for any object or value in memory. To do this, one calls the System.Object.GetType method. Recall that System.Object is the universal type and that all types are compatible with System.Object. One of the methods of System.Object is GetType. When called on a value, the GetType method simply returns the type object that is implicitly affiliated with the value. When called on an object reference, the GetType method uses the System.RuntimeTypeHandle stored in the object header and calls GetTypeFromHandle.

Calling GetType on an object or value allows you to discover aspects of an object or value's type at runtime. The simplest application of GetType is to discover whether two object references point to instances of the same type:

using System;

public sealed class Util

This test will return true only when o1 and o2 refer to instances of exactly the same type. System.Type also supports type compatibility tests via its IsSubclassOf and IsAssignableFrom methods. The following code tests whether one object is an instance of a subclass of another object's type:

using System;

public sealed class Util

Note that in this example, the test will return true only if one of the objects is an instance of a type that is a direct or indirect base type of the other object's type. That stated, the System.Type.IsSubclassOf method will return false if t1 and t2 refer to the same type. Additionally, System.Type.IsSubclassOf will return false if either t1 or t2 refers to interface types. That cannot happen in this example because the System.Object.GetType method is guaranteed never to return a reference to an interface type.

The System.Type.IsSubclassOf method is rarely useful. A much more useful variation on this method is System.Type.IsAssignableFrom. The System.Type.IsAssignableFrom method tests for type compatibility. If the two types are the same, the result is true. If the specified type derives from the current type, then the result is true. If the current type is an interface, then the result is true if the specified type is compatible with the current type. Consider the following example:

using System;

public sealed class Util

In this example, the IsCompatible method works similarly to the IsRelatedType method shown earlier. The difference is that in this example, if o1 and o2 refer to objects of the same type, the result will now be true. It is also possible to enumerate the base types or interfaces (or both) of a given type. To enumerate a type's interfaces, one calls the System.Type.GetInterfaces method, which returns an array of Type objects, one per supported interface. To enumerate a type's base types, one recursively chases the System.Type.BaseType property. The following code prints the list of every type a given object is compatible with:

using System;
public sealed class Util


This example uses the AssemblyQualifiedName property to get the fully qualified name of the type. If one wanted a more human-friendly version, one could use the FullName property to get the namespace-qualified name or use the Name property to return the local part of the name without the namespace prefix. This example also demonstrates another way in which the CLR treats base types and interfaces differently.

Programming with Metadata

The previous section ended with an example that displayed a list of types a given object was compatible with. That example is just the tip of the iceberg of what is possible when type definitions are made machine-readable. This facility is often called reflection, a term made popular by Java.

Reflection makes all aspects of a type's definition available to programs, both at development time and at runtime. Although reflection is useful for building highly dynamic systems, it is far more applicable as a development-time tool. The primary application for reflection is for code generation. Code generation is typically thought of as a development-time activity, and reflection is useful in this capacity. However, even systems that use reflection at runtime can gain performance benefits from dynamically generating code rather than using interpretive-style designs.

The CLR provides ample facilities for generating code on-the-fly. The System.Reflection.Emit library allows CLR-based programs to emit types, modules, and assemblies. The IMetaDataEmit interface provides the same functionality to C++/COM programs. Finally, the System.CodeDOM provides facilities for constructing higher-level C# or VB.NET programs in memory and then compiling them down to modules and assemblies prior to execution. This chapter will focus primarily on the reading of type definitions, which is the purview of the System.Reflection namespace.

Figure 4.3 shows the reflection object model. Note that this object model reflects the fact that types belong to a module, which in turn belongs to an assembly. Additionally, this object model also reflects the fact that types contain members such as fields and methods.

Figure 4.3. Reflection Object Model

The primary type in the System.Reflection namespace is the MemberInfo type. As discussed in the previous chapter, a type contains members. A runtime description of each of these members is available programmatically via the System.Type.GetMembers method. Each member description is an instance of a type that is derived from the System.Reflection.MemberInfo type. As shown in Figure 4.4, the MemberInfo type acts as the generic base type for most of the more specific reflection types.

Figure 4.4. Reflection Type Hierarchy

The MemberInfo type has four significant properties. The Name property returns the name of the member as a string. The MemberType returns a System.Reflection.MemberTypes value indicating whether the member is a field, a method, or another kind of member. The ReflectedType property returns the System.Type that the MemberInfo object is affiliated with. In the face of inheritance, this may or may not be the same as the type that actually declared the member. To access that type, one should use the DeclaringType property.

The following program enumerates all the members of a given type. Note that the array returned by the GetMembers method will consist of fields, methods, and nested types all interleaved. The order the members are returned in will match the order in which they are stored in the module's metadata.

using System;
using System.Reflection;

public sealed class Util ',
members[i].MemberType,
members[i].Name);
}

This implementation will display only those members that are part of the public contract of the type. Additionally, this implementation skips any static members of the type. To change either or both of these characteristics, one must provide a System.Reflection.BindingFlags parameter that indicates which members are to be returned.

System.Reflection.BindingFlags is an enumeration type that is used throughout the reflection type hierarchy. Most methods that allow you to access the members of a type accept an optional BindingFlags parameter to tailor which members will be considered. The Static and Instance flags control whether static or instance members (or both) are to be considered. The Public and NonPublic flags control whether public or nonpublic instance members are to be considered. However, one can discover the private members of base types only by calling GetMembers on the base type and not on a derived type. Finally, even when the NonPublic and Static flags are specified, static members of base types will not be considered. To include these members for consideration, one must specify the FlattenHierarchy flag.

The following variation on the previous example will display all members of the type independent of access level or static/instance status:

using System;
using System.Reflection;

public sealed class Util ',
members[i].MemberType,
members[i].Name);
}

In this example, the only members that will not be displayed are the private members of base types, which are never exposed via reflection.

The GetMembers method returns every member of a type modulo binding flag settings. It is possible to look up individual members by name using the System.Type.GetMember method. This method takes an additional parameter indicating the desired member's name and, like the GetMembers method, will return an array of MemberInfo objects, one per member. By default, only members whose name matches the requested name exactly will be considered. One can perform a case-insensitive search by specifying the BindingFlags.IgnoreCase flag. Finally, the System.Type.GetMember method allows the caller to indicate which kinds of members are to be considered. The caller can provide a MemberTypes flag indicating the desired member kinds. The following example will display all of the methods named DoIt independent of case:

public sealed class Utils ',
members[i].MemberType,
members[i].Name);

}

Note that when one uses the BindingFlags.IgnoreCase flag, one can specify the requested member name using any case.

The previous examples did very little with the member descriptions other than display the member's name. To do anything interesting with a member, one typically needs to use the more specific types. For example, the member descriptions for fields also support the FieldInfo type. The FieldInfo type has properties that indicate which access modifier was used to declare the field. The IsStatic property of FieldInfo indicates whether the field is an instance or a static field. The IsInitOnly and IsLiteral properties of FieldInfo indicate whether the field's value corresponds to a runtime or compile-time constant expression. Finally, the FieldType property of FieldInfo indicates the declared type of the field.

Either the generic GetMembers method or the more specific GetFields method fetches the fields of a type. The following example generates a VB subroutine that sets all of a type's public fields to their default values.

public sealed class Utils )',
type.FullName);
// consider all public/instance fields
FieldInfo[] fields = type.GetFields();
for (int i = 0; i < fields.Length; i++)
= ', fields[i].Name);

// figure out the type-specific literal to use
if (!ft.IsValueType)
Console.WriteLine('Nothing');
else if (ft == typeof(bool))
Console.WriteLine('False');
else
Console.WriteLine('0');
}
Console.WriteLine('End Sub');
}

Now suppose that this routine is run against the following C# type:

public sealed class XYZ

The routine would generate the following VB.NET source code:

Public Sub SetEm(obj as XYZ)
obj.x = 0
obj.y = False
obj.c = 0
obj.s = Nothing
obj.o = Nothing
obj.n = 0
End Sub

No matter how wonderful Visual Studio's IntelliSense feature may be, this routine can generate VB code faster than any human on the planet. Additionally, the generated code will perform much faster than would the corresponding reflection-based code. Furthermore, by isolating the machine-generated code from the human-generated code, as the types involved change or the pattern of code to be generated changes, one can regenerate this code, providing the same flexibility as reflection.

The System.Reflection.FieldInfo type is fairly simple and easy to grasp. The descriptions of methods are somewhat more complex due to the increased number of facets that are inherent in methods. Because constructors are largely the same as normal methods, the System.Reflection.MethodInfo and System.Reflection.ConstructorInfo types share a common base: System.Reflection.MethodBase. Because normal methods and constructors can both have parameters, MethodBase exposes the ability to enumerate parameter definitions. Because normal methods and constructors also have access modifiers and can be static or instance, MethodBase exposes these properties. Because only normal methods can return a typed value, this aspect of a method is visible only via the more specific MethodInfo type. Similarly, because only constructors can be used to initialize objects, the ability to create new instances of the type is visible only on the more specific ConstructorInfo type.

One can access the type initializer for a type via the System.Type.TypeInitializer property. One cannot use the ConstructorInfo returned by this property to create new instances. To access a type's (instance) constructors, one must use the System.Type.GetConstructors or System.Type.GetConstructor routine. One can access the nonconstructor methods of a type via the System.Type.GetMethods or System.Type.GetMethod routine.

To make the MethodInfo type concrete, an example is in order. The following code demonstrates the use of MethodInfo by generating the C# method definitions needed to support a given interface.

public sealed class Utils (', meth.Name);
else
Console.Write(' public (', retval.FullName,
meth.Name);

// walk the list of parameters
bool needComma = false;
ParameterInfo [] parameters = meth.GetParameters();
foreach (ParameterInfo param in parameters)

// spit out the parameter type and name
Console.Write(' ', pt.FullName, param.Name);
}
// generate a boilerplate method body
Console.WriteLine(')');
Console.WriteLine(' ');
Console.WriteLine();
}
}

Suppose you provide this method with a description of the following interface:

public interface ILoveLucy

The routine will generate the following C# method bodies:

public System.Double h(System.Int32[] x,
out System.Boolean y)


public void g(System.Int32 x, ref System.Double y)


public void f()

Note that this simple implementation does not bother to translate the names of the primitive types (e.g., System.Int32) into their language-specific keywords (e.g., int). At the risk of sounding trite, that rather simple exercise is left to the reader.

Looking up particular methods or constructors is complicated by the presence of overloading. When looking up methods and constructors, the caller can specify an array of System.Type objects that will be used to match a specific overload's signature. For example, the following code looks up the g method defined in the ILoveLucy interface just described:

Type[] argTypes = ;
Type itf = typeof(ILoveLucy);
MethodInfo method = itf.GetMethod('g', argTypes);

Note that to specify that a parameter is passed by reference, one uses the System.Type.GetType method, passing the desired type name followed by an ampersand. This is the CLR convention for specifying a managed pointer type, as is used in pass-by-reference scenarios.

Special Methods

The motivation for having rich metadata is to more accurately retain and convey the intentions of the programmer. For example, programmers often define a pair of methods that correspond to a named value. Typically, one of the methods gets the value, and the other method sets the value. A CLR type can contain additional metadata that indicates which methods are intended to be used in this fashion. This additional metadata is formally called a property.

A CLR property is a member of a type that specifies one or two methods that correspond to a named value that is affiliated with the type. Like a field, a property has a name and a type. However, unlike a field, a property has no allocated storage. Rather, a property is simply a named reference to other methods in the same type. No more, no less.

The properties of a type are accessible via the System.Type.GetProperties and System.Type.GetProperty methods, both of which return a System.Reflection.PropertyInfo that describes the property. You can determine the name and type of the property via the PropertyInfo.Name and PropertyInfo.PropertyType members, respectively. The more interesting members are the GetGetMethod and GetSetMethod methods. As shown in Figure 4.5, each of these methods returns a MethodInfo that describes the getter or setter method of the property, respectively. These two methods can accept a Boolean that controls whether or not nonpublic methods will be returned. Because either the getter or the setter is optional, either of these methods may return null.

Figure 4.5. CLR Properties

To reduce the surface area of a type that uses properties, the methods that correspond to a property are typically marked with the specialname metadata attribute, which informs compilers and tools to hide the property's individual methods from normal use. Alternatively, many programming languages allow properties to be read or written to using the same syntax as field access.

The following C# code uses a type Invoice that has a property named Balance of type System.Decimal:

public sealed class Utils

Despite the fieldlike syntax, this code will trigger method invocations against the methods that correspond to the Balance property's getter and setter methods. The fact that Balance is not a field becomes apparent when one tries to pass it by reference to another method:

public sealed class Utils
public static void Adjust(Invoice inv)

This is just one example of how syntactic constructs created by programming languages can sometimes obfuscate semantics.

On the topic of obfuscation, each programming language creates its own syntax for defining properties as members of a type. In C#, the syntax is a hybrid of field and method declaration syntax. Consider the following C# type definition:

public sealed class Invoice
set
}
internal decimal currentBalance;

Note that in this property definition, two methods are being declared. The C# compiler will infer the names and signatures of the methods based on the name and type of the property as follows:

public decimal get_Balance()
public void set_Balance(decimal value)

The generated type will also contain the additional metadata that binds the two methods together as a property called Balance, whose type is System.Decimal. Consistent with the discussion so far, each of the methods will be marked specialname to suppress its appearance in Visual Studio's IntelliSense.

In the same spirit as properties, the CLR provides explicit support for designating methods that are used to register or revoke event handlers. A CLR event is a named member of a type that refers to other methods in the same type. One of the referenced methods is used to register an event handler. The other method is used to revoke the registration. A given event can have multiple event handlers, provided that they have distinct names. Like a property, an event is affiliated with a type. The type of an event must be derived from System.Delegate, which is described in detail in Chapter 6.

The events of a type are accessible via the System.Type.GetEvents and System.Type.GetEvent methods, both of which return a System.Reflection.EventInfo that describes the event. An EventInfo object has properties that indicate the name and type of the event. The most interesting members are the GetAddMethod and GetRemoveMethod methods. As shown in Figure 4.6, each of these methods returns a MethodInfo that describes the register and revoke methods of the event, respectively. These two methods can accept a Boolean that controls whether or not nonpublic methods will be returned.

Figure 4.6. CLR Events

Like the methods of a property, an event's methods are marked with the specialname metadata attribute to suppress their direct use. Most programming languages invent their own syntax for registering or revoking event handlers. In VB.NET, you simply add the WithEvents modifier to a field declaration. In C#, the and operators are overloaded to invoke the register or revoke the method of an event, respectively.

The following C# code uses a type Invoice that has an event named OnSubmit of type System.EventHandler:

using System;

public sealed class Utils

Despite the overly cute syntax, the C# compiler will emit calls to the underlying add and remove methods that are designated in the metadata for the OnSubmit event.

As with properties, each programming language creates its own syntax for defining events as members of a type. In C#, the syntax looks strikingly similar to the syntax used by property definitions. Consider the following C# type definition:

using System;

public sealed class Invoice
remove
}
internal EventHandler eh;

C# also provides an abbreviated syntax for declaring events. The following class definition is equivalent to the previous example:

using System;

public sealed class Invoice

In either of these examples, the C# compiler will emit two method definitions whose signatures are as follows:

public void add_OnSubmit(EventHandler value);
public void remove_OnSubmit(EventHandler value);

Note that the first example uses the System.Delegate.Combine and System.Delegate.Remove methods, which are explained in Chapter 6.

One last member kind to investigate is indexed properties. If the methods of a property accept parameters other than the standard value parameter of the setter method, the property is called an indexed property. Indexed properties are a throwback to classic versions of Visual Basic. Indexed properties are fully supported in VB.NET. Indexed properties are only partially supported in C#.

C# supports only one indexed property per type. That property is called an indexer and must be marked as the 'default' member of the type using the [System.Reflection.DefaultMemberAttribute]. Indexers are similar to any other property. They can have a get or a set method (or both). C# treats indexers as distinct from other properties in two ways. For one thing, indexers accept one or more parameters and in fact can be overloaded. Second, unlike arbitrary properties, indexers defined in C# cannot be static.

Many programming languages expose indexers using array syntax. For example, the following C# code uses a type called InvoiceLines that has an indexer of type decimal. The indexer's parameter is a string that corresponds to a simple part number:

public sealed class Utils

As shown in this example, indexers allow variables to be treated as associative arrays.

Each programming language invents its own syntax for defining an indexer. The following is an example of an indexer defined in C#:

using System;

public sealed class InvoiceLines
}

// public void set_Item(string partID, decimal value)
set
}
}

As implied by the comments in this example, the underlying property name will be Item. This name is chosen by the C# compiler, and, in general, the choice of name does not matter. However, some languages (e.g., VB.NET) allow a type's indexer to be accessed explicitly by name. In those languages, the choice of indexer name is significant. To control the name of the indexer, one can use the [System.Runtime.CompilerServices.IndexerName] attribute. For example, to cause the indexer just defined to use the name Quantities, one would declare the indexer as follows:


System.Runtime.CompilerServices.IndexerName('Quantities')

public decimal this[string partID]
public sealed class DocumentedAttribute : System.Attribute

Because these types have System.Attribute as their base type, they can now be used to augment the definition of a field, a method, a type, or any other metadata construct.

Each programming language must provide a mechanism for applying arbitrary attributes to the declaration of a type, field, method, or other CLR construct. The syntax used to apply custom attributes varies from language to language. In C++ and C#, one can precede a definition with a pair of brackets (e.g., [ TestedAttribute ]) containing the custom attributes. VB.NET instead uses angle brackets (e.g., < TestedAttribute > ).

No matter which language one uses, the attribute name must correspond to a type whose direct or indirect base type is System.Attribute.

The following C# type definition applies one or both of our custom attributes to each of its methods:

public sealed class MyCode

[ Tested ]
static void g()

[ Tested, Documented ]
static void h()

Note that in each of these methods, the attribute declarations precede the target of the attribute. Also note that in C#, if an attribute's name ends in Attribute, one can omit the Attribute suffix. Finally, note that when multiple attributes are applied, they can appear either in independent blocks or as a comma-delimited list inside a single block.

Occasionally, the target of the custom attribute may be ambiguous. For example, when a custom attribute precedes a method declaration, it is also preceding the type name of the return value. In this case, which is the target of the attribute? To resolve this ambiguity, each language provides a mechanism for making the target explicit. Consider the following C# code:

[assembly: Red ]
[module: Green ]
[class: Blue ]
[ Yellow ]
public sealed class Widget

[return: Cyan ]
[method: Magenta ]
[ Black ]
public int Splat()

In this example, the Red attribute applies to the declaring assembly. The Green attribute applies to the declaring module. The Blue and Yellow attributes apply to the type Widget. The Magenta and Black attributes apply to the Widget.Splat method. The Cyan attribute applies to the Widget.Splat method's return type.

Custom attributes are stored as passive BLOBs in the CLR metadata. These BLOBs represent serialized constructor calls to the attribute type. The presence (or absence) of a custom attribute does not affect the CLR's treatment of the type. Rather, custom attributes lie dormant, waiting for a program to read them using reflection or other metadata interfaces.

The System.Reflection.ICustomAttributeProvider interface models attributable elements in the reflection object model. The ICustomAttributeProvider interface is supported by MemberInfo, ParameterInfo, Assembly, and Module, and that covers most elements in the reflection object model.

As shown in Listing 4.1, the ICustomAttributeProvider interface supports two methods for discovering attributes: IsDefined and GetCustomAttributes. The IsDefined method takes the System.Type describing the attribute you are querying for and returns a Boolean indicating whether or not the member has an attribute of the specified type. The inherited parameter controls whether or not attributes applied in the base type are to be considered. The IsDefined method is relatively cheap, as no new objects need to be initialized to determine the presence of an attribute. A simple yes or no answer is all that is returned.

Listing 4.1 System.Reflection.ICustomAttributeProvider
namespace System.Reflection

Listing 4.2 shows IsDefined at work. This example checks each of the methods of a type to see whether either the TestedAttribute or the DocumentedAttribute attribute has been applied. For simple attributes such as these, IsDefined is all one needs because all one cares about is the presence or absence of the attribute.

Listing 4.2 Detecting a Custom Attribute
using System;
using System.Reflection;

public sealed class Utils {
static void DisplayMethodStatus(Type type) {
foreach (MethodInfo m in type.GetMethods()) {
Console.Write(' : ', m.Name);

// check the doc'ed attribute
if (m.IsDefined(typeof(DocumentedAttribute),true))
Console.Write('Documented');
else
Console.Write('Undocumented');

// check the tested attribute
if (m.IsDefined(typeof(TestedAttribute),true))
Console.WriteLine(' - OK');
else
Console.WriteLine(' - Broken');
}
}

Custom attributes can also accept parameters as part of their declaration. The public constructor methods of the attribute type dictate the type and number of parameters that are allowed. For example, consider the following update to the TestedAttribute type just defined:

public sealed class TestedAttribute : System.Attribute

public TestedAttribute()
public TestedAttribute(string tester)
public TestedAttribute(string tester,
double confidence)

Given this new version, one could use the custom attribute as follows:

public sealed class MyCode {
[ Tested ]
static void f()

[ Tested('Don Box') ]
static void g()

[ Tested('Chris Sells', 100) ]
static void h()

The parameters were allowed simply because the attribute type had compatible constructor methods. The CLR restricts the types of attribute parameters to primitives, strings, and System.Type references.

The previous example showed the custom attribute parameters being passed by position based on a constructor signature. It is also legal to pass parameters to a custom attribute by name. Passing attribute parameters by name requires that the underlying attribute type have a public property or field whose name matches the parameter name specified. Consider this updated version of the DocumentedAttribute type defined earlier:

public sealed class DocumentedAttribute : System.Attribute

public DocumentedAttribute(string w)
public string Writer;
public int WordCount;
public bool Reviewed;

This attribute can be applied in the following ways:

public sealed class MyCode {
[ Documented('Don Box', WordCount = 42) ]
static void f()

[ Documented(WordCount = 42, Reviewed = false) ]
static void g()
[ Documented(Writer = 'Don Box', Reviewed = true) ]
static void h()

The positional parameter applied to the f method is allowed because the attribute type has a compatible constructor method that accepts a lone string parameter. The named parameters are allowed because the attribute type has public fields or properties of the same name.

The parameters passed to a custom attribute are serialized into the metadata for the attribute when the compiler emits the module. To allow the parameter values to be recovered at runtime via reflection, custom attributes typically store their parameters as instance fields for later inspection. To read these fields, you must use the ICustomAttributeProvider.GetCustomAttributes method to acquire a reference to the attribute object in memory.

The GetCustomAttributes method returns an array of attribute objects to allow interrogation of an attribute's fields and properties. Calling GetCustomAttributes causes the serialized attribute constructors to execute as the attribute objects are instantiated from the metadata prior to your code seeing them.

The GetCustomAttributes method is overloaded to accept an optional System.Type object, which indicates what type of attribute is desired. If no System.Type is specified, GetCustomAttributes will return all of the custom attributes regardless of type. In either case, the method returns a (potentially empty) array of System.Object references.

The routine in Listing 4.3 demonstrates how one can inspect a custom attribute's fields at runtime. This example accesses the DocumentedAttribute object that is affiliated with each method and displays the various fields of that attribute.

Listing 4.3 Inspecting Custom Attribute Parameters
using System;
using System.Reflection;

public sealed class Utils {
static void DisplayMethodStatus2(Type type) {

Type attType = typeof(DocumentedAttribute);
foreach (MethodInfo m in type.GetMethods()) {
Console.Write(' : ', m.Name);

// check the doc'ed attribute
if (!m.IsDefined(attType,true))
object[] atts = m.GetCustomAttributes(attType, true);
DocumentedAttribute att = (DocumentedAttribute)atts[0];
Console.WriteLine(' : words [ ]',
att.Writer,
att.WordCount,
(att.Reviewed ? 'ok' : 'hold' ));
}


Finally, it is often desirable to restrict where and how a custom attribute may be applied. To allow a custom attribute type to control its usage, the CLR defines the System.AttributeUsageAttribute attribute. This custom attribute is to be applied to a custom attribute's type definition and supports three properties. The ValidOn property indicates which constructs (e.g., fields, methods) the attribute can be applied to. The Inherited property indicates whether or not the attribute's presence should be visible to derived types. The AllowMultiple property specifies whether one can apply the attribute multiple times to the same target. For simple attributes, this usage makes no sense. For custom attributes that correspond to lists or sets (think security roles), AllowMultiple=true would allow one to apply the attribute once for each member of the list.

Consider this last variation on the DocumentedAttribute type used throughout this discussion:

using System;


AttributeUsage(AttributeTargets.Method,
Inherited = false,
AllowMultiple = true)

public sealed class DocumentedAttribute : System.Attribute

public DocumentedAttribute(string w)
public string Writer;
public int WordCount;
public bool Reviewed;

This new definition indicates that one can apply the DocumentedAttribute only to methods. Attempts to apply it to a field or type will fail. The fact that the AllowMultiple parameter is true allows the following usage:

public sealed class MyCode {
[ Documented('Don Box', WordCount = 193) ]
[ Documented('Chris Sells', WordCount = 1722) ]
[ Documented('John Smith', Reviewed = true) ]
static void f()

In this usage, the f method will have three instances of the attribute applied. When one calls GetCustomAttributes on the corresponding MethodInfo object, the resultant array will contain three elements, one per attribute declaration.

Where Are We?

Types are the fundamental unit of currency in CLR metadata. Unlike classic C++ and Visual Basic types, CLR types are made visible to running programs and are an integral part of the runtime execution of all CLR-based programs. Type definitions are easily emitted and read programmatically, and one can extend the format for a type definition's metadata in a clean, strongly typed manner using custom attributes. The extensibility and expressiveness afforded by broadly accessible custom attributes are arguably the most profound features of the CLR. To paraphrase a famous COM guy, Charlie Kindel, custom attributes are more powerful than you think.



Politica de confidentialitate | Termeni si conditii de utilizare



DISTRIBUIE DOCUMENTUL

Comentarii


Vizualizari: 1860
Importanta: rank

Comenteaza documentul:

Te rugam sa te autentifici sau sa iti faci cont pentru a putea comenta

Creaza cont nou

Termeni si conditii de utilizare | Contact
© SCRIGROUP 2024 . All rights reserved