Pluggable framework
Imagine a simple pluggable system, which is pretty straightforward using inheritance polymorphism:
- We have a graphics rendering system
- There are different types of graphics shapes (monochrome, color, etc.) that need rendering
- Rendering is done by a data-specific plugin, e.g. a ColorRenderer will render a ColorShape.
- Every plugin implements
IRenderer
, so they can all be stored in anIRenderer[]
. - On startup,
IRenderer[]
is populated with a series of specific renderers - When data for a new shape is received, a plugin is chosen from the array based on the type of the shape.
- The plugin is then invoked by calling its
Render
method, passing the shape as its base type. - The
Render
method is overridden in each descendant class; it casts the Shape back to its descendant type and then renders it.
Hopefully the above is clear-- I think it is a pretty common sort of setup. Very easy with inheritance polymorphism and run-time casting.
Doing it without casting
Now the tricky part. In response to this question, I wanted to come up with a way to do this all without any casting whatsoever. This is tricky because of that IRenderer[]
array-- to get a plugin out of the array, you would normally need to cast it to a specific type in order to use its type-specific methods, and we can't do that. Now, we could get around that by interacting with a plugin only with its base class members, but part of the requirements was that the renderer must run a type-specific method that has a type-specific data packet as an argument, and the base class would not be able to do that because there is no way to pass it a type-specific data packet without a casting it to the base and then back to the ancestor. Tricky.
At first I thought it was impossible, but after a few tries I found I could make it happen by juking the c# generic system. I create an interface that is contravariant with respect to both plugin and shape type and then used that. Resolution of the renderer is decided by the type-specific Shape. Xyzzy, the contravariant interface makes the cast unnecessary.
Here is the shortest version of the code I could come up with as an example. This compiles and runs and behaves correctly:
public enum ColorDepthEnum { Color = 1, Monochrome = 2 }
public interface IRenderBinding<in TRenderer, in TData> where TRenderer : Renderer
where TData: Shape
{
void Render(TData data);
}
abstract public class Shape
{
abstract public ColorDepthEnum ColorDepth { get; }
abstract public void Apply(DisplayController controller);
}
public class ColorShape : Shape
{
public string TypeSpecificString = "[ColorShape]"; //Non-virtual, just to prove a point
override public ColorDepthEnum ColorDepth { get { return ColorDepthEnum.Color; } }
public override void Apply(DisplayController controller)
{
IRenderBinding<ColorRenderer, ColorShape> renderer = controller.ResolveRenderer<ColorRenderer, ColorShape>(this.ColorDepth);
renderer.Render(this);
}
}
public class MonochromeShape : Shape
{
public string TypeSpecificString = "[MonochromeShape]"; //Non-virtual, just to prove a point
override public ColorDepthEnum ColorDepth { get { return ColorDepthEnum.Monochrome; } }
public override void Apply(DisplayController controller)
{
IRenderBinding<MonochromeRenderer, MonochromeShape> component = controller.ResolveRenderer<MonochromeRenderer, MonochromeShape>(this.ColorDepth);
component.Render(this);
}
}
abstract public class Renderer : IRenderBinding<Renderer, Shape>
{
public void Render(Shape data)
{
Console.WriteLine("Renderer::Render(Shape) called.");
}
}
public class ColorRenderer : Renderer, IRenderBinding<ColorRenderer, ColorShape>
{
public void Render(ColorShape data)
{
Console.WriteLine("ColorRenderer is now rendering a " + data.TypeSpecificString);
}
}
public class MonochromeRenderer : Renderer, IRenderBinding<MonochromeRenderer, MonochromeShape>
{
public void Render(MonochromeShape data)
{
Console.WriteLine("MonochromeRenderer is now rendering a " + data.TypeSpecificString);
}
}
public class DisplayController
{
private Renderer[] _renderers = new Renderer[10];
public DisplayController()
{
_renderers[(int)ColorDepthEnum.Color] = new ColorRenderer();
_renderers[(int)ColorDepthEnum.Monochrome] = new MonochromeRenderer();
//Add more renderer plugins here as needed
}
public IRenderBinding<T1,T2> ResolveRenderer<T1,T2>(ColorDepthEnum colorDepth) where T1 : Renderer where T2: Shape
{
IRenderBinding<T1, T2> result = _renderers[(int)colorDepth];
return result;
}
public void OnDataReceived<T>(T data) where T : Shape
{
data.Apply(this);
}
}
static public class Tests
{
static public void Test1()
{
var _displayController = new DisplayController();
var data1 = new ColorShape();
_displayController.OnDataReceived<ColorShape>(data1);
var data2 = new MonochromeShape();
_displayController.OnDataReceived<MonochromeShape>(data2);
}
}
If you run Tests.Test1()
the output will be:
ColorRenderer is now rendering a [ColorShape]
MonochromeRenderer is now rendering a [MonochromeShape]
Beautiful, it works, right? Then I got to wondering... what if ResolveRenderer
returned the wrong type?
Type safe?
According to this MSDN article,
Contravariance, on the other hand, seems counterintuitive....This seems backward, but it is type-safe code that compiles and runs. The code is type-safe because T specifies a parameter type.
I am thinking, there is no way this is actually type safe.
Introducing a bug that returns the wrong type
So I introduced a bug into the controller so that is mistakenly stores a ColorRenderer where the MonochromeRenderer belongs, like this:
public DisplayController()
{
_renderers[(int)ColorDepthEnum.Color] = new ColorRenderer();
_renderers[(int)ColorDepthEnum.Monochrome] = new ColorRenderer(); //Oops!!!
}
I thought for sure I'd get some sort of type mismatch exception. But no, the program completes, with this mysterious output:
ColorRenderer is now rendering a [ColorShape]
Renderer::Render(Shape) called.
What the...?
My questions:
First,
Why did MonochromeShape::Apply
call Renderer::Render(Shape)
? It is attempting to call Render(MonochromeShape)
, which obviously has a different method signature.
The code within the MonochromeShape::Apply
method only has a reference to an interface, specifically IRelated<MonochromeRenderer,MonochromeShape>
, which only exposes Render(MonochromeShape)
.
Although Render(Shape)
looks similar, it is a different method with a different entry point, and isn't even in the interface being used.
Second,
Since none of the Render
methods are virtual (each descendant type introduces a new, non-virtual, non-overridden method with a different, type-specific argument), I would have thought that the entry point was bound at compile time. Are method prototypes within a method group actually chosen at run-time? How could this possibly work without a VMT entry for dispatch? Does it use some sort of reflection?
Third,
Is c# contravariance definitely not type safe? Instead of an invalid cast exception (which at least tells me there is a problem), I get an unexpected behavior. Is there any way to detect problems like this at compile time, or at least to get them to throw an exception instead of doing something unexpected?