Объяснение методов LINQ в .NET с помощью изображений

Довольно часто разработчики, начиная работать с LINQ методами, не до конца понимают принцип их работы, в этой статье объясняем как работают основные LINQ методы с помощью иллюстраций
Select

С помощью select мы создаем проекцию одного элемента на другой. Проще говоря, мы сопоставляем наш заданный тип с желаемым типом. Результирующий набор содержит то же количество элементов, что и исходный набор данных.
Пример использования Select:
var people = new List<Person>
{
new Person ("Tom", 23),
new Person ("Bob", 27),
new Person ("Sam", 29),
new Person ("Alice", 24)
};
var names = people.Select(u => u.Name);
foreach (string n in names)
Console.WriteLine(n);
Where

Where фильтрует набор значений базируясь на Func<TSource,bool>
предикате. В данном примере мы хотим получить только зеленые круги. Набор результатов может быть таким же, меньшим или даже пустым.
Пример использования Where
List<string> fruits =
new List<string> { "apple", "passionfruit", "banana", "mango",
"orange", "blueberry", "grape", "strawberry" };
IEnumerable<string> query = fruits.Where(fruit => fruit.Length < 6);
foreach (string fruit in query)
{
Console.WriteLine(fruit);
}
/*
This code produces the following output:
apple
mango
grape
*/
SelectMany

SelectMany используется для выравнивания списков. Если у вас есть список внутри списка, мы можем использовать его, чтобы свести его к одномерному представлению.
Пример использования SelectMany
class PetOwner
{
public string Name { get; set; }
public List<string> Pets { get; set; }
}
public static void SelectManyEx2()
{
PetOwner[] petOwners =
{ new PetOwner { Name="Higa, Sidney",
Pets = new List<string>{ "Scruffy", "Sam" } },
new PetOwner { Name="Ashkenazi, Ronen",
Pets = new List<string>{ "Walker", "Sugar" } },
new PetOwner { Name="Price, Vernette",
Pets = new List<string>{ "Scratches", "Diesel" } },
new PetOwner { Name="Hines, Patrick",
Pets = new List<string>{ "Dusty" } } };
// Project the items in the array by appending the index
// of each PetOwner to each pet's name in that petOwner's
// array of pets.
IEnumerable<string> query =
petOwners.SelectMany((petOwner, index) =>
petOwner.Pets.Select(pet => index + pet));
foreach (string pet in query)
{
Console.WriteLine(pet);
}
}
// This code produces the following output:
//
// 0Scruffy
// 0Sam
// 1Walker
// 1Sugar
// 2Scratches
// 2Diesel
// 3Dusty
Zip

С помощью Zip мы «объединяем» два списка с помощью заданной функции слияния. Мы объединяем объекты вместе, пока не закончатся объекты в любом из списков. Как видно из примера: в первом списке 2 элемента, в втором — 3. Следовательно, результирующий набор содержит только 2 элемента.
Пример использования Zip
int[] numbers = { 1, 2, 3, 4 };
string[] words = { "one", "two", "three" };
var numbersAndWords = numbers.Zip(words, (first, second) => first + " " + second);
foreach (var item in numbersAndWords)
Console.WriteLine(item);
// This code produces the following output:
// 1 one
// 2 two
// 3 three
OrderBy

OrderBy сортирует элементы последовательности в порядке возрастания. Результирующий набор содержит то же количество элементов, что и исходный набор.
Пример использования OrderBy
class Pet
{
public string Name { get; set; }
public int Age { get; set; }
}
public static void OrderByEx1()
{
Pet[] pets = { new Pet { Name="Barley", Age=8 },
new Pet { Name="Boots", Age=4 },
new Pet { Name="Whiskers", Age=1 } };
IEnumerable<Pet> query = pets.OrderBy(pet => pet.Age);
foreach (Pet pet in query)
{
Console.WriteLine("{0} - {1}", pet.Name, pet.Age);
}
}
/*
This code produces the following output:
Whiskers - 1
Boots - 4
Barley - 8
*/
Distinct

Distinct возвращает новый enumerable, в котором удаляются все дубликаты, что-то похожее на Set. Обратите внимание, что для ссылочного типа по умолчанию проверяется равенство ссылок, что может привести к ложным результатам. Результирующий набор может быть таким же или меньшим.
Пример использования Distinct
List<int> ages = new List<int> { 21, 46, 46, 55, 17, 21, 55, 55 };
IEnumerable<int> distinctAges = ages.Distinct();
Console.WriteLine("Distinct ages:");
foreach (int age in distinctAges)
{
Console.WriteLine(age);
}
/*
This code produces the following output:
Distinct ages:
21
46
55
17
*/
DistinctBy

