.NET

индекс
121,03

Возвращаясь к конструкции foreach с Duck Typing для LINQ

Обещаю, что в этот раз будет короткая статья (относительно). Все вы знаете языковую конструкцию foreach в C#, не так ли? Но подумайте дважды прежде чем сказать как именно работает следующий код:
  1. foreach (int x in src)
  2. {
  3.   // Do something with x.
  4. }
* This source code was highlighted with Source Code Highlighter.

Уже знаете ответ? Позвольте мне разочаровать вас: если у вас только один ответ, то вы ошибаетесь. Нет единственного ответа на поставленный вопрос, поскольку вы должны знать больше о типе переменной src чтобы принять окончательное решение насчет того, как вышеприведенный код работает…

Очевидно, вы, должно быть, скажете, что объект должен реализовывать IEnumerable или IEnumerable<T> и, может быть, вы даже упомянете, что в первом случае компилятор приводит тип за вас когда получает значение «x», вызывая свойство IEnumerator.Current. Другими словами, вы преобразуете код в нечто вроде этого:
  1. var e = src.GetEnumerator();
  2. while (e.MoveNext())
  3. {
  4.   var x = (int)e.Current; // without the cast if src was an IEnumerable<T>
  5.   // Do something with x.
  6. }
* This source code was highlighted with Source Code Highlighter.

Достойная попытка, но не совсем верная. Прежде всего, переменная x объявлена во внешней зоне видимости (что причиняет некоторые неприятности, если говорить о замыканиях, но сейчас у нас совсем другая тема...). Во-вторых, перечислитель может реализовывать IDisposable, и в этом случае конструкция foreach обеспечивает корректное высвобождение а ля “using”:
  1. {
  2.   int x;
  3.  
  4.   using (var e = src.GetEnumerator())
  5.   {
  6.     while (e.MoveNext())
  7.     {
  8.       x = (int)e.Current; // without the cast if src was an IEnumerable<T>
  9.       // Do something with x.
  10.     }
  11.   }
  12. }
* This source code was highlighted with Source Code Highlighter.

Это уже более разумно, но мы пропустили другой тип источника, с которым может работать foreach: это любой объект, до тех пор, пока он предоставляет шаблон перечисления GetEnumerator в тандеме с MoveNext и Current. Вот для примера объект, который просто замечательно работает с конструкцией foreach.
  1. class Source
  2. {
  3.   public SourceEnumerator GetEnumerator()
  4.   {
  5.     return new SourceEnumerator();
  6.   }
  7. }
  8.  
  9. class SourceEnumerator
  10. {
  11.   private Random rand = new Random();
  12.  
  13.   public bool MoveNext()
  14.   {
  15.     return rand.Next(100) != 0;
  16.   }
  17.  
  18.   public int Current
  19.   {
  20.     get
  21.     {
  22.       return rand.Next(100);
  23.     }
  24.   }
  25. }
* This source code was highlighted with Source Code Highlighter.

Как это используется, показано ниже:
  1. foreach (int x in new Source())
  2.   Console.WriteLine(x);
* This source code was highlighted with Source Code Highlighter.

Ok, гибко, не правда ли? В самом деле, можно сказать, что в конструкции foreach утиная типизация: имеет значение не номинальный тип (т.е. когда Source явно объявлен как IEnumerable и SourceEnumerator как IEnumerator), а лишь структура объекта, которая и определяет «совместимость» с конструкцией foreach.

Но кто сказал, что foreach над коллекцией сразу начинает думать о LINQ? Допустим, класс Source используется вот так:
  1. List<int> res = new List<int>();
  2. foreach (int x in new Source())
  3.   if (x % 2 == 0)
  4.     res.Add(x);
* This source code was highlighted with Source Code Highlighter.

Выглядит как прекрасный кандидат для LINQ, особенно, если бы мы начали добавлять все больше и больше логики в наш «запрос». Ничего удивительно в таком заключении, но в реальности, к сожалению, это падает и не компилируется:



