第13章 LINQ to Object

第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<orders>
		{
			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<string[]>,结果如图13-4所示。

在这里插入图片描述

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> orders {get; set;}
}

class Program
{
	static void Main  (string  [] args)

	var employeeList = new List<Employees>();
	var orders= new List <orders>();

    //二重 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<string[]>

var querySelectMany=ary.SelectMany(a=>a.Split('r'));   //输出什么?

querySelectMany Dump("SelectMany result", false);

在这里插入图片描述
由于 SelectMan中也是对输入进行了操作,输出本来应该也是 IEnumerable<string[]>的,但是, string[]是可列举的(列举为 string,正如

列举为char),于是, SelectMan把 IEnumerable<string[]>“降级”为 IEnumerable,如图13-6所示。

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<Employees>
{
	public int id { get; set;}

	public List<Orders> 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<Employess>(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<T,K>,其中T的类型和分组标准(e.Country)相同,而K则和原表格相同。所以,本例子中g的类型为IGrouping<string, Employees>。

IGRoupings<T, K>继承了 Enumerable,所以,可以对它调用所有 Enumerable类的扩展方法,包括 Count,Sum,求平均值等操作。因此,如果我们select g,我们会得到衣哥IOrderedQueryable<IGrouping<string,Employees>>。拥有两项成员,它们的Key分别是UK和USA,而g本身则是对应4个国籍为UK的 Employees和5个国籍为USA的 Employees分组操作符如图13-7所示。
在这里插入图片描述
我们再看一个例子:

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<string,strdentData>,它继承IEnumerable<studentData>
		 	 //所以,它除了有Key之外,还可以对它进行基于 TEnumerab1e<T>的操作
		 	 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);
}

其结果如图13-8所示:
在这里插入图片描述

由于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};

foreachvar 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作为结果。

因此,查询的结果将会如图13-10所示。
在这里插入图片描述

进行右外连接时,语法大同小异:

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<TKey, TValue>。

口 ToLookup:根据指定的键选择器函数,从 IEnumerable创建一个 System.Linq.Lookup<TKey, TElement>。

口 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服务”?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值