C#复习笔记(4)--C#3:革新写代码的方式(查询表达式和LINQ to object(下))

查询表达式和LINQ to object(下)

接下来我们要研究的大部分都会涉及到透明标识符

let子句和透明标识符

let子句不过是引入了一个新的范围变量。他的值是基于其他范围变量的。let 标识符=表达式;

首先展示一个不适用let操作符来使用的按用户名称长度来排序:

...
 var queryWithoutLet = from user in SampleData.AllUsers
                                  orderby user.Name.Length
                                  select user;
            foreach (User user in queryWithoutLet)
            {
                Console.WriteLine($"{user.Name}‘s length is {user.Name.Length}");
            }
...

可以看得出为了按名称排序被迫使用了两次Name.Lengthl来进行查询。这是相当耗费性能的。所以,需要有一种手段来避免这种冗余的计算方式,这就引出了let操作:它对一个表达式进行求值, 并引入一个新的范围变量。

...
  var query = from user in SampleData.AllUsers
                        let length = user.Name.Length
                        orderby length
                        select new { length = length, Name = user.Name };
            foreach (var item in query)
            {
                Console.WriteLine($"{item.Name}‘s length is {item.length}");
            }
...

上述代码产生的结果都相同,只不过只计算了一次Length操作。代码清单引入了一个新的范围变量:length,它包含了用户名的长度(针对原始序列中的当前用户)。我们接着把新的范围变量用于排序和最后的投影。 你发现问题了吗? 我们需要使用两个范围变量, 但 Lambda表达式只会给Select传递一个参数(具体原因在后面)! 这就该透明标识符出场了。

我们在最后的投影中使用了两个范围变量, 不过Select方法只对单个序列起作用。 如何把范围变量合并在一起呢? 答案是,创建一个匿名类型来包含两个变量,不过需要进行一个巧妙的转换, 以便看起来就像在select和orderby子句中实际应用了两个参数。

下图展示了这个过程:

上述代码清单的执行过程,其中let子句引入了length范围变量。

下面是转译后的代码:

...
var translatedQuery = SampleData.Users
                .Select(user => new {user, length = user.Name.Length})
                .OrderBy(z => z.length)
                .Select(z => new {Name = z.user.Name, Length = z.length});
...

查询的每个部分都进行了适当的调整:对于原始的查询表达式直接引用user或length的地方,如果引用发生在let子句之后, 就用z.user或 z.length来代替。这里z这个名称是随机选择的——一切都被编译器隐藏起来。

需要进行说明的是,匿名类型只是一种实现的方式,因为在C#规范上面没有严格规定透明标识符的转移过程,C#规范只是描述了透明标识符应该以怎样的形式去表现。C#的现有编译器是通过匿名类型来实现的,以后不知道会怎样。

联结

LINQ中的联结与Sql上面的联结的概念相似,只不过LINQ上面的联结操作的序列。LINQ有三种各类型的联结,但并不是都是用join关键字,首先来看与sql中的内联结相似的join联结。

关于联结,我准备先说一个最重要的结论:联结的左边会进行流式传输,而右边会进行缓冲传输,所以,在联结两个序列时,应该尽可能的将较小的那个序列放到联结的右侧。这个结论很重要,所以我准备在章节中多次提及。

MSDN文档在描述计算内联结的jon方法时,将相关的序列称作inner和outer(可以查看IEnumerable<T>.Join()方法。)。这个只是用来区分两个序列的叫法而已,不是真的在指内联结和外联结。对于IEnumerable<T>.Join()来说,outer是指Join的左边,inner是指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();
        }

我们可以将join两边的序列进行反转,结果返回的内容相同,只是顺序不同,在linq to object的实现中,返回条目的顺序为:先使左边序列中第1个元素的所有成对数据能被返回(按右边序列的顺序),接着返回使用左边序列中第2个元素的所有成对数据,依次类推。右边 序列被缓冲处理,不过左边序列仍然进行流处理—— 所以,如果你打算把一个巨大的序列联接到一个极小的序列上, 应尽可能把小序列作为右边序列。这种操作仍然是延迟的:在访问第1个数据对时,它才会开始执行,然后再从某个序列中读取数据。这时,它会读取整个右边序列,来建立一个从键到生成这些键的值的映射。之后,它就不需要再次读取右边的序列了, 这时你可以迭代左边的序列,生成适当的数据对。