DistinctBy работает аналогично Distinct, но вместо уровня самого объекта мы можем определить проекцию на свойство, где мы хотим получить отдельный набор результатов.
Пример использования DistinctBy
public record Agent
{
public string Name { get; init; }
public byte Age { get; init; }
}
var agents = new[]
{
new Agent() {Name = "Ethan Hunt", Age = 40},
new Agent() {Name = "James Bond", Age = 40},
new Agent() {Name = "Jason Bourne", Age = 35},
new Agent() {Name = "Evelyn Salt", Age = 30},
new Agent() {Name = "Jack Ryan", Age = 36},
new Agent() {Name = "Jane Smith", Age = 35},
new Agent() {Name = "Oren Ishii", Age = 30},
new Agent() {Name = "Natasha Romanoff", Age = 33}
};
var distinctlyAgedAgents = agents.DistinctBy(x => x.Age);
foreach (var agent in distinctlyAgedAgents)
{
Console.WriteLine($"Agent {agent.Name} is {agent.Age}");
}
Agent Ethan Hunt is 40
Agent Jason Bourne is 35
Agent Evelyn Salt is 30
Agent Jack Ryan is 36
Agent Natasha Romanoff is 33
Aggregate

Также известен как «reduce». Основная идея состоит в том, чтобы объединить/свести набор входных данных к одному значению. Сумма списка будет примером агрегата. Также яркими примерами могут быть определение среднего / максимального / минимального значения. Он всегда начинается с начального значения, и каждый отдельный элемент в списке агрегируется заданной пользователем функцией сверху.
Пример использования Aggregate
string[] fruits = { "apple", "mango", "orange", "passionfruit", "grape" };
// Determine whether any string in the array is longer than "banana".
string longestName =
fruits.Aggregate("banana",
(longest, next) =>
next.Length > longest.Length ? next : longest,
// Return the final result as an upper case string.
fruit => fruit.ToUpper());
Console.WriteLine(
"The fruit with the longest name is {0}.",
longestName);
// This code produces the following output:
//
// The fruit with the longest name is PASSIONFRUIT.
Chunk

Работает начиная с .NET 6. Разбивает 1 список на несколько списков с заданым размером.
Пример использования Chunk
// Giving an enumerable
var e = Enumerable.Range(1, 999);
// Here it is. Enjoy :)
var chunks = e.Chunk(29);
// Sample, iterating over chunks
foreach(var chunk in chunks) // for each chunk
{
foreach(var item in chunk) // for each item in a chunk
{
Console.WriteLine(item);
}
}
Share
Edit
Union

Самое простое объяснение того как это работает: представьте, что у вы смержили 2 списка в один, и вызвали Distinct.
Пример использования Union
int[] ints1 = { 5, 3, 9, 7, 5, 9, 3, 7 };
int[] ints2 = { 8, 3, 6, 4, 4, 9, 1, 0 };
IEnumerable<int> union = ints1.Union(ints2);
foreach (int num in union)
{
Console.Write("{0} ", num);
}
/*
This code produces the following output:
5 3 9 7 8 6 4 1 0
*/
Intersect

Работает аналогично Union, но теперь мы проверяем, какие элементы присутствуют в списке A и списке B. В результирующем наборе будут только элементы, присутствующие в обоих списках. Также здесь: в новом списке только уникальные айтемы. Дубликаты удаляются автоматически.
Пример использования Intersect
int[] id1 = { 44, 26, 92, 30, 71, 38 };
int[] id2 = { 39, 59, 83, 47, 26, 4, 30 };
IEnumerable<int> both = id1.Intersect(id2);
foreach (int id in both)
Console.WriteLine(id);
/*
This code produces the following output:
26
30
*/
Any

Any проверяет, удовлетворяет ли хотя бы один элемент вашему условию. Если это так, он возвращает true. Если нет элемента, удовлетворяющего условию, возвращается false.
Пример использования Any
List<int> numbers = new List<int> { 1, 2 };
bool hasElements = numbers.Any();
Console.WriteLine("The list {0} empty.",
hasElements ? "is not" : "is");
// This code produces the following output:
//
// The list is not empty.
All

