C#复习笔记(4)--C#3:革新写代码的方式(查询表达式和LINQ to object(下))
查询表达式和LINQ to object(下)
连接
LINQ中的连接与Sql上面的连接的概念相似,只不过LINQ上面的连接操作的序列。LINQ有三种各类型的联结,但并不是都是用join关键字,首先来看与sql中的内连接(inner join)相似的join联结。
关于连接,我准备先说一个最重要的结论:①使用join的连接(内连接)左边会进行流式传输,而右边会进行缓冲传输,所以,在连接两个序列时,应该尽可能的将较小的那个序列放到连接的右侧。②使用join...into的连接(左连接)与内连接一样,分组连接要对右边序列进行缓冲,而对左边序列进行流处理。③多个from子句的连接(交叉联接)的执行完全是流式的。这个结论很重要,所以我准备在章节中多次提及。
勘误:MSDN文档在描述计算内联结的jon方法时,将相关的序列称作inner和outer(可以查看IEnumerable<T>.Join()方法。)。这个只是用来区分两个序列的叫法而已,不是真的在指内联结和外联结。对于IEnumerable<T>.Join()来说,outer是指Join的左边,inner是指Join的右边。
使用join做内连接
特点:延迟执行,右边序列缓冲执行,左边序列流式执行。会被编译器转译为Join()
首先看一下join的语法:
left-key-selector的类型必须要与right-key-selector的类型匹配(能够进行合理的转换也是有效的),意义上面来说也要相等,我们不能吧一个人的出生日期和一个城市的人口做关联。
联结的符号是”equals“而不是“=”或者“==”。
我们也完全有可能用匿名类型来作为键, 因为匿名类型实现了适当的相等性和散列。 如果想创建一个多列的键, 就可以使用匿名类型。
实例:
static void Main(string[] args)
{
var query = from defect in SampleData.AllDefects
join subscription in SampleData.AllSubscriptions
on defect.Project equals subscription.Project
select new { defect.Summary, subscription.EmailAddress };
foreach (var item in query)
{
Console.WriteLine($"{item.EmailAddress}-{item.Summary}");
}
Console.ReadKey();
}
我们通常需要对序列进行过滤,而在联接前进行过滤比在联接后过滤效率要高得多。
static void Main(string[] args)
{
var query = from defect in SampleData.AllDefects
where defect.Status==Status.Closed
join subscription in SampleData.AllSubscriptions
on defect.Project equals subscription.Project
select new { defect.Summary, subscription.EmailAddress };
foreach (var item in query)
{
Console.WriteLine($"{item.EmailAddress}-{item.Summary}");
}
Console.ReadKey();
}
我们也能在join右边的序列上执行类似的查询,不过稍微麻烦一些:
static void Main(string[] args)
{
var query = from subscription in SampleData.AllSubscriptions
join defect in (from defect in SampleData.AllDefects
where defect.Status == Status.Closed
select defect) on
subscription.Project equals defect.Project
select new {subscription.EmailAddress, defect.Summary};
foreach (var item in query)
{
Console.WriteLine($"{item.EmailAddress}-{item.Summary}");
}
Console.ReadKey();
}
内联被编译器转译后的结果如下:
用于LINQ to object的重载签名如下:
当联接的后面不是select子句时,C#3编译器就会引入透明标识符,这样,用于两个序列的范围变量就能用于后面的子句,并且创建了一个匿名类型,简化了对resultSelector参数使用的映射。然而,如果查询表达式的下一 部分是select子句,那么select子句的投影就直接作为resultSelector参数—— 当你可以一步完成这些转换的时候,创建元素对,然后调用Select是没有意义的。你仍然可以把它看做是“select” 步骤所跟随的“join” 步骤, 尽管两者都被压缩到了一个单独的方法调用中。在我看来,这样在思维模式上更能保持一致,而且这种 思维模式也容易理解。除非你打算研究生成的代码,不然可以忽略编译器为你完成的这些优化。令人高兴的是,在学懂了内联接的相关知识后,下一种联接类型就很容易理解了。
使用join... into子句进行分组连接
特点:相当于sql的左连接。延迟执行。右边的序列缓冲执行,左边的序列流式执行。
分组联接结果中的每个元素由左边序列(使用它的原始范围变量)的某个元素和右边序列的所有匹配元素的序列组成。
我们吧之前的例子改成分组联接:
static void Main(string[] args)
{
var query = from defect in SampleData.AllDefects
join subscription in SampleData.AllSubscriptions
on defect.Project equals subscription.Project
into groupedSubscriptions
select new {Defect = defect, Subscriptions = groupedSubscriptions};
foreach (var itemOuter in query)
{
Console.WriteLine(itemOuter.Defect.Summary);
foreach (var itemInner in itemOuter.Subscriptions)
{
Console.WriteLine(itemInner.EmailAddress);
} Console.WriteLine("--------------------");
} Console.ReadKey();
}
每个条目的Subscriptions都是一个内嵌的序列。该序列包含了匹配defect的所有subscription。
分组连接和普通分组(下面会讲这个group....by的连接),对于分组连接来说,在左边序列和结果序列之间是一对一的对应关系, 即使左边序列中的某些元素在右边序列中没有任何匹配的元素, 也无所谓。 这是非常重要的, 有时会用于模拟SQL的左外联接。在左边元素不匹配任何右边元素的时候,嵌入序列就是空的。
下面是一个按时间进行累计的例子:
static void Main(string[] args)
{
var dates = new DateTimeRange(SampleData.Start, SampleData.End);
var query = from date in dates
join defect in SampleData.AllDefects on date equals defect.Created.Date
into defectDate
select new { Date = date, Count = defectDate.Count() };
foreach (var item in query)
{
Console.WriteLine($"{item.Date}---{item.Count}");
} Console.ReadKey();
}
上面的例子使用了书上的样例。
编译器将分组联接转译为简单地调用GroupJoin方法,就像Join一样。Enumerable.GroupJoin的签名如下:
这个方法签名和内联接的方法签名非常相似,只不过resultSelector参数必须要用于处理右边元素的序列,不能处理单一的元素。同内联接一致,如果分组联接后面紧跟着select子句,那么投影就用作GroupJoin调用的结果选择器, 否则,就引入一个 透明标识符。在这个 例子中,分组联接之后紧接着select子句,所以转译后的查询如下:
使用多个from子句的交叉联接
特点:相当于sql的交叉连接。延迟执行。完全的流式执行。
交叉联接的结果和SQL的笛卡儿积一样,是从两个序列中找出所有可能的匹配结果:
static void Main(string[] args)
{
var query = from firstNumber in Enumerable.Range(, )
from secondNumber in Enumerable.Range(, )
select new
{
firstNumber,
secondNumber
};
foreach (var item in query)
{
Console.WriteLine($"{item.firstNumber}--{item.secondNumber}");
} Console.ReadKey();
}
上面输出的结果是firstNumber序列中的结果乘以secondNumber序列中的结果,一共是2*2=4个结果。
交叉联接就是通过多个from子句来进行交叉匹配的——前两个from子句的匹配结果在和第三个from子句继续交叉,以此类推。
交叉联接非常像笛卡儿积,但是它更加强大:在任意特定时刻使用的右边序列依赖于左边序列的“当前” 值。也就是说,左边序列中的每个元素都用来生成右边的一个序列,然后左边这个元素与右边新生成序列的每个元素都组成一对。这并不是通常意义上的交叉联接,而是将多个序列高效地合并(flat)成一个序列。不管我们是否使用真正的交叉联接,查询表达式的转译是相同的, 所以为了理解转译过程,我们需要研究一下更复杂的情形。
static void Main(string[] args)
{
var query = from firstNumber in Enumerable.Range(, )
from secondNumber in Enumerable.Range(, )
select new
{
firstNumber,
secondNumber
};
var anotherQuery = Enumerable.Range(, 2).SelectMany(a => Enumerable.Range(1, 1), (b, c) => new {b, c});
foreach (var item in anotherQuery) { Console.WriteLine($"{item.b}--{item.c}"); }
Console.ReadKey(); }
上面的代码段中query是查询表达式,anotherQuery是查询表达式转译后的代码,二者的输出完全一致。下面是Enumerable.SelectMany的方法签名:
和其他联接一样,如果查询表达式中联接操作后面紧跟的是select子句,那么投影就作为最后的实参;否则,引入一个透明标识符,从而使左右序列的范围变量在后续查询中都能被访问。
SelectMany的一个有意思的特性是,执行完全是流式的——一次只需处理每个序列的一个元素,因为它为左边序列的每个不同元素使用最新生成的右边序列。把它与内联接和分组联接进行比较,就能看出:在开始返回任何结果之前,它们(内联接和分组联接)都要完全加载右边序列。你应该在心中谨记如下问题:序列的预期大小,以及计算多次可能的资源开销,何时考虑要使用哪种类型的联接,哪个作为左边序列,哪个作为右边序列。
SelectMany的合并行为是非常有用的。例如,你可能需要处理大量日志文件,每次处理一行。几乎不用花太多力气,我们就能处理所有行的无缝序列。在可下载的源代码中(C# in depth,网上有源代码,百度吧)有下面伪代码的完整版本,其完整的含义和有效性已经非常清晰:
在短短的5行代码中,我们检索、解析并过滤了整个日志文件集,返回了表示错误的日志项的序列。至关重要的是,我们不会一次性向内存加载单个日志文件的全部内容, 更不会一次性加载所有文件——所有的数据都采用流式处理。
还有一个好的例子是将一个字符串数组分解成一个char数组
分组(group......by)和延续(into子句)
分组
特点:被转译为GroupBy()。延迟执行。缓冲执行。
分组的语法是:group projection by grouping
该子句与select子句一样,出现在查询表达式的末尾。但它们的相似之处不止于此:projection表达式和select子句使用的投影是同样的类型。只不过生成的结果稍有不同。
grouping表达式通过其键来决定序列如何分组。整个结果是一个序列,序列中的每个元素本身就是投影后元素的序列,还具有一个 Key属性,即用于分组的键;这样的组合是封装在IGrouping<TKey,TElement>接口中的,它扩展了IEnumerable <TElement>。同样,如果 你想根据多个值来进行分组,可以使用一个匿名类型作为键。
static void Main(string[] args)
{
var query = from defect in SampleData.AllDefects
where defect.AssignedTo != null
group new{defect.Status,defect.Summary} by defect.AssignedTo;
foreach (var item in query)
{
Console.WriteLine(item.Key.Name);
foreach (var defect in item)
{
Console.WriteLine($" {defect.Status} ({defect.Summary})");
}
}
Console.ReadKey();
}
请注意group new{defect.Status,defect.Summary} by defect.AssignedTo;这个group后面跟的这个new{defect.Status,defect.Summary}就是投影,如果有需要的话,和select一样,可以用一个匿名对象来进行横向的缩小序列。
by后面定义的是要作为分组的键(key)。还可以在by后面跟一个匿名类型,如果你有这个需求的话。
注意,分组无法对结果进行流处理,因为要进行分组,它会对每个元素应用键选择和投影,并缓冲被投影元素的分组序列。尽管它不是流式的,但执行仍然是延迟的,直到开始获取其结果。
分组(group...by)和分组联接(join...into)在功能上面很相似,但是有一个需要注意的地方是分组联接(join...into)会显示左边序列的所有元素,不管左边序列中的元素有没有匹配到东西,这个很像SQL中的左联接,而分组(group...by)则不是。
分组(group...by)被转译成GroupBy。很简单,不说了。
查询延续
到目前为止,我们的查询表达式以至于整个表达式都是以select或group...by子句作为结尾。而有些时候,你也许打算对结果进行更多的处理——此时,就可以用到查询延续。
查询延续提供了一种方式,把一个查询表达式的结果用作另外一个查询表达式的初始序列。它可以应用于group...by和select子句上,语法对于两者是一样的——你只需使用上下文关键字into,并为新的范围变量提供一个名称就可以了。范围变量接着能用在查询表达式的下一部分。
C#语言规范在解释这个术语时,将它从这种形式:
转译为
下面是一个例子:
static void Main(string[] args)
{
var query = from defect in SampleData.AllDefects
where defect.AssignedTo != null
group defect by defect.AssignedTo
into grouped
select new { Ass=grouped.Key,Count=grouped.Count()};
foreach (var item in query)
{
Console.WriteLine(item.Ass+"-"+item.Count);
}
Console.ReadKey();
}
延续的前提是使用into来将前面投影的结果作为范围变量进行了传递。上述代码是用另外一个投影(select new { Ass=grouped.Key,Count=grouped.Count()};)来延续分组结果。
按照规范,上述的查询表达式被转译为下述:
接着,又会被转译为这样:
理解延续的另一种方式是, 可以把它们看做两个分开的语句。从实际编译器转译的角度看,这不够准确,不过我发现这样可以更容易明白所发生的事情。在这个例子中,查询表达式(以及对query变量进行分配的表达式)可以看做是如下两个语句:
如果你发现这样更容易阅读的话也可以在代码中做这样的拆分,并且在你开始执行前,不会执行任何计算。
注意:join ... into不是延续 你很容易掉进这样的陷阱, 即看到了上下文关键字into,就认为这是查询延续。对于联接来说,这是不对的。用于分组联接的join ...into子句不能形成一个延续的结构。主要的区别在于,在分组联接中,你仍然可以使用所有的早期范围变量( 用于联接右边名称的范围变量除外)。而对比本节的查询不难发现,延续会清除之前的范围变量,只有在延续中声明的范围变量才能在供后续使用。
在查询表达式和点标记之间做出选择
正如我们在本章看到的,查询表达式在编译之前,先被转译成普通的C#。用普通的C#调用LINQ查询操作符来代替查询表达式,这种做法并没有官方名称,很多开发者称其为点标记(dot notation)。每个查询表达式都可以写成点标记的形式,反之则不成立:很多LINQ操作符在C#中不存在等价的查询表达式。最重要的问题是:什么时候使用哪种语法?(when,which)
最明显的必须使用点标记的情形是调用Reverse、ToDictionary这类没有相应的查询表达式语法的方法。然而即使查询表达式支持你要使用的查询操作符,也很有可能无法使用你想使用的特定重载。
我总结的一些使用查询表达式要优于使用点标记操作符的地方如下:
存在表(序列)的联接的地方,并且需要使用let操作符定义一个透明标识符的时候。
排序,复杂的排序。
C#复习笔记(4)--C#3:革新写代码的方式(查询表达式和LINQ to object(下))的更多相关文章
- C#复习笔记(4)--C#3:革新写代码的方式(扩展方法)
扩展方法 扩展方法有以下几个需求: 你想为一个类型添加一些 成员: 你不需要为类型的实例添加任何更多的数据: 你不能改变类型本身, 因为是别人的代码. 对于C#1和C#2中的静态方法,扩展方法是一种更 ...
- C#复习笔记(4)--C#3:革新写代码的方式(Lambda表达式和表达式树)
Lambda表达式和表达式树 先放一张委托转换的进化图 看一看到lambda简化了委托的使用. lambda可以隐式的转换成委托或者表达式树.转换成委托的话如下面的代码: Func<string ...
- C#复习笔记(4)--C#3:革新写代码的方式(用智能的编译器来防错)
用智能的编译器来防错 本章的主要内容: 自动实现的属性:编写由字段直接支持的简单属性, 不再显得臃肿不堪: 隐式类型的局部变量:根据初始值推断类型,简化局部变量的声明: 对象和集合初始化程序:用一个表 ...
- Java 10 的 10 个新特性,将彻底改变你写代码的方式!
Java 9才发布几个月,很多玩意都没整明白,现在Java 10又快要来了.. 这时候我真尼玛想说:线上用的JDK 7 甚至JDK 6,JDK 8 还没用熟,JDK 9 才发布不久不知道啥玩意,JDK ...
- Java 10的10个新特性,将彻底改变你写代码的方式!
Java 9才发布几个月,很多玩意都没整明白,现在Java 10又快要来了.. 这时候我真尼玛想说:线上用的JDK 7 甚至JDK 6,JDK 8 还没用熟,JDK 9 才发布不久不知道啥玩意,JDK ...
- Java 8 到 Java 14,改变了哪些你写代码的方式?
前几天,JDK 14 正式发布了,这次发布的新版本一共包含了16个新的特性. 其实,从Java8 到 Java14 ,真正的改变了程序员写代码的方式的特性并不多,我们这篇文章就来看一下都有哪些. La ...
- 《C#本质论》读书笔记(15)使用查询表达式的LINQ
15.1 查询表达式的概念 简单的查询表达式 private static void ShowContextualKeywords1() { IEnumerable<string> sel ...
- 文档驱动 —— 表单组件(五):基于Ant Design Vue 的表单控件的demo,再也不需要写代码了。
源码 https://github.com/naturefwvue/nf-vue3-ant 特点 只需要更改meta,既可以切换表单 可以统一修改样式,统一升级,以最小的代价,应对UI的升级.切换,应 ...
- Java基础复习笔记系列 八 多线程编程
Java基础复习笔记系列之 多线程编程 参考地址: http://blog.csdn.net/xuweilinjijis/article/details/8878649 今天的故事,让我们从上面这个图 ...
随机推荐
- IE和其他浏览器内核
1.qq急速 2.qq的IE兼容模式 3.Edge 4.IE11 5.chrome js获取浏览器内核 <script language="JavaScript" type= ...
- chrome postman插件手动安装
注:转自 http://blog.csdn.net/u010246789/article/details/51481134 1.下载postman插件,可以自己到网上下载,也可以点击http://do ...
- UVA1374-Power Calculus(迭代加深搜索)
Problem UVA1374-Power Calculus Accept:323 Submit:2083 Time Limit: 3000 mSec Problem Description I ...
- UVA12188-Inspector's Dilemma(欧拉回路+连通性判断)
Problem UVA12188-Inspector's Dilemma Time Limit: 3000 mSec Problem Description In a country, there a ...
- [matlab] 1.拟合
x = [1 2 3 4 5 6 7 8 9 ]; y = [9 7 6 3 -1 2 5 7 20]; p=polyfit(x,y,3); %数字代表拟合函数的阶数 xi=0:0.01:10; yi ...
- TensorFlow——循环神经网络基本结构
1.导入依赖包,初始化一些常量 import collections import numpy as np import tensorflow as tf TRAIN_DATA = "./d ...
- 自然语言处理之LDA主题模型
1.LDA概述 在机器学习领域,LDA是两个常用模型的简称:线性判别分析(Linear Discriminant Analysis)和 隐含狄利克雷分布(Latent Dirichlet Alloca ...
- 前端框架Vue.js——vue-i18n ,vue项目中如何实现国际化
本项目利用 VueI18n 组件进行国际化,使用之前,需要进行安装 $ npm install vue-i18n 一.框架引入步骤: 1. 先在 main.js 中引入 vue-i18n. // 国 ...
- 001_HTTP参数中Etag的重要性
在研究tornado时,有个Etag比较好奇,从网上查询摘录如下:
- ArrayList中ConcurrentModificationException
java中两种基本的集合结构ArrayList和LinkedList底层有两种不同的存储方式实现,ArrayList为数组实现,属于顺序存储,LinkedList为链表实现,属于链式存储,在对Arra ...