左外连接使用了组连接和into子句。它有一部分语法与组连接相同,只不过组连接不使用DefaultIfEmpty方法。
使用组连接时,可以连接两个独立的序列,对于其中一个序列中的某个元素,另一个序列中存在对应的一个项列表。
下面的示例使用了两个独立的序列。一个是前面例子中已经看过的冠军列表。另一个是一个Chanpionship类型的集合。下面的代码段显示了Championship类型。该类包含冠军年份以及该年份中获得第一名、第二名和第三名的赛车手,对应的属性分别为Year、First、Second和Third:
public class Championship
{
public Championship(int year,string first,string second,string third)
{
Year = year;
First = first;
Second = second;
Third = third;
}
public int Year{get;}
public string First{get;}
public string Second{get;}
public string Third{get;}
}
GetChampionships方法返回冠军集合,如下面的代码段所示:
private static List<Championship> s_championships;
public static IEnumerable<Championship> GetChampionships()
{
if(s_championships == null)
{
s_championships = new List<Championship>
{
new Championship(1950,"Nino Farina","Juan Manuel Fangio","Luigi Fagioli"),
new Championship(1951,"Juan Manuel Fangio","Alberto Ascari","Froilan Gonzalez")
};
}
return s_championships;
}
冠军列表应与每个冠军年份中获得前三名的赛车手构成的列表组合起来,然后显示每一年的结果。
因为冠军列表中的每一项都包含3个赛车手,所以首先需要把这个列表摊平。一种方式是使用复合的from子句。由于没有集合可用于单个项目的属性,而是需要将三个属性(First、Second和Third)合并、摊平,因此创建了一个新的List<T>,其中填充了这些属性的信息。对于新建的对象,可以使用自定义类和匿名类型,如前所述。这次使用C#7中的一个新特性:创建一个元组。元组包含不同类型的成员,可以使用带括号的元组字面量创建,如下面的代码片段所示。这里,元组的一个摊平列表包含年份、冠军的位置、赛车手的名字和姓氏信息:
static void GroupJoin()
{
var racers = from cs in Formula1.GetChampionships()
from r in new List<(int Year,int Position,string FirstName,string LastName)>(){
(cs.Year,Position: 1,FirstName: cs.First.FirstName(),LastName: cs.First.LastName()),
(cs.Year,Position: 2,FirstName: cs.Second.FirstName(),LastName: cs.Second.LastName()),
(cs.Year,Position: 3,FirstName: cs.Third.FirstName(),LastName: cs.Third.LastName())
}
select r;
}
扩展方法FirstName和LastName使用空格字符拆分字符串:
public static class StringExtension {
public static string FirstName(this string name)=>
name.Substring(0,name.LastIndexOf(" "));
public static string LastName(this string name)=>
name.Substring(name.LastIndexOf(" ")+1);
}
现在就可以连接两个序列。Formulal.GetChampions返回一个Racers列表,racers变量返回包含年份、比赛结果和赛车手姓名的一个元组。仅使用姓氏比较两个集合中的项是不够的。有时列表中可能同时包含了一个赛车手和他的父亲(如Damon Hill和Graham Hill),所以必须同时使用FirstName和LastName进行比较。这是通过为两个列表创建一个新的元组实现的。通过使用into子句,第二个集合中的结果被添加到变量yearResults中。对于第一个集合中的每一个赛车手,都创建了一个yearResults,它包含在第二个集合中匹配名和姓的结果。最后,用LINQ查询创建了一个包含所需信息的新元组类型:
static void GroupJoin()
{
var racers = from cs in Formula1.GetChampionships()
from r in new List<(int Year,int Position,string FirstName,string LastName)>(){
(cs.Year,Position: 1,FirstName: cs.First.FirstName(),LastName: cs.First.LastName()),
(cs.Year,Position: 2,FirstName: cs.Second.FirstName(),LastName: cs.Second.LastName()),
(cs.Year,Position: 3,FirstName: cs.Third.FirstName(),LastName: cs.Third.LastName())
}
select r;
var q = from r in Formula1.GetChampions()
join r2 in racers on
(
r.FirstName,
r.LastName
)equals
(
r2.FirstName,
r2.LastName
)
into yearResults
select
(
r.FirstName,
r.LastName,
r.Wins,
r.Starts,
Result: yearResults
);
foreach(var r in q)
{
if(r.Result.Count() > 0)//若第一个集合中的每一个赛车手,在第二个集合中没有匹配到结果,即Count属性为0,不予遍历
{
System.Console.WriteLine($"{r.FirstName} {r.LastName}");
foreach(var results in r.Result )
{
System.Console.WriteLine($"\t{results.Year} {results.Position}");
}
}
}
}
下面显示了foreach循环得到的最终结果。如Juan Manuel Fangio 2次进入前三:1950年是第二名,1951年是第一名。
Nino Farina
1950 1
Alberto Ascari
1951 2
Juan Manuel Fangio
1950 2
1951 1
使用GroupJoin和扩展方法,语法可能看起来更容易理解。首先,使用SelectMany方法完成复合的from子句。这一部分没有太大的不同,并且再次使用了元组。调用GroupJoin方法时,传递赛车手作为第一个参数,把冠军与摊平的赛车手连接起来,用第二个和第三个参数来匹配两个集合。第四个参数接收第一个集合和第二个集合的赛车手。结果包含位置和年份,被写入Results元组成员:
static void GroupJoinWithMethods()
{
var racers = Formula1.GetChampionships()
.SelectMany(cs=>new List<(int Year,int Position,string FirstName,string LastName)>()
{
(cs.Year,Position: 1,FirstName: cs.First.FirstName(),LastName: cs.First.LastName()),
(cs.Year,Position: 2,FirstName: cs.Second.FirstName(),cs.Second.LastName()),
(cs.Year,Position: 3,FirstName: cs.Third.FirstName(),cs.Third.LastName())
});
var q = Formula1.GetChampions()
.GroupJoin(racers,
r1=>(r1.FirstName,r1.LastName),
r2=>(r2.FirstName,r2.LastName),
(r1,r2s)=>(r1.FirstName,r1.LastName,r1.Wins,r1.Starts,Results: r2s));
}