1/3/2008 8:01:10 PM

Очень часто разработчики используют массивы и коллекции. С появлением Generics разработчики стали очень часто использовать типизированные листы (List<T>). Даже для того, чтобы просто сделать поиск одного элемента из множества, или для того, чтобы подготовить набор данных и перебрать его разработчики используют массивы, коллекции и листы.

А ведь между тем интерфейс IEnumerable и его типизированная версия IEnumerable<T> зачастую позволяют решить те же задачи и решить даже более выгодно с различных точек зрения. Кстати, те же массивы, листы и коллекции реализуют этот интерфейс и общаясь с ними мы имеем дело все с тем же IEnumerable, но...

Чем же IEnumerable выгоднее листа в таком случае?

Посмотрим на пару функций (даже пока не обращая внимания на количество строк кода):

C использованием IEnumerable:

public IEnumerable<TrackItem> GetEnumerableItems(IEnumerable<string> itemIds)
{
    foreach (string id in itemIds)
    {
        yield return new TrackItem(id);
    }
}

И с использованием листа:

public List<TrackItem> GetListItems(string[] itemIds)
{
    List<TrackItem> resList = new List<TrackItem>();
    foreach (string id in itemIds)
    {
        resList.Add(new TrackItem(id));
    }
    return resList;
}
 

Здесь функция GetListItems принимая на вход идентификаторы подготавливает для всех этих идентификаторов набор экземпляров типа TrackItem.

В это же время функция GetEnumerableItems не делает с нашими идентификаторами и TrackItem'ами вообще ничего! Она просто возвращает экземпляр енумератора, а создание экземпляров TrackItem будет производиться по ходу движения по енумератору.

Из этого следует три важных замечания:

  1. Никаких затрат на подготовку коллекции к работе.
    Естественно, суммарные затраты для всей коллекции будут те же самые, однако они будут как бы "размазаны" в процессе обработки и нам, возможно, не придется объяснять пользователю, куда деваются драгоценные секунды перед тем, как список появится на экране. Вместо этого он будет видеть в режиме "реального времени", как заполняется этот самый список.
  2. Затраты могут быть существенно ниже в случае использования енумератора.
    Например, в случае поиска экземпляра TrackItem с какими-либо параметрами. Мы закончим перебирать енумератор как только встретим подходящий нам элемент, а вместе с этим закончим и создавать ненужные нам TrackItem'ы. В случае же с листом мы в любом случае создаем полный набор.
  3. Таким образом мы получаем некий вариант отложенного выполнения кода.
    Мы можем безвозмездно передавать полученный енумератор из функции в функцию и, возможно, даже никогда не начнем его енумерировать (в результате работы какой-либо логики это может просто не понадобиться, например, параметр в функцию пришел, а функция обнаружила отсуствие других необходимых для работы условий и т.д.). В этом случае мы вообще не будем иметь потерь, связанных с созданием списка TrackItem'ов, который, к тому же, оказался нам не нужен :)

Далее давайте обратим внимание на то, что в функцию GetListItem я передаю массив идентификаторов, а в функцию GetEnumerableItems - опять же енумератор. Черт его знает, чего нам стоило подготовить этот список. Догадываетесь, к чему клоню? ;)

Передавая набор идентификаторов в виде енумератора я опять же передаю этакую "пустышку" и позволяю себе не тратить время (и другие ресурсы) на заполнение этого списка. Ведь функция GetEnumerableItems может обнаружить, например, отсутствие данных, на основе которых она будет строить TrackItem'ы и вообще, скажем, возбудить исключение. Или, например, исключение может произойти при создании 2-го, 5-го или 50-го элемента. Так зачем мне до этого готовить полный список идентификаторов?! По мере прохода по результату GetEnumerableItems, эта функция каждый раз будет получать из енумератора itemIds следующий идентификатор и "строить" для него TrackItem. И все это прекратится сразу с прекращением енумерирования, не важно уж по какой причине: нашли ли нужный элемент, произошло ли исключение...

Таким образом мы можем как бы комбинировать IEnumerable'ы, создавая лишь сколь угодно длинную цепочку обработки. На создание этой цепочки мы практически не тратим никаких ресурсов и можем без потерь ее передавать куда угодно либо для наращивания ее, либо же уже для обработки.

Еще раз: в случае работы с листами или моссивами мне пришлось бы на каждом этапе готовить полный набор промежуточной информации. То есть, если среди 500 идентификаторов нужный нам окажется 5-м, то в случае листов мы лист из 500 идентификаторов подготовим как минимум, а потом еще и лист из 500 TrackItem'ов, потом вернем его лишь для того, чтобы получатель начал проход по листу и нашел нужное ему на 5-м месте!
В случае же енумератора будет "отработано" лишь 5 идентификаторов и 5 TrackItem'ов, так как потом получатель п��осто прекратил бы проход по листу.

Кроме того, понадобилось бы объяснить пользователю, куда девалось время, пока все это дело готовилось и отдельно на каждом этапе обработать ошибки.

Возможно, работа с IEnumerable вместо тех же листов поначалу и покажется несколько неудобной, как в силу привычки, так и богатого набора методов самого листа. Однако, тот же LINQ сводит на нет эти неудобства.
Кроме того, существует еще замечательная библиотека PowerCollection, разработанная специально для таких целей.
Представим, что нам нужно получить первый встреченный TrackItem с наименованием "Nikon D300", если у нас есть лишь список идентификаторов "ids", среди которых надо искать.

