Generic Polymorphism - Strange Behavior

Pluggable framework

Imagine a simple pluggable system that's pretty straightforward using inheritance polymorphism:

  • We have a graphics rendering system.
  • There are various types of graphic shapes (monochrome, color, etc.) that need to be rendered
  • Rendering is done by a data-specific plugin like. The ColorRenderer will display the ColorShape.
  • Each plugin implements IRenderer

    , so they can all be saved in IRenderer[]

    .
  • At startup, IRenderer[]

    populated with a series of individual renderers
  • When data for a new form is received, a plugin based on the form type is selected from the array.
  • The plugin is then called by calling its method Render

    , passing in the form as the base type.
  • The method is Render

    overridden in every descendant class; it returns the form back to its offspring and then displays it.

Hope this is clear. I think this is a fairly common setting. Very easy with inheritance polymorphism and runtime casting.

Execution without quotes

Now for the tricky part. In response to this question, I wanted to come up with a way to do it without casting. It is difficult because of this array IRenderer[]

, to get a plugin from an array you usually had to cast it to a specific type in order to use its type specific methods and we cannot do that. Now we can work around this by interacting with the plugin only with its base classes, but part of the requirement was that a type-specific method must be run for the renderer, in which a data packet of a particular type is used as an argument, and the base class cannot do this. because there is no way to pass it a type-specific data packet without dropping it to the base and then back to the ancestor. Tricky.

At first I thought it was not possible, but after several tries I found I could do it by blaming the C # generic system. I create an interface that is contravariant to both the plugin and the form type, and then I used that. The renderer resolution is determined by the type. Xyzzy, the contravariant interface makes the cast unnecessary.

Here is the shortest version of the code I could give as an example. This will compile and work 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, she works, right? Then I wondered ... what if it ResolveRenderer

returned the wrong type?

Safe type?

According to this MSDN article ,

Contravariance, on the other hand, seems to be inconsistent ... It seems laggy, but it is type-safe code that compiles and runs. The code is type safe because T specifies the type of the parameter.

I think it is actually not safe.

Introducing an error that returns the wrong type

So, I introduced an error to the controller, so it mistakenly stores the ColorRenderer where the MonochromeRenderer belongs, like this:

public DisplayController()
{
    _renderers[(int)ColorDepthEnum.Color] = new ColorRenderer();
    _renderers[(int)ColorDepthEnum.Monochrome] = new ColorRenderer(); //Oops!!!
}

      

I thought I would get some kind of type mismatch exception. But no, the program ends, with a mysterious exit:

ColorRenderer is now rendering a [ColorShape]
Renderer::Render(Shape) called.

      

What...?

My questions:

Firstly,

Why did you MonochromeShape::Apply

call Renderer::Render(Shape)

? It tries to call Render(MonochromeShape)

, which obviously has a different method signature.

The code inside the method MonochromeShape::Apply

only has a reference to the interface, in particular IRelated<MonochromeRenderer,MonochromeShape>

, which only provides Render(MonochromeShape)

.

Although Render(Shape)

it looks similar, it is a different method with a different entry point and is not even used in the interface being used.

Secondly,

Since none of the methods Render

are virtual (each descendant type introduces a new, not virtual, not overridden method with a different type-specific argument), I would have thought the entry point was compile-time related. Are prototypes of methods within a method group actually selected at runtime? How is it possible to work without VMT for sending? Does it use some kind of reflection?

Thirdly,

C # contravariance is definitely not safe? Instead of invalidating the exception (which at least tells me there is a problem), I get unexpected behavior. Is there a way to detect such problems at compile time, or at least make them throw an exception instead of doing something unexpected?

+3


source to share


2 answers


OK, first of all, don't write generic types like this one . As you've discovered, it quickly becomes a huge mess. Never do this:

class Animal {}
class Turtle : Animal {}
class BunchOfAnimals : IEnumerable<Animal> {}
class BunchOfTurtles : BunchOfAnimals, IEnumerable<Turtle> {}

      

HE IS PAIN. We now have two ways to get IEnumerable<Animal>

from BunchOfTurtles

: either ask a base class to implement it, or ask a derived class to implement IEnumerable<Turtle>

it and then covariantly convert it IEnumerable<Animal>

to.The consequences are as follows: you can ask a bunch of turtles about a sequence of animals and giraffes may appear. This is not a contradiction; all base class capabilities are present in the derived class and include sequencing of giraffes on demand.

Let me emphasize this point again to be very clear. This pattern can create, in some cases, implementation-defined situations where it becomes impossible to determine statically which method will actually be called. In some cases with odd angles, you can actually have the order in which the methods appear in the source code is a decisive factor at runtime. Just don't go there.

For more on this fascinating topic, I recommend you read all the comments on my 2007 blog post on the topic: https://blogs.msdn.microsoft.com/ericlippert/2007/11/09/covariance-and-contravariance-in-c -part-ten-dealing-with-ambiguity / p>

Now, in your particular case, everything is well defined, it just isn't defined how you think it should be.

