Невозможно использовать методы LINQ для базового класса IEnumerable из производного класса

Я пытаюсь реализовать IEnumerable<Turtle> в классе, полученном из базового класса, который уже реализует IEnumerable<Animal>.

Почему вызов base.Cast<Turtle>() (или любой метод LINQ в базовом элементе) в любом методе из класса Turtle не скомпилируется?

Невозможно заменить base на this, поскольку это, очевидно, приводит к StackOverflowException.

Вот пример минимального кода для репликации проблемы:

public interface IAnimal {}

public class Animal : IAnimal {}

public class Turtle : Animal {}

public class AnimalEnumerable : IEnumerable<Animal> {
    List<Animal> Animals = new List<Animal>();
    IEnumerator<Animal> IEnumerable<Animal>.GetEnumerator() {
        return Animals.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator() {
        return Animals.GetEnumerator();
    }
}

public class TurtleEnumerable : AnimalEnumerable, IEnumerable<Turtle> {
    IEnumerator<Turtle> IEnumerable<Turtle>.GetEnumerator() {
        return base.Cast<Turtle>().GetEnumerator(); //FAILS WITH "CANNOT RESOLVE SYMBOL Cast"
    }
}

По какой-то причине замена base.Cast<Turtle>().GetEnumerator(); на this.OfType<Animal>().Cast<Turtle>().GetEnumerator(); работает, не бросая StackOverflowException, но я понятия не имею, почему.

Ответ 1

Существует множество проблем с кодом, который дает другие ответы. Я хочу ответить на ваш конкретный вопрос:

Почему вызов base.Cast<Turtle>() (или любой метод LINQ в базовом элементе) в любом методе из класса Turtle не скомпилируется?

Перейдите к спецификации, раздел 7.6.8.

Базовый доступ используется для доступа к элементам базового класса, которые скрыты аналогично именованными членами в текущем классе или структуре.

Вы получаете доступ к члену базового класса? NO. Метод расширения является членом статического класса, который содержит метод расширения, а не базовый класс.

Базовый доступ разрешен только в блоке конструктора экземпляра, метода экземпляра или экземпляра экземпляра.

Вы здесь хорошо.

Когда base.I встречается в классе или структуре, я должен обозначать член базового класса этого класса или структуры.

Опять же, Cast<T> не является членом базового класса.

Когда базовый доступ ссылается на элемент виртуальной функции (метод, свойство или индекс), определение того, какой член функции вызывается во время выполнения (§7.5.4), изменяется.

Вы не получаете доступ к виртуальному. Методы расширения являются статическими.

Выбранный член функции определяется путем нахождения наиболее производной реализации члена функции по отношению к B (а не по отношению к типу времени выполнения, как это было бы обычно при не-базовом доступе), Таким образом, в пределах переопределения элемента виртуальной функции базовый доступ может использоваться для вызова унаследованной реализации члена функции.

Итак, теперь мы видим, какова цель базового доступа: , чтобы включить виртуальную диспетчеризацию в виртуальный член, который был переопределен в текущем типе, или , чтобы вызвать базу член класса, который был скрыт элементом new в текущем типе. Это не то, что вы пытаетесь использовать base для, и поэтому вы обречены на провал. Прекратите использовать base вне метки, как это. Используйте его только при попытке сделать виртуальную отправку виртуальному члену или получить доступ к скрытому члену.

Ответ 2

Эрик Липперт заявил ранее, что это было несколько сознательное дизайнерское решение здесь. Вы никогда не хотели использовать методы расширения в случае, когда вы впервые имеете доступ к реализации вашего базового класса.

И если вы думаете об этом, вам также не нужно делать это здесь. Создайте свойство или метод GetEnumerator, который защищен и использует его! Основная объектная ориентация; не нужно пытать здесь.

EDIT: Было указано, что мое предыдущее предложение не сработало. Поэтому позвольте мне предложить просто не реализовывать два разных интерфейса IEnumerable, так как это в любом случае вызовет много головных болей с foreach.

Я пришел к выводу, что эта реализация может быть тем, что вы действительно хотите:

public interface IAnimal { }

public class Animal : IAnimal { }

public class Turtle : Animal { }

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


    IEnumerator IEnumerable.GetEnumerator()
    {
        throw new NotImplementedException();
    }
}

public class TurtleEnumerable : AnimalEnumerable
{
}

Затем вы можете перечислить через Animal и их производные все вроде

Ответ 3

Я отвечу на этот вопрос:

Почему вызывается base.Cast() (или любой метод LINQ на базе элемент) в любом методе из класса Turtle не удается скомпилировать?

Причиной этого исключения является Cast, а другие такие методы - методы расширения. И методы расширения являются статическими.

Например, давайте посмотрим, что:

public static class Extensions
{
    public static void Method2(this Base b) ...
}

public class Base 
{
    public void Method1() ...
}

public class Derived:Base
{
    public void Test()
    {
        base.Method1();

        base.Method2(); // Does not contain a definition
    }
}

