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

2021-04-19 02:25

阅读:343

查询表达式和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()方法。)。这个只是用来区分两个序列的叫法而已,不是真的在指内联结和外联结。对于IEnumerable.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” 步骤, 尽管两者都被压缩到了一个单独的方法调用中。在我看来,这样在思维模式上更能保持一致,而且这种 思维模式也容易理解。除非你打算研究生成的代码,不然可以忽略编译器为你完成的这些优化。令人高兴的是,在学懂了内联接的相关知识后,下一种联接类型就很容易理解了。

 


评论


亲,登录后才可以留言!