Класс Enumerable LINQ

ОГЛАВЛЕНИЕ

В качестве части работы над недавним программным проектом мне пришлось создавать случайным образом упорядоченный список целых чисел из интервала от 1 до 100. Да, очевидно, что это базовая проблема информатики типа 101. Исходно я использовал то же решение, которое написал в школьном классе в 1985 году, используя множество циклов, сравнений, датчиков случайных чисел и несколько массивов. Но теперь, в эпоху декларативного программирования, такое решение меня очень смущало.

После некоторого исследования мне удалось свести задачу всего лишь к двум строкам кода.

Dim rnd As New System.Random()
Dim numbers = Enumerable.Range(1, 100). _
  OrderBy(Function() rnd.Next)

После выполнения этих двух строк переменная numbers содержала целые числа из интервала от 1 до 100 в случайном порядке.

Поведение этого простого, но элегантного решения опирается на класс System.Linq.Enumerable, обеспечивающий несколько десятков совместно используемых методов, которые дают возможность манипулировать данными в любом классе, реализующем интерфейс IEnumerable(Of T). Кроме того, как видно из данного примера, эти методы могут также создавать новые коллекции данных.

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

В качестве примера я создал простое приложение на базе Windows®. В этом проекте я добавил простые классы, представляющие данные о клиентах, категориях и продуктах, и добавил класс с именем SimpleDataContext, наполняющий экземпляры этих классов данными из примера Northwind базы данных SQL Server®. Помимо этого, данный класс обеспечивает свойства с именами Customers, Categories и Products; каждое из них содержит, соответственно, общие экземпляры List of Customer, Category и Product. (Хотя можно было бы использовать конструктор LINQ к SQL для создания «настоящего» класса DataContext, такой подход не позволил бы мне продемонстрировать методы расширения класса Enumerable: когда описанные методы вызываются в контексте LINQ к SQL, в действительности вызываются методы расширения класса Queryable поставщика LINQ к SQL, которые имеют такую же сигнатуру, как и приводимые здесь методы. Хотя такие примеры продемонстрировали бы такое же поведение, в них не использовался бы класс Enumerable.) Я настойчиво рекомендую загрузить пример приложения и воспользоваться этим приложением — код будет гораздо понятнее, если вы сможете экспериментировать с ним напрямую, а не просто читать статью.

Для облегчения работы со списком методов я разбил их на логические группы. Будут исследоваться методы, занимающиеся созданием, выбором, извлечением отдельного элемента, фильтрацией, преобразованием, позиционированием в рамках последовательности, упорядочиванием, проверкой, вычислениями и выполнением набора операций с последовательностями данных. Во всех последующих разделах будут исследоваться различные примеры, демонстрирующие один или несколько методов класса Enumerable. К концу вы освоите все методы класса и исследуете множество перегрузок для более сложных методов.

Обратите внимание, что примеры, как правило, дают возможность Visual Basic® логически выводить типы данных вместо того, чтобы иметь код, указывающий тип возвращаемого значения для вызовов, обращенных к методам расширения класса Enumerable. Кроме этого, пример опирается на пример базы данных Northwind, установленной в среде SQL Express. Если потребуется выполнить пример, необходимо будет либо сделать копию этой среды, либо изменить пример приложения.


Enumerable изнутри

Класс Enumerable играет важную роль в каждом создаваемом запросе LINQ. Например, представьте, что в ваш проект входит класс Product и общие экземпляры List of Product (как в примере проекта). Вы создали на основе примера экземпляр класса SimpleDataContext, который наполняет данными список Products, и пишете запрос, подобный следующему.

Dim db As New SimpleDataContext()
Dim query = _
  From prod In db.Products _
  Where prod.CategoryID = 1 _
  Order By prod.ProductName _
  Select prod.ProductName

