12/06/2005

Limitations of Polymorphic Behavior with Generics vs. Interfaces

Steve Michelotti exposes an interesting limitation of polymorphism with Generics:

In .NET 2.0, Generics is clearly the single most important language enhancement. However, to use generics to the full potential, developers should understand both the capabilities and limitations of generics as they relate specifically to polymorphism. In short, while generics do support some polymorphic behavior, the use of Interfaces should still be the preferred polymorphic mechanism in many cases.

Here's my code take on this, as a little "exercise". This is pretty "linear" so it should be fairly easy to follow. The actual demo "test" code lines are in the "class program". You can paste this into a VS.NET 2005 Console app if you want to play around with it by uncommenting some lines:


using System;
using System.Collections.Generic;
using System.Text;

namespace GenericVsInterface
{

public abstract class Developer
{
public string FirstName;
public string LastName;
public void WriteCode()
{
}
}

class CSharpDeveloper : Developer
{
public int NumLinesOfCodeWritten=0;
}

class VBNetDeveloper : Developer
{
public int NumLinesOfCodeWritten=0;
}

//Since both CSharpDeveloper and VBNetDeveloper inherit from Developer
//, the above compiles.
//With constraints we can define generic sub-classes:
class DeveloperAction<T> where T : Developer
{
public void DoAction(T item)
{
}
}

interface IAction
{
void DoAction(Developer item);
}

//And we'll change DeveloperAction to:
class DeveloperAction2<T> : IAction where T : Developer
{
public void DoAction(T item) { }
public void DoAction(Developer item)
{ this.DoAction((T)item); }
}



class Program
{
static
void
Main(string[] args)
{

//Using System.Collections.Generics.List<>, one could consume it like this:
List<Developer> list = new List<Developer>();

list.Add(new CSharpDeveloper());
list.Add(new VBNetDeveloper());

//As the example above, we can consume like this:
DeveloperAction<Developer> DeveloperAction =
new DeveloperAction<Developer>();
DeveloperAction.DoAction(new CSharpDeveloper());
DeveloperAction.DoAction(new VBNetDeveloper());

DeveloperAction<CSharpDeveloper> codeAction =
new DeveloperAction<CSharpDeveloper>();
codeAction.DoAction(new CSharpDeveloper());

// next line does not compile because of constraint:
// codeAction.DoAction(new VBNetDeveloper());
//However, if we try to push generic polymorphic behavior one step further,
// this is where things stop:
List<DeveloperAction<Developer>> DeveloperActionList =
new List<DeveloperAction<Developer>>();

// next does not compile:
// DeveloperActionList.Add(new DeveloperAction<CSharpDeveloper>());
// Next line does not compile:
// DeveloperActionList.Add(new DeveloperAction<VBNetDeveloper>());
// But now, using the interface we can get polymorphic behavior with generic classes:
List<IAction> developerActionList2 = new List<IAction>();
developerActionList2.Add(new DeveloperAction2<CSharpDeveloper>());
developerActionList2.Add(new DeveloperAction2<VBNetDeveloper >());
}
}
}


The key point is that a generic class is a type itself. The compiler doesn't care about CSharpDeveloper vs Developer, it cares about DeveloperAction vs DeveloperAction. In other words, it needs a relationship between the two DeveloperAction types, not the generic type parameters that created them.

One way to express this:

abstract class DeveloperAction
{
}

class DeveloperAction : DeveloperAction where T : Developer
{
public void DoAction(T item) { }
}
List DeveloperActionList = new List();

In order to get this to compile, we had to use a NON-generic class. And with that you get no polymorphic behavior - the abstract DeveloperAction class has no methods. In this case, there'd be no way to get at the DoAction() method without casting. That's the difference in polymorphism with generic versus non-generic classes.

The lesson learned is simple: You can get real, usable polymorphic behavior plus the benefits of Generics, provided you stick to your old friend, the interface.