С помощью PowerCollections это может выглядеть так:

private IEnumerable<TrackItem> GetItemsByIds(IEnumerable<string> ids)
{
    return Algorithms.Convert<string, TrackItem>(
        Algorithms.RemoveDuplicates<string>(Algorithms.Sort<string>(ids)),
        delegate(string id) { return new TrackItem(id);
    );
}
public TrackItem GetFirstNikonD300(IEnumerable<string> ids)
{
    Algorithms.FindFirstWhere<TrackItem>(
        GetItemsByIds(ids),
        delegate(TrackItem item) { return item.Title == "Nikon D300"; }
    );
}

Или, для тех, кто использует лямбда-выражения и так же как и я не любит синтаксис делегатов:

private IEnumerable<TrackItem> GetItemsByIds(IEnumerable<string> ids)
{
    return Algorithms.Convert<string, TrackItem>(
        Algorithms.RemoveDuplicates<string>(Algorithms.Sort<string>(ids)),
        id => new TrackItem(id)
    );
}
public TrackItem GetFirstNikonD300(IEnumerable<string> ids)
{
    Algorithms.FindFirstWhere<TrackItem>(
        GetItemsByIds(ids),
        item => item.Title == "Nikon D300"
    );
}

Здесь мы используем функцию GetItemsByIds, передавая ей список идентификаторов. Среди результатов мы ищем то, что нас устраивает. И хорошо, что функция возвращает IEnumerable, а не List :)

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

Итак, если вы обрабатываете коллекции, подготавливаете какие-либо данные к дальнейшему процессингу и собираетесь пользоваться листами, задумайтесь: а так ли необходимо раздувать код, перекладывая данные из одного листа в другой, препарируя их там и только потом отдавая наружу. Не воспользоваться ли для этого интерфейсом IEnumerable<T>?
Принимая в функцию параметр, подумайте, так ли вам критично, чтобы в качестве параметра был передан именно массив или лист? В подавляющем большинстве случаев это далеко не обязательное условие и может быть оставлено на усмотрение программиста, использующего эту самую функцию.
Возвращая значение, подумайте, критично ли возвращать именно лист или массив? Хотели бы вы, чтобы кто-то имел возможность изменять список, который вы возвращаете? Например, взять и удалить там половину элементов - по ошибке? А если это был список каких-то скешированных данных, который вы взяли и выдали наружу? Лучше пусть это будет IEnumerable, а если кому-то нужен лист - он его сам себе сделает и сам же себе и испортит ;)

В конце концов, сделать из IEnumerable<T> какой-нибудь List<T> не составляет никаких проблем, для этого есть соответствующий конструктор. И если в каком-то конкретном случае требуется получить именно лист, а не IEnumerable, то всегда можно воспользоваться им :)

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

Tags:

Comments (5) -

1/6/2008 2:58:37 AM

star

Ох уж эти коды. почему всё так сложно?

star

1/6/2008 11:27:37 AM

Alexey Raga

Что сложно? Где сложно? ;)

Alexey Raga

1/13/2008 4:05:37 PM

kastex

Давно не читал тебя, что то подзабыл... Frown

Но это пост - круто. Реально порадовал. Лучшее, что я за последнее время узнавал.
Ты то откуда с такими "извратими" сталкиваешься?  Просто интересовался или по работе?

Есть у вас в закромах еще такие триксы? ПИШИ!
Интересно еще бы посмотреть как в реальности вы пользуете PowerCollection. Полезная вешь в быту?

Alexander

kastex Russia

1/13/2008 4:35:57 PM

Alexey Raga

"Извраты" эти вовсе и не "извраты" ;)

Сталкиваюсь с ними и сам, и по работе. В частности c IEnumerable просто получается код менее размазанный, более функциональный стиль, к тому же работает это побыстрее и править проще.

С коллекциями все мы сталкиваемся, а PowerCollection используется в реальной работе, конечно. Почему нет? Smile Вещь очень полезная. В том смысле, что там ничего "магического" и сверхсложного не делается, например, сортировку IEnumerable или, там, конвертацию одного типа в другой ты и сам сможешь легко сделать, но уже ведь сделано. Сам Рихтер руку приложил (винтеллектовская поделка) ;)
Algorithms там, в частности, сделан, как было сказано, "для программистов С++, которые привыкли к алгоритмам, которых нет во Framework". Код PowerCollections открыт на CodePlex, если что. Можно свои дописывать, если нужно.

Alexey Raga

6/30/2008 11:47:14 PM

AI_

Забавное наблюдение. К счастью я всегда реализовывал в своих контейнерах IEnumerable и не искал дешевых альтернатив. Набравшись горького опыта на С++, души не чаю в концепции интерфейсов.
Но вот что мне не понятно, так это причина, по которой доступ к IEnumerator'у происходит посредственно. Никак не возьму в толк, зачем нужно заворачивать инумератор таким тонким интерфейсом инумерэйбл.
Растолкуйте пожалуйста.

AI_

Comments are closed

Powered by BlogEngine.NET 2.5.0.6

About the author

Alexey Raga Alexey Raga
.NET software developer.

E-mail me Send mail

Twitter


Recent posts

Archive

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2012

Sign in