Если вы долго размышляли над этим, вероятно, вы осознали, что компилятор Visual Basic должен «за кадром» преобразовать синтаксис типа SQL в некоторый набор вызовов методов из некоторого экземпляра подобного типа. Действительно, в этом примере свойство Products класса SimpleDataContext имеет тип List(Of Product), и, как вы, возможно, догадались, тип List(Of T) реализует интерфейс IEnumerable(Of T). Компилятор преобразует ключевые слова LINQ в соответствующие вызовы методов расширения из класса типа Enumerable. А именно, можно создать в точности такой же набор команд, переписав запрос LINQ следующим образом.

Dim query = _
  db.Products _
  .Where(Function(prod) CBool(prod.CategoryID = 1)) _
  .OrderBy(Function(prod) prod.ProductName) _
  .Select(Function(prod) prod.ProductName)

Если изучить промежуточный язык (IL), созданный компилятором, с помощью запроса LINQ, использующего ключевые слова Visual Basic, будет видно, что компилятор преобразует ключевые слова Visual Basic в вызовы соответствующих методов класса Enumerable. В результате расширяется поведение класса System.Collections.Generic.List(Of T). (По существу, это легко определить — загляните в документацию по членам этого конкретного класса, и вы увидите список методов расширения, большая часть которых определяется классом Enumerable.)

Что все это означает для вас? Поскольку методы расширения класса Enumerable могут обрабатывать многие другие классы, включая Array и List, есть возможность использовать методы класса Enumerable не только для создания запросов LINQ, но также для манипулирования поведением массивов и других структур данных.

Если изучить документацию по методам расширения класса Enumerable, выяснится, что все члены являются общими (Shared). В итоге у вас есть два варианта вызова метода. Во-первых, его можно вызывать как метод декларативного типа, передавая экземпляр расширенного класса (в предположении, что переменная с результатами имеет тип, который может быть обработан методом Count), следующим образом.

Dim itemCount = _
  Enumerable.Count(results, _
  Function(prod) prod.CategoryID = 3)

Count можно вызвать также, как если бы он был методом экземпляра самой коллекции. При таком способе вызова метода расширения вам не требуется передавать первый параметр.

Dim itemCount = _
  results.Count(Function(prod) _
  prod.CategoryID = 3)

Visual Basic и C# по-разному выполняют преобразование ключевых слов в вызовы методов расширения. Visual Basic предоставляет большее, чем C#, число ключевых слов, которые можно использовать в запросах LINQ. Например, с помощью ключевого слова Take можно создать запрос LINQ в Visual Basic следующим образом.

Dim db As New SimpleDataContext()
Dim query = _
  From prod In db.Products _
  Where prod.CategoryID = 1 _
  Order By prod.ProductName _
  Take 10 _
  Select prod.ProductName

Можно также использовать метод расширения Take, предоставляемый классом Enumerable, следующим образом.

Dim query = _
  (From prod In db.Products _
  Where prod.CategoryID = 1 _
  Order By prod.ProductName _
  Select prod.ProductName).Take(10)

Поскольку C# не предоставляет ключевого слова Take, не остается другого выбора, кроме как явно вызвать метод Take. На рис. 1 представлено соответствие ключевых слов запросов LINQ в Visual Basic и соответствующих методов класса Enumerable.

Рис. 1 Связь ключевых слов LINQ и методов класса Enumerable

Ключевое слово запроса LINQ в Visual BasicМетод класса Enumerable
Aggregate … Into … AllAll
Aggregate … Into … AllAny
Aggregate … Into … AverageAverage
Aggregate … Into … CountCount
Aggregate … Into … LongCountLongCount
Aggregate … Into … MaxMax
Aggregate … Into … MinMin
Aggregate … Into … SumSum
DistinctDistinct
Group ByGroupBy
Group JoinGroupJoin
Order ByOrderBy
Order By … DescendingOrderByDescending
Order By (с несколькими полями)ThenBy
Order By … Descending (с несколькими полями)ThenByDescending
SelectSelect
SkipSkip
TakeTake
Take WhileTakeWhile
WhereWhere


Создание последовательностей

Класс Enumerable предоставляет несколько общих методов, которые не являются методами расширения, но существуют для обеспечения возможности создания новых последовательностей. Начнем с изучения этих простых методов.

