认识LINQ的第一步---从查询表达式开始
学习和使用C#已经有2个月了,在这两个月的学习中,深刻体会到,C#这门语言还真不适合编程初学者学习,因为它是吸取了很多其他语言,不仅是面向对象,还包括函数式语言的很多特性,导致它变成特性大爆炸的语言。它的许多方面单独拿出来讲,就得是一本书的规模,而且还不一定让人一下子明白。
LINQ,Language INtegrated Query,语言集成查询,是其中一个非常重要的部分,有关它的功能增强,贯穿了整个C#的发展。
先从基本的查询表达式下手。
在讲查询表达式前,我们必须明白:查询表达式不仅仅是针对数据库,它针对的是所有数据源,因为LINQ的意图就是为所有数据源提供统一的访问方式。因为最近的项目使用的是LINQ to SQL,所以这里只讲LINQ to SQL。
查询表达式非常像SQL语句,但书写方式却是反过来:
var articles = from article in db.Articles
where a.Name == "JIM"
select article;
我们必须明白,查询表达式返回的结果是一个序列,哪怕这个序列只有一个元素。
正因为查询表达式返回的是一个序列,才使得它的行为非常有趣。序列的基本特点就是每次只取一个元素,这使得每个转换都处理一个数据,而且只在结果序列的第一个元素被访问的时候才会开始执行查询表达式。像是上面的数据流是这样的:select转换会在它返回的序列中的第一元素被访问时,为该元素调用where转换,where转换会返回数据列表中第一个元素,检查这个谓词(a.Name == "JIM")是否匹配,再把该元素返回给select。
这就是查询表达式的延迟执行,在它被创建的时候没有处理任何数据,只是在内存中生成了这个查询的委托。这样子是非常高效和灵活的,但并不是所有情况都适合,像是reverse这类的操作,就要访问整个数据源。所以,我们一般在返回另一个序列的操作中使用延迟执行,如果是返回单一值就使用即时执行。
使用查询表达式,首先就是声明数据序列的数据源:
from article in db.Articles
where子句用来进行过滤。它会进入数据流中每个元素的谓词,只有返回true的元素才能出现在结果序列中。我们可以使用多个where子句,只有满足所有谓词的元素才能进入结果序列。
编译器会将where子句转换为带有Lambda表达式的where方法调用,像是这样:
where(article => article.Name == "JIM")
所以我们也可以直接使用Lambda表达式,它同样是返回一个序列:
var articles = db.Articles.Where(article => article)
使用Lambda表达式能使我们的代码的简洁度大幅上升,这也是它为什么会被引进C#中的原因之一。
select子句就是投影,相信学过数据库的同学一定非常熟悉。它同样也会有对应的方法调用,应该说,几乎所有的LINQ to SQL操作都有对应的方法调用(因为编译器就是讲它们转换为方法调用),所以接下来就不再讲方法调用形式了。
前面讲过,编译器会将查询表达式转换为普通C#代码的方法调用,以select为例,它并不会像我们预期的那样,转换为Enumerable.Select或者List<T>.Select,它就只是对代码进行转换,然后再寻找适当方法。该方法的参数会是一个委托类型或者一个Expression<T>。为什么转换后的方法中的参数是一个Lambda表达式呢?因为Lambda表达式可以被转换为委托实例或者表达式树,所以使用Lambda表达式就可以对应所有情况。这种转换并不依赖与特定类型,而只依赖与方法名称和参数,也就是所谓的动态类型(Duck Typing)的编译时形式。
因为这样,我们可以实现自己的LINQ提供器,但除非真的有特殊需要,一般我们都不需要做到这点。
我们来看看查询表达式中两个最重要的组成:范围变量和投影表达式。
from article in db.Articles
其中,article就是范围变量,而:
select article
投影表达式就使用了该范围变量。编译器在转换的时候,Lambda表达式的左边,参数名称就是范围变量,右边来自于投影表达式,所以,我们决不能这样写:
from article in db.Articles
select person
从上面来看,范围变量应该是隐式类型,编译器会自己推断它的具体类型。当然,我们也可以使用显式类型的范围变量,像是下面这样:
from Article article in db.Articles
select article
其中,db.Articles是一个List<Article>。
这样的情况是因为我们想要在强类型的容器中进行查询。这样的机制能够运行的保障源于一个方法:Cast()。Cast()会强制将类型转换为目标类型,如果无法转换则会出错。
上面的表达式会转换为以下的代码:
list.Cast<Article>().Select(article => article);
为什么需要我们对范围变量进行显式声明呢?因为Select方法只是IEnumerable<T>的扩展方法,而不是IEnumerable,所以我们必须要显式的声明范围变量的类型才能调用Select方法。
让我们更加深入的研究一下投影表达式。
前面的Select其实是不做事的,但它也并不会返回源数据,查询表达式的结果和源数据必须不一样,否则我们对结果进行的操作可能就会影响到源数据了。这样的Select有一个专门的称呼:退化查询表达式。如果没有其他子句,像是where子句的调用,编译器还是会为select表达式生成Select方法调用,但像是前面,结果生成的表达式并没有Select方法调用。
所有的查询表达式都需要select或者group...by结尾,所以退化查询表达式才会诞生。
数据库的操作经常需要对元素进行排序,所以我们接下来就讲orderby子句。
orderby子句可以有多个排序规则,默认是升序,也可以选择降序descending。我们来看一个例子:
from article in db.Articles
orderby article.Age descending, article.Height
select article;
编译器生成的表达式如:
db.Articles.OrderbyDescending(article => article.Age).ThenBy(article => article.Height);
OrderBy对排序规则起决定作用,而ThenBy只是在OrderBy的结果中进行排序,而且它本身就被定义为IOrderedEnumerable<T>的扩展方法,这是OrderBy返回的类型,这注定我们无法单独使用ThenBy(当然,ThenBy本身也返回该类型,这是为了进一步的连锁)。
最好只使用一个orderby子句,虽然理论上我们是可以使用多个。
前面的查询表达都是最基本的操作,对于一般的要求就已经足够了,接下来讲解比较复杂的查询表达式。
剩下的查询表达式都会涉及到透明标识符。
使用透明标识符最简单的应用就是let子句:
from article in db.Articles
let length = article.Name.Length
orderby length
select new {Name = article.Name, Length = length};
上面的查询表达式远比之前要复杂得多了!
let子句引入了新的范围变量,它的值是基于其他范围变量。但问题来了,Lambda表达式只会给select传递一个参数!而我们这里有两个范围变量!!所以,前面我们才会创建一个匿名类型来包含这两个变量,于是实际的查询表达式是被转换为这样:
db.Articles.Select(article => new {article, length = article.Name.Length})
.OrderBy(z => z.length).Select(z => new {Name = article.Name, Length = z.length});
z这个名称是编译器随机生成的,我们注意到,在let子句后的查询表达式,凡是涉及到length的地方,都会用z.length来代替。这就是透明标识符。
数据库操作都会涉及到联接,LINQ的联接有3种类型:
1.内联接
内联接涉及到两个序列:键选择器(应用于第二个序列的每个元素)和键选择器表达式(应用于第一个序列的每个元素)。联接的结果是一个所有配对元素的序列,配对的规则是第一个元素的键与第二个元素的键相同。
使用内联接的方式如下:
from article in db.Articles
join comment in db.Comments
on article.Name equals comment.ArticleName
select new { article.Name, comment.Content};
对于这两个序列,右边序列db.Comments会在左边序列db.Articles进行流处理的时候进行缓冲,所以当我们要把一个巨大的序列联接到一个小序列上的时候,应把小序列作为右边序列。
如果我们想要过滤序列,最好是在内联接之前对左边序列进行过滤,这样查询表达式就会更加简单,对右边序列进行过滤,可能就要使用嵌套查询表达式,这样可读性很差。
2.分组联接
分组联接结果的每个元素由左边序列的范围变量的1个元素组成,由右边序列的所有匹配元素组成的序列显示为一个新的范围变量,该变量由into后面出现的标识符指定:
from article in db.Articles
join comment in db.Comments
on article.Name equals comment.ArticleName
into comments
select new { Article = article, Comments = comments};
Comments是一个嵌入序列,包含了匹配article的所有comment。
内联接和分组联接最大的差异就是分组联接在左边序列和结果序列间存在一对一的对应关系,即使左边序列中的某些元素在右边序列中没有任何匹配的元素,会用空表示。分组联接同样要对右边序列进行缓冲,对左边序列进行流处理。
3.交叉联接
前面的联接都是相等联接(equijion),左边序列中的元素和右边序列进行匹配。但交叉联接并不在序列间进行任何匹配操作,结果包含了每个可能的元素对:
from article in db.Articles
from comment in db.Comments
select new { Article = article, Comment = comment};
交叉联接的结果就是一个笛卡尔积,左边序列被联接到右边序列的每个元素中。
当然,我们必须对数据库有足够的了解才能明白上面的联接是怎么回事,但正如我们在开头强调的,C#是一门综合性非常强大的语言,所以在学习它的特性前我们必须对相关知识有足够的了解。
讨论完联接后,我们接下来就是分组。
分组的实现非常简单:
from article in db.Articles
where article.Name == "JIM"
group article by article.Age;
当然,我们可以对article.Author进行分组,但这时编译器生成的表达式就非常有趣了:
db.Articles.Where(article => article.Name == "JIM").GroupBy(article => article.Age,
article => article.Author);
GroupBy有更加复杂的版本,它们提供的功能可能比使用查询表达式还要更加强大。
如果打算对查询结果进行更急处理,可以使用查询延续。查询延续是把一个查询表达式的结果用作另外一个查询表达式的初始序列。像是这样:
from article in db.Articles
where article.Name == "JIM"
group article by article.Age into grouped
select new { Age = grouped.Key,
count = grouped.Count()};
可怕的是,我们还能在查询延续后面继续延续!
from article in db.Articles
where article.Name == "JIM"
group article by article.Age into grouped
select new { Age = grouped.Key,
count = grouped.Count()} into result
orderby result.Count
select result;
当然,使用查询表达式必须注意查询语句的复杂性,像是上面的例子就已经足够复杂了。
在代码中使用查询表达式必须注意,如果是复杂的情况,查询表达式会非常复杂,这时使用查询表达式就不是一个好主意,所以,C#提供了很多方法调用来简化这些操作。我们之间其中几个比较常见的操作。
First()返回的是满足条件的第一个元素,而Single()返回的是满足条件的唯一一个元素,但如果有多个元素符合就会跑出错误。它们在取出元素的时候非常有用,但因为可能存在错误,所以最好使用FirstOrDefault()和SingleOrDefault()来代替。Include()这个方法主要用于提取表中的子表中的元素,像是Articles这个表,可以有一个List<Comment> Comments用于存放相关的comment,可以这样:
var articles = db.Articles.Include("Comments").FirstOrDefault(c => c.Name == article.Name);
这样我们就能提取出Comments中的元素了。我们要时刻记住,无论是查询表达式还是方法的调用,最后返回的结果都会是一个IQuerable<T>,是一个序列,而不是单个结果,哪怕这个序列只有一个结果,所以,使用ToList()将其转换为List<T>,就可以方便的使用List的方法对该序列进行操作了。
认识LINQ的第一步---从查询表达式开始的更多相关文章
- linq to sql 动态构建查询表达式树
通过Expression类进行动态构造lamda表达式. 实现了以下几种类型,好了代码说话: public Expression<Func<T, bool>> GetAndLa ...
- 查询表达式和LINQ to Objects
查询表达式实际上是由编译器“预处理”为“普通”的C#代码,接着以完全普通的方式进行编译.这种巧妙的发式将查询集合到了语言中,而无须把语义改得乱七八糟 LINQ的介绍 LINQ中的基础概念 降低两种数据 ...
- 《C#本质论》读书笔记(15)使用查询表达式的LINQ
15.1 查询表达式的概念 简单的查询表达式 private static void ShowContextualKeywords1() { IEnumerable<string> sel ...
- LinQ实战学习笔记(三) 序列,查询操作符,查询表达式,表达式树
序列 延迟查询执行 查询操作符 查询表达式 表达式树 (一) 序列 先上一段代码, 这段代码使用扩展方法实现下面的要求: 取进程列表,进行过滤(取大于10M的进程) 列表进行排序(按内存占用) 只保留 ...
- LINQ之路 5:LINQ查询表达式
书写LINQ查询时又两种语法可供选择:方法语法(Fluent Syntax)和查询表达式(Query Expression). LINQ方法语法的本质是通过扩展方法和Lambda表达式来创建查询.C# ...
- 十五、C# 使用查询表达式的LINQ
使用查询表达式的LINQ 本章介绍了一种新的语法,查询表达式. 1.查询表达式概述 2.特点:投射 筛选 排序 Let 分组 3.作为方法调用 标准查询运算符所实现的查询在功能上 ...
- C#复习笔记(4)--C#3:革新写代码的方式(查询表达式和LINQ to object(下))
查询表达式和LINQ to object(下) 接下来我们要研究的大部分都会涉及到透明标识符 let子句和透明标识符 let子句不过是引入了一个新的范围变量.他的值是基于其他范围变量的.let 标识符 ...
- LINQ查询表达式(1) - 查询表达式基础
LINQ包括五个部分:LINQto Objects.LINQ to DataSets.LINQ to SQL.LINQ to Entities.LINQ to XML. 什么是查询?它有什么用途? “ ...
- [C#.NET 拾遗补漏]13:动态构建LINQ查询表达式
最近工作中遇到一个这样的需求:在某个列表查询功能中,可以选择某个数字列(如商品单价.当天销售额.当月销售额等),再选择 小于或等于 和 大于或等于 ,再填写一个待比较的数值,对数据进行查询过滤. 如果 ...
随机推荐
- HK Openstack Summit 归来有感
4天的Icehouse openstack Summit终于结束,从香港又回到了北京,我们的产品反响相当不错,吸引了很多的注意力和商谈.可是实际上我最近过得很憋屈,心灰意冷,没有了当初那么拼命的动力. ...
- VMware虛擬化技術實作問答
http://www.netadmin.com.tw/article_content.aspx?sn=1202130002&ns=1203280001&jump=3 Q4:啟用VMwa ...
- Java 通过 jacob调用OCX控件
安装好要调用的ocx驱动,并通过注册表查询其对应的clsid. 下载jacob-1.18解压,将jacob-1.18-x86.dll文件复制到D:\Program Files (x86)\Java\j ...
- js 开源k线图开发库
https://github.com/andredumas/techan.js/wiki http://techanjs.org/ A visual, stock charting (Candlest ...
- Twip和Pixel
pixel(像素) 可在屏幕或打印机上显示的最小元素.像素与屏幕无关. 各种显示类型的设计 Microsoft Windows 是与设备无关的-基于窗口的应用程序可以在许多不同显示分辨率与颜色浓度的计 ...
- MySql8.0数据库链接报错The driver has not received any packets from the server
1.我使用MySql数据库8.0版本,然后驱动改成了 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306 ...
- MySQL中 PK NN UQ BIN UN ZF AI 的意思
PK Belongs to primary key作为主键 NN Not Null非空 UQ Unique index不能重复 BIN Is binary column存放二进制数据的列 ...
- kettle 如何将excel文件导入oracle数据库?
1.情景展示 昨日,有一批数据需要导入数据库,但是,plsql不知为何不能导了,于是,我选择使用kettle完成excel数据的导入. 2.准备工作 将对应的数据库所需的jar包拷贝至其lib目录 ...
- V-rep学习笔记:视觉传感器1
Vision sensors, which can detect renderable entities(Renderable objects are objects that can be seen ...
- Qt Installer Framework 使用说明(一)
目录 Qt Installer Framework 使用说明 1.Qt Installer Framework概述 选择安装包类型 促进更新 提供安装内容 2.入门指南 支持的平台 从源代码构建 支持 ...