下图展示了上述代码中用作数据源的两个不同序列。(SampleData.AllDefects,缺陷和SampleData.AllSubscrption,订阅)

我们通常需要对序列进行过滤,而在联接前进行过滤比在联接后过滤效率要高得多。

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 Objects中很有用吗? SQL总是会使用内联接。它们实际上是从某个实体导航到相关联的实体上的一种方式, 通常是把某个表的外键和另外一个表的主键进行联接。在面向对象模型中,我们倾向于通过引用来从某个对象导航到另外一个对象。 例如, 在SQL中, 要得到缺陷的概要和处理这个缺陷的用户名称(这里的名词都是针对书面代码的对象的属性),需要进行联接—— 在C#中,我们则使用属性链。如果在我们的模型中存在一个反向关联,从Project对象到与之关联的NotificationSubscription对象列表,我们 不必使用联接也可以实现这个例子的目标。 这并不是说,在面向对象模型里面,内联没有用——只是没有在关系模型中出现得那么频繁而已。

内联被编译器转译后的结果如下:

用于LINQ to object的重载签名如下:

由于刚才已经对inner和outer的含义做了说明,此处就略去了。

当联接的后面不是select子句时,C#3编译器就会引入透明标识符,这样,用于两个序列的范围变量就能用于后面的子句,并且创建了一个匿名类型,简化了对resultSelector参数使用的映射。然而,如果查询表达式的下一 部分是select子句,那么select子句的投影就直接 作为resultSelector参数—— 当你可以一步完成这些转换的时候,创建元素对,然后调用Select是没有意义的。你仍然可以把它看做是“select” 步骤所跟随的“join” 步骤, 尽管两者都被压缩到了一个单独的方法调用中。在我看来,这样在思维模式上更能保持一致,而且这种 思维模式也容易理解。除非你打算研究生成的代码,不然可以忽略编译器为你完成的这些优化。令人高兴的是,在学懂了内联接的相关知识后,下一种联接类型就很容易理解了。

原文地址:https://www.cnblogs.com/pangjianxin/p/8683415.html

时间: 2024-08-24 11:08:12

C#复习笔记(4)--C#3:革新写代码的方式(查询表达式和LINQ to object(下))的相关文章

C#复习笔记(4)--C#3:革新写代码的方式(用智能的编译器来防错)

用智能的编译器来防错 本章的主要内容: 自动实现的属性:编写由字段直接支持的简单属性, 不再显得臃肿不堪: 隐式类型的局部变量:根据初始值推断类型,简化局部变量的声明: 对象和集合初始化程序:用一个表达式就能创建和初始化对象: 隐式类型的数组:根据内容推断数组的类型,从而简化数组的创建过程: 匿名类型:允许创建新的临时类型来包含简单的属性: 自动实现的属性 这个特性简单的我都不想描述,但是为了保持内容的完整性,放一张图: 和匿名方法还有迭代器一样,它在编译器的帮助下会生成一个后备字段. 自动实现

C#复习笔记(4)--C#3:革新写代码的方式(扩展方法)

扩展方法 扩展方法有以下几个需求: 你想为一个类型添加一些 成员: 你不需要为类型的实例添加任何更多的数据: 你不能改变类型本身, 因为是别人的代码. 对于C#1和C#2中的静态方法,扩展方法是一种更优雅的解决方案. 语法 并不是任何方法都能作为扩展方法使用-- 它必须具有以下特征: 它必须在一个非嵌套的. 非泛型的静态类中( 所以必须是一 个静态方法): 它至少要有 一个参数: 第一个参数必须附加 this 关键字作为前缀: 第一个参数不能有其他任何修饰 符(比如out或ref): 第一个参数

C#复习笔记(4)--C#3:革新写代码的方式(Lambda表达式和表达式树)

