第13章
LINQ to Object
在第12章,我们学习了很多C#3的新特性,包括匿名类型、扩展方法、隐式类型等,而这些新特性都是为LNQ服务的。
LINQ to Object将查询语句转换为委托。实际上, LINQ to Object并没有什么全新的东西,它只不过是把之前的语法特性有机地结合了起来,并以我们可以想到的、最易懂、最精简的方式呈现给了开发者。本章将介绍 LINQ to Object的一些常见的操作符以及 LINQPad它是学习 LINQ to Object最好的工具之一
13.1 LINQPad
本书使用 LINQPad工具演示LINQ查询,使用 Northwind数据库作为数据源。
LINQPad工具是一个很好的LNQ查询可视化工具,其界面如图13-1所示。它由 Threadingin C#和C# in a Nutshell的作者Albahari编写,完全免费,下载地址是htp/www.lingpad.net。它有如下的优点
1)支持直接输入查询表达式和 LINQ to Object
2)结果集以列表的方式列出。
3)可以通过点击橙色圈内的各种不同格式,看到查询表达式的各种不同表达方式
一a) Lambda,查询表达式的 LINQ to Object版本。
b)SQL,由编译器转化成的SQL。
c)IL,IL语言。
LINQPad是频繁使用LNQ的开发者的强力辅助工具,可以用于调试和性能优化(通过改善编译后的SQL 规模)。
你可以使用微软提供的 Northwind演示数据库进行LNQ的学习。 Northwind演示数据
库的下载地址是htps://www.microsoft.com/en-us/download/details.aspx?id=23654。连接到数据库之后, LINQPad支持使用SQL或C#语句(点标记或查询表达式)进行查询
进入界面后, LINQPad可以连接到已经存在的数据库,不过就仅限微软的 SQL Server系,如果要连接到其他类型的数据库则需要安装插件,目前也支持 MySql, SQLite和Oracle,见图13-2
13.2 Enumerable是什么
Enumerable是一个静态类型,其中包含了许多方法,绝大部分都是扩展方法(它也有自己的方法,例如 Range),返回 IEnumerable(因为 IEnumerable是延迟加载的,每次访问时候才取值),而且绝大部分扩展的是 IEnumerable
Enumerable是一个静态类型,不能创建 Enumerable类型的实例。 Enumerable类型是LINQ to Object的基础。它赋予 IEnumerable强大的查询能力。
13.3使用 Northwind数据源演示查询操作
对于 LINQ to Object的每种查询操作分为查询表达式和方法语法两种方式,而且它们可以混用。其中查询表达式看起来很像SQL,方法语法则调用一系列 Enumerable类中的扩展方法。大部分人都更喜欢方法语法,因为它具有面向对象的编程特点,而查询表达式实际上和SQL并不相同,而记忆两套语法让人厌烦。不过,有时候查询表达式可能更容易书写。
LNQ查询表达式必须以from子句开头,以select或 group子句结束。以下我们介绍一些常用的LINQ操作,每种操作都给出查询表达式和方法语法两种方式的写法。通过LINQPad,可以非常方便地将查询表达式转化为对应的方法语法写法,还可以获得对应的SQL语句。
13.4 投影操作符与过滤操作符
投影操作符包括 Select和 SelectMan,过滤操作符包括 Where和 Distinct,本节介绍两类操作符。
13.4.1 Select整个表
这大概是最基础的任务了。我们选择 Northwind的 Orders表:
from o in Orders select o
通过点击 LINQPad的编译键,我们可以得到结果(830笔数据)。而且,我们也可以通过点击工具栏的 lambda希腊字母]x获得它的方法语法版本
Orders.Select (o => o)
在这个查询表达式以及它对应的方法语法中, o是一个临时变量(匿名方法的局部变量),可以随意命名(当然不能命名为 Orders)。要注意的是,查询表达式会先转化为方法语法,然后,再转化为委托。在这个例子中,对应的被调用的委托为 Enumerable Select< TSource, TResult>,其中 Tsource和 TResult的类型都是 Orders很难理解么?实际上,下面的代码可以得到相同的结果:
class Orders
{
public int id { get: set; }
public Orders( int a)
{
id=a;
}
}
class Program
{
static void Main(string [] args)
{
var data= new List
{
new Orders(1),
new Orders(2)
};
//查询表达式
vart ret=from d in data select d;
//LINQ TO Object
var ret2 = data.Select(d=>d);
//自建了Selector,还记得什么是d=>d吗?
var ret3 = data.Select(Selector);
Console.WriteLine(ret.Count ()); //2
Console.WriteLine(ret2.Count())); //2
Console.WriteLine (ret3.Count()) ; //2
Console. ReadKey();
}
static Orders Selector(Orders d)
{
return d;
}
}
因此,查询表达式和方法语法实际上都是基于委托的。而这些委托的目标方法都是Enumerable类中的方法。我们通过 ILSpy看看背后的操作,见图13-3
图13-3清楚地显示了编译器对查询表达式做了多少手脚。如果将上面代码中的查询表状式改为方法语法的形式,其编译后的代码也相似。
13.4.2 Select部分列
在Select部分列的例子中,我们可以看到匿名类型的应用。我们随意选择两列:
from o in Orders select new {o.OrderID,o.EmployeeID}
此时,将会产生一个新的匿名类型,它包括两个成员。它对应的方法语法版本如下
Orders.Select (o=>new
{
OrderID=o.OrderID,
EmployeeID = o.EmployeeID
})
这种形式我们可以看得更清楚。
13.4.3 Select结合 Lambda表达式
本节我们讨论 Select在序列中的应用。下面的代码执行完之后,输出会是什么样子?
string[] ary = new string[] {"zero","one","two","three"};
var querySelect = ary.Select (a => a.Split('r')):
querySelect.Dump("Select result", false);
上面的代码中,最后一句是 LINQPad的特有语法,用来执行查询,并显示结果集合。
如果 Select(a=>a),那么结果肯定是一个 Enumerable,含有所有4个原始的字符串。
现在在 Select中加入一个操作,也就是说,对于每个输入a,都会操作一次,将它Spl成一个 string[]。所以,对于第
一个输入zero,将会产生一个拥有2项的string[],成员为ze和o。对于one则只有一项,以此类推。所以,最后的输出类型应
该是 IEnumerable
13.4.4使用 where进行过滤
Where子句的使用方式和SQL中没什么区别,只不过它的判断语句要使用双等号。我们选择 EmployeeID等于1的那些记录
from o in Orders
where o.EmployeeID ==1
select new {o.OrderID, o.EmployeeID}
有趣的是,LNQ提供的查询表达式的顺序是先fom、再 where、最后 select。这和一般的SQL先 select、再fom、最后 where不同。这是因为,当查询表达式被转换为方法语法时,会按照查询表达式的书写顺序,先进行过滤,然后再选择,这使得编译器可以顺序进转换,而且看起来比把过滤语句放在最后自然多了(代码就是顺序地完成它的工作)。它对应的方法语法版本如下:
Orders
.where (o=>(o.EmployeeID == (Int 32?)1))
.Select (o => new
{
OrderID = o.OrderID,
EmployeeID = o.EmployeeID
}
}
注意,如果是选择整个表,那么最后的 select语句会被转译为:
Orders.Where (o=>(o.EmployeeID== (Int32?)1))
这是因为, Where对应的委托返回的是符合要求的表格的记录的所有列,因此不需要再进行选择了。
Where对应的委托为 Enumerable的 Where方法,它的输入有两个,第一个为一个umerable的序列,返回也是 IEnumerable的序列,但其中作了筛选(通过第二个:一个输入T、输出bool的泛型委托)。
根据这些信息,你应该可以通过原始的委托方式来实现 Where了。
13.4.5 SelectMany
我们经常会遇到若干表格通过外码互相关联的情况。例如在 Northwind数据库中,每个Employee)都管理着若干个订单( Order)。所以在 Employees表格中,会有 Ordered将两个表联系起来。Order表格中存储着订单的若干信息,例如,订单城市,地址,订单时间等。
如果我们想获得雇员管理的所有订单中,以及订单城市为Grax的所有订单(这是一个非常普遍的操作),那么依照传统写法,我们需要使用二重 foreach:
//演示用
class Orders
{
public int id {get;set;}
public string ShipCity {get;set;}
}
class Employees
{
public int id { get;set;}
public List orders {get; set;}
}
class Program
{
static void Main (string [] args)
var employeeList = new List();
var orders= new List ();
//二重 foreach配合工If,轻松做到一大堆嵌套
foreach (var employee in employeeList)
{
foreach (var o in employee orders)
{
if (o.Shipcity =="Graz")
{
orders.Add(o);
}
}
}
Console。 ReadKey();
}
}
这样的写法是非常糟糕的,代码看上去很臃肿。这时候, SelectMan就可以发挥作用了,它可以轻松地处理这种父子表情景。它的查询表达式是
from e in Employees
from s in e.Orders
where s.ShipCity =="Graz"
select s
我们可以这样理解:首先,令e为整个 Employees表格;然后,令s为e.Orders,过子句则传入限制条件;最后,选择符合条件的所有的订单s。
相比查询表达式形式, Select Many的方法语法可能更加直观好用。C#编译器会把多连续的fom查询表达式转化为 SelectMan扩展方法:
Employees.SelectMany(e => e.Orders).Where(s=> S Shipcity=="Graz")
当在 SelectMan中书写e=> e.Orders之后,后面的s的类型就变为 Orders了(如果你使用Sect而不是 Select而不是SelectMany,后面的s的类型为List< Orders>,只能调用List的方法,取不到 Orders的任何列)。因此,我们可以看到, SelectMan有一种类似“降级“(摊平)”的功能,把查询的对象从父表降到子表,然后,你可以在子表中根据 Where方法进行筛选的操作,最后选出的结果可以是子表的一部分,也可以是子表和父表的列的集合。如果你想先筛选一部分雇员,然后再筛选 Orders表,也是很容易的:
Employees.Where(e => e.EmployeeID<10).SelectMany(e => e.Orders).Where(s=>s.Shipcity=="Graz”)
看看这行云流水一般的代码吧,最重要的是,书写这些代码基本不用费脑子,看的人很容易就能懂。一般来说,如果方法语法有多个操作符,我们习惯将它们对齐,这是一种较好的代码书写习惯
Employees.Where(e => e.EmployeeID<10)
.SelectMany(e => e.Orders)
.Where(s=>s.Shipcity=="Graz”)
SelectMan会自动将查询的结果摊平,如果结果的个项目都是可以被列举的,例如:
string[] ary = new stringl[]{"one","two","three"};
var querySelect = ary.Select(a => a);
var querySelectMany = ary.SelectMany(a => a);
querySelect.Dump("Select result", false);
queryselectMany.Dump("SelectMany result", false);
那么这两个结果有什么区别呢?因为字符串实际上字符的数组,所以 SelectMan可以进一步把结果摊平,如图13-5所示。
也就是说, SelectMan把结果“降级”一次,而string实际上是char[],故降级到char。那么,如果我们想统计字符串中出现了多少次(e),我们可以写:
var querySelectMany = ary.SelectMany(a => a).Where(a = a).Where(a=>a=="e").Count();
当进行了 SelectMan之后,我们已经得到了所有的char,此时,通过 Where过滤即可得到所有等于e的char。
再看看刚才 Select的例子,我们分析了 Select,那么对于 Select Many呢?请看如下代码:
string[] ary= new string []{"zero","one","two","three"};
var querySelect= ary.Select(a => a.Split('r')); // IEnumerable
var querySelectMany=ary.SelectMany(a=>a.Split('r')); //输出什么?
querySelectMany Dump("SelectMany result", false);
由于 SelectMan中也是对输入进行了操作,输出本来应该也是 IEnumerable
列举为char),于是, SelectMan把 IEnumerable
SelectMan只会摊平一次,不会再摊平到char了。如果你想得到char,你需要再次调用 SelectMan再降级一次:
var querySelectMany= ary.SelectMany (a => a.Split('r')).SelectMany(a=> a);
13.4.6 Distinct
Distinct会删除序列中重复的元素,对重复元素的定义为C#编译器自己默认下的定义(对任意的对象,C#都有一套判等的逻辑,比如值类型和字符串会判断值,引用类型会判引用)。如果你有自己的判等定义,你可以实现 IEqualityComparer接口并书写自己的判等逻辑,之后在 Distinct方法中传入一个新的实例。代码如下:
//实现 IEqualitycomparer< Employees>接口并书写自己的判等逻辑
Class Employees : IEqualityComparer
{
public int id { get; set;}
public List orders { get; set;}
public Employees()
{
}
public Employees (int i)
{
id= i;
}
//ID相等就算相等
public bool Equals(Employees x, Employees y)
{
return x.id = y.id;
}
public int GetHashCode(Employees obj)
{
return base.GetHashcode();
}
}
Class Program
{
static void Main(string [] args)
{
var employeesList = new List(new Employess(1),newEmployess(2), new Employees (1), new Employees(3)};
//打印4,使用默认的判等逻辑,引用对象比较的是引用所以每个对象都不同
Console.WriteLine(employeeList.Distinct().Count());
//打印3,使用自己的判等逻辑
Console.WriteLine(employeeList.Distinct(new Employees()).Count (1);
Console.ReadKey();
}
}
13.5 排序操作符
排序操作符大概是最容易理解和使用的了,包括 Order By、 Order By Descending、Then By、 ThenBy Descending、 Reverse这些操作符必须位于 select前面(而不是像SOL语样必须位于后面)。例如,选择使用City列进行排序的所有雇员:
from e in Employees
orderby e.City
select e
它的方法语法更加简单
Employees.Orderby(e=>e.City)
OrderBy是升序的, OrderByDescending是降序的。如果要为多个列进行排序,可以组合使用 OrderBy和 ThenBy。 ThenBy和 ThenByDescending必须永远跟在 Order By或OrderByDescending之后,不能直接使用,这是因为 OrderBy或 OrderByDescending会输出 IOrderedEnumerable类型,而 ThenBy和 ThenByDescending要求输入必须为IOderedEnumerable< TSource>类型。
13.6分组操作符
在SQL中分组是一个非常常见的情景。分组之后,我们可能会对每组的数据使用Count, Sum、求平均值等操作。例如下面的例子,根据 Country列选出每个不同国家雇员的人数:
from e in Employess
group e by e.Country into g
select new {Country= g Key,Name g Count()}
在查询表达式中,我们将 Employees根据 e.Country分组,并将分组结果放到新的一个量g中,它的类型为 IGrouping
IGRoupings
我们再看一个例子:
public class studentData
{
public string Name {set;get;}
public string Course {set; get;}
public int Score {set; get;}
public string Term {set; get;}
}
class Program
{
static void Main(string[] args)
{
var list = new List<studentData>{
new studentData {Name="张三", erm="第一学期", Course="数学", Score=80},
new studentData {Name="张三", Term="第一学期", Course="语文", Score=90},
new studentData {Name="张三", Term="第一学期", Course="英语", score=70},
new studentData {Name="李四", Term="第一学期", Course="数学", Score=60},
new studentData {Name="李四", Term="第一学期", Course="语文", score=70},
new studentData {Name="李四", Term="第一学期", Course="英语", Score-30},
new tudent Data {Name="王五”,Term=”第一学期", Course="数学", Score=1001},
new studentData {Name="王五", Term="第一学期", Course="语文", Score=80},
new studentData {Name="王五", Term="第一学期", Course="英语", Score=80},
new studentData {Name="赵六",Term="第一学期", Course="数学", Score=90},
new studentData {Name="赵六”,Term=”第一学期”, Course=”语文", Score=801},
new studentData {Name="赵六",Term="第一学期", Course="英语", Score=701},
new studentData {Name="张三",Term="第二学期", Course="数学", Score=10},
new studentData {Name="张三",Term="第二学期", Course="语文", score=80},
new studentData {Name="张三",erm="第二学期", Course="英语", Score=70},
new studentData {Name="李四",Term="第二学期", Course="数学", Score=90},
new studentData {Name="李四",Term="第二学期", Course="语文", Score=50},
new studentData {Name="李四",Term="第二学期", Course="英语", Score=80},
new studentData {Name="王五”,Term="第二学期", Course=数学, Score=90},
new studentData {Name="王五”,Term=”第二学期", Course=”语文", Score=70},
new studentData {Name="王五”,Term="第二学期", Course=”英语", Score=80},
new studentData {Name="赵六”,erm=”第二学期", Course=”数学", Score=70},
new studentData {Name="赵六",Term=”第二学期", Course=”语文", Score=60},
new studentData {Name="赵六",Term=”第二学期", Course="英语", Score=70}
};
// 根据姓名分组
var sum = from l in list
//g的类型是IGrouping,它继承IEnumerable
//所以,它除了有Key之外,还可以对它进行基于 TEnumerab1e的操作
group l by l.Name into g
select new
{
Name = g.Key,
Score = g.Sum(m => m.Score),
Average = g.Average(m => m.Sore)
};
// 等价的写法
var sum2= list.GroupBy(m=> m.Name)
.Select(g => new {Name= g.Key, Scores=g.Sum(l=>
l.Score),Average = g.Average(m => m.Score)};
foreach(var s in sum)
{
Console.Writeline("姓名:"+s.Name +",总分"+ s.Score +",平均分"+s.Average);
}
Console.ReadKey();
}
}
该例子中,使用了带有分组操作符的查询表达式,根据姓名进行了分组。在分组之后,可以对每组的成员进行操作(计算了每个人的平均分和总分)。上面的代码可以在 GroupBy工程中找到。
当打算基于多个列进行分组时,可以将这些列写成一个匿名函数,代码如下:
var sum3= from 1 in list
//当分组的字段是多个的时候,通常把这多个字段合并成一个匿名类型,然后 group by这个匿名类型
//此时key的类型就变成了一个匿名类型
group l by new {l.Name,l.Term} into g
select new
{
Name=g.Key.Name,
Term= g.Key.Term,
Score=g.Sum(m=>m.Score),
Average= g.Average(m=> m.Score)
};
//等价的写法
var sum4= list.GroupBy(m =>new {m.Name, m.Term})
.Select(g => new{Name = g.Key.Name, Term = g.Key.Term,Scores=
g.Sum(l=>l.Score), Average = g.Average(m=>m.Score)});
foreach(var s in sum)
{
Console.Writeline("姓名:"+s.Name+",学期"+s.Term+",总分"+s.Score+",平均分"+s.Average);
}
由于LNQ中没有SQL中的 having语句,可以通过在查询结果后( GroupBy之后,Selet之前)接whe进行过滤以达到相同的效果。下面的代码在上面的结果中,再次筛选均分不低于80分的纪录:
var sum5= from l in list
group l by new{l.Name, l.Term} into g
//相当于having 语句
where g.Average(m=>m.Score)>= 80
select new
{
Name=g.Key.Name,
Term=g.key.Term,
Score=g.Sum(m=>m.Score),
Average = g.Average(m => m.Score)
};
∥等价的写法
var sum6 = list.GroupBy (m => new{m.Name, m.Term})
.Where(m => m.Average (x => x.Score) > 80
.Select(g => new{
Name=g.Key.Name,
Term=g.Key.Term,
Scores= g.Sum(m =>m.Score),
Average= g.Average(m => m.Score)});
13.7通过Let声明局部变量
在分组操作符中,我们通过into声明了一个新的局部变量。一般的情况下,如果你需一个将来会反复使用的局部变量,可以使用let关键字。假设有一个如下的查询:
var query= from car in myCarsEnum
orderby car.PetName.Length
select new (Name = car.PetName,
Length=car.PetName.Length);
我们发现,对 car.PetName.Length引用了两次。可以使用let子句引入一个临时变量:
var query =from car in myCarsEnum
let length =car.PetName.Length
orderby length
select new {Name = car.petName,Length=length};
foreach (var name in query)
{
Console.WriteLine("(0):(1)", name Length, nameName);
}
13.8 连接操作符
使用连接操作符时,查询表达式的语法和SQL很相似,因此比方法语法更容易理解,推荐使用查询表达式:
我们使用下面的两个实体:
public class CustomerId
{
public int CustomerId;
public string Name;
public string Address;
public int PhoneNumber;
}
public class Orders
{
public int Id;
public int Numbe;
public int CustomerId;
}
数据源如下:
var customerLlst new List<Customers>
{
new Customers{CustomerId=1,Name="张三", Address="白宫",PhoneNumber= "12345678"},
new Customers{customerId=2,Name="李图",Address="克里姆林官",PhoneNumber= "23456781"},
new Customers{customerId=3,Name="王五",Address="红扬",PhoneNumber ="34567812"}
};
var orderList= new List<orders>
{
new Orders {CustomerId= 3, Id= 1, Number= 2572},
new Orders {CustomerId= 3, Id= 2, Number= 7375},
new Orders {CustomerId= 1, Id-3, Number=7520},
new Orders {CustomerId= 1, Id= 4, Number= 1054},
new Orders {CustomerId= 5, Id=5,Number=1257}
};
13.8.1使用join子句的内连接
在进行内连接时,必须要指明基于哪个列。例如,下面的查询基于列 Customerld,注意这里的 equals不能换成双等号。
from o in orderList
join c in customerList
on o.CustomerId equals c.CustomerId
select new {c.Name, o.Number];
查询的结果如图13-9所示。
由于是内连接,因此只有四笔结果。这里查询表达式每部分都有严格的规定:
from e in 左表
join o in 右表
on左表的某列 equals右表的某列
因此,在 equals两边的列不能调换。LINQ将会对连接延迟执行,右边( Orders)的序列被缓存起来,左边的则进行流处理:当开始执行时,LNQ会读取整个右边序列( o.EmployeeID),然后不需要再读取右边序列了,这时就开始迭代左边的序列。所以如果要连接一个巨大的表和一个极小的表时,请尽量将小表放在右边。
编译器的转译为:
OrderList.Join(
CustomerList,
O => o.CustomerId,
C=> C.CustomerId,
(o,c)=>
new
{
Name =c.FirstName,
Number= o.Number
}
)
这段话看上去并不难理解,但Join的参数之多仍然会让人感觉不太舒服:第一个参数是右边的表名;第二个和第三个参数表示了基于哪个列进行连接;最后一个参数规定了返回值(又是一个匿名类型)。也许写习惯了,你会觉得两种方式都有它的好处,不过,对于刚从SQL中解脱出来的开发者,查询表达式明显更容易被他们接受。
Join当然也支持基于多个列,和 Group By的方法一样,只需要把所有的列写成一个匿名类型就可以了,这里就不多做介绍了。
13.8.2使用 join into子句进行外连接
左外连接时,我们的左表为 Customers,其中李四没有对应的 Orders数据。和内连接不同的是,左外连接的结果将为5笔,其中李四也被包括进去。
查询表达式如下:
from c in customerList
join o in orderList
on c.CustomerId equals o.CustomerId into prodGroup
from item in prodGroup.DefaultIfEmpty(new Orders{Id =0, Number =0,CustomerId =0})
select new {Name= c.Name, OrderNumber= item.Number};
查询中第一行规定了左表,第二行规定了右表。第三行首先进行on的连接,然后,使用into子句将结果暂时记为 prodGroup。第五行中,选择两列(分别来自两个表,其中,左表肯定有值,右表则不一定,所以右表的数据不来自于o而是来自于第四行的item)。那么,当右边没有值的时候怎么办呢?就要靠第四行了,第四行规定右边( Orders)如果为空时,默认值应该是什么。因此,当查询发现李四没有对应值时,就建立一个新的 Orders,其Number为0,然后将0作为结果。
进行右外连接时,语法大同小异:
from o in orderList
join c in customerList
on o.CustomerId equals c.CustomerId into prodGroup
from item in prodGroup.DefaultIfEmpty(new Customers{CustomerId=o,Name="查无此人",Address=" ", PhoneNumber=0})
select new {Name =item.Name,OrderNumber=o.Number};
查询结果如图13-11所示。
连接的方法语法比较复杂,这里就不再列出了。
13.9 其他常用的操作符
LINQ提供的操作还有很多,限于篇幅,就不一一进行示例了,这里列出一些其他常用的操作符。
1.量词操作符
量词操作符返回布尔值。
口Any:确定序列是否为空;或确定序列中的任何元素是否都满足条件。在判断集合是否存在值时,这个方法性能大大好于 Count(不需要遍历)。如果集合可能有几百万个记录,也可能没有记录,请使用这个方法判断集合是否有记录
口All:确定序列中的所有元素是否满足条件。
2.分区操作符
添加在查询的“最后“,返回集合的一个子集。分区操作符允许灵话地获得集合的任意一个连续段。
口Take:从序列的开头返回指定数量的连续元素,相当于SQL的TOP N。
口 TakeWhile:只要满足指定的条件,就会返回序列的元素。
口sHIP: 跳过序列中指定数量的元素,然后返回剩余的元素,这使利用SQL选择"获得第二大的数”不再是一个难题。
口 SkipWhile:只要满足指定的条件,就跳过序列中的元素,然后返回剩余元素
3.集合操作符
当想操作两个集合时非常有用。下面四个操作符都有对应的SQL关键字
口Uion:并集,返回两个序列的并集,去掉重复元素,相当于SQL的 Union
口 Concat:并集,返回两个序列的并集,不处理重复元素,相当于SQL的 Union All
口Intersect:交集,返回两个序列中都有的元素,即交集。
口 Except:差集,返回只出现在一个序列中的元素,即差集。
4.元素操作符
这些元素操作符仅返回一个元素,不是 Enumerable< TSource>(默认值:值类型默认为0,引用类型默认为null)。
口First:返回序列中的第一个元素;如果是空序列,此方法将引发异常
口 FirstOrDefault:返回序列中的第一个元素;如果是空序列,则返回默认值default(TSource)
口 Last::返回序列的最后一个元素;如果是空序列,此方法将引发异常
口 LastOrDefault:返回序列中的最后一个元素;如果是空序列,则返回默认值default(TSource)
口 Single:返回序列的唯一元素;如果是空序列或序列包含多个元素,此方法将引发异常。该方法非常适合判断序列是否只包含一个元素(并返回它)。
口 SingleOrDefault:返回序列中的唯一元素;如果是空序列、则返回默认值default( TSource);如果该序列包含多个元素,此方法将引发异常。
5.合计操作符
口 Count:返回一个 System.Int32,表示序列中的元素的总数量。
口 Long Count:返回一个 System.Int64,表示序列中的元素的总数量。
口 Sum:计算序列中元素值的总和。
口Max:返回序列中的最大值
口Min:返回序列中的最小值。
口 Average:计算序列的平均值。
口 Aggregate:对序列应用累加器函数。
6。.转换操作符
口Cast:将非泛型的 IEnumerable集合元素转换为指定的泛型类型,若类型转换失败则抛出异常。
口 ToArray:从 IEnumerable创建一个数组。
口 ToList:从 IEnumerable创建一个List。
口 ToDictionary:根据指定的键选择器函数,从 IEnumerable创建一个Dictionary
口 ToLookup:根据指定的键选择器函数,从 IEnumerable创建一个 System.Linq.Lookup
口 DefaultIfEmpty:返回指定序列的元素;如果序列为空,则返回包含类型参数的默认值的单一元素集合。
口 ElementAt:返回序列中指定索引处的元素,索引从0开始;如果索引超出范围,此方法将引发异常。
口 ElementAtOrDefault:返回序列中指定索引处的元素,索引从0开始;如果索引超出范围,则返回默认值 default( TSource)
13.10延迟执行
有些LINQ语句并非马上执行,例如下面的代码,实际上,当这两行代码运行完时ToUpper根本没有运行过。
var strings=new List<string(){"Ben","Lily","Joel","sam", "Annie"};
strings.select(s => s.ToUpper());
或者下面更极端的例子,虽然语句很多,但其实在打算遍历结果之前,这不会占用任何时间:
var links= from xdoc in sites.Select(DownloadHtml)
.Select(ToHtmlDocument)
.Select(GetRss Feed)
.Select(rss => XDocument.Load(rss))
from feedItem in xdoc.Descendants("item")
let postTitle = feedItem.Element("title").value
let postLink = feeditem.Element(link").value
let postDoc=ToHtmlDocument(feedItem.Element(contentNamespace+"encoded").value)
from linkNode in postDoc.DocumentNode.SelectNodes("//a")
let linkTitle=linkNode.InnerText
let linkHref!=null
select new {postTitle, postLink,linkTitle,linkHref};
}
private static string GetRssFeed(HtalDocument htmlDoc)
{
return htmlDoc.DocumentNode
.SelectNodes("//link[(type='application/rss+xml or @type'application/atom+xml') and @rel=")
.Select(n=>n.Attributes["href"].value)
如果我们如下这样写,会不会有任何东西打印出来呢?
var strings=new List<string>(){"Ben","Lily","Joel","Sam","Annie"};
strings = Select(s=>{Console.WriteLine(s); return s.ToUpper();});
答案是不会。问题的关键是, IEnumerable是延迟执行的,所以当没有触发执行从序列中取值时,就不会进行任何运算。 Select方法不会触发LINQ的执行。一般来说,返回另外一个序列(通常为 IEnumerable或 Queryable)的操作,会延迟执行(在最终结果的第一个元素被访问的时候才真正开始运算),而返回单一值的运算,会立即执行。
以下总结了LNQ运算符的延迟计算特性。
口 延迟执行的运算符:Cast, Concat, DefaultIfEmpty, Distinct, Except, GroupBy,GroupJoin, Intersect, Join, OfType, OrderBy, OrderByDescending,Repeat,Reverse, Select, SelectMany, Skip, SkipWhile, Take, TakeWhile, ThenBy
,ThenByDescending, Union, Where, Zipo
口 立即执行的运算符: Aggregate,All,Any, Average, Contains, Count, ElementAt,ElementAtOrDefault, Empty, First, FirstOrDefault, Last, LastOrDefault, LongCount, Max, Min,Range,SequenceEqual, Single, SingleOrDefault,Sum,ToArray, ToDictionary,ToList ,ToLookupo
例如下面的代码:
var strings = new List<string>(){"Ben","Lily","Joel","Sam","Annie"};
var uppercase= strings
.Select(s => {Console.WriteLine(s); return s.Toupper()})
.Where(s =>s.Length >3);
foreach (var upper in uppercase)
{
Console.WriteLine(upper);
}
它的输出是:
Ben
Lily
LILy
Joel
JOEL
Sam
Annie
ANNIE
注意所有名字都打印出来了,而全部大写的名字,只会打印长度大于3的。为什么会交替打印?这是因为在开始 foreach枚举时, uppercase的成员还没确定,我们在每次foreach枚举时,都先运行 select,打印原名,然后筛选,如果长度大于3,才在 foreach中打印,所以结果是大写和原名交替的。所有的原名都会出现,但大写是否出现,要看长度是否大于3。
利用 ToList强制执行LNQ语句
下面的代码和上面的区别在于,我们增加了一个 ToList方法。思考会输出什么?
var strings =new List<string>{"Ben","Lily","Joel","Sam","Annie"};
uppercase= strings
.Select(s =>{Console.writeLine(s); return s.Toupper();})
.Where(s =>s.Length >3)
.ToList();
foreach(var upper in uppercase)
{
Console.WriteLine(upper);
}
ToList方法强制执行了所有LINQ语句,所以 uppercase在 Foreach循环之前就确定了,其将仅仅包含三个成员:Lily、Joel和Anie(都是大写的)。而且在进行 foreach之前,会打印集合所有的五个成员。故结果是:
Ben
Lily
Joel
Sam
Annie
LILY
JOEL
ANNIE
13.11查询表达式和方法语法
很多人爱用方法语法,对这两种写法的优劣有很多说法
口每个查询表达式都可以被转换为方法语法的形式,而反过来则不一定,例如Reverse、Sort这些方法。
口某些方法语法比查询表达式具有更高的可读性(并非对所有人都如此),但例如Join等命令则查询表达式更简单,因为它更像SQL
口方法语法体现了面向对象的性质,而在C#中插入一段类SQL语句让人觉得不伦不类(见仁见智)。
口方法语法可以轻易地接续并且代码量更少
通常来说大部分开发者会选用方法语法。
13.12本章小结
在本章中我们遍览了 LINQ to Object的一些主要方法和使用技巧。LNQ使用了定义于Enumerable静态类中的大量扩展方法,而它们扩展的目标都是 IEnumerable。
在LNQ查询中,由于列的数目不定,因此匿名方法得以大显身手。而且,很多返回类型名都很长,使用var可以显著提高代码的可读性。
编译器会把查询表达式转化为对应的方法语法,然后调用对应的扩展方法执行。很多扩展方法需要传入一个泛型委托,可以通过 Lambda表达式来实现。
13.13思考题
给定一个int[],使用LINQ查询,输出它所有可能的排列。例如,如果输入为1,2,3,则输出将会有六行(1,2,3)(1,3,2)(2,1,3)(2,3,1)(3,1,2,)(3,2,1)。
为什么说“c样30所有特性的提出都是更好地为LINQ服务”?