Как следует из названия, проверяет, удовлетворяют ли ВСЕ ваши элементы в списке определенному условию. Если да, то возвращает true, иначе false.
Пример использования All
class Pet
{
public string Name { get; set; }
public int Age { get; set; }
}
public static void AllEx()
{
// Create an array of Pets.
Pet[] pets = { new Pet { Name="Barley", Age=10 },
new Pet { Name="Boots", Age=4 },
new Pet { Name="Whiskers", Age=6 } };
// Determine whether all pet names
// in the array start with 'B'.
bool allStartWithB = pets.All(pet =>
pet.Name.StartsWith("B"));
Console.WriteLine(
"{0} pet names start with 'B'.",
allStartWithB ? "All" : "Not all");
}
// This code produces the following output:
//
// Not all pet names start with 'B'.
Append

Append помещает данный элемент в конец списка.
Пример использования Append
List<int> numbers = new List<int> { 1, 2, 3, 4 };
List<int> newNumbers = numbers.Append(5).ToList();
// 1, 2, 3, 4, 5
Prepend

Помещает заданный элемент в начало списка.
Пример использования Prepend
List<int> numbers = new List<int> { 1, 2, 3, 4 };
List<int> newNumbers = numbers.Prepend(0).ToList();
// 0, 1, 2, 3, 4
MaxBy

С помощью MaxBy, а также MinBy мы также можем сделать проекцию на свойство нашего класса и получить объект, где именно это свойство является «самым большим».
Пример использования MaxBy
List<Person> people = new List<Person>
{
new Person
{
Name = "John Smith",
Age = 20
},
new Person
{
Name = "Jane Smith",
Age = 30
}
};
Console.Write(people.MaxBy(x => x.Age)); //Outputs Person (Jane Smith)
ToLookup

Создает универсальный объект Lookup<TKey,TElement>
из объекта IEnumerable<T>
. Lookup
определяется тем, что у нас есть ключ, который может указывать на список объектов (отношение 1 к n). Первый аргумент принимает "key"-селектор. Второй селектор — это «значение». Это может быть сам объект или свойство самого объекта. В конце у нас есть список различных ключей, в которых значения имеют именно этот ключ. Объект LookUp неизменяем.
Пример использования ToLookup
class Package
{
public string Company { get; set; }
public double Weight { get; set; }
public long TrackingNumber { get; set; }
}
public static void ToLookupEx1()
{
// Create a list of Packages.
List<Package> packages =
new List<Package>
{ new Package { Company = "Coho Vineyard",
Weight = 25.2, TrackingNumber = 89453312L },
new Package { Company = "Lucerne Publishing",
Weight = 18.7, TrackingNumber = 89112755L },
new Package { Company = "Wingtip Toys",
Weight = 6.0, TrackingNumber = 299456122L },
new Package { Company = "Contoso Pharmaceuticals",
Weight = 9.3, TrackingNumber = 670053128L },
new Package { Company = "Wide World Importers",
Weight = 33.8, TrackingNumber = 4665518773L } };
// Create a Lookup to organize the packages.
// Use the first character of Company as the key value.
// Select Company appended to TrackingNumber
// as the element values of the Lookup.
ILookup<char, string> lookup =
packages
.ToLookup(p => p.Company[0],
p => p.Company + " " + p.TrackingNumber);
// Iterate through each IGrouping in the Lookup.
foreach (IGrouping<char, string> packageGroup in lookup)
{
// Print the key value of the IGrouping.
Console.WriteLine(packageGroup.Key);
// Iterate through each value in the
// IGrouping and print its value.
foreach (string str in packageGroup)
Console.WriteLine(" {0}", str);
}
}
/*
This code produces the following output:
C
Coho Vineyard 89453312
Contoso Pharmaceuticals 670053128
L
Lucerne Publishing 89112755
W
Wingtip Toys 299456122
Wide World Importers 4665518773
*/
ToDictionary

