/查询表达式必须以form子句开头,以select或者group子句结束,在这两个子句之间,可以使用
///where 、orderby、join、let和其他的from子句
本章要点
用列表在对象上执行传统查询
扩展方法
LINQ查询操作符
平行LNQ
表达式树
本章源代码下载地址( wrox. com )
打开网页www.wrox.com/go/procsharp,单击DownloadCode选项卡即可下载本章源代码。本章
弋码分为以下几个主要的示例文件
·LINQ Intro
·Enumerable Sample
·rallel LINQ
· Expression Trees
11.1LNQ概述
语言集成查询( Language Integrated Query,LNQ在C#编程语言中集成了查询语法,可以用相同
的语法访问不同的数据源。LNQ提供了不同数据源的抽象层,所以可以使用相同的语法。
本章介绍LINQ的核心原理和C#中支持C#LNQ查询的语言扩展。
读完本章后,在数据库中使用LNQ的内容可查阅第33章,查询MML数据的内
容可参见第34章
在介绍LNQ的特性之前,本节先介绍一个简单的LNQ查询。C#提供了转换为方法调用的集成查
询语言。本节会说明这个转换的过程,以便用户使用LNQ的全部功能。
111.1列表和实体
本章的LNQ查询在一个包含1950-2011年级方程式锦标赛的集合上进行,这些数据需要使
用实体类和列表来准备。
对于实体,定义类型 Racer. Racer定义了几个属性和个重载的 ToString0方法,该方法以字符
串格式显示赛车手。这个类实现了 IFormattabl接口,以支持格式字符串的不同变体,这个类还实
现了 IComparable
/// 使用这些准备好的列表和实体,进行LINQ查询,例如查询出来自巴西的所有世界冠军,
/// 并按照夺冠次数排序,为此可以使用List类的方法,如FindAll()和Sort()方法,
/// 而使用LINQ的语法非常简单如下:
///
///下面就是一个LINQ查询,子句form、where、orderby、dexcending 和select都是这个查询中预定义的
///关键子。
///查询表达式必须以form子句开头,以select或者group子句结束,在这两个子句之间,可以使用
///where 、orderby、join、let和其他的from子句
///变量query只是指定了LINQ查询,该查询不是通过这个赋值语句执行的,当使用foreach循环访问的时候
///该查询就会执行
///
static void LINQQuery()
{
var query = from r in Formula1.GetChampions()
where r.Country == "Brazil"
orderby r.Wins descending
select r;
foreach (var r in query)
{
Console.WriteLine("{0:A}", r);
}
}
11.1.3 扩展方法
编译器会转换LNQ查询,以调用方法而不是LNQ查询。LNQ为 IEnumerable接口提供
了各种扩展方法,以便用户在实现了该接口的任意集合上使用LNQ查询。扩展方法在静态类中声
明,定义为一个静态方法,其中第一个参数定义了它扩展的类型
扩展方法可以将方法写入最初没有提供该方法的类中。还可以把方法添加到实现某个特定接口
的任何类中,这样多个类就可以使用相同的实现代码
例如, String类没有Foo()方法, String类是密封的,所以不能从这个类中继承。但可以创建
个扩展方法,如下所示
public static class stringExtension
{
public static void Foo(this string s)
{
Console. WriteLine("Foo invoked for (01",s);
}
}
扩展方法定义为静态方法,其第一个参数定义了它扩展的类型,扩展方法在一个静态类中声明
Foo方法扩展了 String类,因为它的第一个参数定义为 String类型,为了区分扩展方法和般的静
态方法,扩展方法还需要对第一个参数使用this关键字
现在就可以使用带 string类型的Foo方法了:
string a="Hel1";
a.Foo() ;
结果在控制台上显示“ Foo invoked for Hello”,因为 Hello是传递给FoO方法的字符串。
也许这看起来违反了面向对象的规则,因为给一个类型定义了新方法,但没有改变该类型或派生
自它的类型。但实际上并非如此。扩展方法不能访问它扩展的类型的私有成员。调用扩展方法只是调
用静态方法的种新语法。对于字符串,可以用如下方式调用Foo()方法,获得相同的结果
string s=“Hel1o” ;
stringExtension Foo (s);
要调用静态方法,应在类名的后面加上方法名。扩展方法是调用静态方法的另一种方式。不必
提供定义了静态方法的类名,相反,编译器调用静态方法是因为它带的参数类型。只需导入包含该
类的名称空间,就可以将Foo()扩展方法放在 String类的作用域中
定义LNQ扩展方法的个类是 System. Linq名称空间中的 Numerable.只需要导入这个名称
空间,就打开了这个类的扩展方法的作用域。下面列出了Where()扩展方法的实现代码。 where()扩
展方法的第一个参数包含了this关键字,其类型是 IEnumerable。这样, where()方法就可以用于
实现了正 IEnumerable的每个类型例如数组和List类实现了 IEnumerable接口。第二个参
数是一个Fun委托,它引用了一个返回布尔值、参数类型为T的方法,这个谓词在实现代
码中调用,检查 IEnumerable源中的项是否应放在目标集合中,如果委托引用了该方法, yield
return语句就将源中的项返回给目标。
public static IEnumerablewhere(
this IEnumerable source,
Func predicate)
{
foreach (TSource item in source)
if (predicate (item))
}
因为Whee()作为一个泛型方法实现,所以它可以用于包含在集合中的任意类型。实现了
IEnumerabl接口的任意集合都支持它
现在就可以使用Enumerable类中的扩展方法Where()、OrderByDescending()和Select()。这些方
法都返回正 IEnumerable< TSource>,所以可以使用前面的结果依次调用这些方法。通过扩展方法的参
数,使用定义了委托参数的实现代码的匿名方法
static void ExtensionMethods()
{
var champions = new List(Formula1.GetChampions());
IEnumerable brazilChampions =
champions.Where(r => r.Country == "Brazil").
OrderByDescending(r => r.Wins).
Select(r => r);
foreach (Racer r in brazilChampions)
{
Console.WriteLine("{0:A}", r);
}
}
11.14推迟查询的执行
在运行期间定义查询表达式时,查询就不会运行。查询会在迭代数据项时运行
再看看扩展方法 Wbere()它使用 yield retum语句返回谓词为ue的元素因为使用了 yield returm
语句,所以编译器会创建一个枚举器,在访问枚举中的项后,就返回它们
public static IEnunerable where(this IEnumerable source, func predicate)
{
foreach (T item in source)
{
if (predicate(item))
{
yield return item;
}
}
}
这是一个非常有趣也非常重要的结果。在下面的例子中,创建了Sting元素的一个集合,用名
称填充它。接着定义一个查询,从集合中找出以字母J开头的所有名称。集合也应是排好序的。在
定义查询时,不会进行迭代。相反,迭代在 foreach语句中进行,在其中迭代所有的项。集合中只有
个元素Juan满足 where表达式的要求,即以字母J开头。迭代完成后,将Juan写入控制台。之后
在集合中添加4个新名称,再次进行迭
var names = new List { " Nino", "Alberto", "Juan", "Mike", "Phil" };
var nameswithJ = from n in names
where n.StartsWith("J")
orderby n
select n;
Console.WriteLine("First iteration");
foreach (string name in nameswithJ)
{
Console.WriteLine(name);
}
Console.WriteLine();
names.Add("John");
names.Add("Jim");
names.Add("Jack");
names.Add("Denny");
Console.WriteLine("Second iteration");
foreach (string name in nameswithJ) {
Console.WriteLine(name);
}
因为选代在查询定义时不会进行,而是在执行每个 foreach语句时进行,所以可以看到其中的变
化,如应用程序的结果所示
First iteration
Juan
Second iteration
Jack
Jim
John
Juan
当然,还必须注意,每次在选代中使用查询时,都会调用扩展方法。在大多数情况下,这是非
常有效的,因为我们可以检测出源数据中的变化。但是在一些情况下,这是不可行的。调用扩展
法 ToArmay(), TList()等可以改变这个操作在示例中, ToList遍历集合返回一个实现了 List
的集合。之后对返回的列表遍历两次,在两次迭代之间,数据源得到了新名称
var names = new List { "Nino", "Alberto", "Juan", "Mike", "Phil" };
var nameswithJ = (from n in names
where n.StartsWith("J")
orderby n
select n).ToList();
Console.WriteLine("First teration");
foreach (string name in nameswithJ)
{
Console.WriteLine(name);
}
Console.WriteLine();
names.Add("Jim");
names.Add("Jack");
names.Add("Denny ");
Console.WriteLine("Second iteration");
foreach (string name in nameswithJ)
{
Console.WriteLine(name);
}
在结果中可以看到,在两次选代之间输出保持不变,但集合中的值改变了
First iteration
Juan
Second iteration
Juan
11.2.1筛选
11.2.1筛选
下面介绍一些查询的示例
使用where子句,可以合并多个表达式,例如,找出赢得至少15场比赛的巴西和奕地利赛车
传递给 where子句的表达式的结果类型应是布尔类型:
var racers = from r in Formula1.GetChampions()
where r.Wins > 15 &&
(r.Country == "Brazil" || r.Country == "Austria")
select r;
foreach (var r in racers) {
Console.WriteLine("(0: A)", r);
}
用这个LNQ查询启动程序,会返回 Nia Lauda、 Nelson Piquet和 Ayrton Senna,如下
Niki Lauda, Austria, Starts: 173, Wins: 25
Nelson Piquet, Brazil, Starts: 204, wins: 23
Ayrton Senna, Brazil, starts: 16l, Wins: 41
并不是所有的查询都可以用LNQ查询语法完成。也不是所有的扩展方法都映射到LNQ查询
子句上。高级查询需要使用扩展方法。为了更好地理解带扩展方法的复杂查询,最好看看简单的查
询是如何映射的。使用扩展方法 Where()和 Select(),会生成与前面LNQ查询非常类似的结果:
var racers = Formula1.GetChampions().
Where(x => x.Wins > 15 && (x.Country == "Bra211" || x.Country == "Austria")).
Select(r => r);
11.2.2用索引筛选
11.2.2用索引筛选
不能使用LNQ查询的一个例子是Wbere()方法的重载,在Where()方法的重载中,可以传递第
二个参数----索引。索引是筛选器返回的每个结果的计数器可以在表达式中使用这个索引,执行
基于索引的计算。下面的代码由 where扩展方法调用,它使用索引返回姓氏以A开头、索引为偶
数的赛车手(代码文件 EnumerableSample/Program.cs)
static void IndexFiltering()
{
var racers = Formula1.GetChampions().
Where((r, index) => r.LastName.StartsWith("A") && index % 2 != 0);
foreach (var r in racers)
{
Console.WriteLine("{0:A}", r);
}
}
姓氏以A开头的所有赛车手有 Alberto ascan、 Manio Andretti和 Femando alonso。因为 Mano
Andretti的索引是奇数,所以他不在结果中
Alberto Ascari, Italy: starts: 32, wins: 10
Fernando Alonso, Spain: starts: 177, wins: 27
11.23类型筛选
11.23类型筛选
为了进行基于类型的筛选,可以使用 flype扩展方法,这里数组数据包含sng和it对象
使用 OfTypeO扩展方法,把 string类传送给泛型参数,就从集合中仅返回字符串(代码文件
numerableSample/Program.cs)
static void TypeFiltering()
{
object[] data = { "one", 2, 3, "four", "five", 6 };
var query = data.OfType();
foreach (var s in query)
{
Console.WriteLine(s);
}
}
运行这段代码,就会显示字符串one、four和fve
one
four
five
11.24复合的from子句
11.2.4复合的from子句
如果需要根据对象的一个成员进行筛选,而该成员本身是一个系列,就可以使用复合的fom子
句。Racer类定义了一个属性Cars,其中Cars是一个字符串数组。要筛选驾驶法拉利的所有冠军,
可以使用如下所示的LNQ查询第一个from子句访问从 Formula1.GetChampions()方法返回的 Racer
对象,第二个fom子句访问 Racer类的Cars属性,以返回所有 stnng类型的赛车。接着在whee子
句中使用这些赛车筛选驾驶法拉利的所有冠军(代码文件 Enumerable Sample/Program.cs
static void CompoundFrom()
{
var ferrariDrivers = from r in Formula1.GetChampions()
from c in r.Cars
where c == "Ferrari"
orderby r.LastName
select r.FirstName + " " + r.LastName;
foreach (var racer in ferrariDrivers)
{
Console.WriteLine(racer);
}
}
这个查询的结果显示了驾驶法拉利的所有一级方程式冠军:
Alberto Ascari
Juan Manuel Fangio
Mike Hawthorn
hil HiiI
Niki Lauda
R土 koren
Scheckter
I Schumacher
John surtees
C#编译器把复合的form子句和LNQ查询转换为 SelectMary()扩展方法。 SelectMary()方法可
用于迭代序列的序列。示例中 SelectMary()方法的重载版本如下所示
public static IEnunerable SelectMany(
this IEnumerable source,
Func> collectionSelectcr,
Func resulSelector);
第一个参数是隐式参数,它从GetChampions()方法中接收 Racer对象序列。第二个参数是
collectionSelectcr委托,其中定义了内部序列。在 lambda表达式r=>r.Cars中,应返回赛车集合。
第三个参数是个委托,现在为每个赛车调用该委托,接收Racer和Car对象。 lambda表达式创建
了一个匿名类型,它有 Racer和Car属性,这个 SelectMary()方法的结果是摊平了赛车手和赛车的层
次结构,为每辆赛车返回匿名类型的一个新对象集合
这个新集合传递给 Where方法,筛选出驾驶法拉利的赛车手。最后,调用 OrderBy()和Select()
方法
var ferrariDrivers = Formula1.GetChampions().
SelectMany(r => r.Cars, (r, c) =>
new { Racer = r, Car = c }).
Where(r => r.Car == "Ferrari").
OrderBy(r => r.Racer.LastName).
Select(r => r.Racer.FirstName + " " + r.Racer.LastName);
把 SelectMany()泛型方法解析为这里使用的类型,所解析的类型如下所示。在这个例子中,数
据源是Raer类型,所筛选的集合是一个sing数组,当然所返回的匿名类型的名称是未知的,这
里显示为 TResult
public static IEnunerable SelectMany(
this IEnumerable source,
Func> collectionSelectcr,
Func resulSelector);
查询仅从LNQ查询转换为扩展方法,所以结果与前面
11.2.5排序
11.25排序
要对序列排序,前面使用了 orderby子句。下面复习一下前面使用的例子,但这里使用 orderby
descending子句。其中赛车手按照豪得比赛的次数进行降序排序,赢得比赛的次数用关键字选择器
指定(代码文件 EnumerableSample/Program.cs)
static void Filtering()
{
var racers = from r in Formula1.GetChampions()
where r.Wins > 15 && (r.Country == "Brazil" || r.Country == "Austria")
select r;
foreach (var r in racers)
{
Console.WriteLine("{0:A}", r);
}
}
orderby子句解析为 OrderByo方法, orderby descending子句解析为 OrderBy Descending方法:
var racers = from r in Formula1.GetChampions().
Where(r => r.Country == "Brazil").
OrderByDescending(r => r.Wins).
Select(r=>r);
OrderBy()和 OrderByDescending()方法返回 IOrderEnumerable,这个接口派生自
IEnumerable接口,但包含一个额外的方法 CreateOrderedEnumerable()。这个方
法用于进一步给序列排序。如果根据关键字选择器来排序,其中有两项相同,就可以使用 ThenBy()
和 ThenByDescending()方法继续排序。这两个方法需要 IOrderEnumerable接口才能工作,
但也返回这个接口。所以,可以添加任意多个 Then By0和 Then By Descending方法,对集合排序
使用LNQ查询时,只需要把所有用于排序的不同关键字(用逗号分隔开漆加到 orderby子句中
在下例中,所有的赛车手先按照国家排序,再按照姓氏排序,最后按照名字排序。添加到LNQ查
询结果中的Take扩展方法用于提取前10个结果
var racers. (from r in Formula1.Getchampions()
orderby r.Country, r.LastName, r.FirstName
select r). Take (10);
排序后的结果如下
Argentina: Fangio, Juan Man
Australia: Brabham, Jack
Australia: Jones, Alan
Austr⊥a: Lauda,Nk1
Austria: Rindt, Jochen
Brazil: Fittipaldi, Emerson
Brazil: Piquet, Nelson
Brazil: Senna, Ayrto
Canada: Villeneuve, Jacques
F⊥ nland: Hakkinen,M⊥ka
使用 Order ByO和 Then Byo扩展方法可以执行相同的操作
var racers- Formulal Getchampions (
OrderBy(r r Country)
ThenBy(r -> r LastName)
ThenBy (r - r FirstName)
Take (10):
11.2.6分组
11.26分组
要根据一个关键字值对查询结果分组,可以使用group子句,现在一级方程式冠军应按照国家
分组,并列出一个国家的冠军数。子句 group r by r.Country into g根据 Country属性组合所有的赛车
手,并定义一个新的标识符g,它以后用于访问分组的结果信息。 group子句的结果根据应用到分组
结果上的扩展方法Count()来排序,如果冠军数相同,就根据关键字来排序,该关键字是国家,因为
这是分组所使用的关键字。 where子句根据至少有两项的分组来筛选结果,select子句创建一个带
Country 和 Count 属性的匿名类型
static void Grouping()
{
var countries = from r in Formula1.GetChampions()
group r by r.Country into g
orderby g.Count() descending, g.Key
where g.Count() >= 2
select new
{
Country = g.Key,
Count = g.Count()
};
foreach (var item in countries)
{
Console.WriteLine("{0, -10} {1}", item.Country, item.Count);
}
}
结果显示了带Country和Count属性的对象集合
要用扩展方法执行相同的操作,应把 groupby子句解析为 GroupBy()方法。在 GroupBy()方法的
声明中,注意它返回实现了 IGRouping接口的枚举对象, IGRouping接囗定义了Key属性,所以在定
义了对这个方法的调用后,可以访问分组的关键字:
public static IEnumerable> GroupBy(
this IErumerable source, Func keySelector);
把子句 group r by r.Country into g解析为 GroupBy(r=> r.Country),返回分组序列。分组序列首先 用 Order By Descending()方法排序,再用 ThenBy()方法排序。接着调用 Where()和Select()方法
var countries = Formula1.GetChampions().
GroupBy(r => r.Country).
OrderByDescending(g => g.Count()).
ThenBy(g => g.Key).
Where(g => g.Count() >= 2).
Select(g => new
{ Country = g.Key, Count = g.Count()});
11.2.7对嵌套的对象分组
11.2.7对嵌套的对象分组
如果分组的对象应包含嵌套的序列,就可以改变 select子句创建的匿名类型,在下面的例子中
所返回的国家不仅应包含国家名和赛车手数量这两个属性,还应包含赛车手名序列。这个序列用
个赋予 Racers属性的 from/in内部子句指定,内部的form子句使用分组标识符g获得该分组中的所
有赛车手用姓氏对它们排序再根据姓名创建一个新字符串(代码文件 Enumerablesample/programes)
static void GroupingWithNestedObjects()
{
var countries = from r in Formula1.GetChampions()
group r by r.Country into g
orderby g.Count() descending, g.Key
where g.Count() >= 2
select new
{
Country = g.Key,
Count = g.Count(),
Racers = from r1 in g
orderby r1.LastName
select r1.FirstName + " " + r1.LastName
};
foreach (var item in countries)
{
Console.WriteLine("{0, -10} {1}", item.Country, item.Count);
foreach (var name in item.Racers)
{
Console.Write("{0}; ", name);
}
Console.WriteLine();
}
}
11.2.8内连接
11.2.8内连接
使用join子句可以根据特定的条件合并两个数据源,但之前要获得两个要连接的列表。在级
方程式比赛中,有赛车手冠军和车队冠军。赛车手从 GetChampions()方法中返回,车队从 GetConstru-
ctorChampions()方法中返回。现在要获得一个年份列表,列出每年的赛车手冠军和车队冠军
为此,先定义两个查询,用于查询赛车手和车队(代码文件 EnumerableSample/Program.cs)
static void InnerJoin()
{
var racers = from r in Formula1.GetChampions()
from y in r.Years
select new
{
Year = y,
Name = r.FirstName + " " + r.LastName
};
var teams = from t in Formula1.GetContructorChampions()
from y in t.Years
select new
{
Year = y,
Name = t.Name
};
//有了这两个查洵,再通过join子句,根据赛车手获得冠军的年份和车队获得冠军的年份进行连
//接。 select子句定义了一个新的匿名类型,它包含Year、 Racer和Team属性
var racersAndTeams =
(from r in racers
join t in teams on r.Year equals t.Year
orderby t.Year
select new
{
Year = r.Year,
Champion = r.Name,
Constructor = t.Name
}).Take(10);
Console.WriteLine("Year World Champion\t Constructor Title");
foreach (var item in racersAndTeams)
{
Console.WriteLine("{0}: {1,-20} {2}",
item.Year, item.Champion, item.Constructor);
}
}
11.2.9 左外连接
11.2.9左外连接
上一个连接示例的输出从1958年开始,因为从这一年开始,才同时有了赛车手冠军和车队冠军
赛车手冠军出现得更早一些,是在1950年。使用内连接时,只有找到了匹配的记录才返回结果。为
了在结果中包含所有的年份,可以使用左外连接。左外连接返回左边序列中的全部元素,即使它们
在右边的序列中并没有匹配的元素。
下面修改前面的LNQ查询,使用左外连接。左外连接用jon子句和 DefaultlfEmpty方法定义
如果查询的左侧赛车手没有匹配的车队冠军那么就使用 DefaultlfEmpty方法定义其右侧的默认值
(代码文件 Enumerable Sample/Program.cs
static void LeftOuterJoin()
{
var racers = from r in Formula1.GetChampions()
from y in r.Years
select new
{
Year = y,
Name = r.FirstName + " " + r.LastName
};
var teams = from t in Formula1.GetContructorChampions()
from y in t.Years
select new
{
Year = y,
Name = t.Name
};
var racersAndTeams =
(from r in racers
join t in teams on r.Year equals t.Year into rt
from t in rt.DefaultIfEmpty()
orderby r.Year
select new
{
Year = r.Year,
Champion = r.Name,
Constructor = t == null ? "no constructor championship" : t.Name
}).Take(10);
Console.WriteLine("Year Champion\t\t Constructor Title");
foreach (var item in racersAndTeams)
{
Console.WriteLine("{0}: {1,-20} {2}",
item.Year, item.Champion, item.Constructor);
}
}
11.2.10组链接
11.2.10组连接
左外连接使用了组连接和into子句。它有一部分语法与组连接相同,只不过组连接不使用
第1部分群#语言
DefaultlfEmpty方法
使用组连接时,可以连接两个独立的序列,对于其中一个序列中的某个元素,另一个序列中存
在对应的一个项列表
下面的示例使用了两个独立的序列。一个是前面例子中已经看过的冠军列表。另一个是一个
Championship类型的集合。下面的代码段显示了 Championship类,该类包含冠军年份以及该年份中
获得第一名、第二名和第三名的赛车手,对应的属性分别为Year, First、 Second和 Third代码文
public class Championship
{
public int Year { get; set; }
public string First { get; set; }
public string Second { get; set; }
public string Third { get; set; }
}
Championship 方法返回了冠军集合,如下面的代码段所示
private static List championships;
public static IEnumerable GetChampionships()
{
if (championships == null)
{
championships = new List();
championships.Add(new Championship
{
Year = 1950,
First = "Nino Farina",
Second = "Juan Manuel Fangio",
Third = "Luigi Fagioli"
});
championships.Add(new Championship
{
Year = 1951,
First = "Juan Manuel Fangio",
Second = "Alberto Ascari",
Third = "Froilan Gonzalez"
});
...... }
return championships;
}
冠军列表应与每个冠军年份中获得前三名的赛车手构成的列表组合起来,然后显示每一年的结果
RacerInfo 类定义了要显示的信息,如下所示:
public class RacerInfo
{
public int Year { get; set; }
public int Position { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
使用连接语句可以把两个列表中的赛车手组合起来
为冠军列表中的每一项都包含3个赛车手,所以首先需要把这个列表摊平。一种方法是使用
Selectmany方法。该方法使用的 lambda表达式为冠军列表中的每项返回包含3项的一个列表。在
这个 lambda表达式的实现中,因为 RacerInfo包含 FirstName和 LastName属性,而收到的集合只包
含带有 First, Second和 Third属性的一个名称,所以必须拆分字符串,这可以通过扩展方法 FirstName
和 SecondName完成(代码文件 Enumerable Sample/ Programes
var racers = Formula1.GetChampionships().SelectMany(cs => new List()
{
new RacerInfo {
Year = cs.Year,
Position = 1,
FirstName = cs.First.FirstName(),
LastName = cs.First.LastName()
},
new RacerInfo {
Year = cs.Year,
Position = 2,
FirstName = cs.Second.FirstName(),
LastName = cs.Second.LastName()
},
new RacerInfo {
Year = cs.Year,
Position = 3,
FirstName = cs.Third.FirstName(),
LastName = cs.Third.LastName()
}
});
扩展方法FirstName 和SecondName使用空格字符拆分字符串
public static class StringExtension
{
public static string FirstName(this string name)
{
int ix = name.LastIndexOf(' ');
return name.Substring(0, ix);
}
public static string LastName(this string name)
{
int ix = name.LastIndexOf(' ');
return name.Substring(ix + 1);
}
}
现在就可以连接两个序列。Formula1.GetChampions()返回一个racers 列表, racers变量返回包含
年份、比赛结果和赛车手姓名的一个 RacerInfo列表。仅使用姓氏比较两个集合中的项是不够的。有
时候列表中可能同时包含了一个赛车手和他的父亲(如 Damon H和 Graham Hil,所以必须同时使
用 FirstName和 LastName进行比较,这是通过为两个列表创建一个新的匿名类型实现的。通过使用
ino子句,第二个集合中的结果被添加到了变量 yearResults中,对于第一个集合中的每个赛车手,
第I部分c语言 都创建了一个 yearResults,它包含了在第二个集合中匹配名和姓的结果最后,用LNQ查询创建
了一个包含所需信息的新匿名类型
var q = (from r in Formula1.GetChampions()
join r2 in racers on
new
{
FirstName = r.FirstName,
LastName = r.LastName
}
equals
new
{
FirstName = r2.FirstName,
LastName = r2.LastName
}
into yearResults
select new
{
FirstName = r.FirstName,
LastName = r.LastName,
Wins = r.Wins,
Starts = r.Starts,
Results = yearResults
});
foreach (var r in q)
{
Console.WriteLine("{0} {1}", r.FirstName, r.LastName);
foreach (var results in r.Results)
{
Console.WriteLine("{0} {1}", results.Year, results.Position);
}
}
11.1.11集合操作
扩展方法 Distinct()、 Union()、 Intersect()和Excp()都是集合操作。下面创建个驾驶法拉利的
一级方程式冠军序列和驾驶迈凯轮的级方程式冠军序列,然后确定是否有驾驶法拉利和迈凯轮的
冠军。当然,这里可以使用 Intersect()扩展方法。
首先获得所有驾驶法拉利的冠军。这只是一个简单的LNQ查询,其中使用复合的from子句访
问Cars属性,该属性返回一个字符串对象序列(代码文件 Enumerable Sample/Program.cs )
var ferrariDrivers = from r in Formula1.GetChampions()
from c in r.Cars
where c == "Ferrari"
orderby r.LastName
select r.FirstName + " " + r.LastName;
现在建立另一个基本相同的查询,但 where子句的参数不同,以获得所有驾驶迈凯轮的冠军。
最好不要再次编写相同的查询。而可以创建一个方法,给它传递参数car
private static IEnumerable GetRacersByCar(string car){
return from r in Formula1.GetChampions()
from c in r.Cars
where c == car
orderby r.LastName
select r
}
但是,因为该方法不需要在其他地方使用,所以应定义一个委托类型的变量来保存LINQ查询
racerByCar变量必须是一个委托类型,该委托类型需要一个字符串参数,并返回 IEnumerable,
类似于前面实现的方法。为此,定义了几个泛型委托Func<>,所以不需要声明自己的委托。把一个
lambda表达式赋予 racerByCar变量。 lambda表达式的左边定义了一个car变量,其类型是Func委
托的第一个泛型参数(字符串)。右边定义了LNQ查询,它使用该参数和 where子句:
Func> racersByCar =
car => from r in Formula1.GetChampions()
from c in r.Cars
where c == car
orderby r.LastName
select r;
现在可以使用 Intersect()扩展方法,获得驾驶法拉利和迈凯轮的所有冠军:
Console.WriteLine("World champion with Ferrari and McLaren");
foreach (var racer in racersByCar("Ferrari").Intersect(racersByCar("McLaren")))
{
Console.WriteLine(racer);
}
11.2.12
zip()方法是NET4新增的,允许用一个调词函数把两个相关的序列合并为一个
首先,创建两个相关的序列,它们使用相同的选(意大利和排序方法,对于合并,这很重要,
因为第一个集合中的第一项会与第二个集合中的第一项合并,第一个集合中的第二项会与第二个集
合中的第二项合并,依此类推,如果两个序列的项数不同,Zip0方法就在到达较小集合的末尾时
停止
第一个集合中的元素有一个Name属性,第二个集合中的元素有 LastName和Sus两个属性。
在 racerNames集合上使用zp0方法需要把第二个集合 (racerNames AndStarts)作为第一个参数
第二个参数的类型是Func first.Name + ", starts: " + second.Starts);
foreach (var r in racers)
{
Console.WriteLine(r);
}
11.2.3分区
扩展方法Take()和 Skip()等的分区操作可用于分页,例如在第一个页面上只显示5个赛车手
在下一个页面上显示接下来的5个赛车手等
在下面的LNQ查询中,把扩展方法Skip0和Take0添加到查询的最后。 Skip方法先忽略根据
页面大小和实际页数计算出的项数,再使用Take0方法根据页面大小提取一定数量的项(代码文件
EnumerableSample/Program.cs)
int pageSize = 5;
int numberPages = (int)Math.Ceiling(Formula1.GetChampions().Count() /
(double)pageSize);
for (int page = 0; page < numberPages; page++)
{
Console.WriteLine("Page {0}", page);
var racers =
(from r in Formula1.GetChampions()
orderby r.LastName, r.FirstName
select r.FirstName + " " + r.LastName).
Skip(page * pageSize).Take(pageSize);
foreach (var name in racers)
{
Console.WriteLine(name);
}
Console.WriteLine();
}
分页在Windows或Web应用程序中非常有用,可以只给用户显示一部分数据
11.2.14 聚合操作符
聚合操作符(如 Count()、Sum()、Min()、Max()、 Average()和 Aggregate() )不返回一个序列,而返
回一个值。
Coum()扩展方法返回集合中的项数。下面的 Count()方法应用于 Racer的 Years属性,来筛选赛
车手,只返回获得冠军次数超过3次的赛车手。因为同一个查询中需要使用同一个计数超过一次
所以使用let子句定义了一个变量 numberYearst代码文件 Enumerable Sample/Program.cs)
var query = from r in Formula1.GetChampions()
let numberYears = r.Years.Count()
where numberYears >= 3
orderby numberYears descending, r.LastName
select new
{
Name = r.FirstName + " " + r.LastName,
TimesChampion = numberYears
};
foreach (var r in query)
{
Console.WriteLine("{0} {1}", r.Name, r.TimesChampion);
}
Sum()方法汇总序列中的所有数字,返回这些数字的和。下面的Sum()方法用于计算一个国家赢
得比赛的总次数。首先根据国家对赛车手分组,再在新创建的匿名类型中,把Wins属性赋予某个
国家赢得比赛的总次数。
var countries = (from c in
from r in Formula1.GetChampions()
group r by r.Country into c
select new
{
Country = c.Key,
Wins = (from r1 in c
select r1.Wins).Sum()
}
orderby c.Wins descending, c.Country
select c).Take(5);
foreach (var country in countries)
{
Console.WriteLine("{0} {1}", country.Country, country.Wins);
}
根据获得一级方程式冠军的次数,最成功的国家是
UK167
Germany 112
Brazil 78
France 5
Finland 42
方法MmnO、MaxO、 Average0和 Aggregate0的使用方式与 Count0和Sum相同。MinO方法返回
集合中的最小值,Max0方法返回集合中的最大值, Averageo方法计算集合中的平均值。对于
Aggregate方法,可以传递一个 lambda表达式,该表达式对所有的值进行聚合
11.2.15转换操作符
11.2.15转换操作符
本章前面提到,查询可以推迟到访问数据项时再执行。在迭代中使用查询时,查询会执行。而
使用转换操作符会立即执行查询,把查询结果放在数组、列表或字典中。
在下面的例子中,调用 ToListo扩展方法,立即执行查询,得到的结果放在Lit←T>类中(代码文
fF EnumerableSample/Program. cs)
List racers"(from r in Formulal Getchampions()
where r.Starts >150
orderby r.Starts descending
select r),ToList();
foreach (var racer in racers ){
Console.writeLine("(0) {0:S}", racer);
}
把返回的对象放在列表中并没有这么简单。例如,对于集合类中从赛车到赛车手的快速访问,
可以使用新类 Lookup, TElement >
Dictionary类只支持一个键对应一个值,在 System. Lin名称空间的类
Lookup cr.car, cr=> cr.Racer)
if (racers. Contains("williams")) {
foreach (var williamaRacer in racers"williams")) {
Console.Writaline(willlamsRacer)
}
}
用 Lookup类的索引器访问的所有 Williams冠军如下
Alan Jones
Keke Rosberg
Nigel Mansell
ALa⊥ n prost
Damon Hill
Jacques Villeneuve
如果需要在非类型化的集合上(如 Array List使用LNQ查询,就可以使用Cas40方法,在下面的
例子中,基于 Object类型的 ArrayList:集合用 Racer对象填充,为了定义强类型化的查询,可以使用
Cast()方法
11.2.16 生成操作符
参考C#高级编程第九版本