可插拔框架
想象一个简单的可插拔系统,使用继承多态性相当直接:
- 我们有一个图形渲染系统。
- 有不同类型的图形形状(单色、彩色等)需要渲染。
- 渲染由特定于数据的插件完成,例如ColorRenderer将呈现ColorShape。
- 每个插件都实现了
IRenderer
,因此它们都可以存储在IRenderer[]
中。 - 启动时,
IRenderer[]
填充为一系列特定的渲染器。 - 当接收到新形状的数据时,根据形状的类型从数组中选择插件。
- 然后通过调用其
Render
方法来调用插件,传递形状作为其基本类型。 Render
方法在每个派生类中被重写;它将形状强制转换回其派生类型,然后进行渲染。
希望以上内容清晰明了 - 我认为这是一种非常常见的设置。 使用继承多态性和运行时转换非常容易。
无需转换
现在是棘手的部分。响应于this question,我想出了一种方法来完成所有这些事情完全不需要任何转换。这很棘手,因为那个IRenderer[]
数组——要从数组中取出插件,通常需要将其强制转换为特定类型,以便使用其类型特定的方法,但我们不能这样做。现在,我们可以通过仅使用其基类成员与插件交互来解决这个问题,但要求的一部分是渲染器必须运行具有类型特定数据包作为参数的类型特定方法,而基类将无法做到这一点,因为没有办法传递类型特定数据包而不将其强制转换为基类然后再返回祖先。棘手。起初我认为这是不可能的,但经过几次尝试,我发现可以通过操纵C#泛型系统来实现。我创建了一个相对于插件和形状类型都是逆变的接口,然后使用它。渲染器的解析由类型特定的形状决定。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
返回了错误的类型会怎么样?
类型安全?
根据这篇MSDN文章,
相反变性似乎是不合理的...这似乎是倒退的,但它是编译和运行的类型安全代码。代码是类型安全的,因为T指定了一个参数类型。
我在想,这绝对不是类型安全的。
引入一个返回错误类型的错误
所以我故意在控制器中引入了一个错误,使其错误地存储了一个ColorRenderer,而MonochromeRenderer应该被存储,就像这样:
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
方法是虚拟的,我本以为入口点在编译时被绑定。方法组中的方法原型实际上是在运行时选择的吗?如果没有VMT分派的条目,这怎么可能工作?它使用某种反射吗?
第三个问题,
c#逆变是否绝对不安全?我得到的是意外行为,而不是无效的转换异常(至少告诉我有问题)。是否有任何方法可以在编译时检测出这样的问题,或者至少让它们抛出异常而不是执行一些意外的操作?