Почему? Потому что в LINQ статическая типизация (update: в этом месте автор просит прочитать комментарии к его статье и соглашается с тем, что более точным было бы в данном случае говорить о LINQ to Objects), так что LINQ ожидает, что я сошлюсь на номинальную имплементацию перечислителя: на что-то, что явно определено как IEnumerable, а не на что-то, что «случайно» оказалось похожим на IEnumerable. Вопрос дня: как преобразовать существующий структурный перечислитель в номинальный так, чтобы его можно было использовать с LINQ? Конечно, мы можем написать специальный код для объекта Source, который создаст необходимый итератор из Source:
  1. static void Main()
  2. {
  3.   var res = from x in IterateOver(new Source())
  4.        where x % 2 == 0
  5.        select x;
  6.  
  7.   foreach (var x in res)
  8.     Console.WriteLine(x);
  9. }
  10.  
  11. static IEnumerable<int> IterateOver(Source s)
  12. {
  13.   foreach (int i in s)
  14.     yield return i;
  15. }
* This source code was highlighted with Source Code Highlighter.

Но быть может вы в такой ситуации, когда вокруг целое изобилие таких структурных перечислителей (например, некоторые библиотеки автоматизации Office предоставляют GetEnumerator в типах вроде Range, в то время как тип Range не реализует интерфейс IEnumerable, следовательно, он не подходит для использования с LINQ), так что вы хотите обобщить вышеприведенное решение. По сути нам нужна возможность надстроить над любым объектом итератор с утиной типизацией и это подходящая задача для расщиряющего метода и ключевого слова dynamic из C# 4.0:
  1. static class DuckEnumerable
  2. {
  3.   public static IEnumerable<T> AsDuckEnumerable<T>(this object source)
  4.   {
  5.     dynamic src = source;
  6.  
  7.     var e = src.GetEnumerator();
  8.     try
  9.     {
  10.       while (e.MoveNext())
  11.         yield return e.Current;
  12.     }
  13.     finally
  14.     {
  15.       var d = e as IDisposable;
  16.       if (d != null)
  17.       {
  18.         d.Dispose();
  19.       }
  20.     }
  21.   }
  22. }
* This source code was highlighted with Source Code Highlighter.

Вопрос к читателю: почему мы не можем просто написать цикл foreach над «объектом, который приведен к dynamic»? Подсказка: как тогда вы реализуете перевод конструкции foreach в dynamic-объекте?

Да, вы нагромоздите необходимый список методов на System.Object, так что будьте осторожны с использованием этого или же просто используйте вызов старого плоского метода, чтобы «перевести» структурное в номинальное. Обратите внимание каким легким выглядит динамически типизированный код в C# 4.0. С большим количеством приведений типов это выглядит примерно так:
  1. static class DuckEnumerable
  2. {
  3.   public static IEnumerable<T> AsDuckEnumerable<T>(this object source)
  4.   {
  5.     dynamic src = (dynamic)source;
  6.  
  7.     dynamic e = src.GetEnumerator();
  8.     try
  9.     {
  10.       while ((bool)e.MoveNext())
  11.         yield return (T)e.Current;
  12.     }
  13.     finally
  14.     {
  15.       var d = e as IDisposable;
  16.       if (d != null)
  17.       {
  18.         d.Dispose();
  19.       }
  20.     }
  21.   }
  22. }
* This source code was highlighted with Source Code Highlighter.

И теперь мы можем написать так:
  1. var res = from x in new Source().AsDuckEnumerable<int>()
  2.      where x % 2 == 0
  3.      select x;
  4.  
  5. foreach (var x in res)
  6.   Console.WriteLine(x);
* This source code was highlighted with Source Code Highlighter.

Динамический клей – почему бы нет? Фактически, даже объекты из других языков (как Ruby или Python), которые следуют парадигме утиной типизации теперь работают с LINQ, и для существующих совместимых объектов вызов оператора безвреден (но расточителен). Ох, и обратите внимание, что вы можете также иметь IEnumerable в «динамических» объектах, если вы имеете дело с объектами из динамических языков…

Можете ли вы реализовать метод AsDuckEnumerable в C# 3.0? Конечно, если вы ограничите себя методами основанными на рефлексии (оставлено в качестве упражнения для читателя).

Наслаждайтесь!
_________
Текст подготовлен в ХабраРедакторе
+22
1 сентября 2009, 04:54
12

комментарии (23)

