As I began, though, I kept finding member names that were formatted oddly. For example, using my program to list out the contents of the mscorlib assembly, I found names like Dictionary`2 and ReadOnlyCollection`1.
It turns out that names like those exist for a reason. The Microsoft .NET Framework exposes information about generic types and methods differently than it does for those that are non-generic. This realization led me to investigate—and now document—reflection features as they relate to generics. While generics are new as of the .NET Framework 2.0, using reflection to investigate and fabricate or call generic types and methods isn't terribly difficult. My sample application (available from the MSDN® Magazine Web site) shows off as many of the features exposed at the intersection of generics and reflection as I could discuss.
If generics are completely new to you, see "Overview of Generics in the .NET Framework" at msdn2.microsoft.com/ms172193. Most importantly, review the terms described in the article because my goal is to show you how to extend existing reflection-based applications to handle generic types and methods.
Given an assembly, you can easily use reflection to investigate its members. You will need to use the Type.IsGenericType property to learn whether each type is generic in order to later create an instance of it. In my sample, the TestForGenericType procedure loads the contents of the mscorlib assembly and displays information about all the public generic types:
Dim asm As Assembly = Assembly.ReflectionOnlyLoad("mscorlib")
For Each typ As Type In asm.GetTypes()
If typ.IsPublic AndAlso typ.IsGenericType Then
AddToResults(typ.Name)
End If
Next
In the sample, the AddToResults method and the DisplayResults method (not shown here) simply build output text and display it in an alert. So that you can investigate types that you created, the sample also includes a TestClasses.vb file that contains several sample classes, as shown in Figure 1.
TestForGenericType continues by testing these three classes:
AddToResults("GenericClass(Of Integer) is generic: {0}", _
GetType(GenericClass(Of Integer)).IsGenericType)
AddToResults("NonGenericClass is generic: {0}", _
GetType(NonGenericClass).IsGenericType)
AddToResults( _
"BaseClass(Of Integer, String) is generic: {0}", _
GetType(BaseClass(Of Integer, String)). _
IsGenericType)
This code creates constructed versions of each of the generic types shown previously. (A constructed type is a generic type for which you've supplied specifics for each placeholder type.) GenericClass and BaseClass are generic types, whereas NonGenericClass is not. Note that when the .NET Framework returns the Name property of a generic type, it appends the number of generic parameters. That's why the Dictionary class appears as Dictionary`2.
You can repeat the same sort of experimentation for methods. When it comes time to execute a method dynamically, you'll need to know if the method is generic so you can construct a version of it in which you've satisfied all its type placeholders. A method is considered generic if it includes two sets of type arguments: one that describes the method as a whole, and the other that supplies the method's parameters. In GenericClass, for example, you'll find two overloaded versions of the Swap method. The first one isn't generic—its parameter types are defined by the class's type placeholder. The second one is generic, however. In this case, the compiler infers the type of the placeholder from the type of the first parameter to the procedure. (If you make an attempt to call the procedure, passing in two different types, the compiler will complain.) The TestForGenericMethod procedure uses the MethodInfo.IsGenericMethod property to determine if a method is generic and calls the following code to test the property:
Dim typ As Type = GetType(GenericClass(Of Integer))
For Each method As MethodInfo In typ.GetMethods( _
BindingFlags.Public Or BindingFlags.Instance _
Or BindingFlags.DeclaredOnly)
AddToResults(method.Name & " is generic: {0}", _
method.IsGenericMethod)
Next
This code uses the BindingFlags enumeration in its call to the Type.GetMethods method, so that it only retrieves public instance methods that are declared directly in the class. The output looks like this:
Swap is generic: True GenericMethod is generic: True Swap is generic: False
If you want to create an instance of a type, you'll need to know if it's a generic type definition or a constructed generic type. A generic type definition has one or more of its type placeholders unsatisfied—in other words, a generic type definition isn't aware of its parameter types. A constructed generic type (as you've seen in the previous examples) has its type placeholders replaced with specific types. You can't create an instance of a non-constructed generic type—you must supply its types before you attempt to create the instance. Use the Type.IsGenericTypeDefinition property to determine the status of the type, as in the code from the TestForGenericTypeDefinition procedure shown in Figure 2.
You've already seen how to retrieve a Type object corresponding to a constructed generic type. You can also do the same with a generic type definition, as you saw in the first line of Figure 2 (make sure you include a comma separator between each of the required type placeholders):
Dim type1 As Type = GetType(Dictionary(Of ,))
Given a constructed type, you can use the Type.GetGenericTypeDefinition method to retrieve the generic type's definition. The following code creates a constructed instance of the generic Dictionary class, retrieves its type, and then retrieves the generic type definition of the constructed generic type:
Dim type2 As New Dictionary(Of String, DateTime) Dim type3 As Type = type2.GetType() Dim type4 As Type = type3.GetGenericTypeDefinition()
Finally, the procedure compares the references in type4 and type1; they should be the exact same reference (that is, the generic type definition of the Dictionary instance). The procedure's output looks like the following, indicating that type1 and type4 are the same generic type definition:
type1 is generic type definition: True type3 is generic type definition: False type4 is generic type definition: True type1 is type4: True
Type3, a constructed generic type, isn't a generic type definition.
You can use a similar technique to determine if the type you're inspecting is a standard type, a generic type, or a generic type definition. The following illustrates the MethodInfo.IsGenericMethod and MethodInfo.IsGenericMethodDefinition properties:
Dim type1 As Type = GetType(GenericClass(Of ))
Dim method As MethodInfo = type1.GetMethod("GenericMethod")
If method.IsGenericMethodDefinition Then
Dim miConstructed As MethodInfo = _
method.MakeGenericMethod(GetType(Integer))
AddToResults("Is generic method definition: {0}", _
miConstructed.IsGenericMethodDefinition)
AddToResults("Is generic method: {0}", _
miConstructed.IsGenericMethod)
End If
Every generic method definition is also a generic method, but the converse is not true; every generic method is not a generic method definition. If you've supplied the type placeholder values for a method, it's a generic method but not a generic method definition. This sample also demonstrates the MethodInfo.MakeGenericMethod method, which allows you to take a generic method definition and supply types for its type placeholders, thereby constructing a generic method from the definition. The sample procedure displays the following output:
Is generic method definition: False Is generic method: True
Again, this sample is operating on the generic type definition:
Public Function GenericMethod(Of M)(ByVal item1 As M) As String
Return String.Format("You passed in: {0}", item1)
End Function
And by calling the MakeGenericMethod method, passing in the Integer type, the code then proceeds to create the following generic method at run time (item1 represents the type you've passed in from the calling code):
Public Function GenericMethod (ByVal item1 As Integer) As String
Return String.Format("You passed in: {0}", item1)
End Function
Assuming that one of your goals is to be able to create an instance of a generic type and perhaps to call a constructed generic method, you'll need a few more techniques. In order to create an instance of a type, there can't be generic type definitions or generic method definitions with unsatisfied type placeholders anywhere in the entire chain of dependencies for the type. Generic types for which all the type placeholders have been specified are called closed generic types (the same terminology applies to methods). You can only create an instance of a closed generic type, and you can only invoke a closed generic method.
But what if you're handed a generic type that has a complex inheritance hierarchy? How can you be sure there aren't any open types in the type's ancestry? To determine this, you can retrieve the Type.ContainsGenericParameters property, which indicates definitively if you're working with a closed or open generic type: if the property returns True, you'll know you have an open generic type, and you won't be able to create an instance of it.
The TestForOpenOrClosedGeneric procedure demonstrates this behavior using two classes in the TestClasses file, which are shown here:
Public Class BaseClass(Of T, U)
Public Items As New Dictionary(Of T, U)
End Class
Public Class DerivedClass(Of V)
Inherits BaseClass(Of Integer, V)
End Class
The TestForOpenOrClosedGeneric procedure calls the DisplayGenericTypeInfo procedure in Figure 3, displaying information about several different types.
Given a type, the DisplayGenericTypeInfo procedure first displays the type name, whether the type is a generic type definition, and whether it's a generic type. Next, it displays whether the type contains generic parameters (if it's a generic type definition, it contains generic parameters and can't be instantiated). The code in Figure 3 then calls the GetGenericArguments method to retrieve a list of all the generic arguments and displays each generic argument in the output. Finally, the code uses the ContainsGenericParameters property to indicate whether the type is open or closed. (Note that you can apply the same sort of logic to MethodInfo instances in order to determine if a particular MethodInfo represents an open or closed generic method.)
The TestForOpenOrClosedGeneric method includes the following code, which tests several of the sample methods, both as generic definitions and as constructed types. See if you can predict the output for each:
Dim baseType As Type = GetType(BaseClass(Of ,)) DisplayGenericTypeInfo(baseType) Dim derivedType As Type = GetType(DerivedClass(Of )) DisplayGenericTypeInfo(derivedType) DisplayGenericTypeInfo(derivedType.BaseType) Dim constructedType As Type = GetType(DerivedClass(Of String)) DisplayGenericTypeInfo(constructedType) DisplayGenericTypeInfo(constructedType.BaseType)
The CreateGenericTypeGivenGenericTypeDefinition sample procedure shows how you can create a closed generic type given a generic type definition. You've already seen this technique applied to a method definition: just as you called the MethodInfo.MakeGenericMethod method to construct a generic method from its definition, you can call the Type.MakeGenericType method to construct a generic type from its definition. The sample code creates a constructed type at run time and compares it to a constructed type it created at design time, as you can see in Figure 4.
In order to call a generic method given its generic method definition, you must call the MethodInfo.MakeGenericMethod method, passing an array containing Type objects corresponding to each of the method's type placeholders. Given the new constructed method, you can call the MethodInfo.Invoke method, supplying a host object instance and an array of parameters for the method. Of course, the type of the parameters must correctly match the type of the placeholder type you specified when you constructed the generic method instance.
The CallGenericMethodGivenGenericMethodDefinition sample procedure in Figure 5 demonstrates this technique. This sample starts by retrieving a MethodInfo instance corresponding to the GenericMethod method in GenericClass. This method accepts a single type placeholder and simply returns a string containing the value you supply at run time:
Public Function GenericMethod(Of M)(ByVal item1 As M) As String
Return String.Format("You passed in: {0}", item1)
End Function
The code does its work by constructing a specific generic class, supplying a type for the class's type placeholder:
Dim type1 As Type = GetType(GenericClass(Of Integer))
Dim mi As MethodInfo = type1.GetMethod("GenericMethod")
DisplayGenericMethodInfo(mi)
As you learned earlier, you won't be able to call a generic method if there are any unsatisfied type placeholders in its ancestry (that is, if it's not a closed generic method). To prove that point, run the procedure once as is and then modify it so that you specify a generic type definition for type1. The code will compile, but it will fail at run time:
Dim type1 As Type = GetType(GenericClass(Of ))
The sample calls the DisplayGenericMethodInfo procedure, which displays information about the generic method and about each of its generic arguments.
The code continues by creating an array of Type instances and then calls the MethodInfo.MakeGenericMethod method to create a constructed generic method, as you see here:
Dim argTypes() As Type = {GetType(String)}
Dim miConstructed As MethodInfo = mi.MakeGenericMethod(argTypes)
DisplayGenericMethodInfo(miConstructed)
Given the constructed generic method, the code creates an instance of GenericClass (you'll see exactly how to do this in the next demonstration), creates an array of parameters to pass to the method, and calls MethodInfo.Invoke in order to execute the constructed generic method:
Dim host As New GenericClass(Of Integer)
' Supply the method parameters, and then invoke the method:
Dim args() As Object = {"test value"}
AddToResults("{0}", miConstructed.Invoke(host, args))
To see this in action, you can create multiple, different constructed versions of the same generic method definition. The code will then create a new generic method definition, this time indicating that it will pass an integer (as opposed to a string). The code creates the constructed generic method and then invokes it, as before:
argTypes(0) = GetType(Integer)
miConstructed = mi.MakeGenericMethod(argTypes)
DisplayGenericMethodInfo(miConstructed)
' Supply the method parameters, and then invoke the method:
args(0) = 13
AddToResults("{0}", miConstructed.Invoke(host, args))
The CreateClosedGenericInstance sample method uses a similar technique, but creates a constructed generic type instead. In the previous example, the code created the constructed type at design time. In this example, it happens at run time.
Although you've already seen code that retrieved information about generic arguments, doing so involves some subtleties worth examining. The RetrieveTypeParameterInfo sample procedure works with several different generic types and generic type definitions, displaying generic argument information for each. The Type.GetGenericArguments method returns an array, and if the current type is a generic type definition, the array contains the type parameters for the generic type definition in the order in which they appear in the type definition. If the current type is a closed constructed type, the array contains the types that have been assigned to the generic type parameters. If the current type is a generic type definition, the array contains the type parameters. If the current type is an open constructed type (that is, not all the parameters have been satisfied and the ContainsGenericParameters property returns True), the array contains both types and type parameters. You can use the IsGenericParameter property to distinguish between the two.
The RetrieveTypeParameterInfo procedure contains within it the following code:
Dim type1 As Type = GetType(DerivedClass(Of String)) DisplayGenericParameterInfo(type1) Dim type2 As Type = type1.GetGenericTypeDefinition DisplayGenericParameterInfo(type2) Dim type3 As Type = type2.BaseType DisplayGenericParameterInfo(type3)
What this code displays is the parameter information for a constructed generic type, for the corresponding generic type definition, and for the base type of the generic type definition (the BaseClass sample class).
DisplayGenericParameterInfo does most of the work in this sample and includes the code in Figure 6. After displaying the type name, the code displays indications of whether the type is a generic type definition, whether it's a generic type, and whether it contains generic parameters. If it is a generic type, the code retrieves the array of generic arguments:
Dim args As Type() = t.GetGenericArguments()
Then, for each Type instance in the array, the code determines if it has a generic parameter (or a Type, from a constructed type). If so, it displays the GenericParameterPosition property value; if not, it displays the type name. (Retrieving the GenericParameterPosition property for types that aren't generic parameters will trigger a run-time exception.)
Determining the Source of a Generic Parameter
If some code hands you a generic parameter, you may need to determine if it was for a generic type or a generic method. It might come from a type you are examining, from an enclosing type, or from a generic method. How can you determine the source? First, check the type's DeclaringMethod property. If the value isn't Nothing, it will be a MethodInfo instance, and you will know that the type parameter came from a generic method, and the MethodInfo's IsGenericMethodDefinition property will return True. If the source isn't a generic method, the DeclaringMethod property returns Nothing, and you can then retrieve the DeclaringType property. This type will always be a generic type definition. You can only retrieve the DeclaringMethod property if the IsGenericParameter property of the type returns True; otherwise, an exception will be raised.
Finally, you may need to investigate generic type argument constraints. These constraints enforce rules on type arguments, ensuring that generic types adhere to the application requirements. You can add constraints that force a type parameter to inherit from a specific class, implement one or more specific interfaces, be a value type, be a reference type, and/or provide a default constructor. You might want to determine programmatically which constraints, if any, apply to a type argument, so that you can construct a type dynamically that meets the constraint requirements.
In order to examine constraints, you must take two sets of steps. In order to retrieve information about a single class or one or more interfaces that a type argument must inherit or implement, you need to call the Type.GetGenericParameterConstraints method. This method returns an array of Type objects, each of which indicates one class or interface.
To retrieve information about variance and other special constraint types, you must work with two separate bit masks. For variance constraints, you retrieve the type's GenericParameterAttributes property. Then you must apply the GenericParameterAttributes.VarianceMask bit mask to the returned value. Compare the result to the GenericParameterAttributes.Covariant or Contravariant enumerated value to determine if the type argument has a variance constraint.
To retrieve information about the special constraints, apply the GenericParameterAttributes.SpecialConstraintMask bit mask. This time, compare the results against the ReferenceTypeConstraint, NotNullableValueTypeConstraint, and the DefaultConstructorConstraint enumerated values to determine the status of each special constraint. The ExamineGenericParameterAttributes method does all this work for you, using the IsBitSet helper procedure to determine if a particular bit has been set:
Private Function IsBitSet( _
ByVal value As Integer, ByVal bitValue As Integer) As Boolean
Return (value And bitValue) = bitValue
End Function
The ExamineGenericParameterAttributes procedure includes the code in Figure 7, which follows the preceding steps.
The sample DisplayConstraintInfo procedure includes the code in Figure 8, which displays parameter constraint information for the type it's handed.
As mentioned earlier, this procedure loops through all the parameters and, for each, first calls the ExamineGenericParameterAttributes procedure (handling variance and other special constraints) and then loops through all the types that resulted from calling the GetGenericParameterConstraints method.
The ExamineConstraints sample procedure is shown here:
DisplayConstraintInto(GetType(TestClass1(Of ))) DisplayConstraintInto(GetType(TestClass2(Of ))) DisplayConstraintInto(GetType(TestClass3(Of )))
It examines the behavior of the DisplayConstraintInfo procedure for three different classes, all of which have different argument constraints. The sample classes look like Figure 9.
Your Turn
At this point, you've seen just about every method and property exposed by the .NET Framework at the intersection of reflection and generics. All you need to do now is download the sample application, follow through all the code and output, read the associated documentation topics, and you should be ready to tackle any problems related to reflection and generics.
Send your questions and comments for Ken to basics@microsoft.com.