LINQ项目.NET语言集成查询
摘要:了解有关添加到.NETFramework的常规查询工具的信息,这些工具适用于所有信息源,而不只是关系数据或XML数据。该工具名为.NET语言集成查询(LINQ)。
本页内容二十年之后,业界在面向对象(OO)编程技术的发展过程中趋于稳定。现在,程序员已经认为诸如类、对象和方法等特性是理所当然的。在探究当前的和下一代技术时,明显可以看出,有关编程技术的下一个难题是降低访问和集成特定信息(这些信息不是使用OO技术进行原始定义的)的复杂性。非OO信息的两个最常见源是关系数据库和XML。
对于LINQ项目,我们采取了更为普通的方法,并向.NETFramework中添加了适用于所有信息源(而不只是关系数据或XML数据)的通用查询工具,而不是在编程语言和运行库中添加相关功能或特定于XML的功能。该工具名为.NET语言集成查询(LINQ)。
我们使用语言集成查询这一术语表明,该查询是开发人员主要编程语言(例如,C#、VisualBasic)的集成功能。语言集成查询使得查询表达式能够得益于丰富的元数据、编译时语法检查、静态输入和智能感知(以前只能用于命令代码)。语言集成查询还答应将单个通用的声明查询工具应用于所有内存中信息,而不只是来自外部源的信息。
.NET语言集成查询定义了一组通用的标准查询操作符,答应在任何基于.NET的编程语言中通过直接的声明方式进行遍历、筛选和投影操作。标准查询操作符答应将查询应用于任何基于IEnumerable的信息源。LINQ答应第三方使用特定于域的新操作符(适用于目标域或技术)来补充标准查询操作符集。更重要的是,第三方还可以使用自己提供附加服务(例如,远程计算、查询转换、优化等)的实现来自由替换标准查询操作符。通过符合LINQ样式的约定,此类实现可以享受与标准查询操作符相同的语言集成和工具支持。
查询体系结构的可扩展性在LINQ项目本身中用于提供可同时处理XML和SQL数据的实现。处理XML的查询操作符(XLinq)使用一个高效、易于使用的内存中XML工具来提供宿主编程语言中的XPath/XQuery功能。处理关系数据的查询操作符(DLinq)将基于SQL的架构定义集成构建到CLR类型系统中。该集成通过关系数据提供强类型化,同时直接在底层存储中保留关系模型的表达功能和查询计算的性能。
为了查看执行中的语言集成查询,我们将从一个简单的C#3.0程序开始,该程序使用标准的查询操作符来处理数组的内容:
usingSystem;usingSystem.Query;usingSystem.Collections.Generic;classapp{staticvoidMain(){string[]names={"Burke","Connor","Frank","Everett","Albert","George","Harris","David"};IEnumerableexpr=fromsinnameswheres.Length==5orderbysselects.ToUpper();foreach(stringiteminexpr)Console.WriteLine(item);}}
假如您编译并运行该程序,将看到以下输出:
BURKEDAVIDFRANK
要了解语言集成查询如何工作,我们需要剖析该程序的第一个语句。
IEnumerableexpr=fromsinnameswheres.Length==5orderbysselects.ToUpper();
使用一个查询表达式初始化局部变量expr。通过应用一个或多个标准查询操作符或特定于域的操作符,查询表达式可以操作一个或多个信息源。该表达式使用了三个标准查询操作符:Where、OrderBy和Select。
VisualBasic9.0也支持LINQ。以下是以VisualBasic9.0编写的上述语句:
DimexprAsIEnumerable(OfString)=_Selects.ToUpper()_Fromsinnames_Wheres.Length=5_OrderBys
这里显示的C#和VisualBasic语句均使用查询语法。与foreach语句一样,查询语法是一个方便的声明性代码缩写,您可以手动编写它。上述语句在语义上与以下所示的以C#编写的显式语法完全相同:
IEnumerableexpr=names.Where(s=>s.Length==5).OrderBy(s=>s).Select(s=>s.ToUpper());
Where、OrderBy和Select操作符的参数称为λ表达式,它们是类似于委托的代码片段。它们答应将标准查询操作符单独定义为方法,并使用点标记串连在一起。这些方法共同构成了可扩展查询语言的基础。
LINQ完全构建于通用的语言功能之上,其中某些是在C#3.0和VisualBasic9.0中新增的功能。每个功能都有自己的实用工具,但这些功能共同提供了一种定义查询和可查询API的可扩展方法。在本部分中,我们将探究这些语言功能,以及它们如何提供更为直接和声明性的查询模式。
λ表达式和表达式树
许多查询操作符都答应用户提供执行筛选、投影或键值提取的函数。基于λ表达式的概念而生成的查询工具为开发人员提供了一种编写函数的简便方法,这些函数可以作为后续计算的参数进行传递。λ表达式类似于CLR委托,它必须符合委托类型定义的方法签名。为了进行说明,我们可以使用Func委托类型将上述语句扩展为更为显式的等效形式:
Funcfilter=s=>s.Length==5;Funcextract=s=>s;Funcproject=s=>s.ToUpper();IEnumerableexpr=names.Where(filter).OrderBy(extract).Select(project);
λ表达式是C#2.0匿名方法的自然演化结果。例如,我们可以使用匿名方法编写上述示例,如下所示:
Funcfilter=delegate(strings){returns.Length==5;};Funcextract=delegate(strings){returns;};Funcproject=delegate(strings){returns.ToUpper();};IEnumerableexpr=names.Where(filter).OrderBy(extract).Select(project);
总之,开发人员可以自由地将命名方法、匿名方法或λ表达式与查询操作符一起使用。λ表达式的优点是,能够提供最直接而简洁的创作语法。更重要的是,λ表达式可以编译为代码,也可以编译为数据,从而答应优化器、转换器和计算器在运行时处理λ表达式。
LINQ定义了一个非凡类型Expression(在System.Expressions命名空间中),该类型用于指示给定λ表达式需要表达式树,而不是基于IL的传统方法体。表达式树是λ表达式的有效内存中数据表示形式,它使表达式的结构透明且显式。
编译器是发出可执行IL还是表达式树取决于λ表达式的用法。假如将λ表达式指定给委托类型的变量、字段或参数,则编译器将发出与匿名方法等效的IL。假如将λ表达式指定给Expression类型的变量、字段或参数,则编译器将发出表达式树。
例如,请考虑以下两个变量声明:
Funcf=n=>n<5;Expression<FUNC>e=n=>n<5;
变量f是对委托的引用,可以直接执行:
boolisSmall=f(2);//isSmallisnowtrue
变量e是对表达式树的引用,不可直接执行:
boolisSmall=e(2);//compileerror,expressions==data
与委托(有效的不透明代码)不同,我们可以像与程序中的任何其他数据结构交互那样与表达式树进行交互。例如,以下程序:
Expression<FUNC>filter=n=>n<5;BinaryExpressionbody=(BinaryExpression)filter.Body;ParameterExpressionleft=(ParameterExpression)body.Left;ConstantExpressionright=(ConstantExpression)body.Right;Console.WriteLine("{0}{1}{2}",left.Name,body.NodeType,right.Value);
在运行时分解表达式树,并显示以下字符串:
nLT5
对于启用第三方库(利用属于平台一部分的基本查询抽象)的环境,这种在运行时将表达式视为数据的功能很重要。DLinq数据访问实现利用该功能将表达式树转换为适用于在存储中计算的T-SQL语句。
扩展方法
λ表达式是查询体系结构的一个重要部分。扩展方法是另一个重要部分。扩展方法将动态语言中常见的“快速输入”的灵活性与静态输入语言的性能和编译时验证结合在一起。通过扩展方法,第三方可以使用新方法增加一个类型的公共协定,同时仍然答应单个类型创作者为这些方法提供他们自己的特定实现。
扩展方法在静态类中定义为静态方法,但在CLR元数据中以[System.Runtime.CompilerServices.Extension]属性标记。我们鼓励语言为扩展方法提供直接语法。在C#中,扩展方法由this修饰符指示,该修饰符必须应用于扩展方法的第一个参数。我们来看一下最简单的查询操作符Where的定义:
namespaceSystem.Query{usingSystem;usingSystem.Collections.Generic;publicstaticclassSequence{publicstaticIEnumerableWhere(thisIEnumerablesource,Funcpredicate){foreach(Titeminsource)if(predicate(item))yieldreturnitem;}}}
扩展方法第一个参数的类型指示该扩展应用于哪种类型。在上述示例中,Where扩展方法将扩展IEnumerable类型。由于Where是静态方法,因此我们可以像调用任何其他静态方法那样直接调用它:
IEnumerableexpr=Sequence.Where(names,s=>s.Length<6);
但是,扩展方法的非凡之处在于,它们还可以通过实例语法来调用:
IEnumerableexpr=names.Where(s=>s.Length<6);
扩展方法在编译时根据哪些扩展方法在范围内进行解析。当一个命名空间与C#的using语句或VB的Import语句一起导入时,由该命名空间的静态类定义的所有扩展方法将导入范围中。
标准查询操作符将定义为System.Query.Sequence类型的扩展方法。在检查标准查询操作符时,您将注重到,除了一个以外,所有操作符都可以定义为IEnumerable接口(这个例外是OfType,我们将在后文加以说明)。这意味着,每个与IEnumerable兼容的信息源都可以通过在C#中添加以下using语句来轻松地获得标准查询操作符:
usingSystem.Query;//makesqueryoperatorsvisible
希望将标准查询操作符替换为特定类型的用户可以:(a)使用兼容的签名在特定类型上定义他们自己的同名方法,或者(b)定义可扩展特定类型的新的同名扩展方法。希望完全避免标准查询操作符的用户只能将System.Query置于范围以外,并为IEnumerable编写他们自己的扩展方法。
对于解析而言,扩展方法具有最低的优先权,并且只有在没有合适的目标类型及其基类型的匹配时才使用。这答应用户定义的类型提供他们自己的、优于标准操作符的查询操作符。例如,请考虑以下自定义集合:
publicclassMySequence:IEnumerable{publicIEnumeratorGetEnumerator(){for(inti=1;i<=10;i )yieldreturni;}IEnumeratorIEnumerable.GetEnumerator(){returnGetEnumerator();}publicIEnumerableWhere(Funcfilter){for(inti=1;i<=10;i )if(filter(i))yieldreturni;}}
假定使用该类定义,以下程序:
MySequences=newMySequence();foreach(intitemins.Where(n=>n>3))Console.WriteLine(item);
将使用MySequence.Where实现,而不是扩展方法,因为实例方法优于扩展方法。
前面提到的OfType操作符是一个无法扩展基于IEnumerable的信息源的标准操作符。下面,我们来看一下OfType查询操作符:
publicstaticIEnumerableOfType(thisIEnumerablesource){foreach(objectiteminsource)if(itemisT)yieldreturn(T)item;}
OfType不仅接受基于IEnumerable的源,还接受针对非参数化IEnumerable接口(在.NETFramework1.0版本中提供)编写的源。OfType操作符答应用户将标准查询操作符应用于以下传统的.NET集合:
//"classic"cannotbeuseddirectlywithqueryoperatorsIEnumerableclassic=newOlderCollectionType();//"modern"canbeuseddirectlywithqueryoperatorsIEnumerablemodern=classic.OfType();
在本例中,变量modern生成了与classic相同的值序列,但其类型与现在的IEnumerable代码兼容,包括标准查询操作符。
OfType操作符对于较新的信息源也很有用,因为它答应根据类型从源筛选值。在生成新序列时,OfType只省略原始序列中与类型参数不兼容的成员。请考虑下面这个简单的程序,它将从异类数组中提取字符串:
object[]vals={1,"Hello",true,"World",9.1};IEnumerablejustStrings=vals.OfType();
当我们在foreach语句中枚举justStrings变量时,将获得一个由两个字符串(“Hello”和“World”)组成的序列。
延迟的查询计算
观察力敏锐的读者可能会注重到,标准的Where操作符是使用C#2.0中引入的yield结构实现的。该实现技术常用于返回值序列的所有标准操作符。使用yield的一个有趣的优点是,查询实际上是在迭代完毕后计算的(通过使用foreach语句,或者手动使用基础的GetEnumerator和MoveNext方法)。该延迟计算答应将查询保留为基于IEnumerable的值,这些值可以计算多次,每次都可能生成不同的值。
对于许多应用程序而言,这正是所需的行为。对于希望缓存查询计算结果的应用程序而言,提供的两个操作符(ToList和ToArray)会强制立即计算查询,并返回包含查询计算结果的List或数组。
要了解延迟查询计算如何工作,请考虑以下程序,该程序对数组运行了一个简单的查询:
//declareavariablecontainingsomestringsstring[]names={"Allen","Arthur","Bennett"};//declareavariablethatrepresentsaqueryIEnumerableayes=names.Where(s=>s[0]=='A');//evaluatethequeryforeach(stringiteminayes)Console.WriteLine(item);//modifytheoriginalinformationsourcenames[0]="Bob";//evaluatethequeryagain,thistimeno"Allen"foreach(stringiteminayes)Console.WriteLine(item);
每次迭代变量ayes时,都会计算查询。要指示所需结果的缓存副本,我们只需在查询中追加一个ToList或ToArray操作符,如下所示:
//declareavariablecontainingsomestringsstring[]names={"Allen","Arthur","Bennett"};//declareavariablethatrepresentstheresult//ofanimmediatequeryevaluationstring[]ayes=names.Where(s=>s[0]=='A').ToArray();//iterateoverthecachedqueryresultsforeach(stringiteminayes)Console.WriteLine(item);//modifyingtheoriginalsourcehasnoeffectonayesnames[0]="Bob";//iterateoverresultagain,whichstillcontains"Allen"foreach(stringiteminayes)Console.WriteLine(item);
ToArray和ToList都可以强制立即执行查询计算,这与返回单个值的标准查询操作符(例如,First、ElementAt、Sum、Average、All和Any)一样。
初始化复合值
λ表达式和扩展方法为我们提供了只从值序列筛选成员的查询所需的全部内容。大多数查询表达式还针对这些成员执行投影,将原始序列的成员有效地转换为值和类型可能不同于原先的成员。要支持编写这些转换,LINQ依靠一个名为对象初始化表达式的新结构,以创建结构化类型的新实例。在本文其余部分中,我们将假设定义了以下类型:
publicclassPerson{stringname;intage;boolcanCode;publicstringName{get{returnname;}set{name=value;}}publicintAge{get{returnage;}set{age=value;}}publicboolCanCode{get{returncanCode;}set{canCode=value;}}}
对象初始化表达式使我们能够根据类型的公共字段和属性轻松地生成值。例如,要创建Person类型的新值,我们可以编写以下语句:
Personvalue=newPerson{Name="ChrisSmith",Age=31,CanCode=false};
从语义上说,该语句等效于以下语句序列:
Personvalue=newPerson();value.Name="ChrisSmith";value.Age=31;value.CanCode=false;
对象初始化表达式是语言集成查询的一个重要功能,因为它们答应在仅答应表达式的上下文(如λ表达式和表达式树)中生成新的结构化值。例如,请考虑以下查询表达式,它为输入序列中的每个值创建了新的Person值:
IEnumerableexpr=names.Select(s=>newPerson{Name=s,Age=21,CanCode=s.Length==5});
对象初始化语法也可以方便地用于初始化结构化值的数组。例如,请考虑以下数组变量,该变量是使用单个的对象初始值设定项来初始化的:
staticPerson[]people={newPerson{Name="AllenFrances",Age=11,CanCode=false},newPerson{Name="BurkeMadison",Age=50,CanCode=true},newPerson{Name="ConnorMorgan",Age=59,CanCode=false},newPerson{Name="DavidCharles",Age=33,CanCode=true},newPerson{Name="EverettFrank",Age=16,CanCode=true},};
结构化值和类型
LINQ项目支持以数据为中心的编程样式,其中,某些类型的存在主要是为了通过结构化值提供静态“形式”,而不是提供同时具有状态和行为的完整对象。根据它的逻辑结论推测,通常,开发人员所关心的只是值的结构,以及对命名类型的需要,因为该形式很少使用。这就引出了对匿名类型的介绍,匿名类型答应将新的结构定义为与它们的初始化进行“内联”。
在C#中,匿名类型的语法与对象初始化语法完全相同(除了省略了类型的名称)。例如,请考虑以下两个语句:
objectv1=newPerson{Name="ChrisSmith",Age=31,CanCode=false};objectv2=new{//notetheomissionoftypenameName="ChrisSmith",Age=31,CanCode=false};
变量v1和v2都指向一个内存中对象,该对象的CLR类型有三个公共属性—Name、Age和CanCode。变量的不同之处在于,v2引用了匿名类型的实例。在CLR术语中,匿名类型与任何其他类型没有区别。匿名类型的非凡之处在于,它们在编程语言中没有有意义的名称—创建匿名类型实例的唯一方法就是使用上述语法。
要使变量能够引用匿名类型的实例,同时仍然从静态类型获益,C#引入了var要害字,以便用于替换局部变量声明的类型名称。例如,请考虑以下合法的C#3.0程序:
vars="Bob";varn=32;varb=true;
var要害字会告诉编译器,从用于初始化变量的表达式的静态类型推断出变量的类型。在本例中,s、n和b的类型分别是string、int和bool。该程序与以下程序完全相同:
strings="Bob";intn=32;boolb=true;
var要害字方便用于其类型名称有意义的变量,但对于引用匿名类型实例的变量而言是必需的。
varvalue=new{Name="ChrisSmith",Age=31,CanCode=false};
在上述示例中,变量value是匿名类型,其定义与以下伪C#等效:
internalclass???{string_Name;int_Age;bool_CanCode;publicstringName{get{return_Name;}set{_Name=value;}}publicintAge{get{return_Age;}set{_Age=value;}}publicboolCanCode{get{return_CanCode;}set{_CanCode=value;}}}
匿名类型不能跨程序集边界共享;但是,编译器可确保在每个程序集中,属性名/类型对的给定序列最多有一个匿名类型。
由于匿名类型通常用于投影,以选择现有结构化值的一个或多个成员,因此我们只需从匿名类型初始化的另一个值中引用字段或属性。这将导致一个新的匿名类型,其属性的名称、类型和值均从所引用的属性或字段复制而来。
例如,请考虑以下示例,该示例通过组合其他值的属性创建了一个新的结构化值:
varbob=newPerson{Name="Bob",Age=51,CanCode=true};varjane=new{Age=29,FirstName="Jane"};varcouple=new{Husband=new{bob.Name,bob.Age},Wife=new{Name=jane.FirstName,jane.Age}};intha=couple.Husband.Age;//ha==51stringwn=couple.Wife.Name;//wn=="Jane"
对上述字段或属性的引用只是一种方便的语法,可用于编写以下更显式的窗体:
varcouple=new{Husband=new{Name=bob.Name,Age=bob.Age},Wife=new{Name=jane.FirstName,Age=jane.Age}};
在这两个示例中,couple变量从bob和jane获得了自己的Name和Age属性副本。
匿名类型通常用于查询的select子句。例如,请考虑以下查询:
varexpr=people.Select(p=>new{p.Name,BadCoder=p.Age==11});foreach(variteminexpr)Console.WriteLine("{0}isa{1}coder",item.Name,item.BadCoder?"bad":"good");
在本例中,我们能够通过Person类型创建新投影,以完全匹配处理代码所需的形式,同时仍然提供静态类型的优势。
除了上述基本查询工具之外,许多操作符也提供了操作序列和编写查询的有用方法,从而在标准查询操作符的方便架构中为用户提供对结果的高级控制。
排序和分组
一般而言,对查询表达式的计算会导致以某种顺序生成一系列值,该顺序是底层信息源的固有顺序。要使开发人员能够显式控制这些值的生成顺序,应定义标准查询操作符来控制该顺序。这些操作符中最基本的就是OrderBy操作符。
OrderBy和OrderByDescending操作符可应用于任何信息源,并答应用户提供可生成用于排序结果的值的键值提取函数。OrderBy和OrderByDescending还接受可用于对键施加部分顺序的可选比较函数。下面我们来看一个基本示例:
string[]names={"Burke","Connor","Frank","Everett","Albert","George","Harris","David"};//unitysortvars1=names.OrderBy(s=>s);vars2=names.OrderByDescending(s=>s);//sortbylengthvars3=names.OrderBy(s=>s.Length);vars4=names.OrderByDescending(s=>s.Length);
前两个查询表达式会生成根据字符串比较排序源成员的新序列。后两个查询会生成根据每个字符串的长度排序源成员的新序列。
要答应多个排序准则,OrderBy和OrderByDescending都应该返回SortedSequence,而不是通用的IEnumerable。两个操作符仅在SortedSequence上定义,分别名为ThenBy和ThenByDescending,它们将应用附加(从属)的排序准则。ThenBy/ThenByDescending自身会返回SortedSequence,以答应应用任何数量的ThenBy/ThenByDescending操作符:
string[]names={"Burke","Connor","Frank","Everett","Albert","George","Harris","David"};vars1=names.OrderBy(s=>s.Length).ThenBy(s=>s);
在本例中,计算由s1引用的查询将生成以下值序列:
"Burke","David","Frank","Albert","Connor","George","Harris","Everett"
除了OrderBy系列的操作符,标准查询操作符还包括Reverse操作符。Reverse只枚举序列并以相反的顺序生成相同的值。与OrderBy不同,Reverse在决定顺序时不考虑实际值本身,而仅仅依靠于底层源生成的值的顺序。
OrderBy操作符可对值序列施加排序顺序。标准查询操作符还包括GroupBy操作符,该操作符可根据键值提取函数对值序列进行分区。GroupBy操作符会返回一列Grouping值,其中每一个对应于所碰到的不同的键值。每个Grouping都包含键,以及映射到该键的值集合。Grouping的公共协定如下所示:
publicsealedclassGrouping{publicGrouping(Kkey,IEnumerablegroup);publicGrouping();publicKKey{get;set;}publicIEnumerableGroup{set;get;}}
最简单的GroupBy应用程序如下所示:
string[]names={"Albert","Burke","Connor","David","Everett","Frank","George","Harris"};//groupbylengthvargrouping=names.GroupBy(s=>s.Length);foreach(Groupinggroupingrouping){Console.WriteLine("Stringsoflength{0}",group.Key);foreach(stringvalueingroup.Group)Console.WriteLine("{0}",value);}
运行后,该程序会显示出以下结果:
Stringsoflength6AlbertConnorGeorgeHarrisStringsoflength5BurkeDavidFrankStringsoflength7Everett
Select和GroupBy答应您提供用于填充组成员的投影函数。
string[]names={"Albert","Burke","Connor","David","Everett","Frank","George","Harris"};//groupbylengthvargrouping=names.GroupBy(s=>s.Length,s=>s[0]);foreach(Groupinggroupingrouping){Console.WriteLine("Stringsoflength{0}",group.Key);foreach(charvalueingroup.Group)Console.WriteLine("{0}",value);}
该变体会显示以下结果:
Stringsoflength6ACGHStringsoflength5BDFStringsoflength7E
从该示例中可以注重到,投影类型不需要与源相同。在本例中,我们从字符串序列创建了整数-字符的分组。
聚合操作符
定义几个标准查询操作符,以便将一列值聚合到单个值中。最常见的聚合操作符是Fold,它定义如下:
publicstaticUFold(thisIEnumerablesource,Useed,Funcfunc){Uresult=seed;foreach(Telementinsource)result=func(result,element);returnresult;}
Fold操作符使得针对值序列进行计算很简单。Fold的工作方法是,为底层序列的每个成员调用一次λ表达式。每次Fold调用λ表达式时,都会传递序列的成员和一个聚合值(初始值基于Fold的种子参数)。λ表达式的结果会替换先前的聚合值,Fold将返回λ表达式的最终结果。
例如,以下程序使用Fold针对一个字符串数组计算总字符数:
string[]names={"Albert","Burke","Connor","David","Everett","Frank","George","Harris"};intcount=names.Fold(0,(c,s)=>c s.Length);//count==46
除了通用的Fold操作符,标准查询操作符还包含一个通用的Count操作符和四个数值聚合操作符(Min、Max、Sum和Average),以便简化这些常见的聚合操作。只要提供将序列成员投影到数值类型的函数,数值聚合函数就可以处理数值类型的序列(例如,int、double、decimal)或任意值序列。
以下程序演示Sum操作符的上述两种形式:
int[]numbers={1,2,3,4,5,6,7,8,9,10};string[]names={"Albert","Burke","Connor","David","Everett","Frank","George","Harris"};inttotal1=numbers.Sum();//total1==55inttotal2=names.Sum(s=>s.Length);//total2==46
请注重,第二个Sum语句等效于前面使用Fold的示例。
Select与SelectMany
Select操作符要求转换函数为源序列中的每个值生成一个值。假如转换函数返回的值本身就是一个序列,则应该由使用者手动遍历子序列。例如,请考虑以下程序,该程序使用现有的String.Split方法将字符串拆分为标记:
string[]text={"Albertwashere","Burkesleptlate","Connorishappy"};vartokens=text.Select(s=>s.Split(''));foreach(string[]lineintokens)foreach(stringtokeninline)Console.Write("{0}.",token);
运行后,该程序会显示以下文本:
Albert.was.here.Burke.slept.late.Connor.is.happy.
理想情况下,我们应该让查询返回标记的合并序列,并且不对使用者公开中间string[]。为此,我们将使用SelectMany操作符,而不是Select操作符。SelectMany操作符的工作方式类似于Select操作符。但不同之处在于,转换函数返回的序列随后由SelectMany操作符扩展。下面是使用SelectMany重新编写的程序:
string[]text={"Albertwashere","Burkesleptlate","Connorishappy"};vartokens=text.SelectMany(s=>s.Split(''));foreach(stringtokenintokens)Console.Write("{0}.",token);
使用SelectMany会导致每个中间序列扩展为正常计算的一部分。
C#的现有foreach语句通过.NETFramework的IEnumerable/IEnumerator方法为迭代提供声明性语法。foreach语句完全是可选的,但经过证实,它是一个非常方便和常用的语言机制。
以此为先例,查询语法通过声明性语法为以下最常用的查询操作符简化了查询表达式:Where、Select、SelectMany、GroupBy、OrderBy、ThenBy、OrderByDescending和ThenByDescending。
下面我们首先看一下本文开头的简单查询:
IEnumerableexpr=names.Where(s=>s.Length==5).OrderBy(s=>s).Select(s=>s.ToUpper());
使用查询语法,我们可以按如下方式重新编写这个语句:
IEnumerableexpr=fromsinnameswheres.Length==5orderbysselects.ToUpper();
与C#的foreach语句一样,查询语法表达式更加简洁、易读,但完全是可选的。每个可以用查询语法编写的表达式都有一个相应的使用点标记的语法(虽然较为冗长)。
下面,我们首先看一下查询表达式的基本结构。C#中的每个语法查询表达式都以from子句开始,以select或group子句结束。最初的from子句后可以跟随零个或多个from或where子句。每个from子句都是一个在序列中引入迭代变量的生成器,每个where子句都是一个从结果中排除项目的筛选器。最终的select或group子句之前可能会添加指定结果顺序的orderby子句。单个查询表达式的简化语法如下所示:
fromitemNameinsrcExpr((fromitemNameinsrcExpr)|(wherepredExpr))*(orderby(keyExpr(ascending|descending)?) )?((selectselExpr)|(groupselExprbykeyExpr))
例如,请考虑以下两个查询表达式:
varquery1=frompinpeoplewherep.Age>20orderbyp.Agedescending,p.Nameselectnew{p.Name,Senior=p.Age>30,p.CanCode};varquery2=frompinpeoplewherep.Age>20orderbyp.Agedescending,p.Namegroupnew{p.Name,Senior=p.Age>30,p.CanCode}byp.CanCode;
编译器处理这些查询表达式的方式,就似乎它们是使用以下显式点标记编写的:
varquery1=people.Where(p=>p.Age>20).OrderByDescending(p=>p.Age).ThenBy(p=>p.Name).Select(p=>new{p.Name,Senior=p.Age>30,p.CanCode});varquery2=people.Where(p=>p.Age>20).OrderByDescending(p=>p.Age).ThenBy(p=>p.Name).GroupBy(p=>p.CanCode,p=>new{p.Name,Senior=p.Age>30,p.CanCode});
查询表达式基于方法名称执行机械转换。所选择的查询操作符实现取决于所查询的变量类型以及范围内的扩展方法。
到目前为止,所展示的查询表达式只使用了一个生成器。假如使用多个生成器,则每个后续的生成器将在其前一个生成器的上下文中进行计算。例如,请考虑以下对查询的小修改:
varquery=froms1innameswheres1.Length==5froms2innameswheres1==s2selects1 "" s2;
假如运行以下输入数组:
string[]names={"Burke","Connor","Frank","Everett","Albert","George","Harris","David"};
我们将得到以下结果:
BurkeBurkeFrankFrankDavidDavid
上述查询表达式扩展为以下点标记表达式:
varquery=names.Where(s1=>s1.Length==5).SelectMany(s1=>names.Where(s2=>s1==s2).Select(s2=>s1 "" s2));
请注重,使用SelectMany会导致内部查询表达式对外部结果失效。
本部分前面所述的查询表达式的简化语法忽略了一个非常有用的功能。将一个查询的结果视为后续查询的生成器通常很有用。为此,查询表达式将使用into要害字在select或group子句后拼接一个新的查询表达式。下面是简化的语法,它展示into要害字如何适应其余的语法:
fromitemNameinsrcExpr((fromitemNameinsrcExpr)|(wherepredExpr))*(orderby(keyExpr(ascending|descending)?) )?((selectselExpr)|(groupselExprbykeyExpr))(intoitemName((fromitemNameinsrcExpr)|(wherepredExpr))*(orderby(keyExpr(ascending|descending)?) )?((selectselExpr)|(groupselExprbykeyExpr)))*
对于后续处理groupby子句的结果,into要害字非凡有用。例如,请考虑以下程序:
varquery=fromiteminnamesorderbyitemgroupitembyitem.LengthintolengthGroupsorderbylengthGroups.KeydescendingselectlengthGroups;foreach(vargroupinquery){Console.WriteLine("Stringsoflength{0}",group.Key);foreach(varvalingroup.Group)Console.WriteLine("{0}",val);}
该程序将输出以下内容:
Stringsoflength7EverettStringsoflength6AlbertConnorGeorgeHarrisStringsoflength5BurkeDavidFrank
本部分描述C#如何实现查询表达式。其他语言可能选择通过显式语法支持其他查询操作符。
需要注重的是,查询语法绝对不是硬连接到标准查询操作符的。它是纯粹的语法功能,通过以适当的名称和签名实现基础方法,来应用于任何符合LINQ样式的类型。上述标准查询操作符是使用扩展方法增加IEnumerable接口来实现这一点的。开发人员可以对任何所需的类型使用查询语法,只要确保它符合LINQ样式(通过直接实现必需的方法,或者将它们添加为扩展方法)即可。
这种可扩展性在LINQ项目本身中采用,方法是:提供两个支持LINQ的API,分别名为DLinq(为基于SQL的数据访问实现LINQ样式)和Xlinq(答应LINQ通过XML数据查询)。这两者将在以下部分中描述。
.NET语言集成查询可用于查询关系数据存储,而不必离开本地编程语言的语法或编译时环境。该工具(代号为DLinq)利用SQL架构信息到CLR元数据的集成。该集成将SQL表和视图定义编译为可以从任何语言访问的CLR类型。
DLinq定义了两个核心属性([Table]和[Column]),它们指示哪些CLR类型和属性对应于外部SQL数据。[Table]属性可以应用于类,并将CLR类型与命名的SQL表或视图相关联。[Column]属性可以应用于任何字段或属性,并将成员与命名的SQL列相关联。这两个属性均被参数化,以答应保留特定于SQL的元数据。例如,请考虑以下这个简单的SQL架构定义:
createtablePeople(Namenvarchar(32)primarykeynotnull,Ageintnotnull,CanCodebitnotnull)createtableOrders(OrderIDnvarchar(32)primarykeynotnull,Customernvarchar(32)notnull,Amountint)
CLR等效形式如下所示:
[Table(Name="People")]publicclassPerson{[Column(DbType="nvarchar(32)notnull",Id=true)]publicstringName;[Column]publicintAge;[Column]publicboolCanCode;}[Table(Name="Orders")]publicclassOrder{[Column(DbType="nvarchar(32)notnull",Id=true)]publicstringOrderID;[Column(DbType="nvarchar(32)notnull")]publicstringCustomer;[Column]publicint?Amount;}
在该示例中,请注重,可以为空的列映射到CLR中可以为空的类型(可以为空的类型首次出现在.NETFramework版本2中),并且对于无法1:1对应于CLR类型的SQL类型(例如,nvarchar、char、text),原始SQL类型会保留在CLR元数据中。
要针对关系存储发出查询,LINQ样式的DLinq实现会将查询从表达式树形式转换为SQL表达式以及适用于远程计算的ADO.NETDbCommand对象。例如,请考虑以下这个简单的查询:
//establishaquerycontextoverADO.NETsqlconnectionDataContextcontext=newDataContext("InitialCatalog=petdb;IntegratedSecurity=sspi");//grabvariablesthatrepresenttheremotetablesthat//correspondtothePersonandOrderCLRtypesTablecusts=context.GetTable();Tableorders=context.GetTable();//buildthequeryvarquery=fromcincusts,oinorderswhereo.Customer==c.Nameselectnew{c.Name,o.OrderID,o.Amount,c.Age};//executethequeryforeach(variteminquery)Console.WriteLine("{0}{1}{2}{3}",item.Name,item.OrderID,item.Amount,item.Age);
DataContext类型提供一个轻量转换器,它的工作是将标准查询操作符转换为SQL。DataContext使用现有的ADO.NETIDbConnection来访问存储,并且可以使用已建立的ADO.NET连接对象或者可用于创建连接对象的连接字符串来进行初始化。
GetTable方法提供与IEnumerable兼容的变量,这些变量可用于查询表达式,以表示远程表或视图。调用GetTable不会导致与数据库进行交互–虽然它们表示使用查询表达式与远程表或视图进行交互的潜在可能。在上述示例中,直到程序迭代完查询表达式,才会将查询传送到存储,在这种情况下,使用的是C#中的foreach语句。当程序首次迭代完查询后,DataContext机制会将表达式树转换为以下将发送给存储的SQL语句:
SELECT[t0].[Age],[t1].[Amount],[t0].[Name],[t1].[OrderID]FROM[Customers]AS[t0],[Orders]AS[t1]WHERE[t1].[Customer]=[t0].[Name]
需要注重的是,通过将查询功能直接构建到本地编程语言中,开发人员可以完全控制关系模型,而不必将关系静态转换为CLR类型。完整的对象/关系映射还可以利用这个核心查询功能,以方便需要该功能的用户。
用于XML的.NET语言集成查询(XLinq)答应使用标准查询操作符以及特定于树的操作符(提供类似于XPath的子代、父代和同辈导航)来查询XML数据。它提供了有效的XML内存中表示形式,该表示形式与现有的System.Xml读取器/写入器基础结构集成在一起,比W3C文档更易于使用。将XML与查询相集成的大部分工作由以下三个类型执行:XName、XElement和XAttribute。
XName提供一种易于使用的方法来处理命名空间限定的标识符(QName)(既用作元素又用作属性名)。XName可以透明地处理高效的标识符原子化,并在需要QName时答应使用符号或纯字符串。
XML元素和属性分别通过XElement和XAttribute表示。XElement和XAttribute支持普通的结构语法,以答应开发人员使用自然语法编写XML表达式:
vare=newXElement("Person",newXAttribute("CanCode",true),newXElement("Name","LorenDavid"),newXElement("Age",31));vars=e.ToString();
这对应于以下XML:
<PersonCanCode="true">Â<Name>LorenDavid</Name>Â<Age>31</Age></Person>
请注重,创建XML表达式不需要基于DOM的工厂模式,并且ToString实现会生成XML文本。XML元素还可以从现有的XmlReader或字符串文字生成:
vare2=XElement.Load(xmlReader);vare1=XElement.Parse(@"<PersonCanCode='true'><Name>LorenDavid</Name><Age>31</Age></Person>");
XElement还支持使用现有的XmlWriter类型发出XML。
XElement与查询操作符吻合,从而答应开发人员针对非XML信息编写查询,以及通过将XElements构建到select子句的正文中来生成XML结果:
varquery=frompinpeoplewherep.CanCodeselectnewXElement("Person",newXAttribute("Age",p.Age),p.Name);
该查询将返回一个XElement序列。要答应XElement的生成超出这种类型的查询结果,XElement构造函数应答应将元素序列直接作为参数进行传递:
varx=newXElement("People",frompinpeoplewherep.CanCodeselectnewXElement("Person",newXAttribute("Age",p.Age),p.Name));
该XML表达式将生成以下XML:
<People><PersonAge="11">AllenFrances</Person><PersonAge="59">ConnorMorgan</Person></People>
上述语句可以直接转换为VisualBasic。但是,VisualBasic9.0还支持使用XML文字,这答应直接从VisualBasic使用声明性XML语法来表达查询表达式。上面的示例可以通过以下VisualBasic语句生成:
Dimx=_<People>Select<PersonAge=(p.Age)>p.Name</Person>_FrompInpeople_Wherep.CanCode</People>
到目前为止,这些示例已经展示了如何使用语言集成的查询生成新的XML值。XElement和XAttribute类型还简化了从XML结构提取信息的过程。XElement还提供了访问器方法,以便答应将查询表达式应用于传统的XPath轴。例如,以下查询仅从上述XElement中提取名称:
IEnumerablejustNames=fromeinx.Descendants("Person")selecte.Value;//justNames=["AllenFrances","ConnorMorgan"]
要从XML中提取结构化值,我们只需在select子句中使用对象初始值设定项表达式:
IEnumerablepersons=fromeinx.Descendants("Person")selectnewPerson{Name=e.Value,Age=(int)e.Attribute("Age")};
请注重,XAttribute和XElement都支持显式转换操作符将文本值作为基元类型来提取。要处理缺失数据,我们只需强制转换为可以为空的类型:
IEnumerablepersons=fromeinx.Descendants("Person")selectnewPerson{Name=e.Value,Age=(int?)e.Attribute("Age")??21};
在本例中,当Age属性缺失时,我们使用默认值21。
VisualBasic9.0为XElement的Elements、Attribute和Descendants等访问器方法提供了直接的语言支持,以答应使用更为简洁、直接的语法来访问基于XML的数据。我们可以使用此功能来编写前面的C#语句,如下所示:
Dimpersons=_SelectnewPerson{.Name=e.Name.Age=e.@Age??21}FromeInx...Person
在VisualBasic中,表达式e.Name通过名称Name查找所有的XElement,表达式e.@Age通过名称Age查找XAttribute,而表达式x...Person则通过名称Person获得x的Descendants集合中的所有项。
.NET语言集成查询为CLR以及针对CLR的语言添加了查询功能。该查询工具在λ表达式和表达式树的基础上生成,以答应将谓词、投影和键值提取表达式用作不透明的可执行代码,或者用作适于下游处理或转换的透明内存中数据。LINQ项目定义的标准查询操作符可在任何基于IEnumerable的信息源上工作,并且与ADO.NET(DLinq)和System.Xml(XLinq)集成在一起,以答应关系数据和XML数据获得语言集成查询的优点。
|
OfType |
基于类型从属关系进行筛选 |
|
Select/SelectMany |
基于转换函数进行投影 |
|
Where |
基于谓词函数进行筛选 |
|
Count |
基于可选谓词函数进行计数 |
|
All/Any |
基于谓词函数的通用/存在量 |
|
First/FirstOrDefault |
基于可选的谓词函数访问初始成员 |
|
ElementAt |
访问指定位置上的成员 |
|
Take/Skip |
访问指定位置之前/之后的成员 |
|
TakeWhile/SkipUntil |
在满足谓词函数之前/之后访问成员 |
|
GroupBy |
基于键值提取函数进行分区 |
|
ToDictionary |
基于键值提取函数创建键/值字典 |
|
OrderBy/ThenBy |
基于键值提取函数和可选的比较函数以升序排列 |
|
OrderByDescending/ ThenByDescending |
基于键值提取函数和可选的比较函数以降序排列 |
|
Reverse |
反转序列的顺序 |
|
Fold |
基于聚合函数对多个值进行聚合 |
|
Min/Max/Sum/Average |
数值聚合函数 |
|
Distinct |
筛选重复成员 |
|
Except |
对作为指定集合成员的元素进行筛选 |
|
Intersect |
筛选不是非指定集合成员的元素 |
|
Union |
组合两个集合中的不同成员 |
|
Concat |
串联两个序列的值 |
|
ToArray/ToList |
在数组或List中缓存查询结果 |
|
Range |
创建某个范围内的数字序列 |
|
Repeat |
创建给定值的多个副本序列 |

浙公网安备 33010602011771号