首页 > 解决方案 > 如何将通用实例添加到通用对象列表中

问题描述

我有一个接口 IShape 和抽象类 Shape。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 的实例添加到此列表中,我需要进行哪些更改?

标签: c#oopgenericsinheritancepolymorphism

解决方案


通用协变和逆变,我们开始吧!

所以你有一些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);

也将是合法的。它应该是,给一个IShape对象IDrawer<IShape>似乎是合理的。感觉应该可以。但事实并非如此。你只是CircleDrawer.Draw(Circle shape)通过给它一个矩形来代替圆来调用一个方法。会发生什么?这不是任何CircleDrawer人都愿意遇到的情况。想象一下,如果你被教导如何在你的一生中画圆,突然有人给你一个矩形来画:O

所以类型系统不允许Add列表中的。通常,当这种情况发生时,您可以通过标记泛型类型协变或逆变来修复它。但是在这种情况下,您想要做的事情实际上是不可能的和荒谬的 - 您不知道确切类型的抽屉的集合对您毫无用处。你不知道他们能画什么,所以每次Draw打电话你都在玩俄罗斯轮盘赌,希望你刚通过的矩形变成 aRectangleDrawer而不是 a CircleDrawer

你唯一能做的就是反其道而行之——假设你有 aRectangleDrawer和 aSquareDrawer

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

这告诉编译器 aIDrawer<T>也可以绘制Uif U : T。它还不允许指定T任何成员方法的返回类型。

更多关于协变和逆变的信息可以在 MSDN 文档中找到。


推荐阅读