Метод Enumerable.Range создает новый последовательный список целых чисел. Указывается начальное значение и число элементов в списке. Метод возвращает последовательность IEnumerable(Of Integer). Этот метод можно использовать для решения исходной задачи данной статьи (т.е. для получения списка чисел, находящихся между двумя конечными точками и идущих в случайном порядке).

Следующий код заполняет список с помощью метода Range, затем вызывает метод Enumerable.OrderBy при помощи простого лямбда-выражения для обеспечения случайного порядка в последовательности.

' From RangeDemo in the sample:
Dim rnd As New System.Random
Dim items = Enumerable.Range(1, 10)
Dim randomList = _
  items.OrderBy(Function() rnd.Next())

При нескольких запусках примера процедуры каждый раз возвращаются разные результаты, но при каждом выполнении кода вы увидите список, подобный следующему.

6, 8, 7, 1, 2, 9, 10, 4, 3, 5

Метод Enumerable.Reverse возвращает входную последовательность в обратном порядке. В следующем коде метод Reverse вызывается и в качестве общего метода класса Enumerable, и в качестве метода экземпляра со списком, полученным в результате вызова метода Enumerable.Range.

'From ReverseDemo in the sample:
Dim items = Enumerable.Range(1, 10)
Dim reversed = items.Reverse()
reversed = Enumerable.Reverse(items)

Каким бы способом ни вызывался метод Reverse, в результате получается следующий список.

10, 9, 8, 7, 6, 5, 4, 3, 2, 1

Метод Enumerable.Repeat создает список, содержащий поданное на вход значение, повторенное указанное число раз.

'From RepeatDemo in the sample:
Dim repeated = Enumerable.Repeat("Hello", 5)

После вызова предыдущего кода повторяющаяся переменная содержит следующие элементы.

Hello, Hello, Hello, Hello, Hello

Метод Enumerable.Empty служит для создания пустой последовательности IEnumerable(Of T), подготовленной для приема данных указанного типа. С помощью этого метода можно создать пустой экземпляр коллекции и по необходимости добавлять в нее элементы. Если инициализировать коллекцию в пустой коллекции, никогда не придется беспокоиться о том, не имеет ли ссылка значение Nothing.

'From EmptyDemo in the sample:
Dim emptyList = _
 Enumerable.Empty(Of Customer)()


Выбор последовательностей

Класс Enumerable предоставляет два разных метода расширения, которые позволяют выбрать последовательность элементов, проецирующую элементы из входной последовательности в выходную последовательность. Метод Enumerable.Select просто проецирует каждый элемент в новую форму. В следующем примере берется последовательность, содержащая клиентов из США из таблицы Customers базы данных Northwind, затем результаты проецируются в последовательность, содержащую нумерованный список имен контактных лиц.

' From SelectDemo in the sample:
Dim db As New SimpleDataContext
Dim results = _
  From cust In db.Customers _
  Where cust.Country = "USA"
Dim selectResults = results.Select( _
  Function(cust, index) _
  String.Format("{0}. {1}", _
  index + 1, cust.ContactName))

Обратите внимание, что у метода Select имеется несколько перегруженных версий. После выполнения этого кода selectResults содержит список значений, сходный со следующим.

  1. Howard Snyder
  2. Yoshi Latimer
  3. John Steel
  4. Jaime Yorres
  5. Fran Wilson
  6. Rene Phillips
  7. Paula Wilson
  8. Jose Pavarotti
  9. Art Braunschweiger
  10. Liz Nixon
  11. Liu Wong
  12. Helvetius Nagy
  13. Karl Jablonski

Метод Enumerable.SelectMany берет коллекцию элементов, каждый из которых может быть коллекцией элементов, и сворачивает двумерные входные данные в одномерную выходную последовательность. Этот метод удобен, если требуется взять, к примеру, список категорий, каждая из которых содержит продукты, и получить в результате список продуктов с соответствующей информацией о категории. В следующем примере извлекается список категорий, содержащих менее семи продуктов, и результат проецируется в один список, содержащий информацию о каждом продукте наряду с информацией о категории.

