С# `foreach` поведение - Уточнение?

Я прочитал статью Eric здесь о перечислении foreach и о различных сценариях, в которых foreach может работать

Чтобы предотвратить старую версию С# для бокса, команда С# включила утиную печать для foreach для запуска в коллекции, отличной от Ienumerable. (Публичный GetEnumerator, который возвращает то, что имеет общедоступные MoveNext и Current достаточно (.

Итак, Эрик написал образец:

class MyIntegers : IEnumerable
{
  public class MyEnumerator : IEnumerator
  {
    private int index = 0;
    object IEnumerator.Current { return this.Current; }
    int Current { return index * index; }
    public bool MoveNext() 
    { 
      if (index > 10) return false;
      ++index;
      return true;
    }
  }
  public MyEnumerator GetEnumerator() { return new MyEnumerator(); }
  IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); }
}

Но я считаю, что у него есть некоторые опечатки (отсутствует get accessor при реализации свойства Current), которые мешают ему компилировать (я уже отправил его по электронной почте).

В любом случае это рабочая версия:

class MyIntegers : IEnumerable
{
  public class MyEnumerator : IEnumerator
  {
    private int index = 0;
      public void Reset()
      {
          throw new NotImplementedException();
      }

      object IEnumerator.Current {
          get { return this.Current; }
      }
    int Current {
        get { return index*index; }
    }
    public bool MoveNext() 
    { 
      if (index > 10) return false;
      ++index;
      return true;
    }
  }
  public MyEnumerator GetEnumerator() { return new MyEnumerator(); }
  IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); }
}

Ok.

Согласно MSDN:

Тип C называется collection type, если он реализует System.Collections.IEnumerable интерфейс или реализует collection pattern, встретив все следующие критерии:

  • C содержит метод публичного экземпляра с подписями GetEnumerator(), который возвращает тип struct-type, class-type или interface-type, который называется E в следующем тексте.

  • E содержит метод общедоступного экземпляра с сигнатурой MoveNext() и возвращаемым типом bool.

  • E содержит свойство публичного экземпляра с именем Current, которое позволяет считывать текущее значение. Тип этого свойства называется типом элемента типа коллекции.

OK. Пусть сопоставление документов с образцом Эрика

Образец Eric называется collection type, потому что он реализует интерфейс System.Collections.IEnumerable (явно, хотя). Но это не (!) a collection pattern из-за пули 3: MyEnumerator не имеет свойства публичного экземпляра с именем Current.

MSDN говорит:

Если выражение коллекции имеет тип, который реализует (как определено выше), расширение foreach утверждение:

E enumerator = (collection).GetEnumerator();
try {
   while (enumerator.MoveNext()) {
      ElementType element = (ElementType)enumerator.Current;
      statement;
   }
}
finally {
   IDisposable disposable = enumerator as System.IDisposable;
   if (disposable != null) disposable.Dispose();
}

В противном случае, выражение коллекции имеет тип, который реализует System.IEnumerable(!), а расширение оператора foreach:

IEnumerator enumerator = 
        ((System.Collections.IEnumerable)(collection)).GetEnumerator();
try {
   while (enumerator.MoveNext()) {
      ElementType element = (ElementType)enumerator.Current;
      statement;
   }
}
finally {
   IDisposable disposable = enumerator as System.IDisposable;
   if (disposable != null) disposable.Dispose();
}

Вопрос № 1

Кажется, что образец Эрика не реализует   collection pattern и System.IEnumerable - поэтому он не должен соответствовать ни одному из указанных выше условий. Итак, почему я все еще могу перебрать его через:

 foreach (var element in (new MyIntegers() as IEnumerable ))
             {
                 Console.WriteLine(element);
             }

Вопрос № 2

Почему я должен упомянуть new MyIntegers() as IEnumerable? он уже Ienumerable (!!) и даже после этого, разве компилятор уже выполняет эту работу самостоятельно посредством кастинга:

((System.Collections.IEnumerable)(collection)).GetEnumerator() ?

