使用该 join 子句关联不同源序列中没有直接关系的元素。 唯一的要求是每个源中的元素共享一些可以比较相等的值。 例如,食品经销商可能具有特定产品的供应商列表和买家列表。 可以使用 join 子句创建属于同一指定区域的该产品的供应商和买家的列表。
C# 语言参考记录了 C# 语言的最新发布版本。 它还包含即将发布的语言版本公共预览版中功能的初始文档。
本文档标识了在语言的最后三个版本或当前公共预览版中首次引入的任何功能。
小窍门
若要查找 C# 中首次引入功能时,请参阅 有关 C# 语言版本历史记录的文章。
join 子句将 2 个源序列作为输入。 每个序列中的元素必须是或包含一个属性,可以与其他序列中的相应属性进行比较。 该 join 子句使用特殊 equals 关键字比较指定键是否相等。 子句执行的所有联接 join 都是等联接。 子句输出 join 的形状取决于要执行的联接的特定类型。 以下列表显示了三种最常见的联接类型:
- 内联
- 分组联接
- 左外部联接
内联
以下示例演示了一个简单的内部同等联接。 此查询生成“产品名称/类别”对的平面序列。 同一类别字符串出现在多个元素中。 如果某个 categories 元素没有匹配 products项,则该类别不会显示在结果中。
var innerJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID
select new { ProductName = prod.Name, Category = category.Name }; //produces flat sequence
有关详细信息,请参阅 “执行内部联接”。
分组联接
含有 join 表达式的 into 子句称为分组联接。
var innerGroupJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
select new { CategoryName = category.Name, Products = prodGroup };
组联接生成层次结构结果序列,该序列将左侧源序列中的元素与右侧源序列中的一个或多个匹配元素进行关联。 分组连接在关系术语中没有等效项,它本质上是一系列对象数组。
如果右侧源序列中没有元素与左侧源中的元素匹配,则 join 子句将为该项生成空数组。 因此,分组联接基本上仍然是一种内部同等联接,区别在于分组联接将结果序列组织为多个组。
如果只是选择组联接的结果,则可以访问这些项,但无法标识它们匹配的键。 因此,选择组联接的结果通常更有用,该类型也具有键名称,如前面的示例所示。
当然,还可以将分组联接的结果用作其他子查询的生成器:
var innerGroupJoinQuery2 =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
from prod2 in prodGroup
where prod2.UnitPrice > 2.50M
select prod2;
有关详细信息,请参阅 执行分组联接。
左外部联接
在左外部联接中,查询将返回左侧源序列中的所有元素,即使没有匹配的元素位于右侧序列中。 若要在 LINQ 中执行左外部联接,请结合使用 DefaultIfEmpty 方法与分组联接,指定要在某个左侧元素不具有匹配元素时生成的默认右侧元素。 可以 null 用作任何引用类型的默认值,也可以指定用户定义的默认类型。 在以下示例中,将显示用户定义的默认类型:
var leftOuterJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
from item in prodGroup.DefaultIfEmpty(new Product { Name = String.Empty, CategoryID = 0 })
select new { CatName = category.Name, ProdName = item.Name };
有关详细信息,请参阅执行左外部联接。
等于运算符
join 子句执行同等联接。 换句话说,匹配只能基于两个键的相等性。 不支持其他类型的比较,例如“大于”或“不相等”。 为了明确所有联接都是等联接,join 子句使用 equals 关键字而不是 == 运算符。 关键字 equals 只能在 join 条款中使用,并且它在某些重要方面与 == 运算符不同。 比较字符串时, equals 具有按值进行比较的重载,运算符 == 使用引用相等性。 当比较双方具有相同的字符串变量时,equals 和 == 都会达到相同的结果:true。 这是因为,当程序声明两个或更多等效的字符串变量时,编译器会将所有这些变量存储在同一位置。 这称为“集中”。 另一个重要区别是 NULL 比较:null equals null 使用 equals 运算符的计算结果为 false,而不使用计算结果为 true 的 == 运算符。 最后,范围行为不同:对于 equals,左键使用外部源序列,而右键使用内部源。 外部源仅在左侧 equals 的作用域中,并且内部源序列仅在右侧的作用域内。
非同等联接
可以使用多个 from 子句将新序列独立引入到查询中,以执行非等值连接、交叉连接和其他自定义连接操作。 有关详细信息,请参阅 执行自定义联接作。
对象集合联接与关系表
在 LINQ 查询表达式中,对对象集合执行联接作。 不能以与两个关系表完全相同的方式联接对象集合。 在 LINQ 中,仅当两个源序列没有任何关系时,才需要显式 join 子句。 使用 LINQ to SQL 时,对象模型将外键表表示为主表的属性。 例如,在 Northwind 数据库中,Customer 表与 Orders 表具有外键关系。 将表映射到对象模型时,Customer 类具有包含 Orders 与该 Customer 关联的集合 Orders 的属性。 实际上,已为你完成联接。
有关在 LINQ to SQL 上下文中跨相关表查询的详细信息,请参阅 “如何:映射数据库关系”。
组合键
可以使用复合键测试多个值的相等性。 有关详细信息,请参阅 使用组合键进行联接。 还可以在子句中使用 group 复合键。
示例:
以下示例使用相同的匹配键比较内部联接、组联接和相同数据源上的左外部联接的结果。 向这些示例添加了一些额外的代码,以阐明控制台显示中的结果。
class JoinDemonstration
{
#region Data
class Product
{
public required string Name { get; init; }
public required int CategoryID { get; init; }
}
class Category
{
public required string Name { get; init; }
public required int ID { get; init; }
}
// Specify the first data source.
List<Category> categories =
[
new Category {Name="Beverages", ID=001},
new Category {Name="Condiments", ID=002},
new Category {Name="Vegetables", ID=003},
new Category {Name="Grains", ID=004},
new Category {Name="Fruit", ID=005}
];
// Specify the second data source.
List<Product> products =
[
new Product {Name="Cola", CategoryID=001},
new Product {Name="Tea", CategoryID=001},
new Product {Name="Mustard", CategoryID=002},
new Product {Name="Pickles", CategoryID=002},
new Product {Name="Carrots", CategoryID=003},
new Product {Name="Bok Choy", CategoryID=003},
new Product {Name="Peaches", CategoryID=005},
new Product {Name="Melons", CategoryID=005},
];
#endregion
static void Main(string[] args)
{
JoinDemonstration app = new JoinDemonstration();
app.InnerJoin();
app.GroupJoin();
app.GroupInnerJoin();
app.GroupJoin3();
app.LeftOuterJoin();
app.LeftOuterJoin2();
}
void InnerJoin()
{
// Create the query that selects
// a property from each element.
var innerJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID
select new { Category = category.ID, Product = prod.Name };
Console.WriteLine("InnerJoin:");
// Execute the query. Access results
// with a simple foreach statement.
foreach (var item in innerJoinQuery)
{
Console.WriteLine("{0,-10}{1}", item.Product, item.Category);
}
Console.WriteLine($"InnerJoin: {innerJoinQuery.Count()} items in 1 group.");
Console.WriteLine(System.Environment.NewLine);
}
void GroupJoin()
{
// This is a demonstration query to show the output
// of a "raw" group join. A more typical group join
// is shown in the GroupInnerJoin method.
var groupJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
select prodGroup;
// Store the count of total items (for demonstration only).
int totalItems = 0;
Console.WriteLine("Simple GroupJoin:");
// A nested foreach statement is required to access group items.
foreach (var prodGrouping in groupJoinQuery)
{
Console.WriteLine("Group:");
foreach (var item in prodGrouping)
{
totalItems++;
Console.WriteLine(" {0,-10}{1}", item.Name, item.CategoryID);
}
}
Console.WriteLine($"Unshaped GroupJoin: {totalItems} items in {groupJoinQuery.Count()} unnamed groups");
Console.WriteLine(System.Environment.NewLine);
}
void GroupInnerJoin()
{
var groupJoinQuery2 =
from category in categories
orderby category.ID
join prod in products on category.ID equals prod.CategoryID into prodGroup
select new
{
Category = category.Name,
Products = from prod2 in prodGroup
orderby prod2.Name
select prod2
};
//Console.WriteLine("GroupInnerJoin:");
int totalItems = 0;
Console.WriteLine("GroupInnerJoin:");
foreach (var productGroup in groupJoinQuery2)
{
Console.WriteLine(productGroup.Category);
foreach (var prodItem in productGroup.Products)
{
totalItems++;
Console.WriteLine(" {0,-10} {1}", prodItem.Name, prodItem.CategoryID);
}
}
Console.WriteLine($"GroupInnerJoin: {totalItems} items in {groupJoinQuery2.Count()} named groups");
Console.WriteLine(System.Environment.NewLine);
}
void GroupJoin3()
{
var groupJoinQuery3 =
from category in categories
join product in products on category.ID equals product.CategoryID into prodGroup
from prod in prodGroup
orderby prod.CategoryID
select new { Category = prod.CategoryID, ProductName = prod.Name };
//Console.WriteLine("GroupInnerJoin:");
int totalItems = 0;
Console.WriteLine("GroupJoin3:");
foreach (var item in groupJoinQuery3)
{
totalItems++;
Console.WriteLine($" {item.ProductName}:{item.Category}");
}
Console.WriteLine($"GroupJoin3: {totalItems} items in 1 group");
Console.WriteLine(System.Environment.NewLine);
}
void LeftOuterJoin()
{
// Create the query.
var leftOuterQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
select prodGroup.DefaultIfEmpty(new Product() { Name = "Nothing!", CategoryID = category.ID });
// Store the count of total items (for demonstration only).
int totalItems = 0;
Console.WriteLine("Left Outer Join:");
// A nested foreach statement is required to access group items
foreach (var prodGrouping in leftOuterQuery)
{
Console.WriteLine("Group:");
foreach (var item in prodGrouping)
{
totalItems++;
Console.WriteLine(" {0,-10}{1}", item.Name, item.CategoryID);
}
}
Console.WriteLine($"LeftOuterJoin: {totalItems} items in {leftOuterQuery.Count()} groups");
Console.WriteLine(System.Environment.NewLine);
}
void LeftOuterJoin2()
{
// Create the query.
var leftOuterQuery2 =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
from item in prodGroup.DefaultIfEmpty()
select new { Name = item == null ? "Nothing!" : item.Name, CategoryID = category.ID };
Console.WriteLine($"LeftOuterJoin2: {leftOuterQuery2.Count()} items in 1 group");
// Store the count of total items
int totalItems = 0;
Console.WriteLine("Left Outer Join 2:");
// Groups have been flattened.
foreach (var item in leftOuterQuery2)
{
totalItems++;
Console.WriteLine("{0,-10}{1}", item.Name, item.CategoryID);
}
Console.WriteLine($"LeftOuterJoin2: {totalItems} items in 1 group");
}
}
/*Output:
InnerJoin:
Cola 1
Tea 1
Mustard 2
Pickles 2
Carrots 3
Bok Choy 3
Peaches 5
Melons 5
InnerJoin: 8 items in 1 group.
Unshaped GroupJoin:
Group:
Cola 1
Tea 1
Group:
Mustard 2
Pickles 2
Group:
Carrots 3
Bok Choy 3
Group:
Group:
Peaches 5
Melons 5
Unshaped GroupJoin: 8 items in 5 unnamed groups
GroupInnerJoin:
Beverages
Cola 1
Tea 1
Condiments
Mustard 2
Pickles 2
Vegetables
Bok Choy 3
Carrots 3
Grains
Fruit
Melons 5
Peaches 5
GroupInnerJoin: 8 items in 5 named groups
GroupJoin3:
Cola:1
Tea:1
Mustard:2
Pickles:2
Carrots:3
Bok Choy:3
Peaches:5
Melons:5
GroupJoin3: 8 items in 1 group
Left Outer Join:
Group:
Cola 1
Tea 1
Group:
Mustard 2
Pickles 2
Group:
Carrots 3
Bok Choy 3
Group:
Nothing! 4
Group:
Peaches 5
Melons 5
LeftOuterJoin: 9 items in 5 groups
LeftOuterJoin2: 9 items in 1 group
Left Outer Join 2:
Cola 1
Tea 1
Mustard 2
Pickles 2
Carrots 3
Bok Choy 3
Nothing! 4
Peaches 5
Melons 5
LeftOuterJoin2: 9 items in 1 group
Press any key to exit.
*/
注解
join不后跟into转换为方法调用的Join子句。
join后跟into转换为方法调用的GroupJoin子句。