И как вы знаете, методы расширения - это действительно хороший синтаксический сахар. Они не добавляются к классу, но компилятор заставляет его чувствовать себя таким, как есть. Таким образом, компилятор изменит эту строку кода на следующий:

 Extensions.Method2(base); 

Если вы замените код на этот код, компилятор предоставит более подходящее сообщение об ошибке: Использование ключевого слова base недопустимо в этом контексте.

Как сказано в MSDN:

Доступ к базовому классу разрешен только в конструкторе, экземпляре метод или свойство доступа к экземпляру.

Ответ 4

Есть несколько проблем с вашим подходом. TurtleEnumerable реализует как IEnumerable<Animal>, так и IEnumerable<Turtle>. Чтобы иметь возможность использовать экземпляр TurtleEnumerable в цикле foreach, вам придется отдать его для компиляции кода:

foreach (var turtle in (IEnumerable<Turtle>) turtleEnumerable)

Вы также используете явные реализации интерфейса, чтобы скрыть общие методы GetEnumerator(). Вы должны это сделать, потому что вы не можете разрешать перегрузку только по типу возврата, а два общих метода GetEnumerator() отличаются только типом возврата.

Однако это означает, что метод TurtleEnumerable не может вызвать базовый метод GetEnumerator(). Причиной этого является то, что base не ведет себя как переменная типа "base". Вместо этого это зарезервированное слово, которое можно использовать только для вызова методов базового класса. Следствием этого является то, что методы расширения не могут использоваться с base. Кроме того, вы не можете использовать base, поэтому явные реализации интерфейса в базовом классе нельзя вызывать через base.

Однако вы можете использовать this, но поскольку общий GetEnumerator() на TurtleEnumerable скрывает общий GetEnumerator() на AnimalEnumerable, вы не сможете вызвать базовый класс, чтобы получить переполнение стека потому что в какой-то момент реализация TurtleEnumerable.GetEnumerator() вызовет тот же GetEnumerator.

Чтобы сделать компиляцию кода, вам необходимо создать метод protected IEnumerator<Animal> GetEnumerator() в базовом классе и создать собственный класс TurtleEnumerator, который обертывает экземпляр базового счетчика, который вы можете получить, вызвав защищенный метод.

public class TurtleEnumerable : AnimalEnumerable, IEnumerable<Turtle> {

  IEnumerator<Turtle> IEnumerable<Turtle>.GetEnumerator() {
    return new TurtleEnumerator(base.GetEnumerator());
  }

  sealed class TurtleEnumerator : IEnumerator<Turtle> {

    IEnumerator<Animal> animalEnumerator;

    public TurtleEnumerator(IEnumerator<Animal> animalEnumerator) {
      this.animalEnumerator = animalEnumerator;
    }

    public Turtle Current {
      get { return (Turtle) animalEnumerator.Current; }
    }

    Object IEnumerator.Current {
      get { return Current; }
    }

    public Boolean MoveNext() {
      return animalEnumerator.MoveNext();
    }

    public void Reset() {
      animalEnumerator.Reset();
    }

    public void Dispose() {
      animalEnumerator.Dispose();
    }

  }

}

Всем, у кого есть коллекция, реализует как IEnumerable<Base>, так и IEnumerable<Derived>, вы получите массу неприятностей. Что вы пытаетесь достичь, используя этот дизайн?

Используя общий List<T> и контравариантность, вы можете делать такие вещи:

IEnumerable<Turtle> turtles = new List<Turtle>();
IEnumerable<Animal> animals = (IEnumerable<Animal>) turtles;

Вы также можете заменить List<T> на свой собственный общий тип коллекции, если это необходимо.

Ответ 5

Почему вы реализуете IEnumerable в классе TurtleEnumerator? Кроме того, я не думаю, что доступность AnimalEnumerable, когда вы реализовали интерфейс IEnumerable, верна.

Не было бы реализовано что-то вроде этого:

    public interface IAnimal { }

    public class Animal : IAnimal { }

    public class Turtle : Animal { }

    public class AnimalEnumerable : IEnumerable<Animal>
    {
        protected List<Animal> Animals = new List<Animal>();

        public IEnumerator<Animal> GetEnumerator()
        {
            return Animals.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return Animals.GetEnumerator();
        }
    }

    public class TurtleEnumerable : AnimalEnumerable
    {
        public void AddTurtle(Turtle turtle)
        {
            Animals.Add(turtle);
        }

        public IEnumerable<Turtle> GetTurtles()
        {
            var iterator = GetEnumerator();

            yield return iterator.Current as Turtle;
        }
    }

    [Test]
    public void CanAddTurtles()
    {
        Turtle one = new Turtle();
        Turtle two = new Turtle();

        TurtleEnumerable turtleStore = new TurtleEnumerable();

        turtleStore.AddTurtle(one);
        turtleStore.AddTurtle(two);

        foreach (var turtle in turtleStore.GetTurtles())
        {
            // Do something with the turtles....
        }
    }