Click here to Skip to main content
Click here to Skip to main content

Declarative Generics And Type Converters

, 17 Sep 2004
Rate this:
Please Sign up or sign in to vote.
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:

  1. Get the open type of the generic (note the name mangling of generics in .NET 2.0)
  2. Create an array of types describing the specific types we want the instance to support
  3. 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 = <BR>                      (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 simpler ConvertTo 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 call ConvertFrom, while CanConvertTo is a very directed test--"can I convert this object to the specified type". What we really need is a ConvertFromTo in which we can specify both the source and destination type. And if you're curious, in my tests the ITypeDescriptorContext 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 <BR>        // type name, otherwise a manual mapping must be used.
        // We need to do this because we don't know the specific type<BR>        // information of the generic instance to completely describe the <BR>        // 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 <BR>        // 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<BR>        // 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 <BR>      // 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Marc Clifton

United States United States
Marc is the creator of two open source projets, MyXaml, a declarative (XML) instantiation engine and the Advanced Unit Testing framework, and Interacx, a commercial n-tier RAD application suite.  Visit his website, www.marcclifton.com, where you will find many of his articles and his blog.
 
Marc lives in Philmont, NY.

Comments and Discussions

 
QuestionUnable to cast to generic type in generic method Pinmemberalias4714-Aug-07 18:58 
GeneralGenerics class Pinmemberabedo198226-May-07 8:06 
GeneralRe: Generics class Pinmemberabedo198226-May-07 9:36 
QuestionCan you instantiate a generic class from a passed type? PinmembertheCodeDevil30-Oct-06 19:20 
AnswerRe: Can you instantiate a generic class from a passed type? PinmemberMichael Epner4-Jan-07 6:52 
QuestionPassing cnInt to a Method - How? PinmemberJohn Stewien9-May-05 22:03 
AnswerRe: Passing cnInt to a Method - How? PinprotectorMarc Clifton10-May-05 4:18 
GeneralGenerics != Templates PinstaffNishant S17-Sep-04 8:38 
GeneralRe: Generics != Templates PinprotectorMarc Clifton17-Sep-04 8:51 
GeneralRe: Generics != Templates PinstaffNishant S17-Sep-04 16:47 
GeneralQuestion Pinmemberleppie17-Sep-04 5:52 
GeneralRe: Question PinprotectorMarc Clifton17-Sep-04 7:12 
GeneralRe: Question PinprotectorMarc Clifton17-Sep-04 7:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140827.1 | Last Updated 17 Sep 2004
Article Copyright 2004 by Marc Clifton
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid