Reflection APIs


Figure 14-1 shows a high-level overview of the reflection subsystem's public types. We discuss these in greater detail throughout this chapter:

  • Info APIs: These permit you to access the CLR's type system in a truly dynamic fashion. You can construct objects, invoke methods, access fields and properties on objects, and access just about any piece of metadata defined in an assembly.

  • Handles (System.RuntimeXxxHandle and ModuleHandle): A handle wraps an integer-based index to an info's corresponding internal CLR data structure. They are lightweight primitives that can be used to cache or keep references to otherwise expensive info instances in memory. Just about each info type has a corresponding handle class.

  • Tokens: Tokens are just integers. But a token/module pair makes up a unique identifier that can be used to resolve any info instance or handle. References to type system abstractions in IL make use of tokens heavily for an efficient referential mechanism.

  • Custom attributes (System.Attribute): Custom attributes (CAs) enable dynamic extensibility through metadata. If you wish to access the CAs attached to a method or type, for instance, you have to work with the reflection system either directly or indirectly.

  • Delegates (System.Delegate and MulticastDelegate): Delegates have static and dynamic support in the runtime, both of which are implemented as part of the reflection system. Custom delegate types (e.g., those specified with the delegate keyword in C#) derive from MulticastDelegate.

  • Activation (System.Activator and ActivatorContext): Activation is a general-purpose feature, enabling you to easily construct instances of objects from a Type and set of arguments. This is useful for scripting and COM interoperability. Similar functions can be accomplished using the System.Reflection.ConstructorInfo APIs, albeit with more steps involved.

  • Binding (System.Reflection.Binder and DefaultBinder): This is the process by which a loose specification of a target is matched with an actual implementation. In many dynamic languages, full type information is not specified and, thus, some form of inferencing is often necessary. We'll see exactly what this means shortly. The Binder type offers an extensibility point for you to plug in your own binding rules, and the DefaultBinder implements a standard set that all binding will use by default.

image from book
Figure 14-1: An overview of the System.Reflection APIs.

In this section, we will take a look at how you can inspect existing metadata using the info APIs, in addition to discussing reflection's primitives (handles, tokens), activation, and binding. Later sections will look at custom attributes, delegates, how to generate new metadata using Reflection.Emit.

The Info APIs

We will examine the info APIs in this section. These provide you with access to the type system meta-data for each type of abstraction the CTS has to offer. This includes the Assembly and Module types, to access units of packaging and reuse; the Type type for working directly with CLR types; and the ConstructorInfo, MethodInfo, FieldInfo, PropertyInfo, EventInfo, and ParameterInfo types, each of which wraps a distinct unit of abstraction in the type system. Many of these types are abstract and have different implementations based on how you've loaded and accessed the metadata. Usually, this is entirely transparent.

The most common use of the info APIs is to read the metadata information associated with an assembly or type. This can be used to inspect a program or library in meaningful ways. Actual object instances can also be dynamically instantiated and/or inspected to find out their runtime type information (RTTI). Programs written with reflection can make logic decisions at runtime based on metadata, or simply report back the metadata information to output, as is the case in a utility like a type hierarchy browser. The Reflection.Emit namespace, as you'll see toward the end of this chapter, builds on top of the same abstractions. It offers builder APIs, which subclass directly from the info APIs, adding functions to modify metadata.

While I don't intend to give a holistic overview of each API at your disposal, we'll take a brief tour of the types available and what they are used for. You can picture the info subsystem as a connection of related abstractions, where these relationships can be traversed bidirectionally. For example, a Type has a method GetMembers that returns an array of MemberInfos. This represents each member the type has defined. Similarly, from each MemberInfo, you can access its Type property, which is a back pointer to the type that defines that member. Each info class exhibits this property and even sometimes enables you to skip a few levels in the hierarchy. For example, Type has an Assembly property, although technically it has to first find out in what Module the type was defined before it can determine the assembly. Since most people don't work directly with modules, this is quite convenient.

Assembly and Module Information

We already saw a bit of the Assembly and Module APIs in Chapter 4, where we discussed units of packaging. An assembly comprises one or more modules, and each module is responsible for exporting public types. There are several ways to get an Assembly instance. Most commonly, you will use one of the Load* or ReflectionOnlyLoad* methods to load an assembly from disk or a stream of bytes. Chapter 4 discussed these techniques in more detail. But you can also locate assemblies that have already been loaded into the AppDomain either by inspecting Type instances or with one of the Assembly.GetXxxAssembly static methods:

 Assembly a1 = Assembly.GetExecutingAssembly(); Assembly a2 = Assembly.GetEntryAssembly(); Assembly a3 = Assembly.GetCallingAssembly(); 

This code snippet obtains a reference to an existing assembly in three different ways. The first obtains a reference to the assembly whose code is actively executing. GetEntryAssembly returns you the assembly that was used to start the AppDomain's execution. Ordinarily, this will be an EXE, but it could be some bit of bootstrapping code for hosting or add-in scenarios where an assembly is executed dynamically via AppDomain.ExecuteAssembly. Lastly, GetCallingAssembly obtains the assembly of your immediate caller. For example:

 ThreadPool.QueueUserWorkItem(delegate {     Console.WriteLine(Assembly.GetCallingAssembly().FullName); }); 

This prints out mscorlib, since the code that actually invokes your delegate is defined in mscorlib's ThreadPool class. Of course, the simpler case is when a virtual function you've overridden is invoked by the Framework or in situations where your API is called directly by somebody. The result is similar.

Module does not have similar methods; you'll have to resolve your module either by traversing links from an Assembly (GetModule or GetModules method, or to access the module in which the assembly manifest resides, the ManifestModule property) or from a Type (Module property).

Reflection Only Load Context

It is substantially less expensive to work with the info APIs for an assembly that has been loaded as reflection-only. This is sometimes called the introspection context. Many internal CLR data structures needn't be created and maintained, and working with the APIs can take more optimized code paths in the implementation. This is because many things such as security and reliability aren't a concern as a result of not being able to execute code. Assembly's instance property ReflectionOnly is a Boolean that returns true if the assembly was loaded in the load-only context. AppDomain also supplies a method ReflectionOnlyGetAssemblies, which returns an Assembly[] containing all of the currently loaded reflection-only assemblies.

Not only is this approach more performant, but it allows you to load an assembly whose code is platform specific into a different platform. For example, a 64-bit-specific assembly cannot be loaded on a 32-bit platform. Attempting to perform an Assembly.Load would fail. However, if you load for reflection-only, metadata can be inspected although code cannot be run.

PE and Platform Kinds

As a result of the new 64-bit support in the .NET Framework 2.0, assemblies can now be specific to a platform. The geeky term for this is bitness. This can happen if you ship some unmanaged code inside your PE file, for example. While IL is platform neutral — the JIT compiler takes care of platform differences — once you ship some chunk of unmanaged code in your assembly, you've taken a platform dependency. Thirty-two-bit unmanaged code can still be run on 64-bit platforms under the WOW64 (Windows-on-Windows64) emulator. If you ship 64-bit specific code, on the other hand, it will not be able to execute on down-level platforms.

Rather than relying on emulation on platforms you haven't explicitly tested against, specifying an assembly bitness is often a better choice. The C# compiler permits you to do this with the /platform switch. It takes one of four values: x86, Itanium, x64, or anycpu. Unfortunately, these terms are not standardized in any way, and thus reflection actually exposes them with subtly different names. They refer to the Intel IA-32 (a.k.a. x86, i386), Intel Itanium IA-64, and AMD-64 (a.k.a. x64) hardware platforms.

To access an assembly's platform information at runtime, you first have to resolve the Assembly and its manifest module. Then you call the GetPEKind function, which returns two output parameters containing the platform information. For example:

 Assembly a = Assembly.GetExecutingAssembly(); Module m = a.ManifestModule; PortableExecutableKinds peKinds; ImageFileMachine imageFileMachine; m.GetPEKind(out peKinds, out imageFileMachine); // 'peKinds' and 'imageFileMachine' now contain the platform information... 

The two enums can be used in conjunction to determine platform dependence. PortableExecutableKinds is a flags-style enumeration; if its value contains the ILOnly value, the assembly does not have a platform dependency. This is the same as the anycpu C# switch discussed above. Otherwise, the ImageFileMachine value will contain the platform on which the assembly depends: either I386,.IA64, or AMD64. These correspond to x86, Itanium, and x64, respectively, in the C# compiler.

 if ((peKinds & PortableExecutableKinds.ILOnly) != 0) {     // Assembly is platform independent. } else {     // assembly is platform dependent     switch (imageFileMachine)     {         case ImageFileMachine.I386:             // i386, x86, IA-32, ... dependent.             break;         case ImageFileMachine.IA64:             // IA-64 dependent.             break;         case ImageFileMachine.AMD64:             // AMD-64, x64 dependent.             break;     } } 

The above code structure contains general logic for performing specialized functionality based on the platform dependency of an assembly.

Type Information

The System.Type class acts as an index into all of the members on a specific type. Of course, it also offers access to various metadata associated with a type, such as whether it is a reference type, value type, abstract class, and so on.

You can get obtain an instance of a Type object through a variety of mechanisms:

  • Using the typeof(t) operator in C#, where t is the name of a type. The type token is loaded directly the IL using the ldtoken instruction and then passed to the Type.GetTypeFromHandle function to turn the token into a Type instance. Other languages have similar constructs that use this instruction. Because the token loading can be jitted into native code, this is faster than doing runtime string-based lookups.

  • Invoke the GetType instance method (defined as a nonvirtual method on System.Object) on an object. This will return you the runtime type of the target object. You can also refer to the Type.GetTypeHandle static method if you wish to obtain just a handle instead of a heavy-weight Type instance; it takes an object as input and returns its RuntimeTypeHandle. Note that these operations do not have anything to do with the type of the variable used to access the object — the type is computed dynamically at runtime by walking the object reference and inspecting the method table data structure.

  • Use the Type.GetType static functions or the Type.ReflectionOnlyGetType function to do a string-based lookup against all loaded types. These methods take a string name and optional bool parameters that indicate whether you'd like the function to throw an exception (rather than returning null) if it fails to find the specified type and whether the match should be case insensitive. These operations can be quite expensive because they perform textual comparisons against the name of all loaded types in the AppDomain. Furthermore, as they perform each comparison, they construct data structures to hold the metadata associated with each type. So even if you are looking for just one type, you could end up accidentally consuming quite a bit of unnecessary memory.

  • There are also GetTypeFromCLSID and GetTypeFromProgID functions. As discussed in Chapter 11, these are for interoperating with COM. Invoking them with a valid CLSID or ProgID will return the Type for the COM proxy. You can then instantiate it using Activator .CreateInstance, for example, at which point you can quite easily interoperate with COM code by making managed method calls.

  • Similar to Type.GetType, both Assembly and Module provide GetType functions to search the types that they export by string. They both offer a GetTypes function that returns a Type[] of all types contained inside the package. Lastly, Module offers a FindTypes method that accepts a delegate for more sophisticated matching. This approach has the same problems as the using Type.GetType, namely that you end up consuming quite a bit of extra memory.

For example, consider this code shows four different ways to get a reference to the Type for System.Int32 in mscorlib.dll:

 int x = 10; Assembly a = Assembly.Load("mscorlib"); Type t1 = typeof(int); Type t2 = x.GetType(); Type t3 = a.GetType("System.Int32"); Type t4 = Type.GetType("System.Int32"); Console.WriteLine(t1 == t2); Console.WriteLine(t2 == t3); Console.WriteLine(t3 == t4); 

All of the equality comparisons will evaluate to true and, assuming that Type's Equals method is transitive (as all Equals implementations are supposed to be), then we can infer that t1 == t2 == t3 == t4. Loading t1 is the most efficient, t2 is a close second, and t3 and t4 are horribly inefficient. t3 is slightly more efficient because it only searches mscorlib.dll, whereas t4 searches all loaded assemblies. In fact, in a little test program I wrote, t2 is twice as slow as t1, t3 is 100 times slower than t2, and t4 is twice as slow as t3.

Once you have a Type instance, there are a set of properties and methods that might be of interest to you. A large set of predicate properties exists to tell you about certain flags and capabilities associated with a type. They are all of the form IsXxx and are self-explanatory — such as IsValueType, IsPublic, and IsAbstract — and thus, I won't detail them here.

The GetXxx methods, by and large, just return instances of other info types. GetProperties returns an array of PropertyInfos, while GetProperty returns a single PropertyInfo, for example. Other member types have similar functions. By default, these overloads deal only with public members, but each has an overload that takes as input a BindingFlags enumeration value, enabling you to filter based on other criteria (including accessing nonpublic members).

Controlling Binding (BindingFlags)

Most APIs that return info types accept a BindingFlags flags-style enumeration value to constrain searches to those abstractions that hold certain properties. Those that don't are usually just convenient overloads that supply some natural default value. A good example is the Type.GetMembers instance method. It returns an array of MemberInfos that can be used to explore the various methods, properties, and fields that a type has defined. The overload that takes no arguments forward to the overload that takes a BindingFlags with some convenient defaults:

 public class Type {     public MemberInfo[] GetMembers()     {         // Implementation in the CLR...         return GetMembers(BindingFlags.Public |             (BindingFlags.Static | BindingFlags.Instance));     }     public MemberInfo[] GetMembers(BindingFlags bindingAttr)     {         /* ... */     }     // ... } 

As you can see, the default for the API is to look for only public members (both static and instance). If you wanted to obtain public, protected, and private members (instead of just public), you could have said so explicitly:

 Type t = /* ... */; MemberInfo[] mm = t.GetMembers(BindingFlags.Public | BindingFlags.NonPublic); 

Many APIs use similar overload schemes. BindingFlags actually has roughly 20 possible values, which can be combined in interesting ways. We won't discuss each of them in detail here.

Method Information

Most useful reflection-based code uses method information in some fashion. Such information is represented with the MethodInfo type, which derives from MethodBase (which specifies most of the interesting functionality). As is the case with the Type class, there is a set of IsXxx properties that enables you to query for specific properties of a certain method. For instance, IsVirtual, IsStatic, and IsGenericMethod each tell you whether a given method is virtual, static, and/or generic, respectively. Given their self-descriptive nature, we won't detail them here.

The GetMethodImplementationFlags method returns a MethodImplAttributes enumeration value that contains information about what, if any, pseudo-custom system attributes are present on the target method. For example, methods implemented as native functions inside the CLR are represented by InternalCall, while ordinary functions will return the value IL.

There are three primary things you can do with a method, other than reading its Name and associated IsXxx predicates. You can access information about its return or input parameters, dynamically invoke the code attached to a method, or inspect its body (IL). We'll briefly take a look at each of these.

Return and Parameter Information

MethodInfo's ReturnType property returns a Type, which indicates the return type for the target method. For example, given a method:

 MethodInfo m = typeof(object).GetMethod("ToString"); Type returnType = m.ReturnType; // == typeof(string) 

returnType will refer to typeof(string) after executing this code because Object.ToString's return type is string.

GetParameters returns a ParameterInfo array of parameters that the method accepts:

 MethodInfo m = typeof(object).GetMethod("ToString"); foreach (ParameterInfo param in m.GetParameters())     // ... 

Each ParameterInfo offers a ParameterType property to indicate the type of the parameter and similarly offers a set of IsXxx predicates. Most of these predicates are based on ParameterAttributes information accessible through ParameterInfo's Attributes property. For example, IsIn indicates whether a parameter is an input parameter, and IsOut indicates whether it's an output parameter. These just check for the presence of ParameterAttributes.In and ParameterAttributes.Out, respectively, for the ParameterInfo's Attributes value. If IsIn returns false and IsOut returns true, the parameter is what C# refers to as an out parameter. If both IsIn and IsOut return true, it's a C# ref parameter.

Code Invocation

Assuming that you've got an instance of MethodInfo, you can invoke its code by calling the Invoke method. The simplest overload takes an object and object[], representing the this pointer and arguments, respectively. If the method is static, null should be passed for the this argument.

This code snippet demonstrates what some general purpose invocation routine might look like (sans error checking and proper argument binding):

 object InvokeMethod<T>(string f, T obj, object[] args) {     MethodInfo method = typeof(T).GetMethod(f);     return method.Invoke(obj, args); } 

This code omits details like ensuring the correct method overload gets selected based on the args passed in. This is often a bit of work. Much as with the Activator type outlined below, if your task is to bind to the correct method — disambiguating overloads along the way — a simpler solution is to use the Type.InvokeMember method. In fact, Type.InvokeMember does nearly the same as the example InvokeMethod<T> method, encapsulating both binding and invocation. It takes as input a string name, a BindingFlags value to constrain which methods it will inspect for a match, a Binder implementation (null just relies on the DefaultBinder — fine in almost all cases), the target object, and an array of arguments. InvokeMember requires that you to pass a BindingFlags value containing at least one of the following values: InvokeMethod, CreateInstance, GetField, SetField, GetProperty, or SetProperty. This tells it what you're trying to bind against, for example:

 object obj = /*...*/; string f = /*...*/; Type t = obj.GetType(); t.InvokeMember(obj, BindingFlags.InvokeMember | BindingFlags.Public |     BindingFlags.Instance, null, obj, f); 

One last note: If the target of the method invocation throws an exception, it gets wrapped in a TargetInvocationException. If you catch this exception, you can access the original exception that got thrown through the InnerException property. TargetSite returns the MethodBase indicating which method threw the original exception.

Inspecting Method Bodies

As of 2.0, you can access the body of a given method. This includes its IL, information about its stack size and locals, and its exception handling clauses. Calling the GetMethodBody method on MethodInfo returns this information wrapped up in a MethodBody instance:

 MethodInfo m = typeof(object).GetMethod("ToString"); MethodBody mb = m.GetMethodBody(); 

You can get a list of LocalVariableInfos, each of which represents a local slot used by the method body, with its LocalVariables property. These info objects give you information about each local's index, its pinning status, and a Type representing what the slot is typed as in the IL. Similarly, the ExceptionHandlingClauses list contains a set of ExceptionHandlingClause objects. Each contains information about the handling clause that mirrors what is available in the IL. For example, the Flags property indicates whether the clause is a Clause, Filter, Fault, or Finally block.

GetILAsByteArray returns a byte[] containing the IL stream that composes the method's body. Unfortunately, if you wish to do anything interesting with it, you'll have to parse the stream yourself. The System.Reflection.Emit.Opcodes index of instructions can be useful for parsing the data, as it has all of the instructions and byte representation information. But it is still nontrivial because each instruction accepts a varying size of arguments.

Constructing Instances of Types

Given a Type instance, you can instantiate an object from it. These mechanisms work dynamically with the runtime to allocate memory and initialize instances, much like the static newobj and initobj IL instructions do.

Invoking Constructors (ConstructorInfo)

A ConstructorInfo derives from MethodBase and offers nearly all the same functionality a MethodInfo object does. You can use this class to refer to both static and instance constructors, the former of which is easily accessible using the Type.TypeInitializer instance property. Type's GetConstructor method will perform a search against available constructors using a BindingFlags, an array of parameter types, and a specific Binder implementation (more often than not you will pass in a null, causing the default binding behavior to be used).

Both CallingConventions and the ParameterModifier array parameters are for advanced scenarios, such as binding to unconventional signatures and/or ref/out parameters; CallingConventions.Any and null are fine values to pass here for the logical default behavior. A simpler overload is available that just takes a Type[] and locates a public instance constructor that accepts those types as parameters. Lastly, GetConstructors will return a collection of constructors; the no-args version returns all public instance constructors, while the other version allows you to pass a BindingFlags.

Given an instance, you can create a new object by calling Invoke and passing an array of arguments:

 ConstructorInfo ci = typeof(string).GetConstructor(     new Type[] { typeof(char[]) }); string s = (string)ci.Invoke(new object[] {     new char[] { 'H', 'e', 'l', 'l', 'o' } }); 

The result is a new dynamically heap allocated object.

Activation

In many situations, you will have a Type and perhaps a set of constructor arguments in hand, and simply want to create a new object out of those. In these cases, you probably won't care to go through the processes of manually binding to the appropriate ConstructorInfo. The System.Activator class can come in handy here. It's the logical equivalent to Type.InvokeMember for method invocation.

Activator offers a static method CreateInstance with several overloads, the simplest of which just takes a Type and returns an object. Alternatively, you can use the generic overload, which takes a single type argument, T, and returns an instance of T:

 Customer c = Activator.CreateInstance<Customer>(); Customer c = (Customer)Activator.CreateInstance(typeof(Customer)); 

The generics-based version obviously looks a bit nicer and is more efficient at runtime because it avoids the superfluous cast instruction (and is less error prone). In either case, the result is the same as calling new Customer(). But unlike using a new instruction directly, it can be done in a general-purpose fashion, for example:

 T CreateAndLogNewObject<T>(string s) {     T t = Activator.CreateInstance<T>();     Log("Created new object '{0}'; {1}", t, s);     return t; } 

The above works for types that offer no-args constructors. But in cases where a type doesn't offer one, you will get a MissingMethodException at runtime. For example, consider if you tried it on string: string s = Activator.CreateInstance<string>(). string does not offer a default no-args constructor. Instead, you'll want to use the overload that takes an object[]of arguments as input. It does the magic to figure out which ConstructorInfo to bind against and then performs the actual invocation. Binding to the string constructor that accepts a char[] is as simple as follows: string s = Activator.CreateInstance<string>(new object[] { new char[] { 'H', 'e', 'l', 'l', 'o' } }). Overloads are also available for invoking private or protected constructors, or otherwise limiting to a precise BindingFlags combination. They are simply variants on what we've already seen. Activator also offers efficiency as a result of caching its bindings and lots of performance tuning.

It turns out that the C# compiler actually uses Activator silently in your code with C# 2.0. This occurs when you constrain a generic type parameter to new(). For example, the above CreateAndLogNewObject<T> function could have been written as follows:

 T CreateAndLogNewObject<T>(string s) where T : new() {     T t = new T();     Log("Created new object '{0}'; {1}", t, s);     return t; } 

The IL that gets emitted will be identical to that for the method above (aside from the presence of a new() constraint).

Properties

A property P in the type system is nothing more than a pair of set_P and get_P methods, along with a metadata index that refers to them and names the property. We discussed this point in depth in Chapter 2. PropertyInfo enables you to access these two methods as MethodInfos. The CanRead and CanWrite predicates indicate whether a getter or setter have been defined for this property, respectively. PropertyType provides the type of the property. GetGetMethod and GetSetMethod return the MethodInfo for the public getter and setter; for nonpublic getters and/or setters, the two methods have overloads that take a Boolean nonpublic.

Working with Generics

With the addition of generics to the type system in 2.0, the Reflection APIs have been changed to accommodate them. You may inspect the type parameters and actual arguments that a type or method has been assigned and even generate new generic instantiations at runtime. For more details on generics terminology and the feature itself, please refer to Chapter 2.

The Type.IsGenericType instance property indicates whether the target Type is a generic type or not. GetGenericArguments returns a Type[] of all unbound and bound type parameters. It might seem odd at first, but unbound type parameters are actually represented by a Type although their actual type hasn't been supplied yet. You can detect this by looking at the IsGenericParameter attribute on Type; it returns true if you're working with a parameter rather than an actual type.

 void PrintTypeParams(Type t) {     Console.WriteLine(t.FullName);     foreach (Type ty in t.GetGenericArguments())         Console.WriteLine("--> {0} {1} {2}",             ty.FullName,             ty.IsGenericParameter,             ty.IsGenericParameter                 ? ty.GenericParameterPosition                 : 0             ); } //... PrintTypeParams(typeof(List<>)); PrintTypeParams(typeof(List<int>)); 

In this code snippet, printing the arguments for List<> will output an empty string for FullName, and true for IsGenericParameter. You can also access the GenericParameterPosition property for any Type for which IsGenericParameter returns true. In this example, it would be 0. The FullName and IsGenericParameter properties for List<int> return "System.Collections.Generic.List`1 [[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken= b77a5c561934e089]]" and false, respectively.

For type parameters that have constraints applied to them, the GetGenericParameterConstraints method returns a Type[] indicating what types an argument must derive from in order to be valid. For example, Nullable<T> requires that T is a value type. This is represented in reflection by a constraint array that contains System.ValueType.

Open and Constructed Types

ContainsGenericTypeParameters indicates whether the generic type is still open. As discussed in the section on generics in Chapter 2, this means it has parameters that have yet to be bound to actual arguments. For example, consider this code:

 Type listType = typeof(List<>); Console.WriteLine("List<>: {0}, {1}",     listType.IsGenericType, listType.ContainsGenericParameters); Type listOfIntType = typeof(List<int>); Console.WriteLine("List<int>: {0}, {1}",     listOfIntType.IsGenericType, listOfIntType.ContainsGenericParameters); 

typeof(List<>) loads the type token for the generic type System.Collections.Generic.List<T>. In this case, T has not yet been bound to a type argument; thus, ContainsGenericParameters evaluates to true. typeof(List<int>) conversely, returns the type token for the fully constructed List<int> type, which returns false for ContainsGenericParameters because each of its parameters has been bound.

Of course, you can have a partially constructed generic type, such as the following:

 class StringKeyDictionary<V> : Dictionary<string, V> { } 

When dealing with such a type, ContainsGenericParameter still returns true. But if you were to inspect the GetGenericParameters array, you'd find typeof(string) as element #0 and an unbound type parameter as element #1. Note also that you can obtain the unconstructed Type from either a constructed or open type by calling the GetGenericTypeDefinition method. In the above example, this means that typeof(List<>).Equals(typeof(List<int>).GetGenericTypeDefinition()) evaluates to true.

Constructing Types

Open generic types can be fully or partially instantiated dynamically using reflection. Supplying a type argument actually generates an entirely new type. If this type hasn't been created before, new CLR data structures will be allocated. Calling methods or otherwise using it will cause the JIT to be loaded. MakeGenericType takes a params array of type arguments and returns the new Type. This example shows how to create a List<int> from a List<> type object:

 Type listType = typeof(List<>); Type listOfIntType = listType.MakeGenericType(typeof(int)); 

Most of the above discussion that is specific to types is also applicable to generic methods. In other words, MethodInfo offers IsGenericMethod, HasGenericArguments, and ContainsGeneric Parameters properties, and GetGenericArguments, GetGenericMethodDefinition, and MakeGenericMethod methods. These can be used to interact with generics with respect to methods. You'll notice naming differences when working with types versus methods, but in each case the mapping is fairly straightforward.

A Short Example: General-Purpose BusinessObject

To illustrate a comprehensive use of the info APIs, consider for a moment that we are defining the root of a business class hierarchy in our application, BusinessObject. As part of this type, we want to create a generic ToString override that iterates over the public properties of a type and returns a string containing their values. It doesn't need to be super high performance because it's only used for printing to I/O, logging, and for debugging purposes. This simple function enables all business objects to share a common ToString format without the authors having to write the same routine:

 abstract class BusinessObject {     // ...     public override string ToString()     {         Type t = this.GetType();         StringBuilder sb = new StringBuilder(             string.Format("#{0}<", t.FullName));         PropertyInfo[] props = t.GetProperties();         for (int i = 0; i < props.Length; i++)         {             if (i > 0) sb.Append(",");             PropertyInfo prop = props[i];             sb.Append(string.Format("{0}={1}",                 prop.Name, prop.GetValue(this, null)));         }         sb.Append(">");         return sb.ToString();     } } 

Calling GetType on this returns the dynamic type of the instance. We then call GetProperties on it to get an array of PropertyInfos representing all of the type's public properties. All we do at that point is walk through the list and print out each PropertyInfo's Name property in addition to calling GetValue to invoke the property accessor on this at runtime. The result? Classes inheriting from Business Object automatically get a ToString method that reports back the contents of each public property defined on the class. For example, a Customer object providing a FirstName and LastName property whose values are "Sean" and "Duffy", respectively, would report back "#Customer<FirstName= Sean,LastName=Duffy>" without the author of Customer even touching a line of ToString code.

As a next step, we might consider a generic factory that enables us to instantiate BusinessObjects and set properties on them dynamically.

 abstract class BusinessObject {     // ...     public static T Create<T>(Dictionary<string, object> props)         where T : BusinessObject     {         T newInstance = Activator.CreateInstance<T>();         Type t = typeof(T);         foreach (KeyValuePair<string, object> prop in props)         {             PropertyInfo pi = t.GetProperty(prop.Key,                 BindingFlags.Public | BindingFlags.Instance);             if (pi == null)                 throw new ArgumentException(                     string.Format("Property '{0}' not found", prop.Key));             if (!pi.CanWrite)                 throw new ArgumentException(                     string.Format("No setter defined for '{0}'", prop.Key));             pi.SetValue(newInstance, prop.Value, null);         }         return newInstance;     } } 

This incorporates activation, shows off a little more of PropertyInfo's capabilities, and also introduces a new problem that is very prevalent in dynamic programming such as this. Somebody might decide to use this API with a new Customer business object; Customer might have the following public API surface area:

 public class Customer : BusinessObject {     public string FirstName { get; set; }     public string LastName ;{ get; set; }     public int Age { get; set; }     // ... } 

Now somebody goes ahead and writes the following code:

 Dictionary<string,object> props = new Dictionary<string,object>(); props.Add("FirstName", "Joe"); props.Add("LastName", 25); props.Add("Agge", "Shmoe"); Customer c = BusinessObject.Create<Customer>(props); // ... 

There are two problems with this bit of code. And neither of them will actually show up until somebody executes the code. The problems are (1) we transposed the values for LastName and Age and will receive an ArgumentException from SetValue at runtime because of the type mismatch; we actually got lucky here, if they were of the same type this mistake likely wouldn't fail immediately and we'd simply assign the wrong values to different properties; (2) the Age property was misspelled "Agge"; our code will pick this up and throw an ArgumentException at runtime.

These types of errors are very common in dynamic and late-bound programming. The best way to identify and fix errors such as this is to employ very targeted and complete test coverage. With statically typed languages, the type system and your compiler work together to catch binding errors such as early as possible. In fact, one of the strongest arguments in favor of statically typed languages (and against dynamic) is this very fact. Whenever you decide to give that up, you're opening yourself up to simple mistakes like this.

Token Handle Resolution

The info APIs are a heavy weight to hold on to. Not only do the actual data structures take up more working set, but an info instance also keeps entries alive in reflection's internal caching mechanisms. As we've already mentioned, if you're storing a reference for a long period, for example for manual caching purposes, a more appropriate data structure to use is either a token or handle. A MethodInfo, for example, can be uniquely identified and looked up via a RuntimeMethodHandle.

Handles are lightweight wrappers over IntPtrs to internal CLR data structures. In fact, they are to reflection data structures what HANDLEs are to process-wide Windows objects. (Note: these handles are not available in reflection-only context because internally they rely on execution data structures that do not get instantiated in reflection-only scenarios.) Tokens are also lightweight. They are simply integers that, when combined with the enclosing module, constitute a unique key for the target reflection abstraction.

All of the info APIs offer properties that retrieve their corresponding handle and token, for example:

 // Get the Info representation: Type typeInfo = typeof(object); MethodInfo methInfo = typeInfo.GetMethod("ToString"); // Obtain the Handles: RuntimeTypeHandle typeHandle = typeInfo.TypeHandle; RuntimeMethodHandle methHandle = methInfo.MethodHandle; // Or alternatively, obtain the Tokens: ModuleHandle moduleHandle = methInfo.Module.ModuleHandle; int typeToken = typeInfo.MetadataToken; int methToken = methInfo.MetadataToken; 

Some APIs can accept and work directly with tokens and handles, meaning that you can avoid instantiating info types once you've stored a token or handle away. But of course, a token or handle by itself doesn't provide nearly the same capabilities as an XxxInfo instance. Thus, there is a set of APIs that permit you to turn an instance of one into another. These are significantly faster to use than string-based lookups (e.g., Type.GetMethod), for example, because both tokens and handles have optimized resolution mechanisms. Handles, for example, refer directly to internal data structures, meaning that accessing them amounts to indexing into an internal table and following the resulting pointer. You can retrieve a handle or token from an info, get the associated handle from a token and its module, and get the info back again using either a handle or token. Figure 14-2 graphically depicts this process.

image from book
Figure 14-2: Relationship between infos, handles, and tokens.

The APIs involved in these resolutions are as follows:

  • The XxxInfo APIs offer XxxHandle properties that retrieve an info's corresponding handle. We saw this above in the code example. For instance, Type has a TypeHandle property, MethodInfo has a MethodHandle property, and so on.

  • Obtaining the corresponding XxxInfo from an XxxHandle instance is done through static methods on the XxxInfo class. Type.GetTypeFromHandle function takes a RuntimeType Handle and returns its corresponding Type instance; similarly, MethodBase.GetMethod FromHandle takes a RuntimeMethodHandle and returns a MethodBase.

  • Getting a token from an XxxInfo is done with the MetadataToken property. You will also need the module in some form to go back to either a handle or info from a token. Its info can be accessed using the Module property of the XxxInfo object. You can then store the module handle, for example, rather than the full-blown Module instance.

  • Going back from an integer token to an XxxInfo requires a Module instance. The Module type offers a set of instance methods such as ResolveType and ResolveMethod that, when passed a token, will obtain the corresponding info object.

  • Lastly, the ModuleHandle class has instance methods to turn tokens into handles. For example, ResolveTypeHandle and ResolveMethodHandle take tokens as input and hand you back the corresponding handle.

Caching Bindings

Imagine that you have a procedure that performs some rudimentary binding from a string-based method name and a set of arguments. Caching the result of a binding will save cost for future binding requests using the same values. Most binding procedures perform a brute force search of the available methods, performing many string comparisons in the process, and sometimes must utilize complex disambiguation schemes. Keeping the original match around for a while in a cache can reduce the runtime cost of late-binding tremendously.

You might be tempted to cache the results using the MethodInfo that was bound to. But as noted previously, handles and tokens are much more appropriate for caching purposes. They are simple wrappers over integers and don't have the runtime working set costs of the info objects to which they refer. For caches, you really just want to keep some structure around so that you can repeat the match in case the same binding is requested again. But in the case that it isn't, you want to have the smallest impact on memory as possible.

Listing 14-1 demonstrates what an implementation of such a cache might look like.

Listing 14-1: Cached reflection bindings using handles

image from book
 class BindingCacheKey {     private RuntimeTypeHandle type;     private string method;     private RuntimeTypeHandle[] argTypes;     public BindingCacheKey(Type type, string method, Type[] argTypes)     {         this.type = type.TypeHandle;         this.method = method;         this.argTypes = new RuntimeTypeHandle[argTypes.Length];         for (int i = 0; i < argTypes.Length; i++)             this.argTypes[i] = argTypes[i].TypeHandle;     }     public override bool Equals(object obj)     {         BindingCacheKey key = obj as BindingCacheKey;         if (key == null)             return false;         return Equals(key);     }     // Equals and GetHashCode implementations omitted for brevity } class CachedLateBinder {     private Dictionary<BindingCacheKey, RuntimeMethodHandle> cache =         new Dictionary<BindingCacheKey, RuntimeMethodHandle>();     public MethodBase GetMethod(object obj, string method, object[] args)     {         Type type = obj.GetType();         Type[] argTypes = new Type[args.Length];         BindingCacheKey key = new BindingCacheKey(type, method, argTypes);         MethodBase match;         if (cache.ContainsKey(key))         {             match = MethodBase.GetMethodFromHandle(cache[key]);         }         else         {             // Perform slow matching behavior...             match = SlowMatch(obj, method, args);             cache[key] = match.MethodHandle;         }         return match;     } } 
image from book

As you can see here, we first try to hit our cache of bindings. If that succeeds, we simply retrieve the associated info from the handle we stored in our dictionary. Otherwise, we call the SlowMatch function to perform the search for a suitable method; assuming that succeeds, we store the result in our cache so that next time we avoid having to do this slow matching. A real implementation would have to address purging the cache periodically to avoid infinite growth.




Professional. NET Framework 2.0
Professional .NET Framework 2.0 (Programmer to Programmer)
ISBN: 0764571354
EAN: 2147483647
Year: N/A
Pages: 116
Authors: Joe Duffy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net