Seskupování dat (C#)
Seskupení odkazuje na operaci vkládání dat do skupin tak, aby prvky v každé skupině sdílely společný atribut. Následující obrázek ukazuje výsledky seskupení posloupnosti znaků. Klíč pro každou skupinu je znak.
Důležité
Tyto ukázky používají System.Collections.Generic.IEnumerable<T> zdroj dat. Zdroje dat založené na System.Linq.IQueryProvider použití System.Linq.IQueryable<T> zdrojů dat a stromů výrazů Stromy výrazů mají omezení povolené syntaxe jazyka C#. Každý zdroj dat, například EF Core, IQueryProvider
může navíc uplatňovat další omezení. Projděte si dokumentaci ke zdroji dat.
Standardní metody operátoru dotazu, které seskupují datové prvky, jsou uvedeny v následující tabulce.
Název metody | Popis | Syntaxe výrazu dotazu jazyka C# | Další informace |
---|---|---|---|
GroupBy | Seskupuje prvky, které sdílejí společný atribut. Objekt IGrouping<TKey,TElement> představuje každou skupinu. | group … by nebo group … by … into … |
Enumerable.GroupBy Queryable.GroupBy |
ToLookup | Vloží prvky do slovníku Lookup<TKey,TElement> 1:N na základě funkce selektoru klíče. | Nevztahuje se. | Enumerable.ToLookup |
Následující příklad kódu používá group by
klauzuli k seskupení celých čísel v seznamu podle toho, zda jsou sudé nebo liché.
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);
}
}
Ekvivalentní dotaz pomocí syntaxe metody se zobrazí v následujícím kódu:
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);
}
}
Poznámka:
Následující příklady v tomto článku používají společné zdroje dat pro tuto oblast.
Každý z nich Student
má úroveň známek, primární oddělení a řadu výsledků. A Teacher
má City
také vlastnost, která identifikuje areál, kde učitel má předměty. A Department
má jméno a odkaz na Teacher
toho, kdo slouží jako vedoucí oddělení.
Ukázkové datové sady najdete ve zdrojovém úložišti.
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; }
}
Seskupení výsledků dotazu
Seskupování je jednou z nejvýkonnějších funkcí LINQ. Následující příklady ukazují, jak seskupit data různými způsoby:
- Jednou vlastností.
- Podle prvního písmena vlastnosti řetězce.
- Vypočítaným číselným rozsahem.
- Logickým predikátem nebo jiným výrazem.
- Složeným klíčem.
Kromě toho poslední dva dotazy promítnou výsledky do nového anonymního typu, který obsahuje pouze jméno studenta a jméno rodiny. Další informace najdete v klauzuli group.
Příklad seskupení podle jedné vlastnosti
Následující příklad ukazuje, jak seskupit zdrojové elementy pomocí jedné vlastnosti elementu jako klíče skupiny. Klíčem je enum
rok studenta ve škole. Operace seskupení používá výchozí porovnávač rovnosti pro typ.
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}");
}
}
Ekvivalentní kód používající syntaxi metody je znázorněn v následujícím příkladu:
// Variable groupByLastNamesQuery is an IEnumerable<IGrouping<string,
// 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}");
}
}
Příklad seskupení podle hodnoty
Následující příklad ukazuje, jak seskupit zdrojové elementy pomocí něčeho jiného než vlastnosti objektu pro klíč skupiny. V tomto příkladu je klíčem první písmeno jména studenta.
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}");
}
}
Pro přístup k položkám skupiny se vyžaduje vnořený foreach.
Ekvivalentní kód používající syntaxi metody je znázorněn v následujícím příkladu:
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}");
}
}
Seskupení podle příkladu rozsahu
Následující příklad ukazuje, jak seskupit zdrojové prvky pomocí číselného rozsahu jako klíče skupiny. Dotaz pak výsledky prodá do anonymního typu, který obsahuje pouze jméno a jméno rodiny a rozsah percentilu, do kterého student patří. Používá se anonymní typ, protože k zobrazení výsledků není nutné použít úplný Student
objekt. GetPercentile
je pomocná funkce, která vypočítá percentil na základě průměrného skóre studenta. Metoda vrátí celé číslo mezi 0 a 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}");
}
}
Vnořená foreach potřebná k iteraci nad skupinami a položkami skupiny Ekvivalentní kód používající syntaxi metody je znázorněn v následujícím příkladu:
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}");
}
}
Příklad seskupení podle porovnání
Následující příklad ukazuje, jak seskupit zdrojové elementy pomocí logického porovnávací výrazu. V tomto příkladu logický výraz testuje, jestli je průměrné skóre zkoušky studenta větší než 75. Stejně jako v předchozích příkladech se výsledky promítají do anonymního typu, protože úplný zdrojový prvek není potřeba. Vlastnosti v anonymním typu se stanou vlastnostmi člena 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}");
}
}
Ekvivalentní dotaz pomocí syntaxe metody se zobrazí v následujícím kódu:
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}");
}
}
Seskupovat podle anonymního typu
Následující příklad ukazuje, jak použít anonymní typ k zapouzdření klíče, který obsahuje více hodnot. V tomto příkladu je první hodnota klíče prvním písmenem jména studenta. Druhá hodnota klíče je logická hodnota, která určuje, jestli student získal skóre více než 85 při první zkoušce. Skupiny můžete uspořádat podle libovolné vlastnosti v klíči.
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}");
}
}
Ekvivalentní dotaz pomocí syntaxe metody se zobrazí v následujícím kódu:
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}");
}
}
Vytvoření vnořené skupiny
Následující příklad ukazuje, jak vytvořit vnořené skupiny ve výrazu dotazu LINQ. Každá skupina vytvořená podle ročníku studenta nebo úrovně známky se pak dále rozdělí do skupin založených na jménech jednotlivců.
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}");
}
}
}
K iteraci vnitřních prvků vnořené skupiny se vyžadují tři vnořené foreach
smyčky.
(Najeďte myší na proměnné iterace, outerGroup
, innerGroup
a innerGroupElement
zobrazte jejich skutečný typ.)
Ekvivalentní dotaz pomocí syntaxe metody se zobrazí v následujícím kódu:
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}");
}
}
}
Provádění poddotazů na skupinách
Tento článek ukazuje dva různé způsoby vytvoření dotazu, který seřazuje zdrojová data do skupin, a potom provede poddotaz pro každou skupinu jednotlivě. Základní technika v každém příkladu je seskupit zdrojové prvky pomocí pokračování s názvem newGroup
, a pak vygenerovat nový poddotaz proti newGroup
. Tento poddotaz se spustí pro každou novou skupinu vytvořenou vnějším dotazem. V tomto konkrétním příkladu není konečný výstup skupinou, ale plochou sekvencí anonymních typů.
Další informace o seskupování najdete v klauzuli group. Další informace o pokračováních najdete v tématu. Následující příklad používá datovou strukturu v paměti jako zdroj dat, ale stejné principy platí pro jakýkoli druh zdroje dat 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}");
}
Dotaz v předchozím fragmentu kódu lze také zapsat pomocí syntaxe metody. Následující fragment kódu obsahuje sémanticky ekvivalentní dotaz napsaný pomocí syntaxe metody.
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}");
}