' From SelectManyDemo in the sample:
Dim db As New SimpleDataContext
Dim categories = _
  From cat In db.Categories Where cat.Products.Count < 7
Dim manyResults As IEnumerable(Of String) = _
  categories.SelectMany(Function(cat, index) _
  cat.Products.Select( _
  Function(prod As Product) String.Format("{0}. Category {1}: {2}", _
  index, prod.CategoryID, prod.ProductName)))

В этом случае лямбда-выражение, переданное методу SelectMany, принимает ссылку на конкретную категорию, работая по очереди со всеми категориями. Данная конкретная перегрузка метода SelectMany передает лямбда-выражению индекс категории наряду с категорией. При данной категории и ее индексе лямбда-выражение вызывает метод Select свойства категории Products, обеспечивая его выполнение для каждого продукта в рамках категории. Данный пример возвращает последовательность, содержащую следующий список строк. Обращение к SelectMany завершается сворачиванием исходной иерархии категория/продукт в единственный список значений.

0. Category 6: Mishi Kobe Niku
0. Category 6: Alice Mutton
0. Category 6: Thuringer Rostbratwurst
0. Category 6: Perth Pasties
0. Category 6: Tourtiere
0. Category 6: Pate chinois
1. Category 7: Uncle Bob's Organic Dried Pears
1. Category 7: Tofu
1. Category 7: Rossle Sauerkraut
1. Category 7: Manjimup Dried Apples
1. Category 7: Longlife Tofu


Извлечение отдельного элемента

Зачастую требуется извлечь из последовательности единственный элемент и поработать с ним. Класс Enumerable предоставляет несколько методов, позволяющих отфильтровать содержимое последовательности до одного элемента. Методы Enumerable.First и Enumerable.Last возвращают соответственно первый или последний элемент последовательности. Метод Enumerable.Single возвращает элемент при наличии функции или лямбда-выражения, указывающего единственный элемент. Метод Enumerable.ElementAt возвращает из последовательности один элемент, обладающий конкретным индексом.

Все эти методы создают исключение, если в последовательности нет запрошенного элемента, или, в случае метода Single, если ограничения приводят либо к нулевому числу элементов, либо к нескольким элементам. Каждый из этих методов позволяет также указывать лямбда-выражение или функцию для ограничения входных данных. В любом случае результирующая последовательность должна содержать единственный элемент.

Класс Enumerable также предоставляет подобные методы, возвращающие единственный элемент, если это возможно, а если невозможно – член по умолчанию с типом, свойственным последовательности. К числу этих методов принадлежат Enumerable.FirstOrDefault, Enumerable.LastOrDefault, Enumerable.SingleOrDefault и Enumerable.ElementAtOrDefault. На рис. 2 показано поведение этих методов. Выполнение примера приводит к помещению следующих данных в выходной поток.

ALFKI: Maria Anders
ALFKI: Maria Anders
Last in USA: Karl Jablonski
Sequence contains no elements
Specified argument was out of the range of valid values.
Parameter name: index
CustomerID = XXXXX is Nothing: True
Customer 2 in USA is Nothing: False
Customer 200 in USA is Nothing: True

Рис. 2 Извлечение элементов с помощью методов класса Enumerable

' From SingleElementDemo in the sample:

Dim oneCustomer As Customer
Dim sw As New StringWriter

Dim db As New SimpleDataContext

Dim query0 = From cust In db.Customers

Dim query1 = _
  From cust In db.Customers _
  Where cust.CustomerID = "ALFKI"

Dim query2 = _
  From cust In db.Customers _
  Where cust.Country = "USA"

Dim query3 = _
  From cust In db.Customers _
  Where cust.CustomerID = "XXXXX"

' This works fine, finds the customer whose CustomerID is ALFKI:
oneCustomer = query1.Single
sw.WriteLine("ALFKI: {0}", oneCustomer.ContactName)

' You can supply an expression to filter, when calling 
' the First, Last, Single, and so on, methods:
oneCustomer = query0.Single( _
  Function(cust) cust.CustomerID = "ALFKI")
