IEnumerable - маленькие хитрости, утиная типизация

В данной небольшой статье заметке, я хочу показать, что даже на, вроде бы, описанных в MSDN how to вещах можно использовать разные решения. И не всегда решение от MSDN будет являться наиболее простым, коротким и изящным.

Так о чем будем сегодня говорить? Возвращаемся к теме интерфейсов, которая была уже косвенно затронута здесь . Предположим, что мы решили показать свои знания (ум, стамину и сноровку…) сделать объект «дружеским» по отношению к циклу foreach.

Идем протоптанными путями, пользуясь картами MSDN


По этому поводу, заходим на MSDN и читаем, что для реализации необходимо унаследовать объект от IEnumerable (using System.Collections), либо IEnumerable (соответственно using System.Collection.Generic). Итак. Наследуемся. Просим Visual studio реализовать данный интерфейс. Радуемся, что реализовывать необходимо не так уж и много. Но… прр.. что? Надо вернуть IEnumerator.. а что это? Идем опять на MSDN за «советом» и понимаем, что реализовывать придется не один. А целую кучку методов. Более того, необходимо будет создать лишний класс IEnumerator, который будет проходить по циклу, стеку, очереди и т.д.
(пример MSDN)
Ну что же… давайте заведем class SomeClass, зададим ему поле с неким целочисленным значением. А затем заведем класс SomeClassList, который будет хранить List. Затем добавим к SomeClassList наследование от IEnumerable и реализуем данный интерфейс в соответствие со статьей на MSDN.
SomeClass:
  1. public class SomeClass
  2.    {
  3.      int _someValue = 0 ;
  4.      public int SomeValue
  5.      {
  6.        get { return _someValue; }
  7.        set { _someValue = value; }
  8.      }
  9.      public SomeClass( int Value)
  10.      {
  11.         SomeValue = Value;
  12.      }
  13.    }  

This code was highlighted with code.xnim.ru

Теперь SomeValueList с комментариями:
  1. public class SomeValueList:IEnumerable
  2.    {
  3.      //создадим закрытое извне поле
  4.      protected List <SomeClass > _mySomeClassList;
  5.      //конструктор занимается ТОЛЬКО инициализацией List
  6.      public SomeValueList()
  7.      {
  8.         _mySomeClassList = new List <SomeClass >();
  9.      }
  10.      //Метод Add для добавления данных в коллекцию
  11.      public void Add(SomeClass a)
  12.      {
  13.         _mySomeClassList.Add(a);
  14.      }
  15.      //Предполагая, что foreach единственный способ работы с хранимой
  16.      // коллекцией в данном классе, реализуем интерфейс: (в соответствии с примером из MSDN)
  17.      //возвращаем IEnumerator
  18.      IEnumerator IEnumerable.GetEnumerator()
  19.      {
  20.         return (IEnumerator)GetEnumerator();
  21.      }
  22.      //Метод, который вызывается из описанного выше метода (эдакая лесенка)
  23.      public IEnumerator GetEnumerator()
  24.      {
  25.         return new SomeValueListEnum(_mySomeClassList);
  26.      }
  27.    }
  28.    //Теперь создаем класс, который реализует интерфейс IEnumerator
  29.    public class SomeValueListEnum : IEnumerator
  30.    {
  31.      //MSDN рекомендует работать с "public" модификатором списка... почему? не знаю
  32.      public List <SomeClass > _mySomeClassList;
  33.      //"указатель" на текущую позицию в списке. Инициализируется "по умолчанию" -1, для работы с методами, описанными в IEnumerator.
  34.      //(-1 - чтобы индексация массивы проходила с 0 ) Чуть ниже, приведу картинку, поясняющую почему происходит так.
  35.      protected int _position = -1;
  36.      public SomeValueListEnum(List <SomeClass > a)
  37.      {//производим не копирование элементов, а копирование ссылки. Чтобы не увеличивать время обработки.
  38.         _mySomeClassList = a;
  39.      }
  40.      //Дошли до методов IEnumerator.
  41.      //Выдает текущий объект коллекции
  42.      public object Current
  43.      {
  44.        get { return _mySomeClassList[_position]; }
  45.      }
  46.      //переводит позицию на 1 по коллекции. Возвращает true, если не наткнулись на конец массива.
  47.      public bool MoveNext()
  48.      {
  49.         _position++;
  50.         return _position < _mySomeClassList.Count;
  51.      }
  52.      //сброс состояния "указателя" (индексации)
  53.      public void Reset()
  54.      {
  55.         _position = -1;  
  56.      }
  57.    }

This code was highlighted with code.xnim.ru

