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

Joh*_* Wu 3 .net c# generics polymorphism contravariance

可插拔框架

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

  1. 我们有一个图形渲染系统
  2. 有不同类型的图形形状(单色,彩色等)需要渲染
  3. 渲染由特定于数据的插件完成,例如ColorRenderer将渲染ColorShape.
  4. 每个插件都实现IRenderer,因此它们都可以存储在一个IRenderer[].
  5. 在启动时,IRenderer[]会填充一系列特定的渲染器
  6. 当接收到新形状的数据时,基于形状的类型从阵列中选择插件.
  7. 然后通过调用其Render方法调用插件,将形状作为其基本类型传递.
  8. Render在每个后代类中重写该方法; 它将Shape转换回其后代类型,然后呈现它.

希望以上是清楚的 - 我认为这是一种非常常见的设置.使用继承多态和运行时强制转换非常容易.

没有铸造它

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

起初我认为这是不可能的,但经过几次尝试,我发现我可以通过juking c#generic系统来实现它.我创建了一个与插件和形状类型相反的接口,然后使用它.渲染器的分辨率由特定类型的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);
    }
}
Run Code Online (Sandbox Code Playgroud)

如果运行Tests.Test1()输出将是:

ColorRenderer is now rendering a [ColorShape]
MonochromeRenderer is now rendering a [MonochromeShape]
Run Code Online (Sandbox Code Playgroud)

美丽,它的作品吧?然后我想知道......如果ResolveRenderer返回错误的类型怎么办?

型号安全吗?

根据这篇MSDN文章,

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

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

介绍一个返回错误类型的错误

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

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

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

ColorRenderer is now rendering a [ColorShape]
Renderer::Render(Shape) called.
Run Code Online (Sandbox Code Playgroud)

什么......?

我的问题:

第一,

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

MonochromeShape::Apply方法中的代码仅具有对接口的引用,具体而言IRelated<MonochromeRenderer,MonochromeShape>,该接口仅公开Render(MonochromeShape).

虽然Render(Shape)看起来很相似,但它是一种具有不同入口点的不同方法,甚至在使用的接口中也是如此.

第二,

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

第三,

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

Eri*_*ert 8

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

class Animal {}
class Turtle : Animal {}
class BunchOfAnimals : IEnumerable<Animal> {}
class BunchOfTurtles : BunchOfAnimals, IEnumerable<Turtle> {}
Run Code Online (Sandbox Code Playgroud)

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

让我再次强调这一点,以便它非常清楚. 在某些情况下,这种模式可以创建实现定义的情况,在这种情况下,无法静态地确定实际调用哪种方法.在一些奇怪的极端情况下,您实际上可以使方法在源代码中出现的顺序成为运行时的决定因素.只是不要去那里.

有关这个引人入胜的主题的更多信息,我建议您阅读关于该主题的2007年博客文章的所有评论:https://blogs.msdn.microsoft.com/ericlippert/2007/11/09/covariance-and-convvariance-in -C-部分十交易,与歧义/

现在,在你的特定情况下,一切都很好地定义,它只是没有你认为它应该定义.

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

IRenderBinding<MonochromeRenderer, MonochromeShape> component = new ColorRenderer();
Run Code Online (Sandbox Code Playgroud)

因为你说它应该是.从编译器的角度来解决它.

  • A ColorRenderer是一个Renderer
  • A Renderer是一个IRenderBinding<Renderer, Shape>
  • IRenderBinding 它的参数都是逆变的,因此总是可以使其具有更具体的类型参数.
  • 因此a Renderer是一个IRenderBinding<MonochromeRenderer, MonochromeShape>
  • 因此转换有效.

完成.

那么为什么Renderer::Render(Shape)叫这里?

    component.Render(this);
Run Code Online (Sandbox Code Playgroud)

你问:

由于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)在基类上实现 ,因此是被调用的类.

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

c#contravariance绝对不是安全的吗?

我向你保证它是安全的.正如您应该注意到的那样:您在没有强制转换的情况下进行的每次转换都是合法的,并且您调用的每个方法都是使用预期的类型调用的.你永远不会被调用的方法ColorShape有一个this指的是MonochromeShape,例如.

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

不,你得到完全预期的行为.您刚刚创建了一个非常令人困惑的类型网格,并且您对类型系统没有足够的理解来理解您编写的代码.不要那样做.

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

首先不要写这样的代码.永远不要实现相同界面的两个版本,以便它们可以通过协变或逆变转换来统一.它只不过是痛苦和困惑.同样,永远不要使用在泛型替换下统一的方法来实现接口.(例如,interface IFoo<T> { void M(int); void M(T); } class Foo : IFoo<int> { uh oh })

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