Это прямо здесь:

 IEnumerator enumerator = 
            ((System.Collections.IEnumerable)(collection)).GetEnumerator();
    try {
       while (enumerator.MoveNext()) {
       ...

Итак, почему он все еще хочет, чтобы я упоминал как Ienumerable?

Ответы

Ответ 1

MyEnumerator не имеет необходимых общедоступных методов

Да, это так - вернее, если бы Current были общедоступными. Все, что требуется, это то, что он имеет:

  • Публичное, читаемое свойство Current
  • Открытый MoveNext() метод без аргументов типа, возвращающих bool

Отсутствие public здесь было просто еще одной опечаткой, в основном. Как бы то ни было, пример не делает то, что он подразумевал (предотвращает бокс). Он использует реализацию IEnumerable, потому что вы используете new MyIntegers() as IEnumerable - поэтому тип выражения IEnumerable, и он просто использует интерфейс повсюду.

Вы утверждаете, что он не реализует IEnumerable, (который является System.Collections.IEnumerable, btw), но он делает это, используя явную реализацию интерфейса.

Проще всего протестировать такие вещи без реализации IEnumerable:

using System;

class BizarreCollection
{
    public Enumerator GetEnumerator()
    {
        return new Enumerator();
    }

    public class Enumerator
    {
        private int index = 0;

        public bool MoveNext()
        {
            if (index == 10)
            {
                return false;
            }
            index++;
            return true;
        }

        public int Current { get { return index; } }
    }
}

class Test
{
    static void Main(string[] args)
    {
        foreach (var item in new BizarreCollection())
        {
            Console.WriteLine(item);
        }
    }
}

Теперь, если вы сделаете Current private, он не будет компилироваться.

Ответ 2

Ссылка на System.IEnumerable на MSDN - это не что иное, как опечатка в спецификации старого языка, такой интерфейс не существует, я считаю, что он должен ссылаться на System.Collections.IEnumerable.

Вы действительно должны прочитать спецификацию языка версии С#, которую вы используете, доступна языковая спецификация С# 5.0 здесь.

Некоторая дополнительная информация о том, почему не удается выполнить этот пример, приводит к ошибке, а не к возврату к использованию System.Collections.IEnumerable:

В спецификации для foreach (раздел 8.8.4) вы увидите, что правила немного изменились (некоторые сокращения были сокращены для краткости):

  • Выполните поиск элемента по типу X с идентификатором GetEnumerator и без аргументов типа. Если поиск элемента не приводит к совпадению или вызывает неоднозначность или создает совпадение, которое не является группой методов, проверьте перечислимый интерфейс, как описано ниже. Рекомендуется, чтобы предупреждение выдавалось, если поиск элемента производит что-либо, кроме группы методов, или нет совпадения.
  • Выполните разрешение перегрузки с помощью полученной группы методов и пустого списка аргументов. Если разрешение перегрузки не приводит к каким-либо применимым методам, приводит к двусмысленности или приводит к одному наилучшему методу, но этот метод является либо статическим, либо не общедоступным, проверьте перечислимый интерфейс, как описано ниже. Рекомендуется выдавать предупреждение, если разрешение перегрузки создает что-либо, кроме однозначного метода публичного экземпляра или не применимых методов.
  • Если тип возврата E метода GetEnumerator не является типом класса, структуры или интерфейса, возникает ошибка и дальнейших шагов не предпринимается.
  • Поиск элемента выполняется на E с идентификатором Current и no type. Если поиск элемента не дает совпадения, результатом является ошибка, или результат - это что-либо, кроме свойства публичного экземпляра, которое разрешает чтение, возникает ошибка и дальнейших шагов не предпринимается.
  • Тип коллекции - X, тип перечислителя - E, а тип элемента - тип свойства Current.

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

При явной передаче экземпляра в IEnumerable все требования удовлетворяются, поскольку тип IEnumerable и связанный с ним IEnumerator удовлетворяют всем требованиям.

Также из документации:

Вышеупомянутые шаги, если они успешны, однозначно создают тип коллекции C, тип перечислителя E и тип элемента T. Оператор foreach формы

foreach (V v in x) embedded-statement

затем расширяется до:

{
    E e = ((C)(x)).GetEnumerator();
    try {
        while (e.MoveNext()) {
            V v = (V)(T)e.Current;
            embedded-statement
        }
    }
    finally {
        … // Dispose e
    }
}

Таким образом, при явном приведении в IEnumerable вы получите следующее:

  • C = System.Collections.IEnumerable
  • x = new MyIntegers() as System.Collections.IEnumerable
  • E = System.Collections.IEnumerator
  • T = System.Object
  • V = System.Object
{
    System.Collections.IEnumerator e = ((System.Collections.IEnumerable)(new MyIntegers() as System.Collections.IEnumerable)).GetEnumerator();
    try {
        while (e.MoveNext()) {
            System.Object element = (System.Object)(System.Object)e.Current;
            Console.WriteLine(element);
        }
    }
    finally {
        System.IDisposable d = e as System.IDisposable;
        if (d != null) d.Dispose();
    }
}

Это объясняет, почему использование литых работ.