sw.WriteLine("ALFKI: {0}", oneCustomer.ContactName)

' This works fine, finds the last customer in the USA:
oneCustomer = query2.Last
sw.WriteLine("Last in USA: {0}", oneCustomer.ContactName)

' These two raise exceptions:
Try
  ' There's no customer use customer ID is "XXXXX":
  oneCustomer = query3.Single
Catch ex As Exception
  sw.WriteLine(ex.Message)
End Try

Try
  ' There aren't 200 customers in the USA:
  oneCustomer = query2.ElementAt(200)
Catch ex As Exception
  sw.WriteLine(ex.Message)
End Try

' These don't raise exceptions, but return default values
' if they fail:
oneCustomer = query3.SingleOrDefault
sw.WriteLine("CustomerID = XXXXX is Nothing: {0}", _
  oneCustomer Is Nothing)

' Because there are at least two customers in the USA,
' method returns a valid, single customer:
oneCustomer = query2.ElementAtOrDefault(2)
sw.WriteLine("Customer 2 in USA is Nothing: {0}", _
  oneCustomer Is Nothing)

oneCustomer = query2.ElementAtOrDefault(200)
sw.WriteLine("Customer 200 in USA is Nothing: {0}", _
  oneCustomer Is Nothing)


Фильтрация последовательностей

Методы Enumerable.Where, Enumerable.Distinct или Enumerable.OfType служат для фильтрации содержимого существующей последовательности и возврата в выходной последовательности подмножества исходных данных. Метод OfType фильтрует входную последовательность в соответствии с указанным типом. Представьте, что требуется выполнить операцию только с конкретным типом элементов управления из формы. С помощью метода OfType можно ограничить коллекцию элементов управления, предоставляемых формой, и выполнить перебор только требуемого подмножества элементов управления.

В следующем примере общий список заполняется данными, и извлекается выходной список целых чисел, а затем извлекается список строк.

' From OfTypeDemo in the sample:
Dim items As New List(Of Object)
items.Add("January")
items.Add(0)
items.Add("Monday")
items.Add(3)
items.Add(5)
items.Add("September")

Dim numbers = items.OfType(Of Integer)()
Dim strings = items.OfType(Of String)()

После выполнения кода два выходных списка содержат следующие данные.

0, 3, 5
January, Monday, September

Метод Enumerable.Where позволяет указать условие фильтрации входной последовательности. Вторая перегруженная версия предоставляет доступ к индексу каждого элемента коллекции, поэтому возможна также фильтрация на основе индексов. В следующем примере используются оба метода, Where и Select. Первый осуществляет фильтрацию и последующее проецирование результатов, извлекая из папки C:\Windows последовательность файлов, имеющих размер менее 100 байт.

' From WhereDemo in the sample:
Dim files As IEnumerable(Of FileInfo) = _
  New DirectoryInfo("C:\Windows").GetFiles()
Dim fileResults = _
  files _
  .Where(Function(file) file.Length < 100) _
  .Select(Function(file) _
  String.Format("{0} ({1})", file.Name, file.Length))

Поскольку массивы реализуют интерфейс IEnumerable, метод Where можно использовать для фильтрации содержимого массивов точно так же, как в случае любой другой коллекции. В этом случае код фильтрует массив объектов FileInfo, возвращаемый методом GetFiles. Обратите также внимание на то, что в примере каскадным способом вызывается метод Where, а затем метод Select. На моем компьютере данный пример вернул следующие результаты.

Addrfixr.ini (62)
bthservsdp.dat (12)
iltwain.ini (36)
S86D5A060.tmp (48)
setuperr.log (0)

Вторую перегрузку метода Where можно использовать для получения доступа к индексу каждого обрабатываемого входного элемента. Например, в результате следующего вызова метода Where возвращаются файлы размера менее 100 байт, находившиеся среди первых 20 входных файлов.

' From WhereDemo in the sample:
fileResults = _
  files _
  .Where(Function(file, index) _
  (file.Length < 100) And (index < 20)) _
  .Select(Function(file) _
  String.Format("{0} ({1})", file.Name, file.Length))