ToDictionary работает аналогично ToLookup с одним ключевым отличием. Метод ToDictionary допускает только отношения 1 к 1. Если два элемента используют один и тот же ключ, это приведет к исключению, что ключ уже присутствует.
Пример использования ToDictionary
class Package
{
public string Company { get; set; }
public double Weight { get; set; }
public long TrackingNumber { get; set; }
}
public static void ToDictionaryEx1()
{
List<Package> packages =
new List<Package>
{ new Package { Company = "Coho Vineyard", Weight = 25.2, TrackingNumber = 89453312L },
new Package { Company = "Lucerne Publishing", Weight = 18.7, TrackingNumber = 89112755L },
new Package { Company = "Wingtip Toys", Weight = 6.0, TrackingNumber = 299456122L },
new Package { Company = "Adventure Works", Weight = 33.8, TrackingNumber = 4665518773L } };
// Create a Dictionary of Package objects,
// using TrackingNumber as the key.
Dictionary<long, Package> dictionary =
packages.ToDictionary(p => p.TrackingNumber);
foreach (KeyValuePair<long, Package> kvp in dictionary)
{
Console.WriteLine(
"Key {0}: {1}, {2} pounds",
kvp.Key,
kvp.Value.Company,
kvp.Value.Weight);
}
}
/*
This code produces the following output:
Key 89453312: Coho Vineyard, 25.2 pounds
Key 89112755: Lucerne Publishing, 18.7 pounds
Key 299456122: Wingtip Toys, 6 pounds
Key 4665518773: Adventure Works, 33.8 pounds
*/
Join

Join работает аналогично к SQL left-join.
Пример использования Join
class Person
{
public string Name { get; set; }
}
class Pet
{
public string Name { get; set; }
public Person Owner { get; set; }
}
public static void JoinEx1()
{
Person magnus = new Person { Name = "Hedlund, Magnus" };
Person terry = new Person { Name = "Adams, Terry" };
Person charlotte = new Person { Name = "Weiss, Charlotte" };
Pet barley = new Pet { Name = "Barley", Owner = terry };
Pet boots = new Pet { Name = "Boots", Owner = terry };
Pet whiskers = new Pet { Name = "Whiskers", Owner = charlotte };
Pet daisy = new Pet { Name = "Daisy", Owner = magnus };
List<Person> people = new List<Person> { magnus, terry, charlotte };
List<Pet> pets = new List<Pet> { barley, boots, whiskers, daisy };
// Create a list of Person-Pet pairs where
// each element is an anonymous type that contains a
// Pet's name and the name of the Person that owns the Pet.
var query =
people.Join(pets,
person => person,
pet => pet.Owner,
(person, pet) =>
new { OwnerName = person.Name, Pet = pet.Name });
foreach (var obj in query)
{
Console.WriteLine(
"{0} - {1}",
obj.OwnerName,
obj.Pet);
}
}
/*
This code produces the following output:
Hedlund, Magnus - Daisy
Adams, Terry - Barley
Adams, Terry - Boots
Weiss, Charlotte - Whiskers
*/
Take

Take позволяет нам получить заданное количество элементов из массива. Если у нас меньше элементов в массиве, чем мы хотим получить, то Take() вернет только оставшиеся объекты.
Пример использования Take
int[] grades = { 59, 82, 70, 56, 92, 98, 85 };
IEnumerable<int> topThreeGrades =
grades.OrderByDescending(grade => grade).Take(3);
Console.WriteLine("The top three grades are:");
foreach (int grade in topThreeGrades)
{
Console.WriteLine(grade);
}
/*
This code produces the following output:
The top three grades are:
98
92
85
*/
Skip

С помощью Skip мы «пропускаем» заданное количество элементов. Если мы пропустим больше элементов, чем содержится в нашем списке, мы получим обратно пустое перечисление. Сочетание Take and Skip может быть очень полезным для таких вещей, как пагинация.
Пример использования Skip
int[] grades = { 59, 82, 70, 56, 92, 98, 85 };
IEnumerable<int> lowerGrades =
grades.OrderByDescending(g => g).Skip(3);
Console.WriteLine("All grades except the top three are:");
foreach (int grade in lowerGrades)
{
Console.WriteLine(grade);
}
/*
This code produces the following output:
All grades except the top three are:
82
70
59
56
*/
OfType

