>我有一个接口IShape和抽象类Shape。形状实现 IShape。形状有 2 个子项 - 圆形和矩形。 我也有通用接口IDrawer,其中T:IShape。我有一个抽象的泛型类 BaseDrawer : IDrawer,其中 T : IShape。
public interface IShape
{
double M1();
double M2();
}
public abstract class Shape : IShape
{
public abstract double M1();
public abstract double M2();
}
public class Circle : Shape
{
}
public class Rectangle: Shape
{
}
public interface IDrawer<T> where T:IShape
{
void Draw(T shape);
}
public abstract class BaseDrawer<T> : IDrawer<T> where T : IShape
{
public abstract void Draw(T shape);
}
public class CircleDrawer : BaseDrawer<Circle>
{
public override void Draw(Circle circle)
{
}
}
public class RectangleDrawer : BaseDrawer<Rectangle>
{
public override void Draw(Rectangle rectangle)
{
}
}
我有一个清单:List<IDrawer<IShape>> Drawers { get; set; }
当我尝试创建CircleDrawer的实例(var drawer = new CircleDrawer();
(并将其添加到此列表中时,我收到一个错误:无法将CircleDrawer转换为IDrawer。
我需要进行哪些更改才能将 circleDrawer 的实例添加到此列表中?
泛型协方差和逆变 我们开始了!
所以你有CircleDrawer
类型的东西,我们可以直接转换为IDrawer<Circle>
.让我们看看会发生什么
var list = new List<IDrawer<IShape>>();
IDrawer<Circle> drawer = new CircleDrawer(); // Completely reasonable cast.
list.Add(drawer); // This results in the error.
好吧,但为什么呢?这是因为圆形是一个形状并不意味着圆形的抽屉是形状的抽屉。一次转换
IDrawer<IShape> shapeDrawer = drawer;
是非法的。你想做的事情实际上是不可能的。圆圈抽屉知道如何画圆圈,而不是形状。假设您要做的演员阵容是合法的。我们将抽屉添加到列表中。
list.Add(drawer);
现在在其他地方,我们把它从列表中拿出来,给它一个形状:
IDrawer<IShape> drawer = list.First();
drawer.Draw(shape);
这是对的吗?好吧,这取决于shape
.如果是
IShape shape = new Circle();
那么是的,我们正在给我们的CircleDrawer
一个圆圈,一切都很好。但请注意,这一行:
IShape shape = new Rectangle();
drawer.Draw(shape);
也是合法的。应该是,给IDrawer<IShape>
一个IShape
对象似乎是合理的。感觉应该可以工作。但事实并非如此。您刚刚通过给它一个矩形代替圆来调用CircleDrawer.Draw(Circle shape)
方法。会发生什么?这不是任何CircleDrawer
都希望陷入的境地。想象一下,如果你一辈子都被教如何画圆,突然有人给你一个矩形来画:O
因此,类型系统不允许列表中的Add
。通常,当发生这种情况时,您可以通过标记泛型类型协变或逆变来修复它。但在这种情况下,你想做的事情实际上是不可能和荒谬的——一组抽屉,你不知道的确切类型对你毫无用处。你不知道他们能画什么,所以每次Draw
电话,你都在玩俄罗斯轮盘赌,希望你刚刚通过的矩形去RectangleDrawer
而不是CircleDrawer
。
你唯一能做的就是反过来做事——所以假设你有一个RectangleDrawer
和一个SquareDrawer
class Rectangle : IShape {}
class Square : Rectangle {}
class RectangleDrawer : IDrawer<Rectangle> {}
class SquareDrawer : IDrawer<Square> {}
那么一系列方形抽屉就完全没问题了,你可能想做一些类似的事情
var list = new List<SquareDrawer>();
var squareDrawer = new SquareDrawer();
var rectangleDrawer = new RectangleDrawer();
list.Add(squareDrawer);
list.Add(rectangleDrawer);
然后你可以用这个列表给抽屉里画正方形。这是有道理的,因为作为RectangleDrawer
意味着您可以绘制任何矩形,包括正方形。
但是,上述行将无法编译 - 您必须将IDrawer
标记为逆变。
interface IDrawer<in T> where T : IShape
{
void Draw(T shape);
}
这告诉编译器,如果U : T
,IDrawer<T>
也可以绘制U
。它还不允许将T
指定为任何成员方法的返回类型。
有关协变和逆变的更多信息,请参阅 MSDN 文档。
让我们考虑一下。
你是说CircleDrawer
是一种IDrawer<IShape>
,因为Circle
是一种IShape
。
但是IDrawer<IShape>
是可以引起IShape
的东西 - 任何IShape
.现在CircleDrawer
可以画一个圆,但没有其他形状,所以它不是IDrawer<IShape>
。
然而,理论上IDrawer<IShape>
可以是IDrawer<Circle>
的实例,因为它可以绘制任何形状,包括一个圆。要正式指定,您必须使用协方差/逆变:https://learn.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance。
在你的情况下,恐怕你必须创建一个List<IDrawer<Circle>>
。
为了显示如果允许您将CircleDrawer
视为IDrawer<IShape>
则可能出错的地方,请考虑以下代码:
IDrawer<IShape> drawer = new CircleDrawer();
drawer.Draw(new Rectangle()); //Throws exception - a circle drawer can't draw a rectangle