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

ОГЛАВЛЕНИЕ

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

Методы 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 можно вызвать, передавая значение по умолчанию для единственного элемента, как при втором вызове метода.