Получается вот такое нагромождение кода.

Пробуем!


Для тестирования, напишем следующий код: (Он не запустится. Почему объясню после приведения кода)
  1. static void Main(string[] args)
  2.      {
  3.         SomeValueList MyList = new SomeValueList();
  4.        for (int i = 0 ; i < 10 ; i++ )
  5.            MyList.Add(new SomeClass(i*10 ));
  6.         foreach (var x in MyList)
  7.         {
  8.            Console.WriteLine(x.SomeValue);
  9.         }
  10.         
  11.      }

This code was highlighted with code.xnim.ru

Вот незадача! Мы не можем вызвать свойство SomeValue в цикле… Почему? Ответ довольно прост – мы использовали IEnumerable не Generic. А такой интерфейс возвращает тип object. Чтобы исправить данную «ошибку» мы должны сменить цикл foreach, к примеру, на такой:
  1. foreach (SomeClass x in MyList)
  2.         {
  3.            Console.WriteLine(x.SomeValue);
  4.         }

This code was highlighted with code.xnim.ru

Здесь будет происходить приведение к типу SomeClass. Чтобы получить возможность писать цикл с помощью var x in MyClass, необходимо наследовать MyClass от интерфейса IEnumerable, где T необходимый нам класс. (такой интерфейс требует те же реализованные методы. Останавливаться на оном не будем)

Немного о вызовах IEnumerable


Как вообще работает данная реализация? Попробую привести картинку:
При попадании на цикл foreach вызывается метод GetEnumerator, который инициализирует экземпляр класса, реализовавшего интерфейс IEnumerator.
IEnumerator работает следующим образом:
Пока MovaNext = true
Возвращаем пользователю (в тело цикла) «результат свойства» Current (точнее просто Current работает в цикле)
Все просто.

А изящней?


Я в начале поста говорил, что можно будет проще, чем в MSDN. Объясняю как:
Да, действительно. На мой взгляд можно сделать проще, и даже без потери функциональности (как мне кажется)
Итак. Как будем получать преимущество в виде «более изящного кода»? Очень просто: в C# имеет место быть утиная типизация. Воспользуемся ею. Привожу пример «измененного» класса:
  1. public class SomeValueList:IEnumerable
  2.    {
  3.      //создадим закрытое извне поле
  4.      protected List <SomeClass > _mySomeClassList;
  5.      //конструктор занимается ТОЛЬКО инициализацией List
  6.      public SomeValueList()
  7.      {
  8.         _mySomeClassList = new List <SomeClass >();
  9.      }
  10.      //Метод Add для добавления данных в коллекцию
  11.      public void Add(SomeClass a)
  12.      {
  13.         _mySomeClassList.Add(a);
  14.      }
  15.      //с использованием данной особенности, для реализации foreach достаточно только реализации одного метода!
  16.      public IEnumerator GetEnumerator()
  17.      {
  18.        for (int i = 0 ; i < _mySomeClassList.Count; i++)
  19.            yield return _mySomeClassList[i];
  20.      }
  21.    }

This code was highlighted with code.xnim.ru

В данной реализации, благодаря «yield» мы освобождаемся от создания дополнительного класса (который, кстати, могут использовать не только как «для реализации интерфейса», что есть нехорошо)
Но у нас остается маааленькая проблемка – до сих необходимо приводить тип.
Данная проблема решается при использовании Generic. К примеру, следующим образом:
  1. public class SomeValueList:IEnumerable <SomeClass >
  2.    {
  3.      //создадим закрытое извне поле
  4.      protected List <SomeClass > _mySomeClassList;
  5.      //конструктор занимается ТОЛЬКО инициализацией List
  6.      public SomeValueList()
  7.      {
  8.         _mySomeClassList = new List <SomeClass >();
  9.      }
  10.      //Метод Add для добавления данных в коллекцию
  11.      public void Add(SomeClass a)
  12.      {
  13.         _mySomeClassList.Add(a);
  14.      }
  15.      //с использованием данной особенности, для реализации foreach достаточно только реализации одного метода!
  16.      public IEnumerator <SomeClass > GetEnumerator()
  17.      {
  18.        for (int i = 0 ; i < _mySomeClassList.Count; i++)
  19.            yield return _mySomeClassList[i];
  20.      }
  21.      IEnumerator IEnumerable.GetEnumerator()
  22.      {
  23.         return GetEnumerator();
  24.      }
  25.    }

This code was highlighted with code.xnim.ru

Вот таким нехитрым преобразованием мы сократили размер кода, и создали изящное, на мой взгляд, решение задачи.
На этом все =)
2012-11-05