Подтвердить что ты не робот

Функции Linq дают странную ошибку компиляции при неоднозначном использовании IEnumerable - возможные обходные пути?

Данный код похож на следующий (с реализацией в реальном случае использования):

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(...) скомпилировать, как ожидалось, хотя это тот же класс, который не компилировался с другим параметром типа!

4b9b3361

Ответ 1

Что делать с универсальным AnimalGroup из родового 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> { }

Ответ 2

Как вы говорите, сообщение об ошибке является неудачным, поскольку проблема заключается в двусмысленности, а не в Where вообще не найденной (если у вас есть директива using для System.Linq). Проблема заключается в том, что компилятор не может вывести аргумент типа для Enumerable.Where, потому что AnimalGroup<Dog> реализует как IEnumerable<Animal>, так и IEnumerable<Dog>.

Параметры, которые не связаны с изменением AnimalGroup:

  • Объявить dogs как IEnumerable<Dog>, а не AnimalGroup<Dog>
  • Укажите аргумент типа напрямую:

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

Вы можете сделать AnimalGroup реализовать не-общий IEnumerable - который не будет путать компилятор, так как нет метода Where для не-общего интерфейса. Затем вы могли бы использовать свой третий вариант использования, используя Cast:

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

В принципе, когда тип реализует IEnumerable<Foo> и IEnumerable<Bar>, у вас будут проблемы с типом вывода - поэтому вам нужно выбирать между не использующим вывод типа (легко для Where - сложнее в других случаях) или не выполнять оба интерфейса.