Lambda表达式和表达式树 先放一张委托转换的进化图 看一看到lambda简化了委托的使用. lambda可以隐式的转换成委托或者表达式树.转换成委托的话如下面的代码: Func<string, int> getLength = s => s.Length; 转换成表达式树的话是下面的代码: Expression<Func<string, int>> getLength = s => s.Length; 委托方面的东西前面都做了详细的介绍.我们主要学习表达

《C#本质论》读书笔记(15)使用查询表达式的LINQ

15.1 查询表达式的概念 15.1.1 投射 15.1.2 筛选 15.1.3 排序 15.1.4 let子句 15.1.5 分组 15.1.6 使用into进行查询延续 15.1.7 用多个from子句"平整"序列的序列 15.2 查询表达式作为方法调用 简单的查询表达式 private static void ShowContextualKeywords1() { IEnumerable<string> selection = from word in Keyword

写出优美代码的方式,两个习惯:一步到位VS迭代优化

最近把手头这个安卓APP的所有事务性方法都写完了,有了以下体会,新手体会,老鸟轻拍 想写成优美代码的人一般都会有这样的想法: 一定要在写每一句代码,写每一个方法,构造每一个类的时候,都要记得优化:解耦以复用,拆分方法以复用,使用循环减少冗余,限制循环次数减少无效操作,等等.. 这个想法一定没有错,但很多时候往往会是这样的情况: 当功能一复杂,比如你已经分解了几个方法,比如你已经使用了几层循环(有点过分...),比如在多线程中 你经常无法一步到位地完成那么多优化 这往往造成你写一句代码会思考很久

程序员如何像写代码一样找女朋友

在程序员的世界里,妹子是稀有动物,女神就更是凤毛麟角了,大部分程序员由于经常面对电脑,缺乏与人的沟通交流,加上软件行业的工作特殊性,因此找女朋友更是难上加难.那么,程序员如何用自己的方法去追求心仪的女生呢?有这个冲动的朋友请继续看下去. 1.需求分析 根据自己的性格特点.经济实力,合理定位:适合自己的女性范围,也就是软件工程里常谈到:需求分析.自己最想找什么样的女孩,譬如:身高在什么范 围,年龄在什么范围,学历在什么范围,相貌有什么要求,对性格有什么偏好,喜静还是偏活泼.你越能更多了解自己,知道

安卓开发复习笔记——Fragment+FragmentTabHost组件(实现新浪微博底部菜单)

记得之前写过2篇关于底部菜单的实现,由于使用的是过时的TabHost类,虽然一样可以实现我们想要的效果,但作为学习,还是需要来了解下这个新引入类FragmentTabHost 之前2篇文章的链接: 安卓开发复习笔记——TabHost组件(一)(实现底部菜单导航) 安卓开发复习笔记——TabHost组件(二)(实现底部菜单导航) 关于Fragment类在之前的安卓开发复习笔记——Fragment+ViewPager组件(高仿微信界面)也介绍过,这里就不再重复阐述了. 国际惯例,先来张效果图: 下面

安卓开发复习笔记——Fragment+ViewPager组件(高仿微信界面)

什么是ViewPager? 关于ViewPager的介绍和使用,在之前我写过一篇相关的文章<安卓开发复习笔记——ViewPager组件(仿微信引导界面)>,不清楚的朋友可以看看,这里就不再重复. 什么是Fragment? Fragment是Android3.0后新增的概念,Fragment名为碎片,不过却和Activity十分相似,具有自己的生命周期,它是用来描述一些行为或一部分用户界面在一个Activity中,我们可以合并多个Fragment在一个单独的activity中建立多个UI面板,或

[Java基础] Java线程复习笔记

先说说线程和进程,现代操作系统几乎无一例外地采用进程的概念,进程之间基本上可以认为是相互独立的,共享的资源非常少.线程可以认为是轻量级的进 程,充分地利用线程可以使得同一个进程中执行多种任务.Java是第一个在语言层面就支持线程操作的主流编程语言.和进程类似,线程也是各自独立的,有自 己的栈,自己的局部变量,自己的程序执行并行路径,但线程的独立性又没有进程那么强,它们共享内存,文件资源,以及其他进程层面的状态等.同一个进程内的 多个线程共享同样的内存空间,这也就意味着这些线程可以访问同样的变量和