在C#的集合查询场景中,我们经常会遇到需要把两个集合按照某个共同字段关联,并且保留左集合所有元素的需求,这和SQL中的左外连接逻辑一致。LINQ提供的GroupJoin方法可以很好地实现这个效果,下面详细介绍具体的使用方式。

GroupJoin基础语法说明
GroupJoin是LINQ的扩展方法,主要用于将两个集合按照键选择器进行关联,左集合的每个元素都会对应右集合中匹配的所有元素组成的序列。它的基本方法签名如下:
public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter, IEnumerable<TInner>, TResult> resultSelector
);
参数说明:
outer:左集合,也就是要保留所有元素的集合inner:右集合,也就是要关联匹配的集合outerKeySelector:从左集合元素中提取关联键的函数innerKeySelector:从右集合元素中提取关联键的函数resultSelector:结果投影函数,接收左集合元素和对应的右集合匹配元素序列,返回最终结果
GroupJoin实现左外连接的逻辑
SQL的左外连接会返回左表的所有记录,即使右表没有匹配的记录,对应右表字段会显示为null。GroupJoin的默认行为是左集合每个元素都会出现在结果中,即使右集合没有匹配的元素,对应的右集合序列会是空序列,这和左外连接的逻辑是契合的。我们只需要处理右集合为空的情况,把空序列转换为对应的默认值即可。
实际示例演示
假设我们有两个业务集合,一个是用户列表,一个是订单列表,一个用户可能有多个订单,也可能没有订单,我们需要查询所有用户以及他们的订单信息,这就是典型的左外连接场景。
定义实体类
// 用户实体
public class User
{
public int UserId { get; set; }
public string UserName { get; set; }
}
// 订单实体
public class Order
{
public int OrderId { get; set; }
public int UserId { get; set; }
public decimal Amount { get; set; }
}
准备测试数据
// 用户列表,包含有订单和无订单的用户
List<User> users = new List<User>
{
new User { UserId = 1, UserName = "张三" },
new User { UserId = 2, UserName = "李四" },
new User { UserId = 3, UserName = "王五" }
};
// 订单列表,只有用户1和用户2有订单
List<Order> orders = new List<Order>
{
new Order { OrderId = 101, UserId = 1, Amount = 199.9m },
new Order { OrderId = 102, UserId = 1, Amount = 299.9m },
new Order { OrderId = 103, UserId = 2, Amount = 399.9m }
};
使用GroupJoin实现左外连接查询
var result = users.GroupJoin(
orders,
user => user.UserId, // 左集合关联键:用户ID
order => order.UserId, // 右集合关联键:用户ID
(user, userOrders) => new // 结果投影
{
UserId = user.UserId,
UserName = user.UserName,
// 如果右集合匹配为空,返回空列表,避免null
Orders = userOrders.DefaultIfEmpty(),
// 也可以计算订单总数,无订单则为0
OrderCount = userOrders.Count()
}
);
// 遍历输出结果
foreach (var item in result)
{
Console.WriteLine($"用户ID:{item.UserId},用户名:{item.UserName},订单数量:{item.OrderCount}");
foreach (var order in item.Orders)
{
if (order != null)
{
Console.WriteLine($" 订单ID:{order.OrderId},订单金额:{order.Amount}");
}
else
{
Console.WriteLine(" 该用户暂无订单");
}
}
}
上述代码的输出结果会包含用户3的信息,即使他没有对应的订单,符合左外连接的要求。
GroupJoin和Join的差异对比
| 对比项 | Join | GroupJoin |
|---|---|---|
| 匹配逻辑 | 内连接,只返回左右集合都匹配的元素 | 类似左外连接,返回左集合所有元素,右集合匹配多个或零个 |
| 结果结构 | 左集合一个元素匹配右集合一个元素,生成一条结果 | 左集合一个元素对应右集合一个匹配序列,生成一条结果 |
| 适用场景 | 一对一关联查询 | 一对多关联查询,需要保留左集合全部元素 |
注意事项
- 关联键的类型必须一致,否则会导致匹配失败,比如左集合键是int,右集合键也必须是int
- 如果右集合可能为空,建议在结果投影中使用
DefaultIfEmpty方法,避免后续遍历出现空引用异常 - GroupJoin的匹配是延迟执行的,只有遍历结果时才会真正执行关联查询