1. 入门
有时候我们得处理两个不同的集合,它们通过某个共同点关联在一起。比如有个订单列表,还有一个单独的买家集合。怎么知道哪个订单属于哪个买家?这就得让两个集合通过一个共同的id关联起来——比如 userId。
在关系型数据库里,这种事就是用 JOIN 操作搞定的,它会把不同表里key相同的行合并起来。LINQ里也有类似的操作符——名字也叫 Join。
想象下有两张表:第一张是图书馆读者名单和他们的id,第二张是图书订单列表,每个订单里有个 id 表示读者是谁。用 join,我们就能把这两张表按 id 拼起来,最后得到「读者+他的订单」的配对。
顺便说一句,如果你觉得 join 只用在「数据库」里,那你可真是错过了不少东西!编程里合并集合其实很常见,尤其是和外部系统或者结构化文件打交道的时候(比如 JSON、XML 或者会计发来的Excel表)。
2. Join 方法签名和原理
Join 方法长这样:
public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer, // 第一个集合(外部)
IEnumerable<TInner> inner, // 第二个集合(内部)
Func<TOuter, TKey> outerKeySelector, // 怎么从外部集合元素拿key
Func<TInner, TKey> innerKeySelector, // 怎么从内部集合元素拿key
Func<TOuter, TInner, TResult> resultSelector) // 生成结果(新元素)的函数
乍一看有点吓人,其实很简单,咱们慢慢拆开说。
工作原理:
LINQ会对第一个集合(outer)里的每个元素,去第二个集合(inner)里找key相同的元素。key对上了,就调用 resultSelector,把结果塞进最终集合里。
3. 示例:合并买家和他们的订单
先来写俩class和集合。这思路就是我们课程里那个小商店项目的延续。
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public string Product { get; set; } = "";
}
// 买家集合
var customers = new List<Customer>
{
new Customer { Id = 1, Name = "瓦夏" },
new Customer { Id = 2, Name = "彼佳" },
new Customer { Id = 3, Name = "玛莎" },
};
// 订单集合
var orders = new List<Order>
{
new Order { Id = 101, CustomerId = 2, Product = "书" },
new Order { Id = 102, CustomerId = 1, Product = "钢笔" },
new Order { Id = 103, CustomerId = 2, Product = "本子" },
new Order { Id = 104, CustomerId = 3, Product = "橡皮" },
};
现在我们想拿到所有买家和他们的订单。比如输出:"彼佳 订购了 书","彼佳 订购了 本子" 之类的。
用 Join
// 按 customer.Id 和 order.CustomerId 合并买家和订单
var query = customers.Join(
orders,
customer => customer.Id, // 怎么从买家拿key
order => order.CustomerId, // 怎么从订单拿key
(customer, order) => new // key对上的pair怎么处理(创建新对象)
{
CustomerName = customer.Name,
Product = order.Product
}
);
// 输出结果
foreach (var item in query)
{
Console.WriteLine($"{item.CustomerName} 订购了 {item.Product}");
}
输出结果:
瓦夏 订购了 钢笔
彼佳 订购了 书
彼佳 订购了 本子
玛莎 订购了 橡皮
注意:如果一个买家有多个订单,他会在列表里出现多次——每个订单一行。就是应该这样!
4. 表格:Join 和 GroupBy + SelectMany 对比
| 操作 | 结果 | 使用场景 |
|---|---|---|
|
扁平的pair列表 | 经典SQL JOIN。每个匹配pair一行。 |
|
带子集合的分组 | 想要「买家+他所有订单」这种一对多结构时用。 |
新手经常在需要「买家→订单列表」结构时用 Join。但 Join 是一行一行配对,不会分组。要分组得用 GroupJoin(下节课讲),或者 GroupBy。
5. Query Syntax(SQL风格LINQ)里的 Join
LINQ支持两种写法:方法链(Method Syntax,比如 Join(...))和SQL风格(Query Syntax),看起来像普通SQL语句。
有些开发者——尤其是干过数据库的——觉得这种写法更直观:
var query2 =
from customer in customers
join order in orders
on customer.Id equals order.CustomerId
select new
{
CustomerName = customer.Name,
Product = order.Product
};
foreach (var item in query2)
{
Console.WriteLine($"{item.CustomerName} 订购了 {item.Product}");
}
注意:
在 Query Syntax 里要用 equals 关键字——这里 == 不管用!
这点面试时经常考,尤其是新手 😉
6. 重要细节和坑
有时候不是所有集合元素都会出现在最终结果里。这是因为 Join 实现的是 「inner join」,只合并key能对上的元素。如果某个买家没订单,他就不会出现在结果里。同理,如果有个订单的 CustomerId 在买家里找不到,这个订单也不会进结果。
那如果想要所有买家都出现(哪怕没订单)怎么办?这种情况要用「左连接」,LINQ里得用 GroupJoin 加 SelectMany 组合搞定。下节课会讲。普通 Join 必须两边都有,key对不上就不会出现在结果里。
7. 多key(composite key)合并
有时候合并集合得用多个字段。比如:按「商品编码+销售年份」合并商品和销售记录。
LINQ里可以用匿名对象做key来搞定:
var sales = ...; // 销售记录
var products = ...; // 商品
var query = products.Join(
sales,
prod => new { prod.Code, prod.Year },
sale => new { sale.ProductCode, sale.Year },
(prod, sale) => new { prod.Name, sale.Amount }
);
注意匿名对象里的属性类型和名字必须一样。不一样就算值一样也匹配不上。
8. 合并后的集合长啥样——示意图
下面这个图简单说明了join的原理:
flowchart LR
subgraph Customers
A1["瓦夏 (Id=1)"]
A2["彼佳 (Id=2)"]
A3["玛莎 (Id=3)"]
end
subgraph Orders
B1["钢笔 (CustomerId=1)"]
B2["书 (CustomerId=2)"]
B3["本子 (CustomerId=2)"]
B4["橡皮 (CustomerId=3)"]
end
A1-->|Id=1|B1
A2-->|Id=2|B2
A2-->|Id=2|B3
A3-->|Id=3|B4
从图里能看出,每个买家会和他所有订单连起来——只要key能对上。
9. 常见错误和注意点
最常见的错误之一就是把参数顺序搞反,尤其是两边key类型一样的时候。编译器和LINQ都不会报错,但结果可能是空的或者很奇怪。
还有个常见坑——想用 Join 做「左连接」,让所有买家都在结果里(哪怕没订单)。但经典 Join 只会配对key能对上的pair,不会包含没匹配的元素。
GO TO FULL VERSION