–14
danmiru #
Мне, как человеку далекому (слава Богу!) от .net, ужасно непонятно, как из foreach можно написать целую статью, причем так, что ее осилить-то можно с трудом.
Или я чего-то не понимаю, или это все overcomplicated.
Если б я писал на .net, я бы использовал более простые конструкции без такого количества подводных камней. А если для того, чтобы использовать foreach вместо for надо знать 50 страниц мануала — а не ну ли его нафиг?
Имхо очевидность и легкочитаемость кода важнее, чем удобство его написания.
+6
pieceofsummer #
Статья не столько о foreach, сколько о Duck Typing в LINQ.

А тот факт, что foreach поддерживает утипизацию, в основном так и остается лишь «любопытным фактом» и в отрыве от IEnumerable используется крайне редко.
+5
centur #
Вы просто «чего-то не понимаете». Статья показывает особенности реализации foreach, который сделан через некоторый грязный хак, и поясняет, почему может возникать ошибка, когда в процессе рефакторинга foreach заменяется на LINQ выражение.
Также показывается как можно обойти эту ошибку и как вообще добавить «истиный» duck typing в программу. Это уже на мой взгляд лишнее ( вернее информация которая нужна будет только если захотите использовать). А вот про хак foreach знать надо.

В конце концов все такие статьи объясняют принципы внутренних механизмов. Это как с автомобилем — можно купить права и жать на две педали, а можно разбираться в принципах работы ДВС, тормозной системы, электронного управления в авто и прочих деталях.

Жаль только что знание этого в отношении автомобиля не позволяет ездить быстрей, маневренней и менее аварийно. А в языках программирования — позволяет.
–1
dborovikov #
>Жаль только что знание этого в отношении автомобиля не позволяет ездить быстрей, маневренней и менее аварийно. А в языках программирования — позволяет.

Извините, а о каких задачах идет речь? Не для кого ни сикрет какого плана разрабатываются приложения на java и C#. Тут уже пробегали примеры кода govnokod.ru/1071

В сложном приложении сложно уйти от таких перлов. Представляете, что если в такой треш намешать еще и linq и лямбды и вообще все возможности C#?

Дискламер. Я не против развития языков и всяких фичей. Я и сам любитель всяких таких штук. Но я правда не понимаю куда это можно реально вкрутить. Точнее что за реальные такие задачи могут быть.
+3
romaklimenko #
ИМХО, в этой статье интересно не столько то, куда эту фичу можно вкрутить прямо сейчас, а именно подробности конкретной реализации конкретного языка.

Я несколько раз сталкивался с утиной типизацией foreach, но никогда руки не доходили разобраться и провести границу — так где же так типизировать можно, а где нельзя. А тут человек разобрал проблему и поигрался с кодом, наглядно показав как это работает. Именно поэтому статья и показалась мне интересной для перевода.
+7
mezastel #
> Не для кого ни сикрет какого плана разрабатываются приложения на java и C#.

Плохо можно писать на любом языке.
–1
dborovikov #
>Плохо можно писать на любом языке.