В этом случае на моем компьютере результаты выглядели следующим образом (отсутствующие файлы не вошли в первые 20 обработанных файлов):

Addrfixr.ini (62)
bthservsdp.dat (12)
iltwain.ini (36)

Метод Enumerable.Distinct позволяет фильтровать список таким образом, чтобы в выходной последовательност содержались только неповторяющиеся элементы из входного списка. Для простых значений (строки, числа, и т.д.) эта процедура не представляет сложностей.

Каким образом в случае списков сложных объектов механизм этапа выполнения справляется с операциями сравнения для определения факта наличия дубликатов? Простое сравнение переменных экземпляров невозможно, поскольку на практике это привело бы к простому сравнению адресов этих объектов в памяти. Вместо этого в случае сложных объектов необходимо предоставить блок сравнения — экземпляр класса, реализующий IEqualityComparer(Of T) для выполнения сравнения. (Этот же вопрос возникает для нескольких методов класса Enumerable и появится снова далее в этой статье.)

Предположим, что имеется последовательность, содержащая информацию о клиентах, и требуется уникальный список стран, соответствующих этим клиентам. Если бы имелся простой список стран, можно было бы для сравнения строк использовать блок сравнения по умолчанию. В данном случае вместо этого имеется список клиентов. (Конечно, можно было бы использовать метод Select для проецирования списка, чтобы получить список, содержащий только страны, но в таком случае были бы потеряны все оставшиеся данные, необходимые для других вычислений на основе списка.)

В пример проекта входит класс CustomerCountryComparer, реализующий интерфейс IEqualityComparer(Of Customer), как показано на рис. 3. В примере кода демонстрируются два разных способа применения метода Distinct.

Рис. 3 IEqualityComparer

Public Class CustomerCountryComparer
  Implements IEqualityComparer(Of Customer)

  Public Function Equals1( _
  ByVal x As Customer, ByVal y As Customer) As Boolean _
  Implements IEqualityComparer(Of Customer).Equals

  Return x.Country.Equals(y.Country)

  End Function

  Public Function GetHashCode1( _
  ByVal obj As Customer) As Integer _
  Implements IEqualityComparer(Of Customer).GetHashCode

  Return obj.Country.GetHashCode

  End Function
End Class

В первом способе сначала список клиентов проецируется на список названий стран, а затем используется блок сравнения строк по умолчанию для сокращения списка до исчезновения повторяющихся элементов. Во втором способе используется специальный класс CustomerCountryComparer для выполнения сравнений, и создается список без повторяющихся элементов, а затем результирующий список клиентов проецируется на список строк.

' From DistinctDemo in the sample:
Dim db As New SimpleDataContext

Dim countries As IEnumerable(Of String) = _
  db.Customers _
  .Select(Function(cust) cust.Country) _
  .Distinct()

countries = customers _
  .Distinct(New CustomerCountryComparer) _
  .Select(Function(cust) cust.Country)

Может возникнуть необходимость в корректной обработке пустых последовательностей, и метод Enumerable.DefaultIfEmpty возвращает либо входную последовательность целиком, либо экземпляр со значением по умолчанию, если последовательность пуста. С помощью этого метода всегда гарантировано получение по крайней мере одного элемента с типом, соответствующим последовательности.

Процедура на рис. 4 демонстрирует один вариант использования метода DefaultIfEmpty. Данный код наполняет переменную noCustomers пустым списком (не существует клиентов, чья страна называется «XXX»), затем создает список noCustomers1, вызывая метод DefaultIfEmpty. Выполнение кода примера дает следующие выходные данные.

noCustomers contains 0 element(s). noCustomers1 contains 1 element(s).
XXXXX

Рис. 4 Использование DefaultIfEmpty

' From DefaultIfEmptyDemo in the sample:
Dim db As New SimpleDataContext
Dim sw As New StringWriter

Dim noCustomers = _
  From cust In db.Customers Where cust.Country = "XXX"
