c# – 与泛型的多态性 – 奇怪的行为
作者:互联网
可插拔框架
想象一个简单的可插拔系统,使用继承多态非常简单:
>我们有一个图形渲染系统
>有不同类型的图形形状(单色,彩色等)需要渲染
>渲染由特定于数据的插件完成,例如, ColorRenderer将渲染ColorShape.
>每个插件都实现了IRenderer,因此它们都可以存储在IRenderer []中.
>启动时,IRenderer []会填充一系列特定的渲染器
>当接收到新形状的数据时,基于形状的类型从阵列中选择插件.
>然后通过调用其Render方法调用插件,将形状作为其基本类型传递.
>在每个后代类中重写Render方法;它将Shape转换回其后代类型,然后呈现它.
希望以上是清楚的 – 我认为这是一种非常常见的设置.使用继承多态和运行时强制转换非常容易.
没有铸造它
现在是棘手的部分.为了回应this question,我想提出一种方法来做到这一切,无需任何铸造.由于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);
}
}
如果运行Tests.Test1(),输出将为:
ColorRenderer is now rendering a [ColorShape]
MonochromeRenderer is now rendering a [MonochromeShape]
美丽,它的作品吧?然后我想知道……如果ResolveRenderer返回了错误的类型怎么办?
型号安全吗?
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.
我在想,这实际上并不是安全的.
介绍一个返回错误类型的错误
所以我在控制器中引入了一个错误,因此错误地存储了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#contravariance绝对不是安全的吗?而不是无效的强制转换异常(至少告诉我有一个问题),我得到一个意外的行为.有没有办法在编译时检测这样的问题,或者至少让它们抛出异常而不是做出意想不到的事情?
解决方法:
好的,首先,不要写这样的泛型类型.正如你所发现的那样,它很快变得非常混乱.永远不要这样做:
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();
因为你说它应该是.从编译器的角度来解决它.
> ColorRenderer是一个渲染器
> Renderer是一个IRenderBinding< Renderer,Shape>
> IRenderBinding在其两个参数中都是逆变的,因此可能总是使其具有更具体的类型参数.
>因此,渲染器是IRenderBinding< MonochromeRenderer,MonochromeShape>
>因此转换有效.
完成.
那么为什么Renderer :: Render(Shape)在这里调用?
component.Render(this);
你问:
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?
我们来看看吧.
component是编译时类型IRenderBinding< MonochromeRenderer,MonochromeShape>.
这是编译时类型MonochromeShape.
所以我们在ColorRenderer上调用任何方法实现IRenderBinding< MonochromeRenderer,MonochromeShape> .Render(MonochromeShape).
运行时必须确定实际意味着哪个接口. ColorRenderer实现了IRenderBinding< ColorRenderer,ColorShape>直接和IRenderBinding< Renderer,Shape>通过它的基类.前者与IRenderBinding< MonochromeRenderer,MonochromeShape>不兼容,但后者是.
因此,运行时推断出你的意思是后者,并执行调用,就像它是IRenderBinding< Renderer,Shape> .Render(Shape)一样.
那叫哪种方法呢?你的类在基类上实现了IRenderBinding< Renderer,Shape> .Render(Shape),这就是被调用的那个.
请记住,接口定义“槽”,每个方法一个.创建对象时,每个接口槽都填充一个方法. IRenderBinding< Renderer,Shape> .Render(Shape)的插槽用基类版本填充,IRenderBinding< ColorRenderer,ColorShape> .Render(ColorShape)的插槽用派生类版本填充.您选择了前者的插槽,因此您可以获得该插槽的内容.
Is c# contravariance definitely not type safe?
我向你保证它是安全的.正如您应该注意到的那样:您在没有强制转换的情况下进行的每次转换都是合法的,并且您调用的每个方法都是使用预期的类型调用的.例如,你从未调用过ColorShape的方法,引用了MonochromeShape.
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?
首先不要写这样的代码.永远不要实现相同界面的两个版本,以便它们可以通过协变或逆变转换来统一.它只不过是痛苦和困惑.同样,永远不要使用在泛型替换下统一的方法来实现接口. (例如,接口IFoo< T> {void M(int); void M(T);} class Foo:IFoo< int> {uh oh})
我考虑过添加一个警告,但很难看到如何在极少数情况下关闭警告.只能通过编译指示关闭的警告是很糟糕的警告.
标签:c,net,polymorphism,contravariance,generics 来源: https://codeday.me/bug/20190717/1487155.html