泛型的多态性 - 奇怪的行为

时间:2017-03-24 20:38:10

标签: c# .net generics polymorphism contravariance

可插入框架

想象一个简单的可插拔系统,使用继承多态非常简单:

  1. 我们有一个图形渲染系统
  2. 有不同类型的图形形状(单色,彩色等)需要渲染
  3. 渲染由特定于数据的插件完成,例如ColorRenderer将渲染ColorShape。
  4. 每个插件都会实现IRenderer,因此它们都可以存储在IRenderer[]中。
  5. 启动时,IRenderer[]会填充一系列特定的渲染器
  6. 当接收到新形状的数据时,基于形状的类型从阵列中选择插件。
  7. 然后通过调用其Render方法调用该插件,并将形状作为其基本类型传递。
  8. 在每个后代类中重写Render方法;它将Shape转换回其后代类型,然后呈现它。
  9. 希望以上是清楚的 - 我认为这是一种非常常见的设置。使用继承多态和运行时转换非常容易。

    不进行投射

    现在是棘手的部分。在回复this question时,我想提出一种方法来完成所有而无需任何投射。这很简单,因为IRenderer[]数组 - 从数组中获取插件,通常需要将其转换为特定类型才能使用其特定于类型的方法,而我们不能这样做那。现在,我们可以通过仅与其基类成员交互插件来解决这个问题,但部分要求是渲染器必须运行特定于类型的方法,该方法具有特定于类型的数据包作为参数,并且基础class无法做到这一点,因为没有办法将它传递给特定类型的数据包而不将其转移到基础然后再回到祖先。棘手。

    起初我认为这是不可能的,但经过几次尝试后,我发现我可以通过使用c#泛型系统来实现它。我创建了一个与插件和形状类型相反的接口,然后使用它。渲染器的分辨率由特定类型的Shape决定。 Xyzzy,逆变界面使得演员不必要。

    以下是我可以提出的代码的最短版本。这会编译并运行并且行为正确:

    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);
        }
    }
    

    如果您运行Tests.Test1(),则输出将为:

    ColorRenderer is now rendering a [ColorShape]
    MonochromeRenderer is now rendering a [MonochromeShape]
    

    漂亮,它有效,对吗?然后我想知道......如果ResolveRenderer返回了错误的类型会怎么样?

    输入安全吗?

    根据this MSDN article

      另一方面,反方差似乎违反直觉......这似乎是落后的,但它是编译和运行的类型安全代码。代码是类型安全的,因为T指定了参数类型。

    我在想,这实际上并不是安全的。

    引入返回错误类型的错误

    所以我在控制器中引入了一个错误,因此错误地存储了MonochromeRenderer所属的ColorRenderer,如下所示:

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

    我确信我会遇到某种类型不匹配的异常。但不,程序完成了,这个神秘的输出:

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

    什么......?

    我的问题:

    首先,

    为什么MonochromeShape::Apply致电Renderer::Render(Shape)?它试图调用Render(MonochromeShape),它显然有不同的方法签名。

    MonochromeShape::Apply方法中的代码只引用了一个接口,特别是IRelated<MonochromeRenderer,MonochromeShape>,它只公开Render(MonochromeShape)

    尽管Render(Shape)看起来很相似,但它是一个使用不同入口点的不同方法,甚至在使用的接口中也是如此。

    其次,

    由于Render方法都不是虚拟的(每个后代类型引入了一个新的,非虚拟的,非重写的方法,具有不同的,特定于类型的参数),我原以为入口点是在编译时绑定。在运行时实际选择的method group范围内的方法原型是什么?如果没有VMT条目发送,这怎么可能有效呢?它是否使用某种反射?

    第三,

    c#c​​ontravariance肯定不是类型安全吗?而不是无效的强制转换异常(至少告诉我有一个问题),我得到一个意外的行为。有没有办法在编译时检测这样的问题,或者至少让它们抛出异常而不是做出意想不到的事情?

2 个答案:

答案 0 :(得分:8)

好的,首先,不要写这样的通用类型。正如你所发现的那样,它很快变得非常混乱。永远不要这样做:

class Animal {}
class Turtle : Animal {}
class BunchOfAnimals : IEnumerable<Animal> {}
class BunchOfTurtles : BunchOfAnimals, IEnumerable<Turtle> {}
痛苦的痛苦。现在我们有两条路径可以从IEnumerable<Animal>获取BunchOfTurtles:要求基类实现它,要么让派生类实现IEnumerable<Turtle>然后协同执行将其转换为IEnumerable<Animal>。结果是:你可以向一群海龟询问一系列动物,长颈鹿可以出来。这并不矛盾;基类的所有功能都存在于派生类中,包括在被问及时生成一系列长颈鹿。

让我再次强调这一点,以便它非常清楚。 这种模式在某些情况下可以创建实现定义的情况,在这种情况下无法确定实际调用哪种方法。在一些奇怪的角落情况下,你实际上可以顺序方法中出现的方法是运行时的决定性因素。只是不去那里。