Dim noCustomers1 = _
  noCustomers.DefaultIfEmpty()

Dim results = _
  String.Format( _
  "noCustomers contains {0} element(s). " & _
  "noCustomers1 contains {1} element(s).", _
  noCustomers.Count, noCustomers1.Count)
sw.WriteLine(results)

' You can specify the exact value to use if the 
' collection is empty:
noCustomers1 = noCustomers.DefaultIfEmpty( _
  New Customer With {.CustomerID = "XXXXX"})
For Each cust In noCustomers1
  sw.WriteLine(cust.CustomerID)
Next

В списке noCustomers1 содержится один элемент (клиент, для которого не внесены никакие сведения). Метод DefaultIfEmpty можно вызвать, передавая значение по умолчанию для единственного элемента, как при втором вызове метода.


Упорядочение последовательностей

Методы Enumerable.OrderBy, Enumerable.ThenBy, Enumerable.OrderByDescending и Enumerable.ThenByDescending можно использовать для обеспечения начального упорядочения, а затем нескольких вторичных упорядочиваний (в порядке возрастания или убывания). В следующем примере берется последовательность клиентов, которая упорядочивается с помощью нескольких из этих методов.

' From OrderByDemo in the sample:
Dim db As New SimpleDataContext
Dim customers = _
  From cust In db.Customers _
  Where cust.ContactTitle = "Owner"

' Sort first by country, then by city descending, then by name:
Dim results = _
  customers _
  .OrderBy(Function(cust) cust.Country) _
  .ThenByDescending(Function(cust As Customer) cust.City) _
  .ThenBy(Function(cust As Customer) cust.ContactName) _
  .Select(Function(cust) String.Format("({0}, {1}) {2}", _
  cust.Country, cust.City, cust.ContactName))

Обратите внимание, что Visual Basic не выразит недовольства, если вы не укажете типы данных для параметра лямбда-выражения при вызове методов ThenBy и ThenByDescending, но если этого не сделать, не будет возможности пользоваться поддержкой со стороны IntelliSense®. Только первый метод упорядочивания в состоянии логически определить тип данных параметра лямбда-выражения. Как это ни странно, метод Select правильно определяет тип, даже в конце длинной цепочки методов. В примере упорядочиваются клиенты, чья должность имеет значение «Owner» (владелец), сначала по странам, затем в убывающем порядке по городам, и, наконец, по именам контактных лиц в пределах каждого города.

(Denmark, Kobenhavn) Jytte Petersen
(France, Paris) Marie Bertrand
(France, Nantes) Janine Labrune
(France, Marseille) Laurence Lebihan
(Germany, Koln) Henriette Pfalzheim
(Mexico, Mexico D.F.) Ana Trujillo
(Mexico, Mexico D.F.) Antonio Moreno
(Mexico, Mexico D.F.) Miguel Angel Paolino
(Norway, Stavern) Jonas Bergulfsen
(Poland, Warszawa) Zbyszek Piestrzeniewicz
...


Проверка последовательностей

Некоторые приложения требуют проверки того факта, что в пределах последовательности соблюдаются определенные условия. Отвечает ли критериям какой-нибудь элемент? Отвечают ли критериям все элементы? Присутствует ли в последовательности некоторый конкретный элемент? Совпадают ли две последовательности? Класс Enumerable предоставляет методы, обеспечивающие получение всех этих сведений.

Для определения факта наличия в последовательности хотя бы одного элемента следует вызвать метод Enumerable.Any без параметров. Для передачи условия и определения того, отвечают ли этому условию какие-нибудь элементы в данной последовательности, методу следует передать функцию. Метод возвращает значение True (истина), если какие-нибудь элементы во входной последовательности присутствуют или соответствуют переданному условию (см. рис. 5).

Рис. 5 Поиск последовательности

' From AnyDemo in sample:
Dim db As New SimpleDataContext
Dim results = _
  From product In db.Products _
  Where product.CategoryID = 1

' Determine if a list has any elements:
Dim anyElements = results.Any()

