Click here to Skip to main content
15,885,216 members
Articles / Programming Languages / C#
Article

Declarative Generics And Type Converters

Rate me:
Please Sign up or sign in to vote.
4.68/5 (13 votes)
17 Sep 20045 min read 85.4K   2   39   13
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:

C#
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:

C#
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:

C#
List<T>

whereas a "closed" generic qualifies the generic type parameters with a specific type, like:

C#
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:

C#
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:

C#
Type cnIntType = GetGenericType("Generics.ComplexNumber", "System.Int32");
Type cnDoubleType = GetGenericType("Generics.ComplexNumber", "System.Double");

The implementation of the GetGenericType method is:

C#
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:

C#
object obj=Activator.CreateInstance(cnIntType);

or if you prefer:

C#
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:

C#
[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:

C#
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:

C#
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:

C#
[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:

C#
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:

C#
// 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:

C#
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


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
QuestionUnable to cast to generic type in generic method Pin
alias4714-Aug-07 18:58
alias4714-Aug-07 18:58 
GeneralGenerics class Pin
abedo198226-May-07 8:06
abedo198226-May-07 8:06 
GeneralRe: Generics class Pin
abedo198226-May-07 9:36
abedo198226-May-07 9:36 
QuestionCan you instantiate a generic class from a passed type? Pin
Mustafa Ismail Mustafa30-Oct-06 19:20
Mustafa Ismail Mustafa30-Oct-06 19:20 
AnswerRe: Can you instantiate a generic class from a passed type? Pin
Michael Epner4-Jan-07 6:52
Michael Epner4-Jan-07 6:52 
QuestionPassing cnInt to a Method - How? Pin
John Stewien9-May-05 22:03
John Stewien9-May-05 22:03 
AnswerRe: Passing cnInt to a Method - How? Pin
Marc Clifton10-May-05 4:18
mvaMarc Clifton10-May-05 4:18 
GeneralGenerics != Templates Pin
Nish Nishant17-Sep-04 8:38
sitebuilderNish Nishant17-Sep-04 8:38 
GeneralRe: Generics != Templates Pin
Marc Clifton17-Sep-04 8:51
mvaMarc Clifton17-Sep-04 8:51 
GeneralRe: Generics != Templates Pin
Nish Nishant17-Sep-04 16:47
sitebuilderNish Nishant17-Sep-04 16:47 
GeneralQuestion Pin
leppie17-Sep-04 5:52
leppie17-Sep-04 5:52 
GeneralRe: Question Pin
Marc Clifton17-Sep-04 7:12
mvaMarc Clifton17-Sep-04 7:12 
GeneralRe: Question Pin
Marc Clifton17-Sep-04 7:15
mvaMarc Clifton17-Sep-04 7:15 

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

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