Универсальный нож для вложенных коллекций

В этой статье я расскажу о различных перегрузках и использовании SelectMany методов LINQ.

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

Итак, чем на самом деле полезно это сглаживание?

Основные операции SelectMany

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

Если бы я хотел перечислить всю свою библиотеку и идентифицировать каждого персонажа - независимо от книги - это более или менее то, что SelectMany делает на основе коллекции.

Другими словами, SelectMany сопоставляет перечислимое свойство каждого элемента коллекции в один плоский список.

Самая простая форма этого кода выглядит так:

var people = books.SelectMany(b => b.Characters);

Эта операция вернет список символов, которые могут выглядеть примерно так (комментарии добавлены для облегчения понимания):

[ 
  // Characters from Sphere 
  "Harry", 
  "Norman", 
  "Beth", 
  "Jerry", 
  // Characters from Jurassic Park 
  "Malcolm",
  "Grant", 
  "Satler", 
  "Nedry", 
  "Hammond", 
  "Gennaro", 
  "Tim", 
  "Lex"
]

Обратите внимание, что результирующая коллекция представляет собой одну плоскую коллекцию, а не несколько групп коллекций. Это делает SelectMany операцию, почти обратную методу GroupBy.

Функция, которую мы предоставили SelectMany для идентификации вложенной коллекции, называется селектором коллекции.

Добавление селекторов результатов

LINQ также предоставляет дополнительную перегрузку, которая дает нам нечто, называемое селектором результатов.

Селектор результатов - это простая функция, которая преобразует отдельный узел в коллекции, который будет возвращен, во что-то другое. Он делает это с помощью функции, которая принимает родительскую коллекцию (Think book, используя предыдущую аналогию), а также дочернюю в этой коллекции (think character в том же примере).

Посмотрите, как это выглядит, используя (b, c) в качестве входных данных для функции, которая возвращает новый анонимный тип, содержащий книгу и персонаж:

Полученная коллекция теперь предоставляет немного больше контекста для каждого символа, как указано ниже в этом сокращенном примере:

[ 
 { "Book": "Sphere", "Character": "Harry" }, 
 // Some results omitted 
 { "Book": "Jurassic Park", "Character": "Lex" } 
]

Обратите внимание, что вам вообще не нужно возвращать объект. Поскольку в нашем примере символы являются строками, мы можем так же легко выполнить некоторые операции форматирования строк следующим образом:

Этот более простой код приводит к гораздо более лаконичному и удобочитаемому выводу:

[ 
  "Harry (Sphere)", 
  "Norman (Sphere)", 
  "Beth (Sphere)", 
  "Jerry (Sphere)", 
  "Malcolm (Jurassic Park)", 
  "Grant (Jurassic Park)", 
  "Satler (Jurassic Park)",
  "Nedry (Jurassic Park)",
  "Hammond (Jurassic Park)", 
  "Gennaro (Jurassic Park)", 
  "Tim (Jurassic Park)", 
  "Lex (Jurassic Park)" 
]

Довольно круто, правда?

Как мы видели, он не только может SelectMany объединять вложенные коллекции в единую коллекцию, но также может преобразовывать или отображать объекты в этих коллекциях в различные объекты по мере необходимости.

Индексные перегрузки

Иногда вам нужно знать, какой индекс находится у элемента в исходной коллекции. Этот случай должен быть несколько редким и обычно включает случаи, когда вам нужно объединить два разных источника данных.

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

Чтобы посмотреть, как это может выглядеть, см. Следующий пример:

Здесь параметр b в селекторе коллекции соответствует объекту Book, а параметр i является индексом коллекции с отсчетом от нуля. Мы извлекаем список символов из коллекции characters по индексу, и SelectMany может использовать свой селектор результатов для полученного объекта.

Опять же, это несколько необычная перегрузка для использования, но она может быть полезна в случаях, когда ваши данные фрагментированы по нескольким коллекциям.

Заключительные мысли

На мой взгляд, SelectMany гораздо полезнее, чем обратная операция GroupBy.

Я настоятельно рекомендую SelectMany в любое время, когда вам нужно объединить вложенные списки в единую коллекцию.

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

В конечном счете, я думаю, вы будете использовать SelectMany нечасто, но надежно только из-за его полезности при работе с вложенными коллекциями. Функции сопоставления могут быть менее необходимы, но все же важны в ключевых сценариях.

Если что-то в этой статье сбивает с толку, сообщите мне или ознакомьтесь с собственной документацией Microsoft по группе методов.

Если вы нашли применение SelectMany, о котором я здесь не говорил, я бы с удовольствием добавил его в свой собственный набор уловок. Пожалуйста, оставьте комментарий и дайте мне знать, что вы нашли.

Первоначально опубликовано на https://killalldefects.com 27 января 2020 г.