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!
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
:
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.
See more on this question at Stackoverflow