Данный код похож на следующий (с реализацией в реальном случае использования):
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(); }
}
Все отлично работает с простым foreach... например. следующие компилируются:
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();
Мы также можем скомпилировать код, чтобы накормить всех голодных животных:
// feed all the hungry animals
foreach (var animal in animals.Where(a => a.IsHungry))
animal.Feed();
... но если мы попытаемся использовать аналогичные изменения кода, чтобы сделать только кошку голодных собак, мы получим ошибку компиляции
// 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?)
Это кажется очень странной ошибкой, так как на самом деле существует метод расширения, который можно использовать. Я предполагаю это, потому что компилятор считает неоднозначным, какой общий параметр ему нужно использовать для Where, и не имеет достаточно определенного сообщения об ошибке для случая неоднозначных общих параметров для лучшей функции расширения соответствия.
Если вместо этого я должен был бы определить AnimalGroup<T>
без интерфейса:
class AnimalGroup<T> : AnimalGroup
where T : Animal
{
public new IEnumerator<T> GetEnumerator() { throw new NotImplementedException(); }
}
Первые 3 тестовых случая все еще работают (потому что foreach использует функцию GetEnumerator, даже если нет интерфейса). Сообщение об ошибке в 4-м случае затем перемещается в линию, где он пытается сделать животное (которое, случается, является собакой, но система типа не ЗНАЕТ собака). Это можно исправить, изменив var
на Dog
в цикле foreach (и для полноты использования dog?.Bark()
на всякий случай, если любые ненужные собаки были возвращены из счетчика).
В моем реальном случае я, скорее всего, буду иметь дело с AnimalGroup<T>
, чем AnimalGroup
(и он фактически использует IReadOnlyList<T>
, а не IEnumerable<T>
). Создание кода, подобного случаям 2 и 4, по-прежнему имеет гораздо более высокий приоритет, чем позволить прямолинейным функциям Linq на AnimalGroup
(также желательно для полноты, но с гораздо более низким приоритетом), поэтому я рассмотрел его, переопределив AnimalGroup
без интерфейса:
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(); }
}
Это переводит ошибку в третий случай "кормить всех голодных животных" (и сообщение об ошибке имеет смысл в этом контексте - на самом деле не существует применимого метода расширения), с которым я могу сейчас жить. (Теперь я думаю об этом, я мог бы оставить не общий интерфейс IEnumerable
в базовом классе, но это не имеет никакого преимущества, поскольку функции Linq работают только на универсальном интерфейсе, foreach не требует его, а вызывающие абоненты используют IEnumerable должен будет отличить результат от object
).
Есть ли какой-то способ, о котором я еще не думал, так что я могу переопределить AnimalGroup
и/или AnimalGroup<T>
, чтобы все 4 из этих тестовых случаев скомпилировались как ожидается с последним 2, напрямую вызывающим Enumerable.Where
(а не какую-либо другую функцию Where
, которую я определяю)?
Интересный граничный случай также var genericAnimals = new AnimalGroup<Animal>;
. Использует как genericAnimals.Where(...)
скомпилировать, как ожидалось, хотя это тот же класс, который не компилировался с другим параметром типа!