3

In the implementation of GetEnumerator() below, the compiler refuses to convert List<T>.Enumerator to IEnumerator<Shape> even though T is constrained by Shape. (1) Why is this, and (2) is there a workaround I'm overlooking?

using System.Collections.Generic;

interface Shape {
    void Draw();
}

class Picture<T> : IEnumerable<Shape> where T : Shape {  
List<T> shapes;
    public IEnumerator<Shape> GetEnumerator()
    {
        return shapes.GetEnumerator(); 
    }
}
  • This ought to be working, as `IEnumerator` is declared covariant. See `` in the documentation, here: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.ienumerator-1?view=net-5.0 – Ben Voigt Jun 29 '21 at 21:55
  • I would have thought so, too. I'm stuck with VS 2015; perhaps the language/library variant I'm using (C# v 6, I believe) isn't up to the task? – Greg Hickman Jun 29 '21 at 21:59

3 Answers3

6

Change the constraint from

where T : Shape

to

where T : class, Shape

Interface covariance doesn't work for value types.

Documented here:

Variance applies only to reference types; if you specify a value type for a variant type parameter, that type parameter is invariant for the resulting constructed type.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
1

Because variance does not work for value types. So if it is suitable for you can constrain T to be a class:

private class Picture<T> : IEnumerable<Shape> where T : class, Shape
{
    private List<T> shapes;
    public IEnumerator<Shape> GetEnumerator()
    {
        return shapes.GetEnumerator();
    }
}
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
0

The compiler simply does not support this kind of type inference. If your types can be reference types, Ben's answer is the way to go. If not you're stuck with workarounds.

There are a few workarounds. Choose the one that makes sense for you.

  1. Use Enumerable.Cast.
public IEnumerator<Shape> GetEnumerator()
{
    return shapes.Cast<Shape>().GetEnumerator(); 
}
  1. Make the list a list of Shape. Drawback is that your implementation will have to cast back to T at some point. Here's an example with some methods I've added.
List<Shape> shapes = new List<Shape>();
public IEnumerator<Shape> GetEnumerator()
{
    return shapes.GetEnumerator(); 
}

public void AddShape(T shape) => shapes.Add(shape);

public T GetShape() => (T)shapes.First();
Kit
  • 20,354
  • 4
  • 60
  • 103
  • There's also `.AsEnumerable().GetEnumerator()` if I'm not mistaken. – Heretic Monkey Jun 29 '21 at 21:53
  • Since List.Enumerator is derived from IEnumerator and the compiler knows T will be Shape or derived from Shape, it seems like a no-brainer for the compiler since implicit conversions from IEnumerator to IEnumerator are supported by the language. Color me surprised, but thanks for your reply! – Greg Hickman Jun 29 '21 at 21:56