OfType проверяет каждый элемент в перечислении на предмет того, относится ли он к заданному типу (унаследованные типы также считаются данным типом) и возвращает их в новом перечислении. Это особенно помогает, если у нас есть нетипизированные массивы (объекты) или нам нужен специальный подкласс данного перечисления.
Пример использования OfType
System.Collections.ArrayList fruits = new System.Collections.ArrayList(4);
fruits.Add("Mango");
fruits.Add("Orange");
fruits.Add("Apple");
fruits.Add(3.0);
fruits.Add("Banana");
// Apply OfType() to the ArrayList.
IEnumerable<string> query1 = fruits.OfType<string>();
Console.WriteLine("Elements of type 'string' are:");
foreach (string fruit in query1)
{
Console.WriteLine(fruit);
}
// The following query shows that the standard query operators such as
// Where() can be applied to the ArrayList type after calling OfType().
IEnumerable<string> query2 =
fruits.OfType<string>().Where(fruit => fruit.ToLower().Contains("n"));
Console.WriteLine("\nThe following strings contain 'n':");
foreach (string fruit in query2)
{
Console.WriteLine(fruit);
}
// This code produces the following output:
//
// Elements of type 'string' are:
// Mango
// Orange
// Apple
// Banana
//
// The following strings contain 'n':
// Mango
// Orange
// Banana
GroupBy

Группирует элементы последовательности в соответствии с заданной функцией селектора ключа и создает результирующее значение для каждой группы и ее ключа
Пример использования GroupBy
class Pet
{
public string Name { get; set; }
public double Age { get; set; }
}
public static void GroupByEx4()
{
// Create a list of pets.
List<Pet> petsList =
new List<Pet>{ new Pet { Name="Barley", Age=8.3 },
new Pet { Name="Boots", Age=4.9 },
new Pet { Name="Whiskers", Age=1.5 },
new Pet { Name="Daisy", Age=4.3 } };
// Group Pet.Age values by the Math.Floor of the age.
// Then project an anonymous type from each group
// that consists of the key, the count of the group's
// elements, and the minimum and maximum age in the group.
var query = petsList.GroupBy(
pet => Math.Floor(pet.Age),
pet => pet.Age,
(baseAge, ages) => new
{
Key = baseAge,
Count = ages.Count(),
Min = ages.Min(),
Max = ages.Max()
});
// Iterate over each anonymous type.
foreach (var result in query)
{
Console.WriteLine("\nAge group: " + result.Key);
Console.WriteLine("Number of pets in this age group: " + result.Count);
Console.WriteLine("Minimum age: " + result.Min);
Console.WriteLine("Maximum age: " + result.Max);
}
/* This code produces the following output:
Age group: 8
Number of pets in this age group: 1
Minimum age: 8.3
Maximum age: 8.3
Age group: 4
Number of pets in this age group: 2
Minimum age: 4.3
Maximum age: 4.9
Age group: 1
Number of pets in this age group: 1
Minimum age: 1.5
Maximum age: 1.5
*/
}
Reverse

Reverse переварачивает массив в обратном порядке
Пример использования Reverse
char[] apple = { 'a', 'p', 'p', 'l', 'e' };
char[] reversed = apple.Reverse().ToArray();
foreach (char chr in reversed)
{
Console.Write(chr + " ");
}
Console.WriteLine();
/*
This code produces the following output:
e l p p a
*/
First

Возвращает первый элемент который подпадает под выборку.
Пример использования First
int[] numbers = { 9, 34, 65, 92, 87, 435, 3, 54,
83, 23, 87, 435, 67, 12, 19 };
int first = numbers.First(number => number > 80);
Console.WriteLine(first);
/*
This code produces the following output:
92
*/
Single

Single не возвращается сразу после первого вхождения. Отличие от first в том, что Single гарантирует отсутствие второго элемента данного типа/предиката. Поэтому Single должен пройти все перечисление (в худшем случае), если он может найти другой элемент. Если существует более 1 записи, так же если элемент не найден он выдает исключение
Пример использования Single
string[] fruits1 = { "orange" };
string fruit1 = fruits1.Single();
Console.WriteLine(fruit1);
/*
This code produces the following output:
orange
*/
FirstOrDefault и SingleOrDefault

Если в данном перечислении не найден ни один элемент, он возвращает его по умолчанию (для ссылочных типов null и для типов значений заданное значение по умолчанию). Начиная с .NET6 мы можем передать то, что для нас означает «по умолчанию». Следовательно, мы можем иметь ненулевые ссылочные типы, если захотим.
Пример использования FirstOrDefault и SingleOrDefault
int[] numbers = { };
int first = numbers.FirstOrDefault(); // или SingleOrDefault
Console.WriteLine(first);
/*
This code produces the following output:
0
*/