' Call as extension method or shared method
' of IEnumerable:
Dim matchingElements = _
  results.Any(Function(prod) prod.ProductName.StartsWith("M"))
matchingElements = _
  Enumerable.Any(results, _
  Function(prod As Product) prod.ProductName.StartsWith("M"))

Для определения того, отвечают ли критерию все члены последовательности, вызовите метод Enumerable.All, передавая ему функцию, указывающую условие. В следующем примере определяется, все ли элементы результирующей коллекции имеют значение UnitsInStock больше 5.

' From AllDemo in sample:
' Is it true that all products have more than 5 units in stock?
Dim db As New SimpleDataContext
Dim results = _
  From product In db.Products _
  Where product.CategoryID = 1
Dim allElements = _
  results.All(Function(prod) CBool(prod.UnitsInStock > 5))

Для определения факта наличия в коллекции конкретного элемента следует вызвать метод Enumerable.Contains. Если выполняется поиск простого значения типа строки или целого числа, можно использовать блок сравнения по умолчанию, и вам понадобится только предоставить искомое значение. Если выполняется попытка определить, присуствует ли в коллекции более сложный объект, необходимо предоставить экземпляр класса, реализующего IEqualityComparer(Of T).

В примере кода на рис. 6 присутсвуют оба варианта вызова Enumerable.Contains. В первом примере просто определяется наличие в списке числа 5; во втором определяется, присутствует ли в списке продуктов элемент, соответствующий Chai (в этом примере используется тот же класс ProductComparer, который уже встречался в этой статье ранее).

Рис. 6 Поиск элементов в последовательности

'

From ContainsDemo in the sample:
' Simple check to see if list contains a value:
Dim numbers = Enumerable.Range(1, 10)
Dim contains5 = numbers.Contains(5)

' More complex objects require more complex comparison:
Dim db As New SimpleDataContext
Dim results = _
  From product In db.Products _
  Where product.CategoryID = 1

Dim item As New Product _
  With {.ProductID = 1, .ProductName = "Chai"}
Dim containsChai = results.Contains(item, New ProductComparer())

Для определения того, являются ли две последовательности одинаковыми, вызовите метод Enumerable.SequenceEqual. Точно так же, как в методе Enumerable.Contains, можно сравнивать либо две последовательности простых значений, используя блок сравнения по умолчанию, либо две последовательности более сложных объектов с помощью пользовательского блока сравнения.

Если две последовательности содержат данные разных типов или имеют разные длины, сравнение незамедлительно завершается ошибкой. Если они содержат данные одного и того же типа и имеют одинаковую длину, метод Enumerable.SequenceEqual сравнивает все элементы с помощью указанного блока сравнения. На рис. 7 показаны оба варианта вызова метода Enumerable.SequenceEqual. В обоих случаях возвращается значение False (ложь).

Рис. 7 Сравнение последовательностей

' From SequenceEqualDemo in sample:
Dim rnd As New Random
Dim start = rnd.Next

Dim count = 10
Dim s1 = Enumerable.Range(start, count)

' Choose a different random starting point:
start = rnd.Next
Dim s2 = Enumerable.Range(start, count)
Dim sequencesEqual = s1.SequenceEqual(s2)
sw.WriteLine("sequencesEqual = {0}", sequencesEqual)

' What if there's no default comparer? Must use your own:
Dim products1 = _
  From prod In db.Products _
  Where prod.CategoryID = 1
Dim products2 = _
  From prod In products1 _
  Where prod.UnitPrice > 5

sequencesEqual = _
  products1.SequenceEqual(products2, New ProductComparer())
sw.WriteLine("sequencesEqual = {0}", sequencesEqual)

Свою следующую статью я собираюсь посвятить оставшимся методам класса Enumerable, включая некоторые забавные методы, выполняющие полезные операции над множествами. А пока вы можете загрузить пример приложения и подвергнуть испытанию методы класса Enumerable. Они наверняка пригодятся вам при работе со структурами данных в Visual Studio 2008.

Автор: Кен Гетц (Ken Getz)
Источник: http://msdn.microsoft.com/ru-ru/magazine/