Saturday, November 15, 2008

Reflection support for generic types in .NET Compact Framework

http://blogs.msdn.com/nazimms/archive/2005/01/25/360324.aspx

Reflection support for generic types in .NET Compact Framework

Version 2 of .NET Compact Framework (NETCF) will provide support for parametric polymorphism aka Generics. You can get a good feel for the feature support from blog posts by Seth Demsey and Roman Batoukov. It would also be useful to go over Generics terminology as presented in this article in section 1.1.



Evidently since this language feature adds to the very nature of types in the CLR, one could safely assume need for special support when reflecting on these types … and such is the case. Below is a list of new Reflection APIs provided to assist in discovering information about a generic type/method through reflection. It should be noted however that previously existent reflection APIs like Type.GetMethods() etc would also discover generic methods and the like.







I. New Reflection APIs for generic support.



(Note: This list is current as of .NET Framework 2.0 Beta 1 and may change henceforth.)



1. System.Type



Properties

ContainsGenericParameters

HasGenericArguments

IsGenericParameter

IsGenericTypeDefinition



Methods

BindGenericParameters

GetGenericArguments

GetGenericTypeDefinition





2. System.Reflection.MethodInfo



Properties

ContainsGenericParameters

HasGenericArguments

IsGenericMethodDefinition



Methods

BindGenericParameters

GetGenericArguments

GetGenericMethodDefinition







II. How and where would I use it?



1. Creating generic types via reflection.



NETCF does not have runtime IL verification and metadata validation and hence cannot validate generic parameter constraints for normal execution scenarios (if the compiler did not catch the error at compile time). However, if one were to use reflection, i.e. get an open generic type and the bind parameters to it via BindGenericParameters() API, constraint validation would be performed. So if constraint validation is important to you, use the reflection API to bind parameters (if the compiler would not catch the error). Here is an example:



Say I have a generic type:



public class MyGenericType

where T: IFormattable

where U: IConvertible

{}



Something like MyGenericType should not be allowed, since System.Object does not implement IConvertible. However if you were to write code that looked as below, we would validate the constraints on MyGenericType and throw an exception.



Type foo = Type.GetType("MyGenericType`2");

Type[] params = new Type[] {typeof(Int32), typeof(Object)};

Type bar = foo.BindGenericParameters(params); // This will throw an exception because constraints are not met.



Type foo above denotes the open type for MyGenericType, meaning no generic parameters have been bound to it yet. The open type is also referred to as the Generic Type Definition. The "`2" (back-tick) notation refers to the number of generic parameters on the type. So a class like



public class GenType2



would have an open type notation of



Type foo2 = Type.GetType("GenType2`4");



Creating a bound type for GenType2 would look something like what is shown below. As one would guess, the types foo3, foo4 and foo5 are identical.



Type foo3 = Type.GetType("GenType2`4[System.Int32, System.Int32, System.Int32, System.Int32]");

GenType2 bar2 = new GenType2();

Type foo4 = bar2.GetType();

Type foo5 = typeof(GenType2);



What would the type string look like if one of the generic parameters above was a type in a different assembly? Well it would look something like what is shown below.



Type foo6 = Type.GetType("MyGenericClass`2[System.Int32, [MyNameSpace.MyExternType, MyExternAssemblyName]]");



Of course you could also go ahead and provide more information like the public key token if you wanted.





2. Invoking generic methods and methods with generic arguments using reflection



Say I have the following type definition.



public class MyGenericType

{

public void Method1 (T x) {}

public void Method2 () {}

}



Method1 is a method which has a generic argument and Method2 would be a generic method. Hopefully this example helped untangling your brain from the topic being discussed. In Method1, the first argument x happens to be of the same type as the first generic parameter that MyGenericType was created with (T). In Method2 however, V is a generic type and has no relation to the generic parameters used to create MyGenericType. Here's how these methods would be called in the normal execution scenario.



MyGenericType mgt = new MyGenericType();

Int32 i = 0;

mgt.Method1( i );

mgt.Method2();



Here's how you would do it using reflection.



MethodInfo mi1 = typeof(mgt).GetMethod("Method1");

MethodInfo mi2 = typeof(mgt).GetMethod("Method2");

mi1.Invoke(mgt, new object[] {5}); // The method argument has to be castable to an Int32.

MethodInfo mi2_bound = mi2.BindGenericParameters(typeof(System.DateTime));

mi2_bound.Invoke(mgt, new object[] {});

MethodInfo mi2_unbound = mi2_bound.GetGenericMethodDefinition();



