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

Original author: Bart De Smet
  • Translation
Обещаю, что в этот раз будет короткая статья (относительно). Все вы знаете языковую конструкцию 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? Конечно, если вы ограничите себя методами основанными на рефлексии (оставлено в качестве упражнения для читателя).

Наслаждайтесь!

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 23

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

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

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

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

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

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

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

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

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

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

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

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

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

                            А тут не в дотнете дело. Вообще прикладухи. То, что я читаю в книжках/блоках это прям утопия какая-то. А то, что я вижу на практике это примерно вот: govnokod.ru/1071. И как не крутись любой более-менее сложный проект обрастает таким ужасом. Скажите кривая архитектура. А где же она не кривая? Я вообще ни разу не видел в реальном проекте реализацию бизнес-логики с помощью модели предметной области: martinfowler.com/eaaCatalog/domainModel.html. Сплошная процедурщина, это в ОО языках то! Может где-то и есть такие умные люди у которых все круто, красиво и объектно и потраченный бюджет при этом не стремится к бесконечности, но я лично таких не встречал. Неужели и я и все вокруг как назло глупые?
                            • UFO just landed and posted this here
                                0
                                >Возможно, у Вас просто был негативный опыт,

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

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

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

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

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

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

                    Only users with full accounts can post comments. Log in, please.