Dedddeded
Dedddeded

Reputation: 23

How to add generic instance to list of generic objects

I have an Interface IShape and abstract class Shape. Shape implements IShape. Shape has 2 children - Circle and Rectangle. Also I have generic interface IDrawer where T:IShape. I have an abstract generic class BaseDrawer : IDrawer where 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)
            {
            }
        }

I have a list: List<IDrawer<IShape>> Drawers { get; set; } When I'm trying to create an instance of CircleDrawer - var drawer = new CircleDrawer(); - and add it to this list, I get an error: Cannot convert CircleDrawer to IDrawer.

What changes do I need to make to add instance of circleDrawer to this list?

Upvotes: 0

Views: 199

Answers (2)

V0ldek
V0ldek

Reputation: 10563

Generic covariance and contravariance here we go!

So you have something of type CircleDrawer which we can straightforwardly convert to IDrawer<Circle>. Let's see what happens

var list = new List<IDrawer<IShape>>();
IDrawer<Circle> drawer = new CircleDrawer(); // Completely reasonable cast.

list.Add(drawer); // This results in the error.

Okay, but why? That's because circle being a shape does not imply that a drawer of circles is a drawer of shapes. A conversion

IDrawer<IShape> shapeDrawer = drawer;

is illegal. What you're trying to do is actually impossible. A drawer of circles knows how to draw circles, not shapes. Let's say that the cast you're trying to do is legal. We add the drawer to the list.

list.Add(drawer);

and now somewhere else we take it out of the list and give it a shape:

IDrawer<IShape> drawer = list.First();
drawer.Draw(shape);

Is this correct? Well, it depends on the shape. If it is

IShape shape = new Circle();

then yeah, we're giving a circle to our CircleDrawer, everything is okay. But note that this line:

IShape shape = new Rectangle();

drawer.Draw(shape);

would also be legal. And it should be, giving an IShape object to IDrawer<IShape> seems reasonable. Feels like it should work. But it doesn't. You just called a CircleDrawer.Draw(Circle shape) method by giving it a rectangle in place of the circle. What's going to happen? This is not a situation any CircleDrawer would like to find itself in. Imagine if you were taught how to draw circles your whole life and suddenly someone gives you a rectangle to draw :O

So the type system disallows the Add on the list. Usually when this happens you can fix it by marking your generic type co- or contravariant. But in this case the thing you want to do is literally impossible and nonsensical - a collection of drawers of which the exact type you don't know is useless to you. You don't know what they can draw, so with every Draw call you're playing russian roulette and hoping the rectangle you just passed went to a RectangleDrawer not a CircleDrawer.

The only thing you could make happen is assing things the other way around - so let's say you have a RectangleDrawer and a SquareDrawer

class Rectangle : IShape {}

class Square : Rectangle {}

class RectangleDrawer : IDrawer<Rectangle> {}

class SquareDrawer : IDrawer<Square> {}

Then a collection of square drawers would be perfectly fine, and you might want to do something like

var list = new List<SquareDrawer>();

var squareDrawer = new SquareDrawer();
var rectangleDrawer = new RectangleDrawer();

list.Add(squareDrawer);
list.Add(rectangleDrawer);

And then you could use that list giving the drawers in it squares to draw. That makes sense, since being a RectangleDrawer implies that you can draw any rectangles, squares included.

The above lines will not compile however - you would have to mark your IDrawer contravariant.

interface IDrawer<in T> where T : IShape
{
    void Draw(T shape);
}

This tells the compiler that a IDrawer<T> can also draw U if U : T. It also disallows specifying T as a return type of any member methods.

More on covariance and contravariance can be found in the MSDN docs.

Upvotes: 1

Yair Halberstadt
Yair Halberstadt

Reputation: 6821

Let's think about this for a second.

You're saying that a CircleDrawer is a type of IDrawer<IShape> since a Circle is a type of IShape.

But an IDrawer<IShape> is something that can draw an IShape - any IShape. Now a CircleDrawer can draw a Circle, but no other shape, so it is not an IDrawer<IShape>.

An IDrawer<IShape> however could in theory be an instance of an IDrawer<Circle>, since it can draw any shape, including a circle. To specify that formally, you'll have to use Covariance/Contravariance: https://learn.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance.

In your case, you'll have to create a List<IDrawer<Circle>> I'm afraid.

To show where things could go wrong if you were allowed to treat a CircleDrawer as IDrawer<IShape> consider the following code:

IDrawer<IShape> drawer = new CircleDrawer();
drawer.Draw(new Rectangle()); //Throws exception - a circle drawer can't draw a rectangle

Upvotes: 0

Related Questions