Не бывает «хорошего» интерпрайз-кода, таковы изменчивые требования бизнеса. Я сам java кодер. В свободное от работы время ковыряюсь с Haskell и Scheme. Так что все эти языковые фишки меня уже не приводят в щенячий восторг как раньше=) Но я так и не придумал реальных задач для всех этих крутых приемов. А дергалку БД я и на отсталой жаве налабаю.
+1
mezastel #
Правильная архитектура и дополнительные уровни абстракции позволяют делать неплохой enterprise-код. Не идеальный конечно, но идеального ничего не бывает — всегда можно сделать лучше.
+2
romaklimenko #
Нет ничего военного ни в generic, ни в делегатах и ивентах, ни в лямдах, ни в extension methods, ни в linq, ни в dynamic. Можно от всего этого отказаться и писать на сишарпе первой версии, надеясь что качество кода станет лучше, но я бы не стал. Многие возможности сишарпа, которые еще недавно вызывали много скепсиса, стали привычными настолько, что когда пишешь на той же джаве, всего этого добра здорово не хватает, как не хватает, например, решарпера в обычной студии.
+4
A1lfeG #
Зато очень быстро начинает не хватать терпения когда решарпер начинает отжирать гигабайты оперативы :(
А так да, вы правы.
+1
centur #
Это не нужно «реально вкручивать», знание этой особенности позволит не допускать глупых ошибок. Например полагать что все коллекции вокруг которых работает foreach подходят как источники для linq — наивно. Пример это демонстрирует.

Добавление query comprehension сильно улучшает читабельность кода, с этим даже спорить не стоит. В примере показывается как можно добавить эту фичу для источников, которые не IEnumerable. Задача рефакторинга для вас реальная? Устроит ответ?
0
dborovikov #
>Задача рефакторинга для вас реальная?

Вполне. Но о читабельности кода стоит говорить когда есть какие-то гарантии его корректности=) Лично для меня это более интересный вопрос. А так да, очень забавный этот Linq, и статья интересная.
+1
centur #
немного не понял — вы утверждаете что скомпилированное query comprehension может быть некорректным или что?
0
dborovikov #
Я имел ввиду другое то, что «рюшечки» дело последнее, хоть и не менее важное чем все остальные. А вот решение первостепенных задач на .NET встретишь реже чем очередную красоту в коде. Никак ловкий маркетинговый ход MS=)
0
centur #
не знаю почему для вас читабельность кода это рюшечки — ведь даже в решениях первостепенных задач код должен быть читабельным.
А какие конкретно первостепенные для вас задачи на дотнете труднорешаемы, что вы так отдельно это выделяете?

PS читающие — не минусуйте без повода товарища, вроде диалог идет. а то уподобляться будем толпе…
0
dborovikov #
>А какие конкретно первостепенные для вас задачи на дотнете труднорешаемы

А тут не в дотнете дело. Вообще прикладухи. То, что я читаю в книжках/блоках это прям утопия какая-то. А то, что я вижу на практике это примерно вот: govnokod.ru/1071. И как не крутись любой более-менее сложный проект обрастает таким ужасом. Скажите кривая архитектура. А где же она не кривая? Я вообще ни разу не видел в реальном проекте реализацию бизнес-логики с помощью модели предметной области: martinfowler.com/eaaCatalog/domainModel.html. Сплошная процедурщина, это в ОО языках то! Может где-то и есть такие умные люди у которых все круто, красиво и объектно и потраченный бюджет при этом не стремится к бесконечности, но я лично таких не встречал. Неужели и я и все вокруг как назло глупые?
+1
hansi #
Идеальных проектов не бывает, но все зависит от людей: по работе знаю примеры, когда грамотный архитектор и хорошие программисты создают решения, на которые приятно глянуть, код, который не страшно читать, Edge-технологии как ASP.NET MVC, jQuery, nHibernate грамотно заюзаны. Возможно, у Вас просто был негативный опыт, поэтому и сложилось такое мнение. Да, это не так часто встречаются хорошо сделанные проекты, но это реалии бизнеса, большинству заказчиков лучше платить дешевле за труд низкоквалифицированных спецов из Индии.
0
dborovikov #
>Возможно, у Вас просто был негативный опыт,

Буду надеяться, что так.
0
centur #
Видать сильно зацепил тот пример, или это вы постили?
Может вы просто не туда смотрите (или не там работаете)? Откройте например код того же community server — domain model. Любая генерация linq2sql по правильно задизайненной бд — тоже почти domain model.
У нас в компании, например, вообще «ректальные кары» за попытки писать бизлогику и общение с бд через что-либо, кроме domain model.
Многие открытые продукты с очень даже неплохой доменной моделью.
0
dborovikov #
>Видать сильно зацепил тот пример, или это вы постили?

Пример не мой, просто увидел родные для взора приемы программирования=)

>Может вы просто не туда смотрите (или не там работаете)?

Может быть. Но другого я пока не нашел.

>Многие открытые продукты с очень даже неплохой доменной моделью.

О, был бы очень признателен, если накидаете ссылочек.
+1
RomanNikitin #
пожалуйста, пользуйтесь.
Это так сказать «джедайские» техники.
foreach вообще очень интересный оператор и о нем действительно можно много и долго разговаривать (в какой ситуации во что он скомпилируется и какую производительность будет иметь) — и мне кажется, что это здорово.
–1
Ai_boy #
Нубство — это плохо…
0
Niraxoid #
Спасибо, весьма занятная статья.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.