Reputation: 23
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
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
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