As you noticed we had to first bind a generic parameter to Method2 before we could Invoke it. In the code above, mi2_unbound would be the same as mi2.







III. Limitations



1. Getting members of open generic types



Getting members, methods or interfaces of open generic types is not supported in NETCF. You will get a NotSupportedException thrown when you attempt to do something similar to what is shown below.



Type t = Type.GetType("MyGenericType`2");

Type[] InterfaceList = t.GetInterfaces() // This will throw a NotSupportedException

MethodInfo mi = t.GetMethod("Method1"); // This will throw a NotSupportedException



What you would need to do to work around this is to simply bind some type parameters to your generic type (instantiate it) and then attempt to get its members.



Type t = Type.GetType("MyGenericType`2[System.Int32, System.Int32]");

MethodInfo mi = t.GetMethod("Method1"); // This will work.





2. Methods/Properties not supported on formal types



If you were to call GetGenericArguments() on an open generic type, you would get the generic parameter list ... also called formal parameters.



Type t = Type.GetType("MyGenericType`2");

Type[] genargs = t.GetGenericArguments();



The array genargs would actually contain the type T and U as defined in the class definition. You are limited to what you can actually call on this formal type. Attempting to access BaseType, DeclaringType, ReflectedType etc. would result in a NotSupportedException being thrown. This is also true if you attempt to get interfaces, methods or members on it.





3. Discovering constraints on parameters through reflection.



NETCF does not support the GetGenericParameterConstraints() API and as such you will not be able to look up the constraints on generic parameters of any generic type/method through reflection. Another mechanism that the full .NET framework provides for discovering this is through discovery on formal parameters, but the restrictions on formal parameters stated above in III.2 would prevent you from doing this on NETCF. This restriction is further elaborated in IV.2.





4. Can't resolve method signature ambiguities because of lack of reflection on open types.



Say I have a type definition that looks something as shown below.



public class Generic2P

{

public void method (T x) {}

public void method (U y) {}

}



public class Generic1P

{

public void method(S s)

{

Generic2P foo = new Generi2P();

foo.method(s);

}

}



If for the instantiation of Generic1P's method has the same type used for both its generic parameters R and S, then it would result in ambiguity as to which method would be called on Generic2P. For example if I have



Generic1P bar = new Generic1P();

bar.method(5);



Then this would result in Generic2P getting created and Generic2P.method(int) being called. However there will be two methods with this exact same signature. Both NETCF and the full .NET framework will default to the same method, but what if that is not the method you want to call?



On the full .NET framework one could get methods on the open type Generic2P and based on the formal parameter used as the argument in the method, you could fetch the right method and bind parameters/arguments to it and invoke it. However since we can not get methods or members on open types in NETCF we would not be able to do this and you would not be able to manually resolve signature ambiguities.







IV. Differences with the full .NET Framework



1. Lack of TargetException, TargetInvocationException, TargetParameterCountException.



NETCF does not support the above mentioned exceptions that are thrown on the full .NET framework when one attempts to call BindGenericParameters() with illegal arguments. On NETCF the following substitutions occur.



TargetException -> ArgumentNullException, InvalidProgramException

TargetInvocationException -> MissingMethodException

TargetParameterCountException -> ArgumentException.





2. Constraint hierarchy vs. Derivation hierarchy.



Consider the following type definitions.



public interface InterfaceA {}

public interface InterfaceB {}

public class BaseA: InterfaceA {}

public class InheritedA: BaseA, InterfaceB {}



public class BaseB

where T: InterfaceA

{}



public class InheritedB

where T: BaseB, InterfaceB

{}



In the case of InheritedA, it inherits from BaseA that implements InterfaceA. This hierarchy would constitute the derivation hierarchy. In the case of InheritedB, its generic parameter is constrained to inheriting from BaseB, whose generic parameter is constrained to implementing InterfaceA. This hierarchy would constitute the constraint hierarchy.



On the full .NET framework, if we were to get the formal type T via a call to GetGenericArguments() on the open type InheritedB; we could traverse the constraint hierarchy as if it were a derivation hierarch by using the BaseType property and calling GetInterfaces() method on it. In NETCF however we distinguish between the two and do not allow you to treat constraint hierarchy as if it were a derivation hierarchy. So you cannot use the BaseType property and the GetInterfaces() method to peruse the constraint hierarchy. Instead you would get a NotSupportedException thrown at you.







3. Differences in exceptions thrown.



There are many places where we throw MissingMethodExceptions instead of ArgumentExceptions and the like. Hopefully this will be addressed in another blog post that will be soon to come.

No comments: