Без сомнения, LINQ относится к одним из самых инте ресных средств языка С#. Эти средства были внедрены в версии C# 3.0 и явились едва ли не самым главным его дополнением, которое состояло не только во внесении совершенно нового элемента в синтаксис С#, добавлении нескольких ключевых слов и предоставлении больших воз можностей, но и в значительном расширении рамок дан ного языка программирования и круга задач, которые он позволяет решать. Проще говоря, внедрение LINQ стало поворотным моментом в истории развития С#.
Аббревиатура LINQ означает Language-Integrated Query, т.е. язык интегрированных запросов. Это понятие охватывает ряд средств, позволяющих извлекать информацию из ис точника данных. Как вам должно быть известно, извлечение данных составляет важную часть многих программ. Напри мер, программа может получать информацию из списка заказчиков, искать информацию в каталоге продукции или получать доступ к учетному документу, заведенному на ра ботника. Как правило, такая информация хранится в базе данных, существующей отдельно от приложения. Так, ката лог продукции может храниться в реляционной базе дан ных. В прошлом для взаимодействия с такой базой данных приходилось формировать запросы на языке структуриро ванных запросов (SQL). А для доступа к другим источникам данных, например в формате XML, требовался отдельный подход. Следовательно, до версии 3.0 поддержка подобных запросов в C# отсутствовала. Но это положение изменилось после внедрения LINQ.
LINQ дополняет C# средствами, позволяющими форми ровать запросы для любого LINQ-совместимого источника данных. При этом синтаксис, используемый для формирования запросов, остается не изменным, независимо от типа источника данных. Это, в частности, означает, что син таксис, требующийся для формирования запроса к реляционной базе данных, прак тически ничем не отличается от синтаксиса запроса данных, хранящихся в массиве. Для этой цели теперь не нужно прибегать к средствам SQL или другого внешнего по отношению к C# механизма извлечения данных из источника. Возможности формиро вать запросы отныне полностью интегрированы в язык С#.
Помимо SQL, LINQ можно использовать вместе с XML-файлами и наборами дан ных ADO.NET Dataset. Не менее важным является применение LINQ вместе с масси вами и коллекциями в C# (подробнее рассматриваемыми в главе 25). Таким образом, средства LINQ предоставляют, в целом, единообразный доступ к данным. И хотя такой принцип уже сам по себе является весьма эффективным и новаторским, преимуще ства LINQ этим не ограничиваются. LINQ предлагает осмыслить иначе и подойти по- другому к решению многих видов задач программирования, помимо традиционной организации доступа к базам данных. И в конечном итоге многие решения могут быть выработаны на основе LINQ.
LINQ поддерживается целым рядом взаимосвязанных средств, включая внедренный в C# синтаксис запросов, лямбда-выражения, анонимные типы и методы расширения. О лямбда-выражениях речь уже шла в главе 15, а остальные средства рассматриваются в этой главе.
ПРИМЕЧАНИЕ LINQ в C# — это, по сути, язык в языке. Поэтому предмет рассмотрения LINQ довольно обширен и включает в себя многие средства, возможности и альтернативы. Несмотря на то что в этой главе дается подробное описание средств LINQ, рассмотреть здесь все их воз можности, особенности и области применения просто невозможно. Для этого потребовалась бы отдельная книга. В связи с этим в настоящей главе основное внимание уделяется глав ным элементам LINQ, применение которых демонстрируется на многочисленных примерах. А в долгосрочной перспективе LINQ представляет собой подсистему, которую придется изу чать самостоятельно и достаточно подробно.
В основу LINQ положено понятие запроса, в котором определяется информация, получаемая из источника данных. Например, запрос списка рассылки почтовых со общений заказчикам может потребовать предоставления адресов всех заказчиков, проживающих в конкретном городе; запрос базы данных товарных запасов — список товаров, запасы которых исчерпались на складе; а запрос журнала, регистрирующего интенсивность использования Интерента, — список наиболее часто посещаемых веб сайтов. И хотя все эти запросы отличаются в деталях, их можно выразить, используя одни и те же синтаксические элементы LINQ.
Как только запрос будет сформирован, его можно выполнить. Это делается, в част ности, в цикле foreach. В результате выполнения запроса выводятся его результаты. Поэтому использование запроса может быть разделено на две главные стадии. На пер вой стадии запрос формируется, а на второй — выполняется. Таким образом, при фор мировании запроса определяется, что именно следует извлечь из источника данных. А при выполнении запроса выводятся конкретные результаты.
Для обращения к источнику данных по запросу, сформированному средствами LINQ, в этом источнике должен быть реализован интерфейс IEnumerable. Он име ет две формы: обобщенную и необобщенную. Как правило, работать с источником данных легче, если в нем реализуется обобщенная форма IEnumerable, где Т обо значает обобщенный тип перечисляемых данных. Здесь и далее предполагается, что в источнике данных реализуется форма интерфейса IEnumerable. Этот интерфейс объявляется в пространстве имен System.Collections.Generic. Класс, в котором реализуется форма интерфейса IEnumerable, поддерживает перечисление, а это означает, что его содержимое может быть получено по очереди или в определенном порядке. Форма интерфейса IEnumerable поддерживается всеми массивами в С#. Поэтому на примере массивов можно наглядно продемонстрировать основные прин ципы работы LINQ. Следует, однако, иметь в виду, что применение LINQ не ограни чивается одними массивами.
А теперь самое время обратиться к простому примеру использования LINQ. В при веденной ниже программе используется запрос для получения положительных значе ний, содержащихся в массиве целых значений.
// Сформировать простой запрос LINQ.
using System;
using System.Linq;
class SimpQuery {
static void Main() {
int[] nums = { 1, -2, 3, 0, -4, 5 };
// Сформировать простой запрос на получение только положительных значений.
var posNums = from n in nums
where n > 0
select n;
Console.Write("Положительные значения из массива nums: ");
// Выполнить запрос и отобразить его результаты.
foreach(int i in posNums) Console.Write(i + " ");
Console.WriteLine();
}
}
Эта программа дает следующий результат.
Положительные значения из массива nums: 1 3 5
Как видите, в конечном итоге отображаются только положительные значения, хра нящиеся в массиве nums. Несмотря на всю свою простоту, этот пример наглядно де монстрирует основные возможности LINQ. Поэтому рассмотрим его более подробно.
Прежде всего обратите внимание на применение в данном примере программы следующего оператора.
using System.Linq;
Для применения средств LINQ в исходный текст программы следует включить про странство имен System.Linq.
Затем в программе объявляется массив nums типа int. Все массивы в C# неявным образом преобразуются в форму интерфейса IEnumerable. Благодаря этому лю бой массив в C# может служить в качестве источника данных, извлекаемых по запросу LINQ.
Далее объявляется запрос, по которому из массива nums извлекаются элементы только с положительными значениями.
var posNums = from n in nums
where n > 0
select n;
Переменная posNums называется переменной запроса. В ней хранится ссылка на ряд правил, определяемых в запросе. Обратите внимание на применение ключевого слова var для объявления переменной posNums неявным образом. Как вам должно быть уже известно, благодаря этому переменная posNums становится неявно типизирован ной. Такими переменными удобно пользоваться в запросах, хотя их тип можно объя вить и явным образом (это должна быть одна из форм интерфейса IEnumerable). Объявляемой переменной posNums в итоге присваивается выражение запроса.
Все запросы начинаются с оператора from, определяющего два элемента. Первым из них является переменная диапазона, принимающая элементы из источника данных. В рассматриваемом здесь примере эту роль выполняет переменная n. Вторым элемен том является источник данных (в данном случае — массив nums). Тип переменной диа пазона выводится из источника данных. Поэтому переменная n относится к типу int. Ниже приведена общая форма оператора from.
from переменная_диапазона in источник_данных
Далее следует оператор where, обозначающий условие, которому должен удо влетворять элемент в источнике данных, чтобы его можно было получить по запросу. Ниже приведена общая форма синтаксиса оператора where.
where булево_выражение
В этой форме булево_выражение должно давать результат типа bool. Такое вы ражение иначе называется предикатом. В запросе можно указывать несколько операто ров where. В данном примере программы используется следующий оператор where.
where n > 0
Этот оператор будет давать истинный результат только для тех элементов массива, значения которых оказываются больше нуля. Выражение n > 0 будет вычисляться для каждого из n элементов массива n при выполнении запроса. В итоге будут по лучены только те значения, которые удовлетворяют этому условию. Иными словами, оператор where выполняет роль своеобразного фильтра, отбирая лишь определенные элементы.
Все запросы оканчиваются оператором select или group. В данном примере используется оператор select, точно определяющий, что именно должно быть по лучено по запросу. В таких простых примерах запросов, как рассматриваемый здесь, выбирается конкретное значение диапазона. Поэтому по данному запросу возвраща ются только те целые значения, которые удовлетворяют условию, указанному в опера торе where. В более сложных запросах можно дополнительно уточнять, что именно следует выбирать. Например, по запросу списка рассылки может быть получена лишь фамилия адресата вместо его полного адреса. Обратите внимание на то, что оператор select завершается точкой с запятой, поскольку это последний оператор в запросе. А другие его операторы не оканчиваются точкой с запятой.
Итак, переменная запроса posNums создана, но результаты запроса пока еще не получены. Дело в том, что сам запрос определяет лишь ряд конкретных правил, а ре зультаты будут только после выполнения запроса. Кроме того, один и тот же запрос может быть выполнен два раза или больше, причем с разными результатами, если в промежутке между последовательно производимыми попытками выполнить один и тот же запрос изменяется базовый источник данных. Поэтому одного лишь объяв ления переменной запроса posNums совершенно недостаточно для того, чтобы она со держала результаты запроса.
Для выполнения запроса в данном примере программы организуется следующий цикл.
foreach(int i in posNums) Console.WriteLine(i + " ");
В этом цикле переменная posNums указывается в качестве коллекции, к которой происходит обращение на каждом шаге цикла. В цикле foreach соблюдаются прави ла, определенные в запросе и доступные по ссылке из переменной posNums. На каж дом шаге цикла возвращается очередной элемент, полученный из массива. Этот про цесс завершается, когда запрашиваемых элементов в массиве больше не обнаружено. В данном примере тип int переменной шага цикла i указывается явно, поскольку по запросу извлекаются элементы именно этого типа. Явное указание типа переменной шага цикла вполне допустимо в тех случаях, когда заранее известен тип значения, вы бираемого по запросу. Но в более сложных случаях оказывается проще, а иногда даже нужно, указывать тип переменной шага цикла неявным образом с помощью ключево го слова var.
Итак, в запросе определяются правила, по которым извлекаются данные, но этого явно недостаточно для получения результатов, поскольку запрос должен быть выпол нен, причем это может быть сделано несколько раз. Если же в промежутке между по следовательно производимыми попытками выполнить один и тот же запрос источник данных изменяется, то получаемые результаты могут отличаться. Поэтому как только запрос определен, его выполнение будет всегда давать только самые последние резуль таты. Обратимся к конкретному примеру. Ниже приведен другой вариант рассматри ваемой здесь программы, где содержимое массива nums изменяется в промежутке между двумя последовательно производимыми попытками выполнить один и тот же запрос, хранящийся в переменной posNums.
// Сформировать простой запрос.
using System;
using System.Linq;
using System.Collections.Generic;
class SimpQuery {
static void Main() {
int[] nums = { 1, -2, 3, 0, -4, 5 };
// Сформировать простой запрос на получение только положительных значений.
var posNums = from n in nums
where n > 0
select n;
Console.Write("Положительные значения из массива nums: ");
// Выполнить запрос и отобразить его результаты.
foreach(int i in posNums) Console.Write(i + " ");
Console.WriteLine();
// Внести изменения в массив nums.
Console.WriteLine("\nЗадать значение 99 для элемента массива nums[1].");
nums[1] = 99;
Consofe.Write("Положительные значения из массива nums\n" +
"после изменений в нем: ");
// Выполнить запрос второй раз.
foreach(int i in posNums) Console.Write(i + " ");
Console.WriteLine();
}
}
Вот к какому результату приводит выполнение этой программы.
Положительные значения из массива nums: 1 3 5
Задать значение 99 для элемента массива nums[l].
Положительные значения из массива nums
после изменений в нем: 1 99 3 5
Как следует из результата выполнения приведенной выше программы, значение элемента массива nums[1] изменилось с -2 на 99, что и отражают результаты повтор ного выполнения запроса. Этот важный момент следует подчеркнуть особо. Каждая попытка выполнить запрос приносит свои результаты, получаемые при перечислении текущего содержимого источника данных. Поэтому если источник данных претерпева ет изменения, то могут измениться и результаты выполнения запроса. Преимущества такого подхода к обработке запросов весьма значительны. Так, если по запросу полу чается список необработанных заказов в Интернет-магазине, то при каждой попытке выполнить запрос желательно получить сведения обо всех заказах, включая и только что введенные.
Как показывает предыдущий пример, запрос включает в себя переменные, типы которых связаны друг с другом. К их числу относятся переменная запроса, переменная диапазона и источник данных. Соблюсти соответствие этих типов данных очень важно, но в то же время нелегко — по крайней мере, так кажется на первый взгляд, поэтому данный вопрос заслуживает более пристального внимания.
Тип переменной диапазона должен соответствовать типу элементов, хранящих ся в источнике данных. Следовательно, тип переменной диапазона зависит от типа источника данных. Как правило, тип переменной диапазона может быть выведен средствами С#. Но выводимость типов может быть осуществлена при условии, что в источнике данных реализована форма интерфейса IEnumerable, где Т обозна чает тип элементов в источнике данных. (Как упоминалось выше, форма интерфейса IEnumerable реализуется во всех массивах, как, впрочем, и во многих других ис точниках данных.) Но если в источнике данных реализован необобщенный вариант интерфейса IEnumerable, то тип переменной диапазона придется указывать явно. И это делается в операторе from. Ниже приведен пример явного объявления типа int переменной диапазона n.
var posNums = from int n in nums
// ...
Очевидно, что явное указание типа здесь не требуется, поскольку все массивы неяв но преобразуются в форму интерфейса IEnumerable, которая позволяет вывести тип переменной диапазона.
Тип объекта, возвращаемого по запросу, представляет собой экземпляр интерфей са IEnumerable, где Т — тип получаемых элементов. Следовательно, тип пере менной запроса должен быть экземпляром интерфейса IEnumerable, а значение Т должно определяться типом значения, указываемым в операторе select. В преды дущих примерах значению Т соответствовал тип int, поскольку переменная п имела тип int. (Как пояснялось выше, переменная n относится к типу int, потому что эле менты именно этого типа хранятся в массиве nums.) С учетом явного указания типа IEnumerable упомянутый выше запрос можно было бы составить следующим образом.
IEnumerable<int> posNums = from n in nums
where n > 0
select n;
Следует иметь в виду, что тип элемента, выбираемого оператором select, должен соответствовать типу аргумента, передаваемого форме интерфейса IEnumerable, используемой для объявления переменной запроса. Зачастую при объявлении пере менных запроса используется ключевое слово var вместо явного указания ее типа, поскольку это дает компилятору возможность самому вывести соответствующий тип данной переменной из оператора select. Как будет показано далее в этой главе, та кой подход оказывается особенно удобным в тех случаях, когда оператор select воз вращает из источника данных нечто более существенное, чем отдельный элемент. Когда запрос выполняется в цикле foreach, тип переменной шага цикла должен быть таким же, как и тип переменной диапазона. В предыдущих примерах тип этой переменной указывался явно как int. Но имеется и другая возможность: предоставить компилятору самому вывести тип данной переменной, и для этого достаточно указать ее тип как var. Как будет показано далее в этой главе, ключевое слово var приходится использовать и в тех случаях, когда тип данных просто неизвестен.
У всех запросов имеется общая форма, основывающаяся на ряде приведенных ниже контекстно-зависимых ключевых слов.
Ascending | by | descending | equals |
from | group | in | into |
join | let | on | orderby |
select | where |
Среди них лишь приведенные ниже ключевые слова используются в начале опера торов запроса.
from | group | join | let |
orderby | select | where |
Запрос должен начинаться с ключевого слова from и оканчиваться ключевым сло вом select или group. Оператор select определяет тип значения, перечисляемого по запросу, а оператор group возвращает данные группами, причем каждая группа может перечисляться по отдельности. Как следует из приведенных выше примеров, в операторе where указываются критерии, которым должен удовлетворять искомый элемент, чтобы быть полученным по запросу. А остальные операторы позволяют уточ нить запрос. Все они рассматриваются далее по порядку.
Как пояснялось выше, оператор where служит для отбора данных, возвращаемых по запросу. В предыдущих примерах этот оператор был продемонстрирован в своей простейшей форме, в которой для отбора данных используется единственное усло вие. Однако для более тщательного отбора данных можно задать несколько условий и, в частности, в нескольких операторах where. В качестве примера рассмотрим сле дующую программу, в которой из массива выводятся только те значения, которые по ложительны и меньше 10.
// Использовать несколько операторов where.
using System;
using System.Linq;
class TwoWheres {
static void Main() {
int[] nums = { 1, -2, 3, -3, 0, -8, 12, 19, 6, 9, 10 };
// Сформировать запрос на получение положительных значений меньше 10.
var posNums = from n in nums
where n > 0
where n < 10
select n;
Console.Write("Положительные значения меньше 10: ");
// Выполнить запрос и вывести его результаты.
foreach(int i in posNums) Console.Write (i + " ");
Console.WriteLine();
}
}
Эта программа дает следующий результат.
Положительные значения меньше 10: 1 3 6 9
Как видите, по данному запросу извлекаются только положительные значения меньше. 10. Этот результат достигается благодаря двум следующим операторам where.
where n > 0
where n < 10
Условие в первом операторе where требует, чтобы элемент массива был больше нуля. А условие во втором операторе where требует, чтобы элемент массива был мень ше 10. Следовательно, запрашиваемый элемент массива должен находиться в пределах от 1 до 9 (включительно), чтобы удовлетворять обоим условиям.
В таком применении двух операторов where для отбора данных нет ничего дурно го, но аналогичного эффекта можно добиться с помощью более компактно составлен ного условия в единственном операторе where. Ниже приведен тот же самый запрос, переформированный по этому принципу.
var posNums = from n in nums
where n > 0 ts n < 10
select n;
Как правило, в условии оператора where разрешается использовать любое допу стимое в C# выражение, дающее булев результат. Например, в приведенной ниже про грамме определяется массив символьных строк. В ряде этих строк содержатся адреса Интернета. По запросу в переменой netAddrs извлекаются только те строки, которые содержат более четырех символов и оканчиваются на ".net". Следовательно, по дан ному запросу обнаруживаются строки, содержащие адреса Интернета с именем .net домена самого верхнего уровня.
// Продемонстрировать применение еще одного оператора where.
using System;
using System.Linq;
class WhereDemo2 {
static void Main() {
string[] strs = { ".com", ".net", "hsNameA.com",
"hsNameB.net", "test", ".network",
"hsNameC.net", "hsNameD.com" };
// Сформировать запрос на получение адресов
// Интернета, оканчивающихся на .net.
var netAddrs = from addr in strs
where addr.Length > 4 && addr.EndsWithC.net",
StringComparison.Ordinal)
select addr;
// Выполнить запрос и вывести его результаты.
foreach(var str in netAddrs) Console.WriteLine(str);
}
}
Вот к какому результату приводит выполнение этой программы.
hsNameB.net
hsNameC.net
Обратите внимание на то, что в операторе where данной программы используется один из методов обработки символьных строк под названием EndsWith(). Он возвра щает логическое значение true, если вызывающая его строка оканчивается последова тельностью символов, указываемой в качестве аргумента этого метода.
Зачастую результаты запроса требуют сортировки. Допустим, что требуется полу чить список просроченных счетов по порядку остатка на счету: от самого большого до самого малого или же список имен заказчиков в алфавитном порядке. Независимо от преследуемой цели, результаты запроса можно очень легко отсортировать, используя такое средство LINQ, как оператор orderby.
Оператор orderby можно использовать для сортировки результатов запроса по одному или нескольким критериям. Рассмотрим для начала самый простой случай сортировки по одному элементу. Ниже приведена общая форма оператора orderby для сортировки результатов запроса по одному критерию:
orderby элемент порядок
где элемент обозначает конкретный элемент, по которому проводится сортировка. Это может быть весь элемент, хранящийся в источнике данных, или только часть одно го поля в данном элементе. А порядок обозначает порядок сортировки по нарастаю щей или убывающей с обязательным добавлением ключевого слова ascending или descending соответственно. По умолчанию сортировка проводится по нарастающей, и поэтому ключевое слово ascending, как правило, не указывается.
Ниже приведен пример программы, в которой оператор orderby используется для извлечения значений из массива типа int по нарастающей.
// Продемонстрировать применение оператора orderby.
using System;
using System.Linq;
class OrderbyDemo {
static void Main() {
int[] nums = { 10, -19, 4, 7, 2, -5, 0 };
// Сформировать запрос на получение значений в отсортированном порядке.
var posNums = from n in nums
orderby n
select n;
Console.Write("Значения по нарастающей: ");
// Выполнить запрос и вывести его результаты.
foreach(int i in posNums) Console.Write(i + " ");
Console.WriteLine();
}
}
При выполнении этой программы получается следующий результат.
Значения по нарастающей: -19 -5 0 2 4 7 10
Для того чтобы изменить порядок сортировки по нарастающей на сортировку по убывающей, достаточно указать ключевое слово descending, как показано ниже.
var posNums = from n in nums
orderby n descending
select n;
Попробовав выполнить этот запрос, вы получите значения в обратном порядке. Зачастую сортировка результатов запроса проводится по единственному критерию. Тем не менее для сортировки по нескольким критериям служит приведенная ниже форма оператора orderby.
orderby элемент_А направление, элемент_В направление, элемент_С направление, ...
В данной форме элемент_А обозначает конкретный элемент, по которому прово дится основная сортировка; элемент_В — элемент, по которому производится сорти ровка каждой группы эквивалентных элементов; элемент_С — элемент, по которому производится сортировка всех этих групп, и т.д. Таким образом, каждый последующий элемент обозначает дополнительный критерий сортировки. Во всех этих критериях указывать направление сортировки необязательно, но по умолчанию сортировка про водится по нарастающей. Ниже приведен пример программы, в которой сортировка информации о банковских счетах осуществляется по трем критериям: фамилии, име ни и остатку на счете.
// Сортировать результаты запроса по нескольким
// критериям, используя оператор orderby.
using System;
using System.Linq;
class Account {
public string FirstName { get; private set; }
public string LastName { get; private set; }
public double Balance { get; private set; }
public string AccountNumber { get; private set; }
public Account(string fn, string ln, string accnum, double b) {
FirstName = fn;
LastName = ln;
AccountNumber = accnum;
Balance = b;
}
}
class OrderbyDemo {
static void Main() {
// Сформировать исходные данные.
Account[] accounts =
{ new Account("Том", "Смит", "132CK", 100.23),
new Account("Том", "Смит", "132CD", 10000.00),
new Account("Ральф", "Джонс", "436CD", 1923.85),
new Account("Ральф", "Джонс", "454MM", 987.132),
new Account("Тед", "Краммер", "897CD", 3223.19),
new Account("Ральф", "Джонс", "434CK", -123.32),
new Account("Capa", "Смит", "543MM", 5017.40),
new Account("Capa", "Смит", "547CD", 34955.79),
new Account("Capa", "Смит", "843CK", 345.00),
new Account("Альберт", "Смит", "445CK", -213.67),
new Account("Бетти", "Краммер","968MM",5146.67),
new Account("Карл", "Смит", "078CD", 15345.99),
new Account("Дженни", "Джонс", "108CK", 10.98)
};
// Сформировать запрос на получение сведений о
// банковских счетах в отсортированном порядке.
// Отсортировать эти сведения сначала по имени, затем
// по фамилии и, наконец, по остатку на счете,
var accInfo = from асе in accounts
orderby acc.LastName, acc.FirstName, acc.Balance
select acc;
Console.WriteLine("Счета в отсортированном порядке: ");
string str = "";
// Выполнить запрос и вывести его результаты.
foreach(Account acc in accInfo) {
if(str != acc.FirstName) {
Console.WriteLine();
str = acc.FirstName;
}
Console.WriteLine("{0}, {1}\tHoмep счета: {2}, {3,10:C}",
acc.LastName, acc.FirstName,
acc. AccountNumber, acc.Balance);
}
Console.WriteLine();
}
}
Ниже приведен результат выполнения этой программы.
Счета в отсортированном порядке:
Джонс, Дженни Номер счета: 108СК, $10.98
Джонс, Ральф Номер счета: 434СК, ($123.32)
Джонс, Ральф Номер счета: 454ММ, $987.13
Джонс, Ральф Номер счета: 436CD, $1,923.85
Краммер, Бетти Номер счета: 968ММ, $5,146.67
Краммер, Тед Номер счета: 897CD, $3,223.19
Смит, Альберт Номер счета: 445СК, ($213.67)
Смит, Карл Номер счета: 078CD, $15,345.99
Смит, Сара Номер счета: 843СК, $345.00
Смит, Сара Номер счета: 543ММ, $5,017.40
Смит, Сара Номер счета: 547CD, $34,955.79
Смит, Том Номер счета: 132СК, $100.23
Смит, Том Номер счета: 132CD, $10,000.00
Внимательно проанализируем оператор orderby в следующем запросе из приве денной выше программы.
var accInfo = from acc in accounts
orderby acc.LastName, acc.FirstName, acc.Balance
select acc;
Сортировка результатов этого запроса осуществляется следующим образом. Снача ла результаты сортируются по фамилии, затем элементы с одинаковыми фамилиями сортируются по имени. И наконец, группы элементов с одинаковыми фамилиями и именами сортируются по остатку на счете. Именно поэтому список счетов вкладчиков по фамилии Джонс выглядит так.
Джонс, Дженни Номер счета: 108СК, $10.98
Джонс, Ральф Номер счета: 434СК, ($123.32)
Джонс, Ральф Номер счета: 454ММ, $987.13
Джонс, Ральф Номер счета: 436CD, $1,923.85
Как показывает результат выполнения данного запроса, список счетов отсортиро ван сначала по фамилии, затем по имени и, наконец, по остатку на счете. Используя несколько критериев, можно изменить на обратный порядок любой со ртировки с помощью ключевого слова descending. Например, результаты следующе го запроса будут выведены по убывающей остатков на счетах.
var accInfo = from acc in accounts
orderby x.LastName, x.FirstName, x.Balance descending
select acc;
В этом случае список счетов вкладчиков по фамилии Джонс будет выглядеть так, как показано ниже.
Джонс, Дженни Номер счета: 108СК, $10.98
Джонс, Ральф Номер счета: 436CD, $1,923.85
Джонс, Ральф Номер счета: 454ММ, $987.13
Джонс, Ральф Номер счета: 434СК, ($123.32)
Как видите, теперь счета вкладчика по фамилии Ральф Джонс выводятся по убы вающей: от наибольшей до наименьшей суммы остатка на счете.
Оператор select определяет конкретный тип элементов, получаемых по запросу. Ниже приведена его общая форма.
select выражение
В предыдущих примерах оператор select использовался для возврата перемен ной диапазона. Поэтому выражение в нем просто обозначало имя переменной диа пазона. Но применение оператора select не ограничивается только этой простой функцией. Он может также возвращать отдельную часть значения переменной диа пазона, результат выполнения некоторой операции или преобразования переменной диапазона и даже новый тип объекта, конструируемого из отдельных фрагментов ин формации, извлекаемой из переменной диапазона. Такое преобразование исходных данных называется проецированием.
Начнем рассмотрение других возможностей оператора select с приведенной ниже программы. В этой программе выводятся квадратные корни положительных значений, содержащихся в массиве типа double.
// Использовать оператор select для возврата квадратных корней всех
// положительных значений, содержащихся в массиве типа double.
using System;
using System.Linq;
class SelectDemo {
static void Main() {
double[] nums =
{ -10.0, 16.4, 12.125, 100.85, -2.2, 25.25, -3.5 };
// Сформировать запрос на получение квадратных корней всех
// положительных значений, содержащихся в массиве nums.
var sqrRoots = from n in nums
where n > 0
select Math.Sqrt(n);
Console.WriteLine("Квадратные корни положительных значений,\n" +
"округленные до двух десятичных цифр:");
// Выполнить запрос и вывести его результаты.
foreach (double r in sqrRoots)
Console.WriteLine("{0:#.##}", r);
}
}
Эта программа дает следующий результат.
Квадратные корни положительных значений,
округленные до двух десятичных цифр:
4.05
3.48
10.04
5.02
Обратите особое внимание в данном примере запроса на следующий оператор select.
select Math.Sqrt(n);
Он возвращает квадратный корень значения переменной диапазона. Для этого зна чение переменной диапазона передается методу Math.Sqrt(), который возвращает квадратный корень своего аргумента. Это означает, что последовательность результа тов, получаемых при выполнении запроса, будет содержать квадратные корни поло жительных значений, хранящихся в массиве nums. Если обобщить этот принцип, то его эффективность станет вполне очевидной. Так, с помощью оператора select мож но сформировать любой требующийся тип последовательности результатов, исходя из значений, получаемых из источника данных.
Ниже приведена программа, демонстрирующая другое применение оператора select. В этой программе сначала создается класс EmailAddress, содержащий два свойства. В первом из них хранится имя адресата, а во втором — адрес его электронной почты. Затем в этой программе создается массив, содержащий несколько элементов данных типа EmailAddress. И наконец, в данной программе создается список, состоя щий только из адресов электронной почты, извлекаемых по запросу.
// Возвратить часть значения переменной диапазона.
using System;
using System.Linq;
class EmailAddress {
public string Name { get; set; }
public string Address { get; set; }
public EmailAddress(string n, string a) {
Name = n;
Address = a;
}
}
class SelectDemo2 {
static void Main() {
EmailAddress[] addrs = {
new EmailAddress("Герберт", "Herb@HerbSchildt.com"),
new EmailAddress("Tom", "Tom@HerbSchildt.com"),
new EmailAddress("Capa", "Sara@HerbSchildt.com")
};
// Сформировать запрос на получение адресов
// электронной почты.
var eAddrs = from entry in addrs
select entry.Address;
Console.WriteLine("Адреса электронной почты:");
// Выполнить запрос и вывести его результаты.
foreach(string s in eAddrs)
Console.WriteLine(" " + s);
}
}
Вот к какому результату приводит выполнение этой программы.
Адреса электронной почты:
Herb@HerbSchildt.com
Tom@HerbSchildt.com
Sara@HerbSchildt.com
Обратите особое внимание на следующий оператор select.
select entry.Address;
Вместо полного значения переменной диапазона этот оператор возвращает лишь его адресную часть (Address). Это означает, что по данному запросу возвращается последовательность символьных строк, а не объектов типа EmailAddress. Именно поэтому переменная s указывается в цикле foreach как string. Ведь как пояснялось ранее, тип последовательности результатов, возвращаемых по запросу, определяется типом значения, возвращаемым оператором select.
Одной из самых эффективных для оператора select является возможность возвра щать последовательность результатов, содержащую элементы данных, формируемые во время выполнения запроса. В качестве примера рассмотрим еще одну программу. В ней определяется класс ContactInfo, в котором хранится имя, адрес электронной почты и номер телефона адресата. Кроме того, в этой программе определяется класс EmailAddress, использовавшийся в предыдущем примере. В методе Main() создает ся массив объектов типа ContactInfo, а затем объявляется запрос, в котором источ ником данных служит этот массив, но возвращаемая последовательность результатов содержит объекты типа EmailAddress. Таким образом, типом последовательности результатов, возвращаемой оператором select, является класс EmailAddress, а не класс ContactInfo, причем его объекты создаются во время выполнения запроса.
// Использовать запрос для получения последовательности объектов
// типа EmailAddresses из списка объектов типа ContactInfo.
using System;
using System.Linq;
class ContactInfo {
public string Name { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public ContactInfo(string n, string a, string p) {
Name = n;
Email = a;
Phone = p;
}
}
class EmailAddress {
public string Name { get; set; }
public string Address { get; set; }
public EmailAddress(string n, string a) {
Name = n;
Address = a;
}
}
class SelectDemo3 {
static void Main() {
ContactInfo[] contacts = {
new Contactlnfo("Герберт", "Herb@HerbSchildt.com", "555-1010"),
new Contactlnfo("Tom", "Tom@HerbSchildt.com", "555-1101"),
new Contactlnfo("Capa", "Sara@HerbSchildt.com", "555-0110")
};
// Сформировать запрос на получение списка объектов типа EmailAddress.
var emailList = from entry in contacts
select new EmailAddress(entry.Name, entry.Email);
Console.WriteLine("Список адресов электронной почты:");
// Выполнить запрос и вывести его результаты.
foreach(EmailAddress е in emailList)
Console.WriteLine(" {0}: {1}", e.Name, e.Address );
}
}
Ниже приведен результат выполнения этой программы.
Список адресов электронной почты:
Герберт: Herb@HerbSchildt.com
Том: Tom@HerbSchildt.com
Сара: Sara@HerbSchildt.com
Обратите особое внимание в данном запросе на следующий оператор select.
select new EmailAddress(entry.Name, entry.Email);
В этом операторе создается новый объект типа EmailAddress, содержащий имя и адрес электронной почты, получаемые из объекта типа ContactInfo, хранящегося в массиве contacts. Но самое главное, что новые объекты типа EmailAddress созда ются в операторе select во время выполнения запроса.
Запрос может состоять из нескольких операторов from, которые оказываются в этом случае вложенными. Такие операторы from находят применение в тех случаях, когда по запросу требуется получить данные из двух разных источников. Рассмотрим простой пример, в котором два вложенных оператора from используются в запросе для циклического обращения к элементам двух разных массивов символов. В итоге по такому запросу формируется последовательность результатов, содержащая все воз можные комбинации двух наборов символов.
// Использовать два вложенных оператора from для составления списка
// всех возможных сочетаний букв А, В и С с буквами X, Y и Z.
using System;
using System.Linq;
// Этот класс содержит результат запроса.
class ChrPair {
public char First;
public char Second;
public ChrPair(char c, char c2) {
First = c;
Second = c2;
}
}
class MultipleFroms {
static void Main() {
char[] chrs = { 'A', 'B', 'C' };
char[] chrs2 = { 'X', 'Y', 'Z' };
// В первом операторе from организуется циклическое обращение
// к массиву символов chrs, а во втором операторе from —
// циклическое обращение к массиву символов chrs2.
var pairs = from ch1 in chrs
from ch2 in chrs2
select new ChrPair(ch1, ch2);
Console.WriteLine("Все сочетания букв ABC и XYZ: ");
foreach(var p in pairs)
Console.WriteLine("{0} {1}", p.First, p.Second);
}
}
Выполнение этого кода приводит к следующему результату.
Все сочетания букв ABC и XYZ:
А X
A Y
A Z
В X
В Y
В Z
С X
С Y
С Z
Этот пример кода начинается с создания класса ChrPair, в котором содержатся результаты запроса. Затем в нем создаются два массива, chrs и chrs2, и, наконец, формируется следующий запрос для получения всех возможных комбинаций двух по следовательностей результатов.
var pairs = from ch1 in chrs
from ch2 in chrs2
select new ChrPair(ch1, ch2);
Во вложенных операторах from организуется циклическое обращение к обоим массивам символов, chrs и chrs2. Сначала из массива chrs получается символ, со храняемый в переменной ch1. Затем перечисляется содержимое массива chrs2. На каждом шаге циклического обращения во внутреннем операторе from символ из массива chrs2 сохраняется в переменной сh2 и далее выполняется оператор select. В результате выполнения оператора select создается новый объект типа ChrPair, содержащий пару символов, которые сохраняются в переменных ch1 и ch2 на каждом шаге циклического обращения к массиву во внутреннем операторе from. А в конечном итоге получается объект типа ChrPair, содержащий все возможные сочетания извле каемых символов.
Вложенные операторы from применяются также для циклического обращения к источнику данных, который содержится в другом источнике данных. Соответствую щий пример приведен в разделе "Применение оператора let для создания времен ной переменной в запросе" далее в этой главе.
Одним из самых эффективных средств формирования запроса является оператор group, поскольку он позволяет группировать полученные результаты по ключам. Ис пользуя последовательность сгруппированных результатов, можно без особого тру да получить доступ ко всем данным, связанным с ключом. Благодаря этому свойству оператора group доступ к данным, организованным в последовательности связанных элементов, осуществляется просто и эффективно. Оператор group является одним из двух операторов, которыми может оканчиваться запрос. (Вторым оператором, завер шающим запрос, является select.) Ниже приведена общая форма оператора group.
group переменная_диапазона by ключ
Этот оператор возвращает данные, сгруппированные в последовательности, при чем каждую последовательность обозначает общий ключ.
Результатом выполнения оператора group является последовательность, состоя
щая из элементов типа IGrouping<TKey, TElement>
, т.е. обобщенного интерфейса,
объявляемого в пространстве имен System.Linq. В этом интерфейсе определена кол
лекция объектов с общим ключом. Типом переменной запроса, возвращающего груп
пу, является IEnumerable<IGrouping<TKey, TElement>>
. В интерфейсе IGrouping
определено также доступное только для чтения свойство Key, возвращающее ключ,
связанный с каждой коллекцией.
Ниже приведен пример, демонстрирующий применение оператора group. В коде этого примера сначала объявляется массив, содержащий список веб-сайтов, а затем формируется запрос, в котором этот список группируется по имени домена самого верхнего уровня, например .org или .com.
// Продемонстрировать применение оператора group.
using System;
using System.Linq;
class GroupDemo {
static void Main() {
string[] websites = { "hsNameA.com", "hsNameB.net", "hsNameC.net",
"hsNameD.com", "hsNameE.org", "hsNameF.org",
"hsNameG.tv", "hsNameH.net", "hsNameI.tv"
};
// Сформировать запрос на получение списка веб-сайтов,
// группируемых по имени домена самого верхнего уровня.
var webAddrs = from addr in websites
where addr.LastIndexOf('.') != -1
group addr by addr.Substring(addr.LastIndexOf('.'));
// Выполнить запрос и вывести его результаты.
foreach(var sites in webAddrs) {
Console.WriteLine("Веб-сайты, сгруппированные " +
"по имени домена" + sites.Key);
foreach(var site in sites)
Console.WriteLine(" " + site);
Console.WriteLine();
}
}
}
Вот к какому результату приводит выполнение этого кода.
Веб-сайты, сгруппированные по имени домена .соm
hsNameA.com
hsNameD.com
Веб-сайты, сгруппированные по имени домена .net
hsNameB.net
hsNameC.net
hsNameH.net
Веб-сайты, сгруппированные по имени домена .org
hsNameE.org
hsNameF.org
Веб-сайты, сгруппированные по имени домена .tv
hsNameG.tv
hsNameI.tv
Как следует из приведенного выше результата, данные, получаемые по запросу, группируются по имени домена самого верхнего уровня в адресе веб-сайта. Обратите внимание на то, как это делается в операторе group из следующего запроса.
var webAddrs = from addr in websites
where addr.LastlndexOf('.') != -1
group addr by addr.Substring(addr.LastIndexOf('.'));
Ключ в этом операторе создается с помощью методов LastIndexOf() и Substring(), определенных для данных типа string. (Эти методы упоминаются в главе 7, посвященной массивам и строкам. Вариант метода Substring(), исполь зуемый в данном примере, возвращает подстроку, начинающуюся с места, обозна чаемого индексом, и продолжающуюся до конца вызывающей строки.) Индекс по следней точки в адресе веб-сайта определяется с помощью метода LastIndexOf(). По этому индексу в методе Substring() создается оставшаяся часть строки, в ко торой содержится имя домена самого верхнего уровня. Обратите внимание на то, что в операторе where отсеиваются все строки, которые не содержат точку. Метод LastIndexOf() возвращает -1, если указанная подстрока не содержится в вызываю щей строке.
Последовательность результатов, получаемых при выполнении запроса, храняще гося в переменной webAddrs, представляет собой список групп, поэтому для доступа к каждому члену группы требуются два цикла foreach. Доступ к каждой группе осу ществляется во внешнем цикле, а члены внутри группы перечисляются во внутреннем цикле. Переменная шага внешнего цикла foreach должна быть экземпляром интер фейса IGrouping, совместимым с ключом и типом элемента данных. В рассматри ваемом здесь примере ключи и элементы данных относятся к типу string. Поэтому переменная sites шага внешнего цикла имеет тип IGrouping<string, string>, а переменная site шага внутреннего цикла — тип string. Ради краткости данного примера обе переменные объявляются неявно, хотя их можно объявить и явным об разом, как показано ниже.
foreach(IGrouping<string, string> sites in webAddrs) {
Console.WriteLine("Веб-сайты, сгруппированные " +
"по имени домена" + sites.Key);
foreach(string site in sites)
Console.WriteLine(" " + site);
Console.WriteLine();
}
При использовании в запросе оператора select или group иногда требуется сфор мировать временный результат, который будет служить продолжением запроса для по лучения окончательного результата. Такое продолжение осуществляется с помощью оператора into в комбинации с оператором select или group. Ниже приведена об щая форма оператора into:
into имя тело_запроса
где имя обозначает конкретное имя переменной диапазона, используемой для ци клического обращения к временному результату в продолжении запроса, на которое указывает тело_запроса. Когда оператор into используется вместе с оператором select или group, то его называют продолжением запроса, поскольку он продолжает запрос. По существу, продолжение запроса воплощает в себе принцип построения нового запроса по результатам предыдущего.
ПРИМЕЧАНИЕ Существует также форма оператора into, предназначенная для использования вместе с оператором join, создающим групповое объединение, о котором речь пойдет далее в этой главе.
Ниже приведен пример программы, в которой оператор into используется вместе с оператором group. Эта программа является переработанным вариантом предыду щего примера, в котором список веб-сайтов формируется по имени домена самого верхнего уровня. А в данном примере первоначальные результаты запроса сохраня ются в переменной диапазона ws и затем отбираются для исключения всех групп, со стоящих менее чем из трех элементов.
// Использовать оператор into вместе с оператором group.
using System;
using System.Linq;
class IntoDemo {
static void Main() {
string[] websites = { "hsNameA.com", "hsNameB.net", "hsNameC.net",
"hsNameD.com", "hsNameE.org", "hsNameF.org",
"hsNameG.tv", "hsNameH.net", "hsNameI.tv"
};
// Сформировать запрос на получение списка веб-сайтов, группируемых
// по имени домена самого верхнего уровня, но выбрать только те
// группы, которые состоят более чем из двух членов.
// Здесь ws — это переменная диапазона для ряда групп,
// возвращаемых при выполнении первой половины запроса.
var webAddrs = from addr in websites
where addr.LastIndexOf('.') != -1
group addr by addr.Substring(addr.LastIndexOf('.'))
into ws
where ws.Count() > 2
select ws;
// Выполнить запрос и вывести его результаты.
Console.WriteLine("Домены самого верхнего уровня " +
"с более чем двумя членами.\n");
foreach(var sites in webAddrs) {
Console.WriteLine("Содержимое домена: " + sites.Key);
foreach(var site in sites)
Console.WriteLine(" " + site);
Console.WriteLine();
}
}
}
Эта программа дает следующий результат:
Домены самого верхнего уровня с более чем двумя членами.
Содержимое домена: .net
hsNameB.net
hsNameC.net
hsNameH.net
Как следует из результата выполнения приведенной выше программы, по запросу возвращается только группа .net, поскольку это единственная группа, содержащая больше двух элементов.
Обратите особое внимание в данном примере программы на следующую последо вательность операторов в формируемом запросе.
group addr by addr.Substring(addr.LastIndexOf('.'))
into ws
where ws.Count() >2
select ws;
Сначала результаты выполнения оператора group сохраняются как временные для последующей обработки оператором where. В качестве переменной диапазона в данный момент служит переменная ws. Она охватывает все группы, возвращаемые оператором group. Затем результаты запроса отбираются в операторе where с таким расчетом, чтобы в конечном итоге остались только те группы, которые содержат боль ше двух членов. Для этой цели вызывается метод Count(), который является мето дом расширения и реализуется для всех объектов типа IEnumerable. Он возвращает количество элементов в последовательности. (Подробнее о методах расширения речь пойдет далее в этой главе.) А получающаяся в итоге последовательность групп возвра щается оператором select.
Иногда возникает потребность временно сохранить некоторое значение в самом запросе. Допустим, что требуется создать переменную перечислимого типа, которую можно будет затем запросить, или же сохранить некоторое значение, чтобы в даль нейшем использовать его в операторе where. Независимо от преследуемой цели, эти виды функций могут быть осуществлены с помощью оператора let. Ниже приведена общая форма оператора let:
let имя = выражение
где имя обозначает идентификатор, получающий значение, которое дает выражение. Тип имени выводится из типа выражения.
В приведенном ниже примере программы демонстрируется применение операто ра let для создания еще одного перечислимого источника данных. В качестве входных данных в запрос вводится массив символьных строк, которые затем преобразуются в массивы типа char. Для этой цели служит еще один метод обработки строк, называе мый ToCharArray() и возвращающий массив, содержащий символы в строке. Полу ченный результат присваивается переменной chrArray, которая затем используется во вложенном операторе from для извлечения отдельных символов из массива. И на конец, полученные символы сортируются в запросе, и из них формируется результи рующая последовательность.
// Использовать оператор let в месте с вложенным оператором from.
using System;
using System.Linq;
class LetDemo {
static void Main() {
string[] strs = ( "alpha", "beta", "gamma" };
// Сформировать запрос на получение символов, возвращаемых из
// строк в отсортированной последовательности. Обратите внимание
// на применение вложенного оператора from.
var chrs = from str in strs
let chrArray = str.ToCharArray()
from ch in chrArray
orderby ch
select ch;
Console.WriteLine("Отдельные символы, отсортированные по порядку:");
// Выполнить запрос и вывести его результаты.
foreach(char с in chrs) Console.Write(с + " ");
Console.WriteLine();
}
}
Вот к какому результату приводит выполнение этой программы.
Отдельные символы, отсортированные по порядку:
a a a a a b e g h l m m p t
Обратите внимание в данном примере программы на то, что в операторе let пе ременной chrArray присваивается ссылка на массив, возвращаемый методом str. ToCharArray().
let chrArray = str.ToCharArray()
После оператора let переменная chrArray может использоваться в остальных
операторах, составляющих запрос. А поскольку все массивы в C# преобразуются в тип
IEnumerable<T>
, то переменную chrArray можно использовать в качестве источника
данных для запроса во втором, вложенном операторе from. Именно это и происходит
в рассматриваемом здесь примере, где вложенный оператор from служит для пере
числения в массиве отдельных символов, которые затем сортируются по нарастающей
и возвращаются в виде конечного результата.
Оператор let может также использоваться для хранения неперечислимого значе ния. В качестве примера ниже приведен более эффективный вариант формирования запроса в программе IntoDemo из предыдущего раздела.
var webAddrs = from addr in websites
let idx = addr.LastIndexOf('.')
where idx != -1
group addr by addr.Substring(idx)
into ws
where ws.Count() > 2
select ws;
В этом варианте индекс последнего вхождения символа точки в строку присваи вается переменной idx. Данное значение затем используется в методе Substring(). Благодаря этому исключается необходимость дважды искать символ точки в строке.
Когда приходится иметь дело с базами данных, то зачастую требуется формиро вать последовательность, увязывающую данные из разных источников. Например, в Интернет-магазине может быть организована одна база данных, связывающая наи менование товара с его порядковым номером, и другая база данных, связывающая по рядковый номер товара с состоянием его запасов на складе. В подобной ситуации мо жет возникнуть потребность составить список, в котором состояние запасов товаров на складе отображается по их наименованию, а не порядковому номеру. Для этой цели придется каким-то образом "увязать" данные из двух разных источников (баз данных). И это нетрудно сделать с помощью такого средства LINQ, как оператор join.
Ниже приведена общая форма оператора join (совместно с оператором from).
from переменная_диапазона_А in источник_данных_А
join переменная_диапазона_В in источник_данных_В
on переменная_диапазона_А. свойство equals переменная_диапазона_В. свойство Применяя оператор join, следует иметь в виду, что каждый источник должен со держать общие данные, которые можно сравнивать. Поэтому в приведенной выше форме этого оператора источник_данных_А и источник_данных_В должны иметь нечто общее, что подлежит сравнению. Сравниваемые элементы данных указываются в части on данного оператора. Поэтому если переменная_диапазона_А.свойство и переменная_диапазона_А.свойство равны, то эти элементы данных "увязыва ются" успешно. По существу, оператор join выполняет роль своеобразного фильтра, отбирая только те элементы данных, которые имеют общее значение.
Как правило, оператор join возвращает последовательность, состоящую из дан ных, полученных из двух источников. Следовательно, с помощью оператора join мож но сформировать новый список, состоящий из элементов, полученных из двух разных источников данных. Это дает возможность организовать данные по-новому.
Ниже приведена программа, в которой создается класс Item, инкапсулирующий наименование товара и его порядковый номер. Затем в этой программе создается еще один класс InStockStatus, связывающий порядковый номер товара с булевым свойством, которое указывает на наличие или отсутствие товара на складе. И нако нец, в данной программе создается класс Temp с двумя полями: строковым (string) и булевым (bool). В объектах этого класса будут храниться результаты запроса. В этом запросе оператор join используется для получения списка, в котором наименование товара связывается с состоянием его запасов на складе.
// Продемонстрировать применение оператора join.
using System;
using System.Linq;
// Класс, связывающий наименование товара с его порядковым номером.
class Item {
public string Name { get; set; }
public int ItemNumber { get; set; }
public Item(string n, int inum) {
Name = n;
ItemNumber = inum;
}
}
// Класс, связывающий наименование товара с состоянием его запасов на складе.
class InStockStatus {
public int ItemNumber { get; set; }
public bool InStock { get; set; }
public InStockStatus(int n, bool b) {
ItemNumber = n;
InStock = b;
}
}
// Класс, инкапсулирующий наименование товара и
// состояние его запасов на складе.
class Temp {
public string Name { get; set; }
public bool InStock { get; set; }
public Temp(string n, bool b) {
Name = n;
InStock = b;
}
}
class JoinDemo {
static void Main() {
Item[] items = {
new Item("Кусачки", 1424),
new Item("Тиски", 7892),
new Item("Молоток", 8534),
new Item("Пила", 6411)
};
InStockStatus[] statusList = {
new InStockStatus(1424, true),
new InStockStatus(7892, false),
new InStockStatus(8534, true),
new InStockStatus(6411, true)
};
// Сформировать запрос, объединяющий объекты классов Item
// и InStockStatus для составления списка наименований товаров
// и их наличия на складе. Обратите внимание на формирование
// последовательности объектов класса Temp.
var inStockList = from item in items
join entry in statusList
on item.ItemNumber equals entry.ItemNumber
select new Temp(item.Name, entry.InStock);
Console.WriteLine("Товар\tНаличие\n");
// Выполнить запрос и вывести его результаты.
foreach(Temp t in inStockList)
Console.WriteLine("{0}\t{1}t.Name, t.InStock);
}
}
Эта программа дает следующий результат.
Товар Наличие
Кусачки True
Тиски False
Молоток True
Пила True
Для того чтобы стал понятнее принцип действия оператора join, рассмотрим каж дую строку запроса из приведенной выше программы по порядку. Этот запрос начи нается, как обычно, со следующего оператора from.
var inStockList = from item in items
В этом операторе указывается переменная диапазона item для источника данных items, который представляет собой массив объектов класса Item. В классе Item ин капсулируются наименование товара и порядковый номер товара, хранящегося на складе.
Далее следует приведенный ниже оператор join.
join entry in statusList
on item.ItemNumber equals entry.ItemNumber
В этом операторе указывается переменная диапазона entry для источника данных statusList, который представляет собой массив объектов класса InStockStatus, связывающего порядковый номер товара с состоянием его запасов на складе. Следо вательно, у массивов items и statusList имеется общее свойство: порядковый но мер товара. Именно это свойство используется в части on/equals оператора join для описания связи, по которой из двух разных источников данных выбираются наимено вания товаров, когда их порядковые номера совпадают.
И наконец, оператор select возвращает объект класса Temp, содержащий наиме нование товара и состояние его запасов на складе.
select new Temp(item.Name, entry.InStock);
Таким образом, последовательность результатов, получаемая по данному запросу, состоит из объектов типа Temp.
Рассмотренный здесь пример применения оператора join довольно прост. Тем не менее этот оператор поддерживает и более сложные операции с источниками дан ных. Например, используя совместно операторы into и join, можно создать групповое объединение, чтобы получить результат, состоящий из первой последовательности и группы всех совпадающих элементов из второй последовательности. (Соответствую щий пример будет приведен далее в этой главе.) Как правило, время и усилия, затра ченные на полное освоение оператора join, окупаются сторицей, поскольку он дает возможность распознавать данные во время выполнения программы. Это очень ценная возможность. Но она становится еще ценнее, если используются анонимные типы, о которых речь пойдет в следующем разделе.
В C# предоставляется средство, называемое анонимным типом и связанное непо средственно с LINQ. Как подразумевает само название, анонимный тип представляет собой класс, не имеющий имени. Его основное назначение состоит в создании объек та, возвращаемого оператором select. Результатом запроса нередко оказывается по следовательность объектов, которые составляются из членов, полученных из двух или более источников данных (как, например, в операторе join), или же включают в себя подмножество членов из одного источника данных. Но в любом случае тип возвращае мого объекта зачастую требуется только в самом запросе и не используется в остальной части программы. Благодаря анонимному типу в подобных случаях отпадает необхо димость объявлять класс, который предназначается только для хранения результата запроса.
Анонимный тип объявляется с помощью следующей общей формы:
new { имя_А = значение_А, имя_В = значение_В, ... }
где имена обозначают идентификаторы, которые преобразуются в свойства, доступ ные только для чтения и инициализируемые значениями, как в приведенном ниже примере.
new { Count = 10, Max = 100, Min = 0 }
В данном примере создается класс с тремя открытыми только для чтения свойства ми: Count, Мах и Min, которым присваиваются значения 10, 100 и 0 соответственно. К этим свойствам можно обращаться по имени из другого кода. Следует заметить, что в анонимном типе используются инициализаторы объектов для установки их полей и свойств в исходное состояние. Как пояснялось в главе 8, инициализаторы объектов обеспечивают инициализацию объекта без явного вызова конструктора. Именно это и требуется для анонимных типов, поскольку явный вызов конструктора для них невоз можен. (Напомним, что у конструкторов такое же имя, как и у их класса. Но у аноним ного класса нет имени, а значит, и нет возможности вызвать его конструктор.) Итак, у анонимного типа нет имени, и поэтому для обращения к нему приходится использовать неявно типизированную переменную. Это дает компилятору возмож ность вывести надлежащий тип. В приведенном ниже примере объявляется перемен ная myOb, которой присваивается ссылка на объект, создаваемый в выражении аноним ного типа.
var myOb = new { Count = 10, Max = 100, Min = 0 }
Это означает, что следующие операторы считаются вполне допустимыми.
Console.WriteLine("Счет равен " + myOb.Count);
if(i <= myOb.Max && i >= myOb.Min) // ...
Напомним, что при создании объекта анонимного типа указываемые идентифи каторы становятся свойствами, открытыми только для чтения. Поэтому их можно ис пользовать в других частях кода.
Термин анонимный тип не совсем оправдывает свое название. Ведь тип оказывается анонимным только для программирующего, но не для компилятора, который присва ивает ему внутреннее имя. Следовательно, анонимные типы не нарушают принятые в C# правила строгого контроля типов.
Для того чтобы стало более понятным особое назначение анонимных типов, рас смотрим переделанную версию программы из предыдущего раздела, посвященного оператору join. Напомним, что в этой программе класс Temp требовался для ин капсуляции результата, возвращаемого оператором join. Благодаря применению анонимного типа необходимость в этом классе-заполнителе отпадает, а исходный код программы становится менее громоздким. Результат выполнения программы при этом не меняется.
// Использовать анонимный тип для усовершенствования
// программы, демонстрирующей применение оператора join.
using System;
using System.Linq;
// Класс, связывающий наименование товара с его порядковым номером.
class Item {
public string Name { get; set; }
public int ItemNumber { get; set; }
public Item(string n, int inum) {
Name = n;
ItemNumber = inum;
}
}
// Класс, связывающий наименование товара с состоянием его запасов на складе.
class InStockStatus {
public int ItemNumber { get; set; }
public bool InStock { get; set; }
public InStockStatus(int n, bool b) {
ItemNumber = n;
InStock = b;
}
}
class AnonTypeDemo {
static void Main() {
Item[] items = {
new Item("Кусачки", 1424),
new Item("Тиски", 7892),
new Item("Молоток", 8534),
new Item("Пила", 6411)
};
InStockStatus[] statusList = {
new InStockStatus(1424, true),
new InStockStatus(7892, false),
new InStockStatus(8534, true),
new InStockStatus(6411, true)
};
// Сформировать запрос, объединяющий объекты классов Item и
// InStockStatus для составления списка наименований товаров и их
// наличия на складе. Теперь для этой цели используется анонимный тип.
var inStockList = from item in items
join entry in statusList
on item.ItemNumber equals entry.ItemNumber
select new { Name = item.Name,
InStock = entry.InStock };
Console.WriteLine("Товар\tНаличие\n");
// Выполнить запрос и вывести его результаты.
foreach(var t in inStockList)
Console.WriteLine("{0}\t{1}", t.Name, t.InStock);
}
}
Обратите особое внимание на следующий оператор select.
select new { Name = item.Name,
InStock = entry.InStock };
Он возвращает объект анонимного типа с двумя доступными только для чтения свойствами: Name и InStock. Этим свойствам присваиваются наименование товара и состояние его наличия на складе. Благодаря применению анонимного типа необхо димость в упоминавшемся выше классе Temp отпадает.
Обратите также внимание на цикл foreach, в котором выполняется запрос. Теперь переменная шага этого цикла объявляется с помощью ключевого слова var. Это не обходимо потому, что у типа объекта, хранящегося в переменной inStockList, нет имени. Данная ситуация послужила одной из причин, по которым в C# были внедре ны неявно типизированные переменные, поскольку они нужны для поддержки ано нимных типов.
Прежде чем продолжить изложение, следует отметить еще один заслуживающий внимания аспект анонимных типов. В некоторых случаях, включая и рассмотренный выше, синтаксис анонимного типа упрощается благодаря применению инициализато ра проекции. В данном случае просто указывается имя самого инициализатора. Это имя автоматически становится именем свойства. В качестве примера ниже приведен дру гой вариант оператора select из предыдущей программы.
select new { item.Name, entry.InStock };
В данном примере имена свойств остаются такими же, как и прежде, а компилятор автоматически "проецирует" идентификаторы Name и InStock, превращая их в свой ства анонимного типа. Этим свойствам присваиваются прежние значения, обозначае мые item.Name и entry.InStock соответственно.
Как пояснялось ранее, оператор into можно использовать вместе с оператором join для создания группового объединения, образующего последовательность, в которой каждый результат состоит из элементов данных из первой последовательности и груп пы всех совпадающих элементов из второй последовательности. Примеры группового объединения не приводились выше потому, что в этом объединении нередко приме няется анонимный тип. Но теперь, когда представлены анонимные типы, можно об ратиться к простому примеру группового объединения.
В приведенном ниже примере программы групповое объединение используется для составления списка, в котором различные транспортные средства (автомашины, суда и самолеты) организованы по общим для них категориям транспорта: назем ного, морского, воздушного и речного. В этой программе сначала создается класс Transport, связывающий вид транспорта с его классификацией. Затем в методе Main() формируются две входные последовательности. Первая из них представля ет собой массив символьных строк, содержащих названия общих категорий транс порта: наземного, морского, воздушного и речного, а вторая — массив объектов типа Transport, инкапсулирующих различные транспортные средства. Полученное в итоге групповое объединение используется для составления списка транспортных средств, организованных по соответствующим категориям.
// Продемонстрировать применение простого группового объединения.
using System;
using System.Linq;
// Этот класс связывает наименование вида транспорта,
// например поезда, с общей классификацией транспорта:
// наземного, морского, воздушного или речного.
class Transport {
public string Name { get; set; }
public string How { get; set; }
public Transport(string n, string h) {
Name = n;
How = h;
}
}
class GroupJoinDemo {
static void Main() {
// Массив классификации видов транспорта.
string[] travelTypes = {
"Воздушный",
"Морской",
"Наземный",
"Речной",
};
// Массив видов транспорта.
Transport[] transports = {
new Transport("велосипед", "Наземный"),
new Transport("аэростат", "Воздушный"),
new Transport("лодка", "Речной"),
new Transport("самолет", "Воздушный"),
new Transport("каноэ", "Речной"),
new Transport("биплан", "Воздушный"),
new Transport("автомашина", "Наземный"),
new Transport("судно", "Морской"),
new Transport("поезд", "Наземный")
};
// Сформировать запрос, в котором групповое
// объединение используется для составления списка
// видов транспорта по соответствующим категориям.
var byHow = from how in travelTypes
join trans in transports
on how equals trans.How
into lst
select new { How = how, Tlist = lst };
// Выполнить запрос и вывести его результаты.
foreach(var t in byHow) {
Console.WriteLine("К категории <{0} транспорт> относится:", t.How);
foreach(var m in t.Tlist)
Console.WriteLine(" " + m.Name);
Console.WriteLine();
}
}
}
Ниже приведен результат выполнения этой программы.
К категории <Воздушный транспорт> относится:
аэростат
самолет
биплан
К категории <Морской транспорт> относится:
судно
К категории <Наземный транспорт> относится:
велосипед
автомашина
поезд
К категории <Речной транспорт> относится:
лодка
каноэ
Главной частью данной программы, безусловно, является следующий запрос.
var byHow = from how in travelTypes
join trans in transports
on how equals trans.How
into lst
select new { How = how, Tlist = lst };
Этот запрос формируется следующим образом. В операторе from используется переменная диапазона how для охвата всего массива travelTypes. Напомним, что массив travelTypes содержит названия общих категорий транспорта: воздушного, наземного, морского и речного. Каждый вид транспорта объединяется в операторе join со своей категорией. Например, велосипед, автомашина и поезд объединяются с наземным транспортом. Но благодаря оператору into для каждой категории транс порта в операторе join составляется список видов транспорта, относящихся к данной категории. Этот список сохраняется в переменной lst. И наконец, оператор select возвращает объект анонимного, типа, инкапсулирующий каждое значение перемен ной how (категории транспорта) вместе со списком видов транспорта. Именно поэтому для вывода результатов запроса требуются два цикла foreach.
foreach(var t in byHow) {
Console.WrifeLine("К категории <{0} транспорт> относится:", t.How);
foreach(var m in t.Tlist)
Console.WriteLine(" " + m.Name);
Console.WriteLine();
}
Во внешнем цикле получается объект, содержащий наименование общей катего рии транспорта, и список видов транспорта, относящихся к этой категории. А во вну треннем цикле выводятся отдельные виды транспорта.
Синтаксис запроса, описанный в предыдущих разделах, применяется при форми ровании большинства запросов в С#. Он удобен, эффективен и компактен, хотя и не является единственным способом формирования запросов. Другой способ состоит в использовании методов запроса, которые могут вызываться для любого перечислимо го объекта, например массива.
Методы запроса определяются в классе System.Linq.Enumerable и реализуются
в виде методов расширения функций обобщенной формы интерфейса IEnumerable.
(Методы запроса определяются также в классе System.Linq.Queryable, расширяю
щем функции обобщенной формы интерфейса IQueryable<T>
, но этот интерфейс
в настоящей главе не рассматривается.) Метод расширения дополняет функции друго
го класса, но без наследования. Поддержка методов расширения была внедрена в вер
сию C# 3.0 и более подробно рассматривается далее в этой главе. А до тех пор доста
точно сказать, что методы запроса могут вызываться только для тех объектов, которые
реализуют интерфейс IEnumerable<T>
.
В классе Enumerable предоставляется немало методов запроса, но основными счи таются те методы, которые соответствуют описанным ранее операторам запроса. Эти методы перечислены ниже вместе с соответствующими операторами запроса. Следует, однако, иметь в виду, что эти методы имеют также перегружаемые формы, а здесь они представлены лишь в самой простой своей форме. Но именно эта их форма использу ется чаще всего.
Оператор запроса | Эквивалентный метод запроса |
---|---|
select | Select(selector) |
where | Where(predicate) |
orderby | OrderBy(keySelector) или OrderByDescending(keySelector) |
join | Join(inner, outerKeySelector, innerKeySelector, resultSelector) |
group | GroupBy(keySelector) |
За исключением метода Join(), остальные методы запроса принимают един ственный аргумент, который представляет собой объект некоторой разновидности обобщенного типа Func<T, TResult>. Это тип встроенного делегата, объявляемый следующим образом:
delegate TResult Func<in Т, out TResult>(T arg)
где TResult обозначает тип результата, который дает делегат, а Т — тип элемента. В методах запроса аргументы selector, predicate или keySelector определяют действие, которое предпринимает метод запроса. Например, в методе Where() аргу мент prediсаte определяет порядок отбора данных в запросе. Каждый метод запроса возвращает перечислимый объект. Поэтому результат выполнения одного метода за проса можно использовать для вызова другого, соединяя эти методы в цепочку.
Метод Join() принимает четыре аргумента. Первый аргумент (inner) представляет собой ссылку на вторую объединяемую последовательность, а первой является последо вательность, для которой вызывается метод Join(). Селектор ключа для первой последо вательности передается в качестве аргумента outerKeySelector, а селектор ключа для второй последовательности — в качестве аргумента LnnerKeySelector. Результат объе динения обозначается как аргумент resultSelector. Аргумент outerKeySelector имеет тип Func<TOuter, ТКеу>, аргумент innerKeySelector — тип Func<TInner, ТКеу>, тогда как аргумент resultSelector — тип Func<TOuter, Tinner, TResult>, где TOuter — тип элемента из вызывающей последовательности; Tinner — тип эле мента из передаваемой последовательности; TResult — тип элемента из объеди няемой в итоге последовательности, возвращаемой в виде перечислимого объекта.
Аргумент метода запроса представляет собой метод, совместимый с указываемой формой делегата Func, но он не обязательно должен быть явно объявляемым мето дом. На самом деле вместо него чаще всего используется лямбда-выражение. Как по яснялось в главе 15, лямбда-выражение обеспечивает более простой, но эффективный способ определения того, что, по существу, является анонимным методом, а компиля тор C# автоматически преобразует лямбда-выражение в форму, которая может быть передана в качестве параметра делегату Func. Благодаря тому что лямбда-выражения обеспечивают более простой и рациональный способ программирования, они исполь зуются во всех примерах, представленных далее в этом разделе.
Используя методы запроса одновременно с лямбда-выражениями, можно форми ровать запросы, вообще не пользуясь синтаксисом, предусмотренным в C# для запро сов. Вместо этого достаточно вызвать соответствующие методы запроса. Обратимся сначала к простому примеру. Он представляет собой вариант первого примера про граммы из этой главы, переделанный с целью продемонстрировать применение мето дов запроса Where() и Select() вместо соответствующих операторов.
// Использовать методы запроса для формирования простого запроса.
// Это переделанный вариант первого примера программы из настоящей главы.
using System;
using System.Linq;
class SimpQuery {
static void Main() {
int[] nums = { 1, -2, 3, 0, -4, 5 };
// Использовать методы Where() и Select() для
// формирования простого запроса.
var posNums = nums.Where(n => n > 0).Select(r => r);
Console.Write("Положительные значения из массива nums: ");
// Выполнить запрос и вывести его результаты.
foreachfint i in posNums) Console.Write(i + " ");
Console.WriteLine();
}
}
Эта версия программы дает такой же результат, как и исходная.
Положительные значения из массива nums: 1 3 5
Обратите особое внимание в данной программе на следующую строку кода.
var posNums = nums.Where(n => n > 0).Select(r => r);
В этой строке кода формируется запрос, сохраняемый в переменной posNums. По этому запросу, в свою очередь, формируется последовательность положительных зна чений, извлекаемых из массива nums. Для этой цели служит метод Where(), отбираю щий запрашиваемые значения, а также метод Select(), избирательно формирующий из этих значений окончательный результат. Метод Where() может быть вызван для массива nums, поскольку во всех массивах реализуется интерфейс IEnumerable, поддерживающий методы расширения запроса.
Формально метод Select() в рассматриваемом здесь примере не нужен, посколь ку это простой запрос. Ведь последовательность, возвращаемая методом Where(), уже содержит конечный результат. Но окончательный выбор можно сделать и по более сложному критерию, как это было показано ранее на примерах использования син таксиса запросов. Так, по приведенному ниже запросу из массива nums возвращаются положительные значения, увеличенные на порядок величины.
var posNums = nums.Where(n => n > 0).Select(r => r * 10);
Как и следовало ожидать, в цепочку можно объединять и другие операции над дан ными, получаемыми по запросу. Например, по следующему запросу выбираются по ложительные значения, которые затем сортируются по убывающей и возвращаются в виде результирующей последовательности:
var posNums = nums.Where(n => n > 0).OrderByDescending(j => j);
где выражение j => j обозначает, что упорядочение зависит от входного параметра, который является элементом данных из последовательности, получаемой из метода Where().
В приведенном ниже примере демонстрируется применение метода запроса GroupBy(). Это измененный вариант представленного ранее примера.
// Продемонстрировать применение метода запроса GroupBy().
// Это переработанный вариант примера, представленного ранее
// для демонстрации синтаксиса запросов.
using System;
using System.Linq;
class GroupByDemo {
static void Main() {
string[] websites = {
"hsNameA.com", "hsNameB.net", "hsNameC.net",
"hsNameD.com", "hsNameE.org", "hsNameF.org",
"hsNameG.tv", "hsNameH.net", "hsNameI.tv"
};
// Использовать методы запроса для группирования
// веб-сайтов по имени домена самого верхнего уровня.
var webAddrs = websites.Where(w => w.LastIndexOf('.') != 1).
GroupBy(x => x.Substring(x.LastIndexOf(".", x.Length)));
// Выполнить запрос и вывести его результаты.
foreach(var sites in webAddrs) {
Console.WriteLine("Веб-сайты, сгруппированные " +
"по имени домена " + sites.Key);
foreach(var site in sites)
Console.WriteLine(" " + site);
Console.WriteLine();
}
}
}
Эта версия программы дает такой же результат, как и предыдущая. Единственное отличие между ними заключается в том, как формируется запрос. В данной версии для этой цели используются методы запроса.
Рассмотрим другой пример. Но сначала приведем еще раз запрос из представлен ного ранее примера применения оператора join.
var inStockList = from item in items
join entry in statusList
on item.ItemNumber equals entry.ItemNumber
select new Temp(item.Name, entry.InStock);
По этому запросу формируется последовательность, состоящая из объектов, инкап сулирующих наименование товара и состояние его запасов на складе. Вся эта инфор мация получается путем объединения двух источников данных: items и statusList. Ниже приведен переделанный вариант данного запроса, в котором вместо синтаксиса, предусмотренного в C# для запросов, используется метод запроса Join().
// Использовать метод запроса Join() для составления списка
// наименований товаров и состояния их запасов на складе.
var inStockList = items.Join(statusList,
k1 => k1.ItemNumber,
k2 => k2.ItemNumber,
(k1, k2) => new Temp(k1.Name, k2.InStock) );
В данном варианте именованный класс Temp используется для хранения результи рующего объекта, но вместо него можно воспользоваться анонимным типом. Такой вариант запроса приведен ниже.
var inStockList = items.Join(statusList,
k1 => k1.ItemNumber,
k2 => k2.ItemNumber,
(k1, k2) => new { k1.Name, k2.InStock} );
Как пояснялось в предыдущем разделе, запросы в C# можно формировать двумя способами, используя синтаксис запросов или методы запроса. Любопытно, что оба способа связаны друг с другом более тесно, чем кажется, глядя на исходный код про граммы. Дело в том, что синтаксис запросов компилируется в вызовы методов запроса. Поэтому код
where х < 10
будет преобразован компилятором в следующий вызов.
Where(х => х < 10)
Таким образом, оба способа формирования запросов в конечном итоге сходятся на одном и том же.
Но если оба способа оказываются в конечном счете равноценными, то какой из них лучше для программирования на С#? В целом, рекомендуется чаще пользоваться син таксисом запросов, поскольку он полностью интегрирован в язык С#, поддерживается соответствующими ключевыми словами и синтаксическим конструкциями.
Помимо методов, соответствующих операторам запроса, поддерживаемым в С#, име ется ряд других методов расширения, связанных с запросами и зачастую оказывающих помощь в формировании запросов. Эти методы предоставляются в среде .NET Framework и определены для интерфейса IEnumerable в классе Enumerable. Ниже приведе ны наиболее часто используемые методы расширения, связанные с запросами. Многие из них могут перегружаться, поэтому они представлены лишь в самой общей форме.
Метод | Описание |
---|---|
All(predicate) | Возвращает логическое значение true, если все элементы в последовательности удовлетворяют условию, задаваемому параметром predicate |
Any(predicate) | Возвращает логическое значение true, если любой элемент в последовательности удовлетворяет условию, задаваемому параметром predicate |
Average() | Возвращает среднее всех значений в числовой последовательности |
Contains(value) | Возвращает логическое значение true, если в последовательности содержится указанный объект |
Count() | Возвращает длину последовательности, т.е. количество составляющих ее элементов |
First() | Возвращает первый элемент в последовательности |
Last() | Возвращает последний элемент в последовательности |
Max() | Возвращает максимальное значение в последовательности |
Min() | Возвращает минимальное значение в последовательности |
Sum() | Возвращает сумму значений в числовой последовательности |
Метод Count() уже демонстрировался ранее в этой главе. А в следующей програм ме демонстрируются остальные методы расширения, связанные с запросами.
// Использовать ряд методов расширения, определенных в классе Enumerable.
using System;
using System.Linq;
class ExtMethods {
static void Main() {
int[] nums ={ 3, 1, 2, 5, 4 };
Console.WriteLine("Минимальное значение равно " + nums.Min());
Console.WriteLine("Максимальное значение равно " + nums.Max());
Console.WriteLine("Первое значение равно " + nums.First());
Console.WriteLine("Последнее значение равно " + nums.Last());
Console.WriteLine("Суммарное значение равно " + nums.Sum());
Console.WriteLine("Среднее значение равно " + nums.Average());
if(nums.All(n => n > 0))
Console.WriteLine("Все значения больше нуля.");
if(nums.Any(n => (n % 2) == 0))
Console.WriteLine("По крайней мере одно значение является четным.");
if(nums.Contains(3))
Console.WriteLine("Массив содержит значение 3.");
}
}
Вот к какому результату приводит выполнение этой программы.
Минимальное значение равно 1
Максимальное значение равно 5
Первое значение равно 3
Последнее значение равно 4
Суммарное значение равно 15
Среднее значение равно 3
Все значения больше нуля.
По крайней мере одно значение является четным
Массив содержит значение 3.
Методы расширения, связанные с запросами, можно также использовать в самом запросе, основываясь на синтаксисе запросов, предусмотренном в С#. И в действитель ности это делается очень часто. Например, метод Average() используется в приве денной ниже программе для получения последовательности, состоящей только из тех значений, которые оказываются меньше среднего всех значений в массиве.
// Использовать метод Average() вместе с синтаксисом запросов.
using System;
using System.Linq;
class ExtMethods2 {
static void Main() {
int[] nums = { 1, 2, 4, 8, 6, 9, 10, 3, 6, 7 };
var ltAvg = from n in nums
let x = nums.Average()
where n < x
select n;
Console.WriteLine("Среднее значение равно " + nums.Average());
Console.Write("Значения меньше среднего: ");
// Выполнить запрос и вывести его результаты.
foreach(int i in ltAvg) Console.Write(i + " ");
Console.WriteLine();
}
}
При выполнении этой программы получается следующий результат.
Среднее значение равно 5.6
Значения меньше среднего: 1 2 4 3
Обратите особое внимание в этой программе на следующий код запроса.
var ltAvg = from n in nums
let x = nums.Average()
where n < x
select n;
Как видите, переменной x в операторе let присваивается среднее всех значений в массиве nums. Это значение получается в результате вызова метода Average() для массива nums.
В LINQ запросы выполняются в двух разных режимах: немедленном и отложенном. Как пояснялось ранее в этой главе, при формировании запроса определяется ряд пра вил, которые не выполняются вплоть до оператора цикла foreach. Это так называе мое отложенное выполнение.
Но если используются методы расширения, дающие результат, отличающийся от последовательности, то запрос должен быть выполнен для получения этого результа та. Рассмотрим, например, метод расширения Count(). Для того чтобы этот метод возвратил количество элементов в последовательности, необходимо выполнить за прос, и это делается автоматически при вызове метода Count(). В этом случае имеет место немедленное выполнение, когда запрос выполняется автоматически для получения требуемого результата. Таким образом, запрос все равно выполняется, даже если он не используется явно в цикле foreach.
Ниже приведен простой пример программы для получения количества положи тельных элементов, содержащихся в последовательности.
// Использовать режим немедленного выполнения запроса.
using System;
using System.Linq;
class ImmediateExec {
static void Main() {
int[] nums = { 1, -2, 3, 0, -4, 5 };
// Сформировать запрос на получение количества
// положительных значений в массиве nums.
int len = (from n in nums
where n > 0
select n).Count ();
Console.WriteLine("Количество положительных значений в массиве nums: " +
len);
}
}
Эта программа дает следующий результат.
Количество положительных значений в массиве nums: 3
Обратите внимание на то, что цикл foreach не указан в данной программе явным образом. Вместо этого запрос выполняется автоматически благодаря вызову метода расширения Count().
Любопытно, что запрос из приведенной выше программы можно было бы сформи ровать и следующим образом.
var posNums = from n in nums
where n > 0
select n;
int len = posNums.Count(); // запрос выполняется здесь
В данном случае метод Count() вызывается для переменной запроса. И в этот мо мент запрос выполняется для получения подсчитанного количества.
К числу других методов расширения, вызывающих немедленное выполнение за проса, относятся методы ТоАrray() и ToList(). Оба этих метода расширения опре делены в классе Enumerable. Метод ToArray() возвращает результаты запроса в мас сиве, а метод ToList() — результаты запроса в форме коллекции List. (Подробнее о коллекциях речь пойдет в главе 25.) В обоих случаях для получения результатов вы полняется запрос. Например, в следующем фрагменте кода сначала получается массив результатов, сформированных по приведенному выше запросу в переменной posNums, а затем эти результаты выводятся на экран.
int[] pnums = posNum.ТоАrrау(); // запрос выполняется здесь
foreach(int i in pnums)
Console.Write(i + " ");
}
Еще одним средством, связанным с LINQ, является дерево выражений, которое пред
ставляет лямбда-выражение в виде данных. Это означает, что само лямбда-выражение
нельзя выполнить, но можно преобразовать в исполняемую форму. Деревья выражений
инкапсулируются в классе System.Linq.Expressions.Expression<TDelegate>
.
Они оказываются пригодными в тех случаях, когда запрос выполняется вне програм мы, например средствами SQL в базе данных. Если запрос представлен в виде данных, то его можно преобразовать в формат, понятный для базы данных. Этот процесс вы полняется, например, средствами LINQ to SQL в интегрированной среде разработки Visual Studio. Таким образом, деревья выражений способствуют поддержке в C# раз личных баз данных.
Для получения исполняемой формы дерева выражений достаточно вызвать метод Compile(), определенный в классе Expression. Этот метод возвращает ссылку, ко торая может быть присвоена делегату для последующего выполнения. А тип делегата может быть объявлен собственным или же одним из предопределенных типов деле гата Func в пространстве имен System. Две формы делегата Func уже упоминались ранее при рассмотрении методов запроса, но существует и другие его формы. Деревьям выражений присуще следующее существенное ограничение: они могут представлять только одиночные лямбда-выражения. С их помощью нельзя предста вить блочные лямбда-выражения.
Ниже приведен пример программы, демонстрирующий конкретное применение дерева выражений. В этой программе сначала создается дерево выражений, данные которого представляют метод, определяющий, является ли одно целое число множи телем другого. Затем это дерево выражений компилируется в исполняемый код. И на конец, в этой программе демонстрируется выполнение скомпилированного кода.
// Пример простого дерева выражений.
using System;
using System.Linq;
using System.Linq.Expressions;
class SimpleExpTree {
static void Main() {
// Представить лямбда-выражение в виде данных.
Expression<Func<int, int, bool>>
IsFactorExp = (n, d) => (d != 0) ? (n % d) == 0 : false;
// Скомпилировать данные выражения в исполняемый код.
Func<int, int, bool> IsFactor = IsFactorExp.Compile();
// Выполнить выражение.
if(IsFactor(10, 5))
Console.WriteLine("Число 5 является множителем 10.");
if(!IsFactor (10, 7))
Console.WriteLine("Число 7 не является множителем 10.");
Console.WriteLine();
}
}
Вот к какому результату приводит выполнение этой программы.
Число 5 является множителем 10.
Число 7 не является множителем 10.
Данный пример программы наглядно показывает два основных этапа применения дерева выражений. Сначала в ней создается дерево выражений с помощью следующе го оператора.
Expression<Func<int, int, bool>>
IsFactorExp = (n, d) => (d != 0) ? (n % d) == 0 : false;
В этом операторе конструируется представление лямбда-выражения в оперативной памяти. Как пояснялось выше, это представление доступно по ссылке, присваиваемой делегату IsFactorExp. А в следующем операторе данные выражения преобразуются в исполняемый код.
Func<int, int, bool> IsFactor = IsFactorExp.Compile();
После выполнения этого оператора делегат IsFactorExp может быть вызван, что бы определить, является ли одно целое число множителем другого.
Обратите также внимание на то, что <Func<int, int, bool>
обозначает тип
делегата. В этой форме делегата Func указываются два параметра типа int и воз
вращаемый тип bool. В рассматриваемой здесь программе использована именно эта
форма делегата Func, совместимая с лямбда-выражениями, поскольку для выраже
ния требуются два параметра. Для других лямбда-выражений могут подойти иные
формы делегата Func в зависимости от количества требуемых параметров. Вообще
говоря, конкретная форма делегата Func должна удовлетворять требованиям лямбда-
выражения.
Как упоминалось выше, методы расширения предоставляют средства для расшире ния функций класса, не прибегая к обычному механизму наследования. Методы рас ширения создаются нечасто, поскольку механизм наследования, как правило, пред лагает лучшее решение. Тем не менее знать, как они действуют, никогда не помешает. Ведь они имеют существенное значение для LINQ.
Метод расширения является статическим и поэтому должен быть включен в со став статического, необобщенного класса. Тип первого параметра метода расширения определяет тип объектов, для которых этот метод может быть вызван. Кроме того, первый параметр может быть указан с модификатором this. Объект, для которого вызывается метод расширения, автоматически передается его первому параметру. Он не передается явным образом в списке аргументов. Следует, однако, иметь в виду, что метод расширения может по-прежнему вызываться для объекта аналогично методу экземпляра, несмотря на то, что он объявляется как статический.
Ниже приведена общая форма метода расширения.
static возращаемый_тип имя (this тип_вызывающего_объекта ob, список_параметров)
Очевидно, что список_параметров окажется пустым в отсутствие аргументов, за исключением аргумента, неявно передаваемого вызывающим объектом ob. Не следует, однако, забывать, что первым параметром метода расширения является автоматиче ски передаваемый объект, для которого вызывается этот метод. Как правило, метод расширения становится открытым членом своего класса.
В приведенном ниже примере программы создаются три простых метода расширения.
// Создать и использовать ряд методов расширения.
using System;
using System.Globalization;
static class MyExtMeths {
// Возвратить обратную величину числового значения типа double.
public static double Reciprocal(this double v) {
return 1.0 / v;
}
// Изменить на обратный регистр букв в символьной
// строке и возвратить результат.
public static string RevCase(this string str) {
string temp = "";
foreach(char ch in str) {
if(Char.IsLower(ch)) temp += Char.ToUpper(ch, CultureInfo.
CurrentCulture);
else temp += Char.ToLower(ch, CultureInfo.CurrentCulture);
}
return temp;
}
// Возвратить абсолютное значение выражения n / d.
public static double AbsDivideBy(this double n, double d) {
return Math.Abs(n / d);
}
}
class ExtDemo {
static void Main() {
double val = 8.0;
string str = "Alpha Beta Gamma";
// Вызвать метод расширения Reciprocal()..
Console.WriteLine("Обратная величина {0} равна {1}",
val, val.Reciprocal());
// Вызвать метод расширения RevCase().
Console.WriteLine(str + " после смены регистра: " +
str.RevCase());
// Использовать метод расширения AbsDivideBy().
Console.WriteLine("Результат вызова метода val.AbsDivideBy(-2): " +
val.AbsDivideBy(-2));
}
}
Эта программа дает следующий результат.
Обратная величина 8 равна 0.125
Alpha Beta Gamma после смены регистра: aLPHA bЕТА gАММА
Результат вызова метода val.AbsDivideBy(-2): 4
В данном примере программы каждый метод расширения содержится в стати ческом классе MyExtMeths. Как пояснялось выше, метод расширения должен быть объявлен в статическом классе. Более того, этот класс должен находиться в области действия своих методов расширения, чтобы ими можно было пользоваться. (Именно поэтому в исходный текст программы следует включить пространство имен System. Linq, так как это дает возможность пользоваться методами расширения, связанными с LINQ.)
Объявленные методы расширения вызываются для объекта таким же образом, как и методы экземпляра. Главное отличие заключается в том, что вызывающий объект передается первому параметру метода расширения. Поэтому при выполнении выра жения
val.AbsDivideBy(-2)
объект val передается параметру n метода расширения AbsDivideBy(), а значение -2 — параметру d.
Любопытно, что методы расширения Reciprocal() и AbsDivideBy() могут вполне законно вызываться и для литерала типа double, как показано ниже, посколь ку они определены для этого типа данных.
8.0.Reciprocal()
8.0.AbsDivideBy(-1)
Кроме того, метод расширения RevCase() может быть вызван следующим образом.
"AbCDe".RevCase()
В данном случае возвращается строковый литерал с измененным на обратный регистром букв.
В версии .NET Framework 4.0 внедрено новое дополнение LINQ под названием PLINQ. Это средство предназначено для поддержки параллельного программирова ния. Оно позволяет автоматически задействовать в запросе несколько доступных про цессоров. Подробнее о PLINQ и других средствах, связанных с параллельным програм мированием, речь пойдет в главе 24.