1. 入门
分组数据——就是把集合里的元素按某个共同特征分成“篮子”。想象一下你在按颜色分苹果:红的放一篮,绿的放一篮,黄的再放一篮。在编程里这就叫分组。
为啥要分组?
实际开发里,经常要按城市分人、按类别分商品、按日期分交易啥的。这样能做各种分析:比如每个班有多少学生,哪个城市学生最多。
LINQ专门有个方法:GroupBy。还有个类似的query syntax操作符——group by。我们就用学生和班级的管理系统举例,手把手教你怎么搞。
2. method syntax里的GroupBy
基本签名和返回值
GroupBy方法看起来有点吓人:
IEnumerable<IGrouping<TKey, TElement>> GroupBy<TElement, TKey>(
this IEnumerable<TElement> source,
Func<TElement, TKey> keySelector
)
解释一下:
- source——原始集合(比如学生列表)。
- keySelector——决定分组特征的函数,比如ClassName或者City属性。
注意!
结果不是你原来对象的数组,而是特殊的分组集合(IGrouping<TKey, TElement>)。每个分组包含:
- 分组key(Key——把元素分到一起的那个东西),
- 属于这个分组的元素集合。
图解:
| Key(分组键) | 分组里的元素(对象组) |
|---|---|
| "9A" | 伊万, 奥列格, 玛丽亚 |
| "10B" | 斯维特拉娜, 彼得 |
| ... | ... |
例子1:按班级名分组学生
假设我们有个Student类:
public class Student
{
public string Name { get; set; }
public string ClassName { get; set; }
public int Grade { get; set; }
}
还有学生集合:
var students = new List<Student>
{
new Student { Name = "伊万", ClassName = "9A", Grade = 5 },
new Student { Name = "奥列格", ClassName = "9A", Grade = 4 },
new Student { Name = "玛丽亚", ClassName = "10B", Grade = 5 },
new Student { Name = "斯维特拉娜", ClassName = "10B", Grade = 3 }
};
按班级分组学生:
var studentsByClass = students.GroupBy(s => s.ClassName);
foreach (var group in studentsByClass)
{
Console.WriteLine($"班级: {group.Key}"); // 分组key
foreach (var student in group)
{
Console.WriteLine($" - {student.Name}, 分数: {student.Grade}");
}
}
发生了啥?
GroupBy方法帮我们分成了两个组:"9A"一组,"10B"一组。遍历分组时,可以输出每组的各种聚合信息。
可视化:GroupBy后集合长啥样
可以这样想:
students.GroupBy(s => s.ClassName)
├─ group "9A" { 伊万, 奥列格 }
└─ group "10B" { 玛丽亚, 斯维特拉娜 }
每个“分支”就是按key分出来的学生小列表。
按分数分学生:随便啥特征都能分组
我们来按分数分组:
var studentsByGrade = students.GroupBy(s => s.Grade);
foreach (var group in studentsByGrade)
{
Console.WriteLine($"分数: {group.Key}");
foreach (var student in group)
{
Console.WriteLine($" - {student.Name} 来自班级 {student.ClassName}");
}
}
输出:
分数: 5
- 伊万 来自班级 9A
- 玛丽亚 来自班级 10B
分数: 4
- 奥列格 来自班级 9A
分数: 3
- 斯维特拉娜 来自班级 10B
3. LINQ里怎么处理分组:GroupBy后还能干嘛
分组后我们经常不只是想输出分组,还想做点聚合——比如统计每组有多少人,算平均分,或者收集名字列表。
例子2:统计每个班有多少学生
var classCounts = students
.GroupBy(s => s.ClassName)
.Select(g => new { Class = g.Key, Count = g.Count() });
foreach (var cc in classCounts)
{
Console.WriteLine($"班级 {cc.Class} — {cc.Count} 个学生");
}
结果:
班级 9A — 2 个学生
班级 10B — 2 个学生
- 先按班级分组。
- 然后对每组用匿名对象保存key(班级名)和数量(Count()方法)。
例子3:按班级收集优秀生名字
var excellentByClass = students
.Where(s => s.Grade == 5)
.GroupBy(s => s.ClassName)
.Select(g => new
{
Class = g.Key,
ExcellentStudents = g.Select(s => s.Name).ToList()
});
foreach (var group in excellentByClass)
{
Console.WriteLine($"班级 {group.Class} 的优秀生: {string.Join(", ", group.ExcellentStudents)}");
}
4. query syntax里的group by
如果你喜欢SQL风格,或者数据库出身,LINQ也支持类似SQL的group by ... into ...语法。
分组的基本例子
var studentsByClass =
from s in students
group s by s.ClassName into classGroup
select classGroup;
foreach (var group in studentsByClass)
{
Console.WriteLine($"班级: {group.Key}");
foreach (var student in group)
{
Console.WriteLine($" - {student.Name}");
}
}
先按特征分组,然后给分组起个名字(into classGroup)——后面就能随便用了。
带聚合的投影
比如想知道每个班有多少学生:
var countsByCity =
from s in students
group s by s.ClassName into classGroup
select new { Class = classGroup.Key, Count = classGroup.Count() };
foreach (var item in countsByCity)
{
Console.WriteLine($"班级 {item.Class} — {item.Count} 个学生");
}
——语法一样,select后面可以做各种聚合、列表、平均值啥的。
5. 进阶玩法
按多个特征分组(复合key)
有时候要按多个属性组合分组,比如班级+分数:
var byClassAndGrade = students
.GroupBy(s => new { s.ClassName, s.Grade });
foreach (var group in byClassAndGrade)
{
Console.WriteLine($"班级: {group.Key.ClassName}, 分数: {group.Key.Grade}");
foreach (var student in group)
{
Console.WriteLine($" - {student.Name}");
}
}
分组key现在是匿名类型(多个字段组合)。
分组底层怎么实现?
LINQ内部小科普
- 你用GroupBy时,LINQ内部会建个特殊“表”:每个唯一key值就是一个分组。
- 分组实现了IGrouping<TKey, TElement>接口。可以用Key属性拿到分组特征值。
延迟执行和性能
- 分组不是立刻算出来的,只有你遍历(foreach)时才真正生成。所以集合很大也不用怕,没用就不占内存。
- 如果你要多次用分组,建议.ToList()或.ToArray(),这样结果就“固定”下来了。
实际应用场景
分组能搞各种报表、统计和分析。举几个实际例子:
- 电商:按用户分组订单,展示每个人的购买历史。
- 教育平台:按老师或科目分组学生。
- HR应用:按部门分组员工,统计人数。
- 面试时:可能让你写个真实数据的分组+聚合。
LINQ让这些需求原型超快,能看懂和写分组是.NET开发者的加分项。
可视化流程图
下面是用GroupBy处理集合的简单流程图:
[学生集合]
|
[按ClassName分组]
|
[分组 "9A"] ---> [伊万, 奥列格]
[分组 "10B"] --> [玛丽亚, 斯维特拉娜]
每个分组就是带key的小集合。
6. 常见错误和坑
有些同学刚开始会被GroupBy的返回值搞晕:
- 用了GroupBy后,你拿到的不是List<List<Student>>这种“组”,而是IGrouping<TKey, TElement>集合。要用Key属性和遍历分组元素来操作。
- 按复合key分组(多个字段):记得用匿名类型,或者自定义类来做结构化key。
- 只想知道每组有多少元素——别忘了Select(g => g.Count()),不然得手动数每组元素。
- 有时候循环嵌套容易搞混:外层遍历分组,内层遍历分组里的元素。
- lambda或key写错了,分组结果就不对(比如key都一样就全在一组)。
GO TO FULL VERSION