Declarative Generics And Type Converters






4.68/5 (11 votes)
Sep 17, 2004
5 min read

86294

2
How to declaratively define a closed generic and use type converters to implement assignment from a string.
Introduction
I'm interested in figuring out how to declaratively define a generic closed type and use type converters to implement custom conversion from a string to an instance of the closed type. Why? Because I want MyXaml to support generics declaratively for the upcoming release of VS2005 and C# v2. This article discusses my findings regarding these issues, illustrates with code examples how to use reflection with generics, and discusses the complications of type conversion. This article will not discuss markup or other declarative syntax issues. If you're interested in that as well, you can read my blog entry on a proposed syntax.
There is no download--all the code is presented in the article--and it also requires the Beta 1 release of Visual Studio 2005.
A Brief Description Of Generics And Terminology
There's a lot of information out there on generics (aka templates, for you C++ people), so I'm not going to provide an exhaustive description--just enough so that you can understand this article if you've never seen generics before.
Generics provide a means of managing other classes with strong type checking and is most frequently seen with lists. Previously, a list was a collection of objects. Once the instance was added to the list, the programmer "lost" type information and needed to cast the object to the specific type when extracting the instance from the list:
List myList=new List();
MyInstance inst=new MyInstance();
myList.Add(inst);
MyInstance inst2=(MyInstance)myList[0];
An object cast to the wrong type would result in a runtime error. With generics, type information is preserved so the compiler can perform type checking, which results in more robust code and eliminates the cast:
List<MyInstance> myList=new List<MyInstance>();
MyInstance inst=new MyInstance();
myList.Add(inst);
MyInstance inst2=myList[0];
Terminology
The words "open" and "closed" generic are often used and it's important to know the difference. An open generic is the definition of the generic, such as:
List<T>
whereas a "closed" generic qualifies the generic type parameters with a specific type, like:
List<int>
Only closed generics can be instantiated because the compiler knows the specific generic type.
The "where" Clause
The "where" clause (see example below) is a nifty way of telling the compiler information about the type parameters. Must the type implement a specific interface? Must it have a parameterless constructor? Must it be a value type? These are useful ways of ensuring that you use the generic class correctly, as the internal implementation might constrain that usage. In the example below, I am constraining the generic class to value types.
The Goal
The goal is to be able at runtime (yes, this sort of defeats the purpose of compile time type checking, more on this later) to say something like this:
First, given the open generic List
, construct an instance with the generic type System.Int32
.
Second, given the instance, provide a type converter so that I can initialize it with a string. For example, if the generic is a complex number class, I might want to initialize it with the string "1, 2".
Implementation
The following discusses the implementation that meets our requirements.
The ComplexNumber Class
First, let's construct a class to work with:
public class ComplexNumber<T> where T : struct
{
protected T real;
protected T imaginary;
public T Real
{
get { return real; }
set { real = value; }
}
public T Imaginary
{
get { return imaginary; }
set { imaginary = value; }
}
public override string ToString()
{
return "("+real.ToString() + ", " + imaginary.ToString() + ")";
}
}
Acquiring The Generic Type
At this point, we can look at how to acquire a closed generic type for the above generic class. Given that we want to say something like this:
Type cnIntType = GetGenericType("Generics.ComplexNumber", "System.Int32");
Type cnDoubleType = GetGenericType("Generics.ComplexNumber", "System.Double");
The implementation of the GetGenericType method is:
public static Type GetGenericType(string genericClass, string typeList)
{
// get the comma delimited type list
string[] types = typeList.Split(',');
// construct the mangled name
string mangledName = genericClass + "`" + types.Length.ToString();
// get the open generic type
Type genericType = Type.GetType(mangledName);
// construct the array of generic type parameters
Type[] typeArgs = new Type[types.Length];
for (int i = 0; i < types.Length; i++)
{
typeArgs[i] = Type.GetType(types[i]);
}
// get the closed generic type
Type constructed = genericType.BindGenericParameters(typeArgs);
return constructed;
}
Note that this is a three step process:
- Get the open type of the generic (note the name mangling of generics in .NET 2.0)
- Create an array of types describing the specific types we want the instance to support
- Get the closed type using the
BindGenericParameters
method
Constructing An Instance
Constructing an instance of this type is now a simple matter of saying:
object obj=Activator.CreateInstance(cnIntType);
or if you prefer:
ComplexNumber<int> cnInt =
(ComplexNumber<int>)Activator.CreateInstance(cnIntType);
Note the cast--yes, by constructing the generic declaratively, we have defeated part of the purpose of generics. However, any internal code that uses cnInt will still benefit from the compile time type checking.
Supporting A Type Converter
Now comes the harder part. First, we decorate the ComplexNumber class with a type converter attribute:
[TypeConverter(typeof(ComplexNumberTypeConverter))]
public class ComplexNumber<T> where T : struct
...
And the first part of the implementation is simple enough:
public class ComplexNumberTypeConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext context, Type t) { return t == typeof(String); } ...
We are providing a test to determine whether we can convert from a String
type to the ComplexNumber
type. But herein lies the problem--we don't actually know the type to which we are converting the string! Unlike a non-generic where there only is one type, the type of a closed generic can be anything! To solve this problem, I've chosen to implement an intermediate class from which to convert the string. This intermediate class supports a type converter allowing us to afterwards convert to the closed generic type, because The ConvertTo
method provides the target type.
One could shortcut the process and eliminate the intermediate class with a simplerConvertTo
implementation:TypeConverter tc = TypeDescriptor.GetConverter(genericType); instance = tc.ConvertTo(stringVal, genericType);and implementing the type converter easily by parsing the string value.
However, in this implementation, the CanConvertTo test is pointless because it's like saying "is an apple an apple?" So I chose a two step process that requires an intermediate object: implementing a type converter FROM a string, and implementing a type converter TO the generic type. Remember that
CanConvertFrom
doesn't tell you what kind of object you get back when you callConvertFrom
, whileCanConvertTo
is a very directed test--"can I convert this object to the specified type". What we really need is aConvertFromTo
in which we can specify both the source and destination type. And if you're curious, in my tests theITypeDescriptorContext
is always null. Maybe this is a .NET 2.0 beta problem, because I believe the context would provide information on the source type if it weren't null.
The full implementation of the ComplexNumber
type converter thus looks like this:
public class ComplexNumberTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type t)
{
return t == typeof(String);
}
// construct intermediate object (in this case a ComplexNumber of
// type double) to act as a placeholder for the string.
public override object ConvertFrom(ITypeDescriptorContext context,
System.Globalization.CultureInfo culture,
object val)
{
IntermediateComplexNumber icn = new IntermediateComplexNumber();
string[] parms = ((string)val).Split(',');
icn.Real = Convert.ToDouble(parms[0]);
icn.Imaginary = Convert.ToDouble(parms[1]);
return icn;
}
}
And the intermediate class is defined as:
[TypeConverter(typeof(IntermediateComplexNumberTypeConverter))]
public class IntermediateComplexNumber
{
protected double real;
protected double imaginary;
public double Real
{
get { return real; }
set { real = value; }
}
public double Imaginary
{
get { return imaginary; }
set { imaginary = value; }
}
}
The type converter for the intermediate class is implemented as:
public class IntermediateComplexNumberTypeConverter : TypeConverter
{
public override bool CanConvertTo(ITypeDescriptorContext context,
Type destinationType)
{
// allow conversion to any ComplexNumber<T> type
return destinationType.FullName.IndexOf("Generics.ComplexNumber`1") == 0;
}
public override object ConvertTo(ITypeDescriptorContext context,
System.Globalization.CultureInfo culture,
object val,
Type destinationType)
{
object ret = null;
if (val is IntermediateComplexNumber)
{
// Construct the target instance.
ret = Activator.CreateInstance(destinationType);
// For each property in the intermediate container...
foreach (PropertyInfo piSrc in val.GetType().GetProperties())
{
// Get the source type for the property.
Type tSrc = piSrc.PropertyType;
// Get the property information for the destination property in
// the generic type.
// IMPORTANT: The intermediate container name must match the generic
// type name, otherwise a manual mapping must be used.
// We need to do this because we don't know the specific type
// information of the generic instance to completely describe the
// instance.
PropertyInfo piDest = destinationType.GetProperty(piSrc.Name);
// Get the destination type.
Type tDest = piDest.PropertyType;
// Get the type converter for the source type.
TypeConverter tcSrc = TypeDescriptor.GetConverter(tSrc);
// This:
// piDestReal.SetValue(ret, icn.Real, null);
// does not work unless the intermediate type is exactly
// the same as the generic instance type.
// Thus, we need to use type conversion again!
// Can we convert from the intermediate type to the generic instance
// type?
if (tcSrc.CanConvertTo(tDest))
{
// Get the intermediate value.
object srcObj = piSrc.GetValue(val, null);
// Convert it to the target type.
object destObj = tcSrc.ConvertTo(srcObj, tDest);
// Assign it to the generic instance.
piDest.SetValue(ret, destObj, null);
}
}
}
return ret;
}
}
Using The Type Converter
Now that all that code is in place, we can use the type converter with a helper method. The usage would look like this:
// this works:
cnInt = ConstructGeneric(cnIntType, "1, 2");
// as does this:
cnDouble = ConstructGeneric(cnDoubleType, "10.5, 3.6");
// and this. :)
cnInt = ConstructGeneric(cnIntType, "1.23, 4.56");
and ConstructGeneric
is implemented as:
public static object ConstructGeneric(Type genericType, string val)
{
object instance = null;
// Get the type converter for the instance, so we can convert from a string
// to the intermediate type.
TypeConverter tcIntermediate = TypeDescriptor.GetConverter(genericType);
// If we can convert...
if (tcIntermediate.CanConvertFrom(typeof(string)))
{
// Convert from the string value to the intermediate type.
object obj = tcIntermediate.ConvertFrom(val);
// Get the type converter for the intermediate type, so we can convert
// from the intermediate type to the final type.
TypeConverter tcFinal = TypeDescriptor.GetConverter(obj.GetType());
// If we can convert...
if (tcFinal.CanConvertTo(genericType))
{
// do the conversion and assign it to the current instance.
// Note: This is a shallow copy, you may need to implement your own
// copy constructor.
instance = tcFinal.ConvertTo(obj, genericType);
}
}
return instance;
}
We can now declaratively construct closed generics and use a type converter to assign a string to properties of the appropriate type in the closed generic!
Conclusion
There are some other interesting issues involving generics, such as sub-types, but I wanted to keep this article as simple as possible while meeting my goals. If you see any blatant errors or simplifications to what I've done here, I'd be more than happy to hear from you.