Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Linq functions give strange compile error when ambiguous use of IEnumerable - possible workarounds?

Given code similar to the following (with implementations in the real use case):

class Animal
{
  public bool IsHungry { get; }
  public void Feed() { }
}
class Dog : Animal
{
  public void Bark() { }
}

class AnimalGroup : IEnumerable<Animal>
{
  public IEnumerator<Animal> GetEnumerator() { throw new NotImplementedException(); }
  IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); }
}

class AnimalGroup<T> : AnimalGroup, IEnumerable<T>
  where T : Animal
{
  public new IEnumerator<T> GetEnumerator() { throw new NotImplementedException(); }
}

Everything works great with a plain foreach... e.g. the following compiles fine:

var animals = new AnimalGroup();
var dogs = new AnimalGroup<Dog>();

  // feed all the animals
  foreach (var animal in animals)
    animal.Feed();

  // make all the dogs bark
  foreach (var dog in dogs)
    dog.Bark();

We can also compile code to feed all the hungry animals:

  // feed all the hungry animals
  foreach (var animal in animals.Where(a => a.IsHungry))
    animal.Feed();

... but if we try to use similar code changes to make only the hungry dogs bark, we get a compile error

  // make all the hungry dogs bark
  foreach (var dog in dogs.Where(d => d.IsHungry))
    dog.Bark();
  // error CS1061: 'AnimalGroup<Dog>' does not contain a definition for 'Where' and
  // no extension method 'Where' accepting a first argument of type 'AnimalGroup<Dog>'
  // could be found (are you missing a using directive or an assembly reference?)

This seems like a very strange error, as in fact there is an extension method that can be used. I assume it's because the compiler considers it ambiguous which generic parameter it needs to use for the Where, and doesn't have a specific enough error message for the case of ambiguous generic parameters to the best match extension function.

If instead, I were to define AnimalGroup<T> without an interface:

class AnimalGroup<T> : AnimalGroup
  where T : Animal
{
  public new IEnumerator<T> GetEnumerator() { throw new NotImplementedException(); }
}

The first 3 test cases still work (because foreach uses the GetEnumerator function even if there's no interface). The error message on the 4th case then moves to the line where it is trying to make an animal (which happens to be a dog, but which the type system doesn't KNOW is a dog) bark. That can be fixed by changing var to Dog in the foreach loop (and for completeness using dog?.Bark() just in case any non-dogs were returned from the enumerator).

In my real use case, I'm much more likely to be wanting to deal with AnimalGroup<T> than AnimalGroup (and it's actually using IReadOnlyList<T> rather than IEnumerable<T>). Making code like cases 2 and 4 work as expected is of far higher priority than allowing Linq functions to be called directly on AnimalGroup (also desirable for completeness, but of much lower priority), so I dealt with it by redefining AnimalGroup without an interface like this:

class AnimalGroup
{
  public IEnumerator<Animal> GetEnumerator() { throw new NotImplementedException(); }
}
class AnimalGroup<T> : AnimalGroup, IEnumerable<T>
  where T : Animal
{
  public new IEnumerator<T> GetEnumerator() { throw new NotImplementedException(); }
  IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); }
}

This moves the error to the third case "feed all the hungry animals" (and the error message makes sense in that context - there really is no applicable extension method), which I can live with for now. (Now I think of it, I could have left a non-generic IEnumerable interface on the base class, but this has no benefit, as Linq functions only operate on the generic interface, foreach doesn't require it, and callers using IEnumerable would have to cast the result from object).

Is there some way that I've not yet thought of, such that I can redefine AnimalGroup and/or AnimalGroup<T> so that all 4 of these test cases compile as expected with the latter 2 directly calling Enumerable.Where (rather than some other Where function I define)?

An interesting boundary case is also var genericAnimals = new AnimalGroup<Animal>;. Uses like genericAnimals.Where(...) compile as expected, even though it is the same class that didn't compile with a different type parameter!

like image 551
Steve Avatar asked Jul 20 '17 09:07

Steve


2 Answers

As you say, the error message is unfortunate, in that the problem is ambiguity rather than Where not being found at all (assuming you have a using directive for System.Linq). The problem is that the compiler can't infer the type argument for Enumerable.Where, because AnimalGroup<Dog> implements both IEnumerable<Animal> and IEnumerable<Dog>.

Options that don't involve changing AnimalGroup:

  • Declare dogs as IEnumerable<Dog> rather than AnimalGroup<Dog>
  • Specify the type argument directly:

    foreach (var dog in dogs.Where<Dog>(d => d.IsHungry))
    

You could make AnimalGroup implement the non-generic IEnumerable - that wouldn't confuse the compiler, as there's no Where method for the non-generic interface. You could then still implement your third use case using Cast:

 foreach (var animal in animals.Cast<Animal>().Where(a => a.IsHungry))

Fundamentally when a type implements IEnumerable<Foo> and IEnumerable<Bar> you are going to have problems with type inference - so you need to choose between not using type inference (easy for Where - harder in other cases) or not implementing both interfaces.

like image 66
Jon Skeet Avatar answered Nov 04 '22 11:11

Jon Skeet


What about implementing non-generic AnimalGroup from generic AnimalGroup<T>:

class AnimalGroup<T> : IEnumerable<T>
    where T : Animal
{
    public IEnumerator<T> GetEnumerator() { throw new NotImplementedException(); }
    IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); }
}

class AnimalGroup : AnimalGroup<Animal> { }
like image 26
pkuderov Avatar answered Nov 04 '22 10:11

pkuderov