C#复习笔记(4)--C#3:革新写代码的方式(查询表达式和LINQ to object(下))
2021-04-19 02:25
查询表达式和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
首先看一下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” 步骤, 尽管两者都被压缩到了一个单独的方法调用中。在我看来,这样在思维模式上更能保持一致,而且这种 思维模式也容易理解。除非你打算研究生成的代码,不然可以忽略编译器为你完成的这些优化。令人高兴的是,在学懂了内联接的相关知识后,下一种联接类型就很容易理解了。
文章标题:C#复习笔记(4)--C#3:革新写代码的方式(查询表达式和LINQ to object(下))
文章链接:http://soscw.com/essay/76454.html