Как стать автором
Обновить

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

Время на прочтение6 мин
Количество просмотров2.8K
Автор оригинала: Bart De Smet
Обещаю, что в этот раз будет короткая статья (относительно). Все вы знаете языковую конструкцию 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
Комментарии25

Публикации

Изменить настройки темы

Истории

Работа

.NET разработчик
72 вакансии

Ближайшие события