有关这个引人入胜的主题的更多信息,我建议您阅读关于该主题的2007年博客文章的所有评论:https://blogs.msdn.microsoft.com/ericlippert/2007/11/09/covariance-and-contravariance-in-c-part-ten-dealing-with-ambiguity/

现在,在您的具体情况下,一切都很好地定义,它只是没有按您认为的那样定义。

首先:为什么这种类型安全?

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

因为你说它应该是。从编译器的角度来解决这个问题。

  • ColorRendererRenderer
  • RendererIRenderBinding<Renderer, Shape>
  • IRenderBinding在其参数中都是逆变的,因此可能总是使其具有更具体的类型参数。
  • 因此RendererIRenderBinding<MonochromeRenderer, MonochromeShape>
  • 因此转换有效。

完成。

那么为什么在这里调用Renderer::Render(Shape)

    component.Render(this);

你问:

  

由于Render方法都不是虚拟的(每个后代类型引入了一个新的,非虚拟的,非重写的方法,具有不同的,特定于类型的参数),我原以为入口点在编译时被绑定了。是否在运行时实际选择了方法组中的方法原型?如果没有VMT条目进行调度,这怎么可能有效呢?它是否使用某种反射?

让我们来看看。

component是编译时类型IRenderBinding<MonochromeRenderer, MonochromeShape>

this是编译时类型MonochromeShape

所以我们在IRenderBinding<MonochromeRenderer, MonochromeShape>.Render(MonochromeShape)上调用实现ColorRenderer的任何方法。

运行时必须确定实际意味着哪个接口。 ColorRenderer通过其基类直接实现IRenderBinding<ColorRenderer, ColorShape>IRenderBinding<Renderer, Shape>。前者与IRenderBinding<MonochromeRenderer, MonochromeShape>不兼容,但后者是。

因此,运行时推断出你的意思是后者,并像IRenderBinding<Renderer, Shape>.Render(Shape)那样执行调用。

那么调用哪种方法?您的类在基类上实现IRenderBinding<Renderer, Shape>.Render(Shape),以便调用它。

请记住,接口定义&#34; slot&#34;,每个方法一个。创建对象时,每个接口槽都填充一个方法。 IRenderBinding<Renderer, Shape>.Render(Shape)的插槽填充了基类版本,IRenderBinding<ColorRenderer, ColorShape>.Render(ColorShape)的插槽填充了派生类版本。您选择了前者的插槽,因此您可以获得该插槽的内容。

  

c#c​​ontravariance肯定不是类型安全吗?

我向你保证它是安全的。正如您应该注意到的那样:您在没有强制转换的情况下进行的每次转换都是合法的,并且您调用的每个方法都是使用预期的类型调用的。例如,您从未使用引用ColorShape的{​​{1}}调用this方法。

  

而不是无效的强制转换异常(至少告诉我有问题),我得到了一个意想不到的行为。

不,你得到完全预期的行为。您刚刚创建了一个非常混乱的类型点阵,并且您对类型系统没有足够的理解以理解您编写的代码。别这么做。

  

有没有办法在编译时检测这样的问题,或者至少让它们抛出异常而不是做出意想不到的事情?

首先不要写这样的代码。 永远不要实现同一界面的两个版本,以便它们可以通过协变或逆变转换来统一。它只不过是痛苦和困惑。同样,永远不要使用在泛型替换下统一的方法来实现接口。 (例如,MonochromeShape

我考虑过为这种效果添加警告,但很难看到如何在极少数情况下关闭警告。只能通过编译指示关闭警告是很糟糕的警告。

答案 1 :(得分:2)

首先。 MonochromeShape::Apply因为以下原因而致电Renderer::Render(Shape)

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;

简而言之:ColorRenderer继承自Renderer,而后者又实现IRenderBinding<Renderer, Shape>接口允许ColorRendered隐式转换为IRenderBinding<MonochromeRenderer, MonochromeShape>。此接口由类Renderer实现,因此在调用Renderer.Render时调用MonochromeShape::Apply并不令人惊讶。您传递MonochromeShape而非Shape实例的事实不是问题,因为TData是逆变的。

关于你的第二个问题。根据定义,按接口调度是虚拟的。实际上,如果方法从接口实现某些方法 - 它在IL中标记为虚拟。考虑一下:

class Test : ITest {
    public void DoStuff() {

    }
}

public class Test2 {
    public void DoStuff() {

    }
}

interface ITest {
    void DoStuff();
}

方法Test.DoStuff在IL中有以下签名(注意virtual

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

方法Test2.DoStuff只是:

.method public hidebysig instance void 
    DoStuff() cil managed

关于第三个问题,我认为从上面可以清楚地看出它的行为符合预期并且类型安全,因为没有无效的强制转换异常是可能的。