1. 入门
想象下经典场景:你有两个集合,它们逻辑上有关联。比如商品分类列表和商品列表。你要为每个分类拿到这个分类下的所有商品。或者有公司部门列表和员工列表,要输出每个部门的所有员工。
在SQL里这叫“分组连接”(GROUP JOIN,更准确点是LEFT OUTER JOIN加分组)。LINQ里有专门的操作符——GroupJoin。它介于普通的Join(每个左边的记录只连一个右边的)和按key分组之间。GroupJoin会把一个集合的每个元素,和另一个集合里所有相关的元素作为集合连起来。
类比
如果普通Join像是按姓氏把“爸爸和儿子”配成对,GroupJoin就是建一棵树:每个爸爸下面挂上所有他的孩子。
图示
分类 商品
+--------------+ +---------------------+
| Id | 名称 | | 名称 | CatId |
+----+---------+ +-----------+---------+
| 1 | 面包 | ---> | 法棍 | 1 |
| 2 | 饮料 | | 香肠 | 3 |
| 3 | 肉类 | | 百事可乐 | 2 |
| | | | 茶 | 2 |
+----+---------+ +-----------+---------+
用GroupJoin后:
- 面包 — [法棍]
- 饮料 — [百事可乐, 茶]
- 肉类 — [香肠]
2. 方法签名和基础概念
扩展方法
public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer, // “外部”集合(比如分类)
IEnumerable<TInner> inner, // “内部”集合(比如商品)
Func<TOuter, TKey> outerKeySelector, // 怎么从外部元素拿key
Func<TInner, TKey> innerKeySelector, // 怎么从内部元素拿key
Func<TOuter, IEnumerable<TInner>, TResult> resultSelector // 结果对象/记录的工厂
)
- outer: 要遍历的集合,其他元素会连到它(比如分类)。
- inner: 要选出要连的元素的集合(比如商品)。
- outerKeySelector: 返回“左边”元素key的lambda。
- innerKeySelector: 返回“右边”元素key的lambda。
- resultSelector: 决定每对(左+右组)结果长啥样的函数。
3. 实战例子:分类和商品
假设我们有如下模型:
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Product
{
public string Name { get; set; }
public int CategoryId { get; set; }
}
例子里的集合:
var categories = new List<Category>
{
new Category { Id = 1, Name = "面包" },
new Category { Id = 2, Name = "饮料" },
new Category { Id = 3, Name = "肉类" }
};
var products = new List<Product>
{
new Product { Name = "法棍", CategoryId = 1 },
new Product { Name = "百事可乐", CategoryId = 2 },
new Product { Name = "茶", CategoryId = 2 },
new Product { Name = "香肠", CategoryId = 3 }
};
用GroupJoin(方法语法)
var groupJoin = categories.GroupJoin(
products,
category => category.Id, // 分类key
product => product.CategoryId, // 商品key
(category, prods) => new // 直接构造结果
{
CategoryName = category.Name,
Products = prods.Select(p => p.Name).ToList() // 该分类下商品名列表
}
);
怎么遍历结果:
foreach (var group in groupJoin)
{
Console.WriteLine($"分类: {group.CategoryName}");
foreach (var product in group.Products)
{
Console.WriteLine($" - {product}");
}
}
输出:
分类: 面包
- 法棍
分类: 饮料
- 百事可乐
- 茶
分类: 肉类
- 香肠
4. GroupJoin:查询语法(Query Syntax)
LINQ支持类似SQL的语法。group join用关键字join ... into ...,写法和上面差不多。
var groupJoin2 = from c in categories
join p in products on c.Id equals p.CategoryId into prodGroup
select new
{
CategoryName = c.Name,
Products = prodGroup.Select(p => p.Name).ToList()
};
这很像SQL里的LEFT OUTER JOIN ... GROUP BY。
可视化:GroupJoin怎么工作的
[分类] [商品] 分组(GroupJoin)
面包 --------> 法棍 => 面包: [法棍]
饮料 --------> 百事可乐 => 饮料: [百事可乐, 茶]
饮料 --------> 茶
肉类 --------> 香肠 => 肉类: [香肠]
每个分类会有自己的“小口袋”(IEnumerable<Product>),里面装着这个分类下的所有商品。
5. 特点和坑
GroupJoin vs. 普通Join
普通Join和GroupJoin的区别在于结果数量。Join每个匹配返回一对,GroupJoin每个外部元素返回一个,里面是所有匹配的集合。
如果有分类没有商品,用GroupJoin它还是会出现,只不过商品集合是空的。这和SQL里的LEFT OUTER JOIN(左外连接)一样。
比如有个没有商品的分类:
categories.Add(new Category { Id = 4, Name = "奶酪" });
var groupJoin3 = categories.GroupJoin(
products,
c => c.Id,
p => p.CategoryId,
(c, prods) => new
{
CategoryName = c.Name,
Products = prods.Select(p => p.Name).ToList()
});
foreach (var group in groupJoin3)
{
Console.WriteLine($"分类: {group.CategoryName}");
if (group.Products.Count == 0)
Console.WriteLine(" (没有商品)");
else
foreach (var product in group.Products)
Console.WriteLine($" - {product}");
}
分类: 面包
- 法棍
分类: 饮料
- 百事可乐
- 茶
分类: 肉类
- 香肠
分类: 奶酪
(没有商品)
这种场景在业务系统里很常见:要显示所有分类(或分组),即使有的分组里啥都没有。
用GroupBy实现?别!
很多人会搞混,想用双GroupBy“模拟”GroupJoin。别这样——GroupJoin就是为这设计的,原生支持“左连接”。
6. 用真实数据
在前面讲的基础上,咱们给学习用的小程序加个报表功能:“每个分类下的商品列表”。这在电商、CRM、管理或报表系统里很常用。
加点代码到demo里:
// 假设已经有Category和Product类和集合
Console.WriteLine("分类和商品报表:");
var categoryReport = categories.GroupJoin(
products,
cat => cat.Id,
prod => prod.CategoryId,
(cat, prods) => new
{
cat.Name,
ProductNames = prods.Select(p => p.Name).ToList()
});
foreach (var row in categoryReport)
{
Console.WriteLine($"分类: {row.Name}");
if (row.ProductNames.Count == 0)
Console.WriteLine(" (没有商品)");
else
foreach (var prodName in row.ProductNames)
Console.WriteLine($" - {prodName}");
}
7. 嵌套分组和聚合
GroupJoin可以和聚合函数一起用,做更复杂的报表。
例子:统计每个分类下商品数量
var reportWithCount = categories.GroupJoin(
products,
category => category.Id,
product => product.CategoryId,
(category, prods) => new
{
Category = category.Name,
Count = prods.Count() // 聚合函数!
});
foreach (var rec in reportWithCount)
{
Console.WriteLine($"{rec.Category}: {rec.Count}个商品");
}
假设categories和products集合如下:
var categories = new[]
{
new { Id = 1, Name = "水果" },
new { Id = 2, Name = "蔬菜" },
new { Id = 3, Name = "乳制品" }
};
var products = new[]
{
new { Id = 1, Name = "苹果", CategoryId = 1 },
new { Id = 2, Name = "香蕉", CategoryId = 1 },
new { Id = 3, Name = "胡萝卜", CategoryId = 2 }
};
控制台输出:
分类: 水果 — 2个商品
分类: 蔬菜 — 1个商品
分类: 乳制品 — 0个商品
8. GroupJoin和新手常见错误
常见错误——以为GroupJoin会返回扁平表,像普通Join那样。这里的“扁平”指的是每行一对:一个外部元素和一个对应的内部元素(像SQL里INNER JOIN后的表)。
这对有点数据库经验的人尤其容易误导:他们以为GroupJoin会像LEFT JOIN那样,但其实它返回的是外部元素加一组相关的内部元素——本质上是嵌套结构。
记得需要时“展开”嵌套集合——比如用SelectMany,如果你想要普通的配对序列。
还有个常见坑——忘了没匹配的元素子集合就是空列表。这是默认行为,不是bug——但要记住,不然会奇怪为啥输出“啥都没有”。
什么时候用GroupJoin,什么时候不用?
用GroupJoin的场景:
- 你有两组数据(比如部门和员工、分类和商品),要按层级显示:每个“父”下面所有“子”。
- 做复杂报表,要显示所有主元素,即使“子元素”没有。
- 需要SQLLEFT OUTER JOIN加按key分组的效果。
不要用GroupJoin在只需要交集或每个匹配只要一对的场景——那用普通Join就行。
GO TO FULL VERSION