应用程序还需要操作存储在其他数据源(如SQL数据库或XML文件)中的数据,甚至通过Web服务访问它们。传统上,查询这些数据源时需要使用不同的语法,且在编译期间不进行类型检查。
一、LINQ
在.NET Framework中,查询表达式是一组统称为语言集成查询(LINQ)的技术的一部分,将查询功能直接集成到了C#语言中。LINQ是.NET Framework 3.5新增的,它提供了适用于所有数据源(SQL数据库、XML文档、Web服务、ADO.NET数据库以及任何支持接口IEnumberable或IEnumerable<T>的集合)的查询语言,从而避免了操作数据和对象时经常出现的语言不匹配问题。
LINQ让查询变成了基本语言构造,就像算术运算和流程控制语句是C#基本概念一样。LINQ 查询将重点放在常用的操作而不是数据结构上,能够以一致的方式从任何支持的数据源检索数据,并对其进行转换。
与SQL(Structured Query Language,结构化查询语言)查询相比,LINQ查询的语法相同,使用的一些关键字相同,提供的很多优点也相同。您可随便修改被查询的底层数据的结构,而不会导致需要修改查询。SQL只能用于操作关系型数据,而LINQ支持的数据结构要多得多。
如下代码是一个用于Contact对象集合的查询
class Contact
{
public int Id { get; set; }
public string Company { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string StateProvince { get; set; }
}
IEnumerable<Contact> contacts = GetContacts();
var result = from contact in contacts select contact.FirstName;
foreach(var name in result)
{
Console.WriteLine(name);
}
这个简单的查询演示了 C#语言支持的声明性语法(declartive syntax,也叫 query comprehension syntax)。这种语法让您能够使用类似于SQL查询的语法编写查询,灵活性和表达能力都极强。虽然查询表达式中所有的变量都是强类型的,但是在大多数情况下,不需要显式地指定类型,因为编译器能够推断出来。
ps:LINQ查询语法
如果您熟悉SQL,就不会对LINQ使用的查询语法感到陌生。最明显的差别是,from运算符位于select运算符的前面,而不像SQL中那样位于后面。
1.1 选择数据
虽然上述所示的代码看起来可能很简单,但是实际上涉及的内容很多。首先应注意到的是,使用了一个类型将隐式确定的变量result,其类型实际上是IEnumerable<string>。查询表达式(赋值运算符右边的代码)的结果为查询,而不是查询的结果。select 子句返回一个对象,该对象表示对一个序列( contacts 列表)执行投影操作的结果(一系列contact.FirstName值)。由于结果是一系列字符串,因此result必然是由字符串组成的可枚举集合。当前,并不会检索数据,而只是返回一个可枚举集合,以后再将数据取回。
这个查询相当于说,从contacts指定的数据源中,选择每个元素(contact)的FirstName字段。from子句中指定的变量contact类似于foreach语句中的迭代变量,它是一个只读局部变量,作用域为查询表达式。in子句指定了要从中查询元素的数据源,而select子句指定在迭代期间只选择每个元素的contact.FirstName字段。
选择单个字段时,这种语法的效果很好,但通常需要选择多个字段,甚至以某种方式对数据进行变换,如合并字段。所幸的是,LINQ通过类似的语法提供了这样的支持。实际上,有多种方法执行这些类型的选择。
第一种方法是在select子句中拼接这些字段,这样将只返回一个字段,如下代码所示:
var result = from contact in contacts select contact.FirstName + " " + contact.LastName;
foreach(var name in result)
{
Console.WriteLine(name);
}
显然,这种选择方式只适用于有限的情形。一种更灵活的方法是返回多个字段,即返回一个数据子集,如下所示:
var result = from contact in contacts
select new
{
Name = contact.LastName + ", " + contact.FirstName;
DateOfBirth = contact.DateOfBirth
};
foreach(var contact in result)
{
Console.WriteLine("{0} born on {1}", contact.Name, contact.DateOfBirth);
}
这里返回的仍是IEnumberable,但其类型是什么呢?如果查看程序中的select子句,就会发现它返回了一种新类型,其中包含字段contact.FirstName和contact.LastName的值。这实际上是一个匿名类型(anonymous type),它包含属性Name和DateOfBirth。这种类型之所以是匿名的,是因为它没有名称。无需显式地声明与返回值对应的新类型,编译器会自动生成。
ps:匿名类型
以这种方式创建匿名类型是LINQ工作方式的核心,如果没有var提供的引用类型,这根本不可能。
1.2 筛选数据
选择数据很重要,但以这种方式选择数据时,无法指定要返回哪些数据。SQL 提供了where子句,同样,LINQ也提供了where子句,它返回一个可枚举的集合,其中包含符合指定条件的元素。如下代码再上一个示例的查询中添加了一个where子句,将结果限定为StateProvince字段为FL的联系人。
var result = from contact in contacts
whrere contact.StateProvince == "FL"
select new {customer.FirstName, customer.LastName};
foreach(var name in result)
{
Console.WriteLine(name.FirstName + " " + name.LastName);
}
首先执行where子句,再对得到的可枚举集合执行select子句,其结果为一个包含属性FirstName和LastName的匿名类型。
1.3 对数据进行分组和排序
为支持更复杂的情形,如对返回的数据进行排序或分组,LINQ 提供了 orderby 子句和group 子句。可将数据按升序(从最小到最大)或降序(从最大到最小)排列,由于升序是默认设置,因此不需要指定升序。如下程序将结果按字段LastName排序。
var result = from contact in contacts
oderby contact.LastName
select contact.FirstName;
foreach(var name in result)
{
Console.WriteLine(name);
}
可根据多个字段进行排序,并混合使用升序和降序,这将创建出非常复杂的 orderby 语句,如下代码所示:
var result = from contact in contacts
oderby
contact.LastName ascending,
contact.FirstName descending
select customer.FirstName;
foreach(var name in result)
{
Console.WriteLine(name);
}
将数据分组的方法与此类似,但将使用group子句替换select子句。对数据分组时的不同之处在于,返回的结果为由 IGrouping<TKey, TElement>对象组成的 IEnumerable,可将其视为由列表组成的列表。这要求使用两条嵌套的foreach语句来访问结果。
如下代码是一个使用group子句的LINQ查询
var result = from contact in contacts
group contact by contact.LastName[0];
foreach(var group in result){
Console.WriteLine("Last names starting width {0}", group.key);
foreach(var name in result)
{
Console.WriteLine(name);
}
Console.WriteLine();
}
如果需要引用分组操作的结果,就可创建一个标识符,使用关键字into将查询结果存储到该标识符中,并对其做进一步查询。这种组合方式称为查询延续(continuation)。
如下代码演示了一个使用group和into的LINQ查询:
var result = from contact in contacts
group contact by contact.LastName[0] into namesGroup
where namesGroup.Count() > 2
select namesGroup;
foreach(var group in result)
{
Console.WriteLine("Last names starting width {0}", group.key);
foreach(var name in result)
{
Console.WriteLine(name);
}
Console.WriteLine();
}
1.4 联接数据
LINQ 还能够合并多个数据源,这是通过利用一个或多个都有的字段将它们联接起来实现的。查询多个没有直接关系的数据源时,联接数据很重要。SQL支持使用很多运算符进行联接,但LINQ联接基于相等性。
前面的示例只使用了Contact类,要执行联接操作,至少需要两个类。程序清单12.9再次列出了Contact类,还列出了新增的JournalEntry类。继续假设通过调用GetContacts填充了contacts列表,并通过调用GetJournalEntries填充了journal列表。
如下代码演示了Contact和JournalEntry类
class Contact
{
public int Id { get; set; }
public string Company { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string StateProvince { get; set; }
}
class JournalEntry
{
public int Id { get; set; }
public int ContactId { get; set; }
public string Description { get; set; }
public string EntryType { get; set; }
public DateTime Date { get; set; }
}
IEnumberable<Contact> contacts = GetContacts();
IEnumberable<JournalEntry> journal = GetJournalEntries();
在LINQ中,最简单的联接查询与SQL内联接等效,这种查询使用join子句。SQL联接可使用很多不同的运算符,而LINQ联接只能使用相等运算符,称之为相等联接(equijoin)。
如下示例的查询使用Contact.ID和JournalEntry.ContactId作为联接键,将一个由Contact对象组成的列表和一个由JournalEntry对象组成的列表联接起来:
var result =
from contact in contacts
join journalEntry in journal
on contact.Id equals journalEntry.ContactId
select new
{
contact.FirstName,
contact.LastName,
journalEntry.Date,
journalEntry.EntryType,
journalEntry.Description
};
如上所示的 join子句创建了一个名为 journalEntry的范围变量(range ariable),其类型为JournalEntry;然后使用equals运算符将两个数据源联接起来。
LINQ还支持分组联接概念,而SQL没有与之对应的查询。分组联接使用关键字into,其结果为层次结构。就像使用group子句时那样,需要使用嵌套foreach语句来访问结果。
ps:顺序很重要
使用LINQ联接时,顺序很重要。被联接的数据源必须位于equals运算符左边,而联接数据源必须位于右边。在这个示例中,contacts是被联接的数据源,而journal是联接数据源。
所幸的是,顺序不正确时,编译器能够捕获并生成编译错误。如果交换join子句中的参数,将出现下面的编译错误:
名称“Journalentry”不在“equals”左侧的范围内。请考虑交换“equals”两侧的表达式。
需要注意的另一个重点是,join子句使用运算符equals,它与相等运算符(==)不完全相同。
如下代码的查询联接contacts和journal,并将结果按联系人姓名分组。在返回的结果集中,每个元素都包含一个由 JournalEntry 组成的可枚举集合,该集合由返回的匿名类型的JournalEntries属性表示。
var result =
from contact in contacts
join journalEntry in journal
on contact.Id equals journalEntry.ContactId
into journalGroups
select new
{
Name = contact.LastName + "," +contact.FirstName,
JournalEntries = journalGroups
};
1.5 数据平坦化
虽然选择和联接数据时,返回的数据是合适的,但层次型数据使用起来比较繁琐。LINQ能够创建返回平坦化数据的查询,就像查询SQL数据源一样。
假设对Contact和JournalEntry类进行了修改:在Contact类中添加了一个Journal字段,其类型为List<JournalEntries>,并删除了JournalEntry类的属性ContactId,如下所示
class Contact
{
public int Id { get; set; }
public string Company { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string StateProvince { get; set; }
public List<JournalEntries> Journal;
}
class JournalEntry
{
public int Id { get; set; }
public string Description { get; set; }
public string EntryType { get; set; }
public DateTime Date { get; set; }
}
IEnumerable<Contact> contacts = GetContacts();
在这种情况下,可使用查询检索特定联系人的JournalEntry列表,如下所示:
var result =
from contact in contacts
where contact.id == 1
select contact.Journal;
foreach(var item in result)
{
foreach(var journalEntry in item)
{
Console.WriteLine(journalEntry);
}
}
虽然这样可行,也返回了所需的结果,但是仍需使用嵌套 foreach 语句来访问结果。所幸的是,LINQ 支持从多个数据源选择数据,从而提供了一种返回平坦化数据的查询语法。如下程序演示了这种查询语法,它使用多个from子句,使得访问数据时只需一条foreach语句。
var result =
from contact in contacts
from journalEntry in contact.Journal
where contact.id == 1
select contact.Journal;
foreach(var item in result)
{
Console.WriteLine(journalEntry);
}
二、标准查询运算符方法
前面介绍的所有查询都使用声明性查询语法,但也可使用标准查询运算符方法来编写这些查询。标准查询运算符方法实际上是命名空间System.Linq中定义的Enumerable类的扩展方法。对于使用声明性语法的查询表达式,编译器将其转换为等价的查询运算符方法调用。
使用using语句包含命名空间System.Linq后,对于任何实现了接口IEnumberable<T>的类,智能感知列表都可包含标准查询运算符方法。
虽然声明性查询语法几乎支持所有的查询操作,但是也有一些操作(如Count和Max)没有对应的查询语法,必须使用方法调用来表示。由于每个方法调用都返回IEnumerable,因此通过串接方法调用,可编写出复杂的查询。编译声明性查询表达式时,编译器就是这样做的。
ps:使用声明性语法还是方法语法
使用声明性语法还是方法语法因人而异,这取决于个人认为哪种语法更容易理解。无论使用哪种语法,执行查询得到的结果都相同。
如下示例演示了使用方法语法的LINQ查询
var result = contacts.
Where(contact => contact.StateProvince == "FL").
Select(contact => new { contact.FirstName, contact.LastName });
foreach(var name in result)
{
Console.WriteLine(name.FirstName + " " + name.LastName);
}
三、Lambda
上述示例中,传递给方法Where和Select的参数看起来与以前使用过的参数不同。这些参数实际上包含的是代码,而不是数据。之前介绍过委托和匿名方法,委托能够将一个方法作为参数传递给另一个方法,而匿名方法能够编写未命名的内联语句块,这些语句块将在调用委托时执行。
Lambda 结合使用了这两个概念,它是可包含表达式和语句的匿名函数。通过使用Lambda,可以更方便、更简洁的方式编写这样的代码,即正常情况下需要使用匿名方法或泛型委托进行编写。
ps:Lambda和委托
由于Lambda是编写委托的更简洁方式,因此可在通常需要使用委托的任何地方使用它们。所以,Lambda的形参类型必须与相应的委托类型完全相同,返回类型也必须隐式地转换为委托的返回类型。
虽然 Lambda 没有类型,但是它们可隐式地转换为任何兼容的委托类型。正是这种隐式转换让您无需显式赋值就能够传递它们。
在C#中,Lambda使用Lambda运算符(=>)。在方法调用中,该运算符左边指定了形参列表,而该运算符右边为方法体。匿名方法的所有限制也适用于Lambda。
在上述示例中,实参 contact => contact.StateProvince == "FL"的意思为,这是一个以 contact为参数的函数,其返回值为表达式 contact.StateProvince == "FL"的结果。
ps:捕获的变量和定义的变量
Lambda还能捕获变量,这可以是Lambda所属方法的局部变量或参数。这使得可在Lambda体内通过名称访问捕获的变量。如果捕获是局部变量,那么必须赋值后才能在Lambda中使用它。ref或out参数无法捕获。
然而,需要注意的是,对于Lambda捕获的变量,在引用它的委托超出作用域前,垃圾收集器将不会收集它们。
Lambda中声明的变量在Lambda所属方法内不可见,输入参数也如此,因此可在多个Lambda中使用同一个标识符。
表达式Lambda
在Lambda中,如果运算符右边为表达式,该Lambda就为表达式Lambda,它返回该表达式的结果。表达式Lambda的基本格式如下:
(input parameters) => expressions
如果只有一个输入参数,那么括号是可选择的;否则(包括没有参数时),括号将是必不可少的。
就像泛型方法可推断其类型参数的类型一样,Lambda 也能推断其输入参数的类型。如果编译器无法推断出类型,您就可以显式地指定类型。
如果将表达式 Lambda 的表达式部分视为方法体,那么表达式 Lambda 包含一条隐式的return语句,它返回表达式的结果。
ps:包含方法调用的表达式Lambda
大部分示例都在右边使用了方法,但是如果创建的Lambda将用于其他域,如SQL Server,就不应使用方法调用,因为它们在.NET Framework公共语言运行时外面没有意义。
语句Lambda
在 Lambda 的右边,可使用一条或多条用大括号括起的语句,这种 Lambda 称为语句Lambda。语句Lambda的基本形式如下:
(input parameters) => { statement; }
与表达式 Lambda 一样,如果只有一个输入参数,那么括号是可选的;否则,括号就必不可少。语句Lambda也遵循同样的类型推断规则。
虽然表达式Lambda包含一条隐式的return语句,但是语句Lambda没有,您必须在语句Lambda中显式地指定return语句。return语句只导致从Lambda表示的隐式方法返回,而不会导致从Lambda所属的方法返回。
语句Lambda不能包含这样的goto、break和continue语句,即其跳转目标在Lambda外。同样,作用域规则禁止从嵌套Lambda分支到外部Lambda。
预定义的委托
虽然 Lambda 是 LINQ 的有机组成部分,但是可将其用于任何可使用委托的地方。因此,.NET Framework提供了很多预定义的委托,可将其作为方法参数进行传递,而无需首先声明显式的委托类型。
由于返回Boolean值的委托很常见,因此.NET Framework定义了一个Predicate<in T>委托,Array和List<T>类的很多方法都使用它。
Predicate<T>定义了一个总是返回Boolean值的委托,而Func系列委托封装了有指定返回值,且接受0~16个输入参数的方法。
Predicate<T>和 Func 系列委托都有返回值,但是 Action 系列委托表示返回类型为 void的方法。就像Func系列委托一样,Action系列委托也接受0~16个输入参数。
四、延迟执行
不同于众多传统的数据查询技术,LINQ 查询要等到实际迭代其结果时才执行,称之为延迟执行(lazy evaluation)。其优点之一是,在指定查询和检索查询指定的数据之间,可修改原始集合中的数据。这意味着您获得的数据总是最新的。
虽然LINQ首选延迟执行,但是使用了任何聚合函数的查询都必须先迭代所有元素。这些函数(如Count、Max、Average和First)都返回一个值,并且无需使用显式foreach语句就能执行。
ps:延迟执行和串接查询
延迟执行的另一个优点是,让您能够串接查询,从而提供编码效率。由于查询对象表示的是查询,而不是查询的结果,因此可轻松地串接或重用它们,而不会导致开销高昂的数据取回操作。
也可强制查询立刻执行,这有时称为贪婪执行(greedy evaluation)。为此,可在查询表达式后面,紧接着放置一条foreach语句,也可调用方法ToList 或ToArray。方法ToList 和ToArray还可用于将数据缓存到一个集合对象中。