For starters: why is this typical?

IRenderBinding<MonochromeRenderer, MonochromeShape> component = new ColorRenderer();

      

Because you said it had to be. Work it out from the compiler's point of view.

  • A ColorRenderer

    isRenderer

  • A Renderer

    isIRenderBinding<Renderer, Shape>

  • IRenderBinding

    is contravariant in both parameters, so you can always make a more specific type argument.
  • Therefore a Renderer

    isIRenderBinding<MonochromeRenderer, MonochromeShape>

  • Therefore, the conversion is valid.

Done.

So why is it called here Renderer::Render(Shape)

?

    component.Render(this);

      

You're asking:

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 think the entry point was bound at compile time, Are the method prototypes within a group methods actually selected at runtime? How is it possible to work without entering VMT to submit? Does it use some kind of reflection?



Pass it over.

component

is of compile-time type IRenderBinding<MonochromeRenderer, MonochromeShape>

.

this

is of compile-time type MonochromeShape

.

So, we call any method that implements IRenderBinding<MonochromeRenderer, MonochromeShape>.Render(MonochromeShape)

on ColorRenderer

.

The runtime has to figure out which interface actually means. ColorRenderer

implements IRenderBinding<ColorRenderer, ColorShape>

directly and IRenderBinding<Renderer, Shape>

through its base class. The former is not compatible with IRenderBinding<MonochromeRenderer, MonochromeShape>

, but the latter is.

Thus, the runtime infers that you meant the latter and makes the call as if it were IRenderBinding<Renderer, Shape>.Render(Shape)

.

So what method is it calling? Your class implements IRenderBinding<Renderer, Shape>.Render(Shape)

in the base class, so the one that called.

Remember that interfaces define "slots", one per method. When an object is created, each interface slot is populated with a method. The slot for is IRenderBinding<Renderer, Shape>.Render(Shape)

populated with the base class version, and the slot for is IRenderBinding<ColorRenderer, ColorShape>.Render(ColorShape)

populated with the derived class version. You selected a slot from the first to get the contents of that slot.

C # contravariance is definitely not safe?

I promise you this is type safe. As you should have noticed, every transformation you made without an actor was legal, and every method you called was called with something like what it expected. You have never called a method ColorShape

with this

referencing for MonochromeShape

example.

Instead of invalidating the exception (which at least tells me there is a problem), I get unexpected behavior.

No, you will get the completely expected behavior. You just created a type lattice, which is unusually confusing, and you don't have enough understanding of the type system to understand the code you wrote. Do not do that.

Is there a way to detect such problems at compile time, or at least make them throw an exception instead of doing something unexpected?

Don't write code like this in the first place. Never use two versions of the same interface so that they can be combined using covariant or contravariant conversions.This is nothing more than pain and confusion. And in a similar way, never implement an interface with methods that combine under common substitution. (For example interface IFoo<T> { void M(int); void M(T); } class Foo : IFoo<int> { uh oh }

)

I thought about adding a warning about this, but it was hard to figure out how to turn off the warning in the rare cases where it is desired. Alerts that can only be disabled with pragmas are bad alerts.

+8


source


Firstly. MonochromeShape::Apply

call Renderer::Render(Shape)

due to the following:

IRenderBinding<ColorRenderer, ColorShape> x1 = new ColorRenderer();
IRenderBinding<Renderer, Shape> x2 = new ColorRenderer();
// fails - cannot convert IRenderBinding<ColorRenderer, ColorShape> to IRenderBinding<MonochromeRenderer, MonochromeShape>
IRenderBinding<MonochromeRenderer, MonochromeShape> c1 = x1;
// works, because you can convert IRenderBinding<Renderer, Shape> toIRenderBinding<MonochromeRenderer, MonochromeShape>
IRenderBinding<MonochromeRenderer, MonochromeShape> c2 = x2;

      

In short: ColorRenderer

inherits from Renderer

, which, in turn, implements IRenderBinding<Renderer, Shape>

. This interface allows ColorRendered

implicit conversion to IRenderBinding<MonochromeRenderer, MonochromeShape>

. This interface is implemented by the class, Renderer

and therefore it is not surprising what Renderer.Render

is called when called MonochromeShape::Apply

. The fact that you are passing an instance MonochromeShape

and not Shape

is not a problem precisely because it TData

is contravariant.

About your second question. Sending on an interface is virtual by definition. In fact, if a method implements some method from the interface, it is marked as virtual in IL. Consider this:

class Test : ITest {
    public void DoStuff() {

    }
}

public class Test2 {
    public void DoStuff() {

    }
}

interface ITest {
    void DoStuff();
}

      



The method Test.DoStuff

has the following signature in IL (note virtual

:

.method public final hidebysig virtual newslot instance void 
    DoStuff() cil managed 

      

The method is Test2.DoStuff

fair:

.method public hidebysig instance void 
    DoStuff() cil managed

      

Regarding the third question, I think it makes it clear that it behaves as expected and is type safe precisely because invalid exceptions are thrown.

+2


source







All Articles