Ескертпе
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Жүйеге кіруді немесе каталогтарды өзгертуді байқап көруге болады.
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Каталогтарды өзгертуді байқап көруге болады.
Группированием называют операцию объединения данных в группы таким образом, чтобы у элементов в каждой группе был общий атрибут. На следующем рисунке показаны результаты операции группирования последовательности символов. Ключ для каждой группы — это символ.
Внимание
В этих примерах используется System.Collections.Generic.IEnumerable<T> источник данных. Источники данных на основе System.Linq.IQueryProvider используют System.Linq.IQueryable<T> источники данных и деревья выражений. Деревья выражений имеют ограничения на допустимый синтаксис C#. Кроме того, каждый IQueryProvider источник данных, например EF Core , может наложить больше ограничений. Ознакомьтесь с документацией по источнику данных.
Стандартные методы оператора запроса, которые группируют элементы данных, перечислены в следующей таблице.
| Имя метода | Описание | Синтаксис выражения запроса C# | Дополнительные сведения |
|---|---|---|---|
| Группировка по | Группирует элементы с общим атрибутом. Объект представляет каждую IGrouping<TKey,TElement> группу. | group … by …–или– group … by … into … |
Enumerable.GroupBy Queryable.GroupBy |
| ToLookup | Вставляет элементы в Lookup<TKey,TElement> (словарь "один ко многим") в зависимости от функции выбора ключа. | Неприменимо. | Enumerable.ToLookup |
В следующем примере кода используется group by, чтобы сгруппировать целые числа в списке в зависимости от того, являются ли они четными или нечетными.
List<int> numbers = [35, 44, 200, 84, 3987, 4, 199, 329, 446, 208];
IEnumerable<IGrouping<int, int>> query = from number in numbers
group number by number % 2;
foreach (var group in query)
{
Console.WriteLine(group.Key == 0 ? "\nEven numbers:" : "\nOdd numbers:");
foreach (int i in group)
{
Console.WriteLine(i);
}
}
Эквивалентный запрос с использованием синтаксиса метода показан в следующем коде:
List<int> numbers = [35, 44, 200, 84, 3987, 4, 199, 329, 446, 208];
IEnumerable<IGrouping<int, int>> query = numbers
.GroupBy(number => number % 2);
foreach (var group in query)
{
Console.WriteLine(group.Key == 0 ? "\nEven numbers:" : "\nOdd numbers:");
foreach (int i in group)
{
Console.WriteLine(i);
}
}
Примечание.
В следующих примерах в этой статье используются общие источники данных для этой области.
Каждый из них Student имеет уровень оценки, основной отдел и ряд показателей. У него Teacher также есть свойство, указывающее на City кампус, где проводит занятия учитель. У Department есть имя и ссылка на Teacher, который является руководителем отдела.
Пример набора данных можно найти в исходном репозитории.
public enum GradeLevel
{
FirstYear = 1,
SecondYear,
ThirdYear,
FourthYear
};
public class Student
{
public required string FirstName { get; init; }
public required string LastName { get; init; }
public required int ID { get; init; }
public required GradeLevel Year { get; init; }
public required List<int> Scores { get; init; }
public required int DepartmentID { get; init; }
}
public class Teacher
{
public required string First { get; init; }
public required string Last { get; init; }
public required int ID { get; init; }
public required string City { get; init; }
}
public class Department
{
public required string Name { get; init; }
public int ID { get; init; }
public required int TeacherID { get; init; }
}
Примечание.
Общие источники данных для этой области см. в статье "Обзор операторов стандартных запросов ".
Группировка результатов запросов
Группирование — одна из самых эффективных функций LINQ. В приведенных ниже примерах демонстрируются различные способы группирования данных:
- по отдельному свойству;
- по первой букве строкового свойства;
- по расчетному числовому диапазону;
- по логическому предикату или другому выражению;
- по составному ключу.
Кроме того, последние два запроса проецируют свои результаты в новый анонимный тип, содержащий только имя и фамилию студента. Дополнительные сведения см. в разделе групповое предложение.
Пример группировки по отдельному свойству
В этом примере показана группировка элементов источника с помощью отдельного свойства элемента в качестве ключа группы. Ключом является enum год обучения студента в школе. При операции группирования используется компаратор равенства, используемый по умолчанию для типа.
var groupByYearQuery =
from student in students
group student by student.Year into newGroup
orderby newGroup.Key
select newGroup;
foreach (var yearGroup in groupByYearQuery)
{
Console.WriteLine($"Key: {yearGroup.Key}");
foreach (var student in yearGroup)
{
Console.WriteLine($"\t{student.LastName}, {student.FirstName}");
}
}
Эквивалентный код с использованием синтаксиса метода показан в следующем примере:
// Variable groupByYearQuery is an IEnumerable<IGrouping<GradeLevel,
// DataClass.Student>>.
var groupByYearQuery = students
.GroupBy(student => student.Year)
.OrderBy(newGroup => newGroup.Key);
foreach (var yearGroup in groupByYearQuery)
{
Console.WriteLine($"Key: {yearGroup.Key}");
foreach (var student in yearGroup)
{
Console.WriteLine($"\t{student.LastName}, {student.FirstName}");
}
}
Пример группировки по значению
В следующем примере показано, как группировать элементы источника, используя в качестве ключа группы что-то, отличное от свойства объекта. В этом примере ключ — это первая буква имени семьи учащегося.
var groupByFirstLetterQuery =
from student in students
let firstLetter = student.LastName[0]
group student by firstLetter;
foreach (var studentGroup in groupByFirstLetterQuery)
{
Console.WriteLine($"Key: {studentGroup.Key}");
foreach (var student in studentGroup)
{
Console.WriteLine($"\t{student.LastName}, {student.FirstName}");
}
}
Для доступа к элементам группы необходим вложенный foreach.
Эквивалентный код с использованием синтаксиса метода показан в следующем примере:
var groupByFirstLetterQuery = students
.GroupBy(student => student.LastName[0]);
foreach (var studentGroup in groupByFirstLetterQuery)
{
Console.WriteLine($"Key: {studentGroup.Key}");
foreach (var student in studentGroup)
{
Console.WriteLine($"\t{student.LastName}, {student.FirstName}");
}
}
Пример группировки по диапазону
В этом примере показана группировка элементов источника путем использования числового диапазона в качестве ключа группы. Затем запрос преобразует результаты в анонимный тип, содержащий только имя и фамилию и диапазон процентилей, к которому относится учащийся. Анонимный тип используется, так как для отображения результатов не требуется использовать полный Student объект.
GetPercentile — это вспомогательная функция, которая вычисляет процент на основе средних результатов учащегося. Метод возвращает целое число в диапазоне от 0 до 10.
static int GetPercentile(Student s)
{
double avg = s.Scores.Average();
return avg > 0 ? (int)avg / 10 : 0;
}
var groupByPercentileQuery =
from student in students
let percentile = GetPercentile(student)
group new
{
student.FirstName,
student.LastName
} by percentile into percentGroup
orderby percentGroup.Key
select percentGroup;
foreach (var studentGroup in groupByPercentileQuery)
{
Console.WriteLine($"Key: {studentGroup.Key * 10}");
foreach (var item in studentGroup)
{
Console.WriteLine($"\t{item.LastName}, {item.FirstName}");
}
}
Вложенный цикл foreach необходим для итерации по группам и элементам группы. Эквивалентный код с использованием синтаксиса метода показан в следующем примере:
static int GetPercentile(Student s)
{
double avg = s.Scores.Average();
return avg > 0 ? (int)avg / 10 : 0;
}
var groupByPercentileQuery = students
.Select(student => new { student, percentile = GetPercentile(student) })
.GroupBy(student => student.percentile)
.Select(percentGroup => new
{
percentGroup.Key,
Students = percentGroup.Select(s => new { s.student.FirstName, s.student.LastName })
})
.OrderBy(percentGroup => percentGroup.Key);
foreach (var studentGroup in groupByPercentileQuery)
{
Console.WriteLine($"Key: {studentGroup.Key * 10}");
foreach (var item in studentGroup.Students)
{
Console.WriteLine($"\t{item.LastName}, {item.FirstName}");
}
}
Пример группировки для сравнения
В этом примере показана группировка элементов источника с помощью выражения логического сравнения. В этом случае логическое выражение проверяет, превосходит ли среднее значение экзаменационного результата учащегося 75 баллов. Как и в предыдущих примерах, результаты проецируются в анонимный тип, так как полный исходный элемент не нужен. Свойства в анонимном типе становятся свойствами Key элемента.
var groupByHighAverageQuery =
from student in students
group new
{
student.FirstName,
student.LastName
} by student.Scores.Average() > 75 into studentGroup
select studentGroup;
foreach (var studentGroup in groupByHighAverageQuery)
{
Console.WriteLine($"Key: {studentGroup.Key}");
foreach (var student in studentGroup)
{
Console.WriteLine($"\t{student.FirstName} {student.LastName}");
}
}
Эквивалентный запрос с использованием синтаксиса метода показан в следующем коде:
var groupByHighAverageQuery = students
.GroupBy(student => student.Scores.Average() > 75)
.Select(group => new
{
group.Key,
Students = group.AsEnumerable().Select(s => new { s.FirstName, s.LastName })
});
foreach (var studentGroup in groupByHighAverageQuery)
{
Console.WriteLine($"Key: {studentGroup.Key}");
foreach (var student in studentGroup.Students)
{
Console.WriteLine($"\t{student.FirstName} {student.LastName}");
}
}
Группировка по анонимному типу
В этом примере показано, как использовать анонимный тип для инкапсуляции ключа, содержащего несколько значений. В этом примере первое ключевое значение — это первая буква имени семьи учащегося. Второе значение ключа является логическим и указывает, набрал ли учащийся более 85 баллов на первом экзамене. Группы можно сортировать по любому из свойств в ключе.
var groupByCompoundKey =
from student in students
group student by new
{
FirstLetterOfLastName = student.LastName[0],
IsScoreOver85 = student.Scores[0] > 85
} into studentGroup
orderby studentGroup.Key.FirstLetterOfLastName
select studentGroup;
foreach (var scoreGroup in groupByCompoundKey)
{
var s = scoreGroup.Key.IsScoreOver85 ? "more than 85" : "less than 85";
Console.WriteLine($"Name starts with {scoreGroup.Key.FirstLetterOfLastName} who scored {s}");
foreach (var item in scoreGroup)
{
Console.WriteLine($"\t{item.FirstName} {item.LastName}");
}
}
Эквивалентный запрос с использованием синтаксиса метода показан в следующем коде:
var groupByCompoundKey = students
.GroupBy(student => new
{
FirstLetterOfLastName = student.LastName[0],
IsScoreOver85 = student.Scores[0] > 85
})
.OrderBy(studentGroup => studentGroup.Key.FirstLetterOfLastName);
foreach (var scoreGroup in groupByCompoundKey)
{
var s = scoreGroup.Key.IsScoreOver85 ? "more than 85" : "less than 85";
Console.WriteLine($"Name starts with {scoreGroup.Key.FirstLetterOfLastName} who scored {s}");
foreach (var item in scoreGroup)
{
Console.WriteLine($"\t{item.FirstName} {item.LastName}");
}
}
Создайте вложенную группу
В следующем примере демонстрируется создание вложенных групп в выражении запроса LINQ. Каждая группа, созданная в соответствии с годом обучения или курсом учащегося, затем подразделяется на группы по именам учащихся.
var nestedGroupsQuery =
from student in students
group student by student.Year into newGroup1
from newGroup2 in
from student in newGroup1
group student by student.LastName
group newGroup2 by newGroup1.Key;
foreach (var outerGroup in nestedGroupsQuery)
{
Console.WriteLine($"DataClass.Student Level = {outerGroup.Key}");
foreach (var innerGroup in outerGroup)
{
Console.WriteLine($"\tNames that begin with: {innerGroup.Key}");
foreach (var innerGroupElement in innerGroup)
{
Console.WriteLine($"\t\t{innerGroupElement.LastName} {innerGroupElement.FirstName}");
}
}
}
Три вложенных foreach цикла требуются для итерации внутренних элементов вложенной группы.
(Наведите указатель мыши на переменные итерации, outerGroupinnerGroupи innerGroupElement чтобы увидеть их фактический тип.)
Эквивалентный запрос с использованием синтаксиса метода показан в следующем коде:
var nestedGroupsQuery =
students
.GroupBy(student => student.Year)
.Select(newGroup1 => new
{
newGroup1.Key,
NestedGroup = newGroup1
.GroupBy(student => student.LastName)
});
foreach (var outerGroup in nestedGroupsQuery)
{
Console.WriteLine($"DataClass.Student Level = {outerGroup.Key}");
foreach (var innerGroup in outerGroup.NestedGroup)
{
Console.WriteLine($"\tNames that begin with: {innerGroup.Key}");
foreach (var innerGroupElement in innerGroup)
{
Console.WriteLine($"\t\t{innerGroupElement.LastName} {innerGroupElement.FirstName}");
}
}
}
Выполнение подзапроса в операции группировки
В этой статье показаны два разных способа создания запроса, упорядочивающего исходные данные в группы и затем выполняющего вложенный запрос для каждой группы по отдельности. Основное действие в каждом примере заключается в группировании исходных элементов с помощью продолжения с именем newGroup с последующим созданием вложенного запроса для newGroup. Этот вложенный запрос выполняется для каждой новой группы, созданной внешним запросом. В этом примере окончательные выходные данные — это не группа, а плоская последовательность анонимных типов.
Дополнительные сведения о способах группирования см. в разделе клауза "group". Дополнительные сведения о продолжениях см. в разделе into. В приведенном ниже примере в качестве источника данных используется структура данных в памяти, но те же принципы действуют для любого типа источника данных LINQ.
var queryGroupMax =
from student in students
group student by student.Year into studentGroup
select new
{
Level = studentGroup.Key,
HighestScore = (
from student2 in studentGroup
select student2.Scores.Average()
).Max()
};
var count = queryGroupMax.Count();
Console.WriteLine($"Number of groups = {count}");
foreach (var item in queryGroupMax)
{
Console.WriteLine($" {item.Level} Highest Score={item.HighestScore}");
}
Запрос в предыдущем фрагменте также можно записать с помощью синтаксиса метода. В следующем фрагменте кода приведен семантически эквивалентный запрос, написанный с использованием синтаксиса метода.
var queryGroupMax =
students
.GroupBy(student => student.Year)
.Select(studentGroup => new
{
Level = studentGroup.Key,
HighestScore = studentGroup.Max(student2 => student2.Scores.Average())
});
var count = queryGroupMax.Count();
Console.WriteLine($"Number of groups = {count}");
foreach (var item in queryGroupMax)
{
Console.WriteLine($" {item.Level} Highest Score={item.HighestScore}");
}