Язык программирования C#9 и платформа .NET5 - Эндрю Троелсен
Шрифт:
Интервал:
Закладка:
ReflectOverQueryResults(subset);
// Вывести результаты.
foreach (string s in subset)
{
Console.WriteLine("Item: {0}", s);
}
Запустив приложение, легко заметить, что переменная subset в действительности представляет собой экземпляр обобщенного типа OrderedEnumerable<TElement, ТКеу> (представленного в коде CIL как OrderedEnumerable`2), который является внутренним абстрактным типом, находящимся в сборке System.Linq.dll:
***** Info about your query using Query Expressions*****
resultSet is of type: OrderedEnumerable`2
resultSet location: System.Linq
Внесите такое же изменение в код метода QueryOverStringsWithExtensionMethods(), но передав во втором параметре строку "Extension Methods":
// Построить выражение запроса для поиска
// в массиве элементов, содержащих пробел.
IEnumerable<string> subset = currentVideoGames
.Where(g => g.Contains(" ")) .OrderBy(g => g).Select(g => g);
ReflectOverQueryResults(subset,"Extension Methods");
// Вывести результаты.
foreach (string s in subset)
{
Console.WriteLine("Item: {0}", s);
}
После запуска приложения выяснится, что переменная subset является экземпляром типа SelectIPartitionIterator. Но если удалить из запроса конструкцию Select(g=>g), то subset снова станет экземпляром типа OrderedEnumerable<TElement, ТКеу>. Что все это значит? Для подавляющего большинства разработчиков немногое (если вообще что-либо). Оба типа являются производными от IEnumerable<T>, проход по ним осуществляется одинаковым образом и они оба способны создавать список или массив своих значений.
***** Info about your query using Extension Methods *****
resultSet is of type: SelectIPartitionIterator`2
resultSet location: System.Linq
LINQ и неявно типизированные локальные переменные
Хотя в приведенной программе относительно легко выяснить, что результирующий набор может быть интерпретирован как перечисление объектов string (например, IEnumerable<string>), тот факт, что подмножество на самом деле имеет тип OrderedEnumerable<TElement, ТКеу>, не настолько ясен.
Поскольку результирующие наборы LINQ могут быть представлены с применением порядочного количества типов из разнообразных пространств имен LINQ, было бы утомительно определять подходящий тип для хранения результирующего набора. Причина в том, что во многих случаях лежащий в основе тип не очевиден и даже напрямую не доступен в коде (и как вы увидите, в ряде ситуаций тип генерируется на этапе компиляции).
Чтобы еще больше подчеркнуть данное обстоятельство, ниже показан дополнительный вспомогательный метод, определенный внутри класса Program:
static void QueryOverInts()
{
int[] numbers = {10, 20, 30, 40, 1, 2, 3, 8};
// Вывести только элементы меньше 10.
IEnumerable<int> subset = from i in numbers where i < 10 select i;
foreach (int i in subset)
{
Console.WriteLine("Item: {0}", i);
}
ReflectOverQueryResults(subset);
}
В рассматриваемом случае переменная subset имеет совершенно другой внутренний тип. На этот раз тип, реализующий интерфейс IEnumerable<int>, представляет собой низкоуровневый класс по имени WhereArrayIterator<T>:
Item: 1
Item: 2
Item: 3
Item: 8
***** Info about your query *****
resultSet is of type: WhereArrayIterator`1
resultSet location: System.Linq
Учитывая, что точный тип запроса LINQ не вполне очевиден, в первых примерах результаты запросов были представлены как переменная IEnumerable<T>, где Т — тип данных в возвращенной последовательности (string, int и т.д.). Тем не менее, ситуация по-прежнему довольно запутана. Чтобы еще больше все усложнить, стоит упомянуть, что поскольку интерфейс IEnumerable<T> расширяет необобщенный IEnumerable, получать результат запроса LINQ допускается и так:
System.Collections.IEnumerable subset =
from i in numbers
where i < 10
select i;
К счастью, неявная типизация при работе с запросами LINQ значительно проясняет картину:
static void QueryOverInts()
{
int[] numbers = {10, 20, 30, 40, 1, 2, 3, 8};
// Здесь используется неявная типизация...
var subset = from i in numbers where i < 10 select i;
// ...и здесь тоже.
foreach (var i in subset)
{
Console.WriteLine("Item: {0} ", i);
}
ReflectOverQueryResults(subset);
}
В качестве эмпирического правила: при захвате результатов запроса LINQ всегда необходимо использовать неявную типизацию. Однако помните, что (в большинстве случаев) действительное возвращаемое значение имеет тип, реализующий интерфейс IEnumerable<T>.
Какой точно тип кроется за ним (OrderedEnumerable<TElement, ТКеу>, WhereArrayIterator<T> и т.п.), к делу не относится, и определять его вовсе не обязательно. Как было показано в предыдущем примере кода, для прохода по извлеченным данным можно просто применить ключевое слово var внутри конструкции foreach.
LINQ и расширяющие методы
Несмотря на то что в текущем примере совершенно не требуется напрямую писать какие-то расширяющие методы, на самом деле они благополучно используются на заднем плане. Выражения запросов LINQ могут применяться для прохода по содержимому контейнеров данных, которые реализуют обобщенный интерфейс IEnumerable<T>. Тем не менее, класс System.Array (используемый для представления массива строк и массива целых чисел) не реализует этот контракт:
// Похоже, что тип System.Array не реализует
// корректную инфраструктуру для выражений запросов!
public abstract class Array : ICloneable, IList,
IStructuralComparable, IStructuralEquatable
{
...
}
Хотя класс System.Array не реализует напрямую интерфейс IEnumerable<T>, он косвенно получает необходимую функциональность данного типа (а также многие другие члены, связанные с LINQ) через статический тип класса System.Linq.Enumerable.
В служебном классе System.Linq.Enumerable определено множество обобщенных расширяющих методов (таких как Aggregate<T>(), First<T>(), Мах<Т>() и т.д.), которые класс System.Array (и другие типы) получают в свое распоряжение на заднем плане. Таким образом, если вы примените операцию точки к локальной переменной currentVideoGames, то обнаружите большое количество членов, которые отсутствуют в формальном определении System.Array.
Роль отложенного выполнения
Еще один важный момент, касающийся выражений запросов LINQ, заключается в том, что фактически они не оцениваются до тех пор, пока не начнется итерация по результирующей последовательности. Формально это называется отложенным выполнением. Преимущество такого подхода связано с возможностью применения одного и того же запроса LINQ многократно к тому же самому контейнеру и полной гарантией получения актуальных результатов. Взгляните