码迷,mamicode.com
首页 > Windows程序 > 详细

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

时间:2018-03-31 16:40:41      阅读:259      评论:0      收藏:0      [点我收藏+]

标签:sel   post   直接   复习   写代码   含义   ted   show   active   

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

本章内容:

  • 流式处理数据和延迟执行序列
  • 标准查询操作符和查询表达式转换
  • 范围变量和透明标识符
  • 投影、过滤和排序
  • 联接和分组
  • 选择要使用的语法

LINQ中的概念介绍

序列

 

你当然应该对序列这个概念感觉很熟悉: 它通过IEnumerable 和 IEnumerable< T> 接口进行封装,序列就像数据项的传送带——你每次只能获取它们一个, 直到你不再想获取数据, 或者序列中没有数据了。

序列与其他的数据集合结构相比最大的区别在于,你通常不知道序列有多少项构成--或者不能访问任意项,只能是当前这个。列表和数组也能作为序列, 因为 List< T> 实现了IEnumerable< T>—— 不过, 反过来并不总是可行。 比如,你不能拥有一个无限的数组或列表。

序列是LINQ的基础。一开始查询表达式总是针对某个序列,随着操作的进行,可能会转换序列,也可能和更多的序列链接在一起。

来看一个列子:

var adaultNames = from person in People
                where person.Age > 18
                select person.Name;

下面以图的形式将这个操作拆分成了步骤:

技术分享图片

在分解这个步骤之前,先讲为什么序列在LINQ中的地位是如此重要: 这是由于, 它们是数据处理的流模型的基础, 让我们能够只在需要的时候才对数据进行获取和处理。

上图中每一个箭头代表一个序列——描述在左边, 示例数据在右边。 每个框都代表查询表达式的一个步骤。 最初,我们具有整个家庭成员(用Person对象表示)。接着经过过滤后, 序列就只包含成人了(还是用Person对象表示)。 而最终的结果以字符串形式包含这些 成人的名字。每个步骤就是得到一个序列, 在序列上应用操作以生成新的序列。 结果不是字符串"Holly" 和"Jon"—— 而是 IEnumerable<String>, 这样,在从里面一个接一个获取元素的时候, 将首先生成"Holly", 其次得到"Jon"。

再来看一下背后的东西:首先创建的这个表达式,创建的这个表达式只是在内存中生成一个查询的表现形式,这个表现形式使用委托来表示的。只有在访问这个结果集(adaultNames)的第一个元素的时候,整个”车轮“才会滚滚向前(把迭代器比喻为车轮)。LINQ的这个 特点称为延迟执行。 在最终结果的第一个元素被访问的时候, Select转换才会为它的第一个元素调用Where转换。 而Where转换会访问列表中的第一个元素, 检查这个谓词是否匹配(在这个例子中,是匹配的), 并把这个元素返回给Select。 最后,依次提取出名称作 为结果返回。当然,相关的各种参数必须执行可控性检查,如果你要实现自己的LINQ操作符,牢记这一点非常重要。

下图展示了当用foreach调用结果序列中的每一项时,查询表达式在运行中的几个阶段

技术分享图片

这就是流式的特点,虽然涉及了几个阶段,不过,这种使用流的方式处理数据是很高效和灵活的。特别是,不管有多少数据源,在某个时间点上你只需要知道其中一个就好了。

与流式传输相比,还有一种缓冲式的,因为你有的时候必须吧序列中的元素全部加载到内存中来进行计算,比如Reverse操作。他需要提取序列中的所有可用数据,以便把最后一个元素作为第一个元素返回。当然这在效率上会对整个表达式的执行造成很大的性能影响。

不管时流式传输还是缓冲式的传输,他们都属于延迟操作,就是只有在枚举结果集中的第一个元素时才会真正的传输数据,与此相对的是立即执行——有一些转换一经调用就会立即执行。一般来说,返回另一个序列的操作(通常是IEnumerable<T>和IQueryable<T>)会进行延迟操作,返回单一值的运算会立即执行。


LINQ标准查询操作符

 LINQ的标准查询操作符是一个转换的集合, 具有明确的含义。标准查询操作符拥有共同的含义,但在不同的LINQ提供器下,语义也不同,有的LINQ提供器可能在获取第一个元素的时候就加载了所有的数据,比如web服务。这提示我们在编写查询操作时要考虑到使用的是什么数据源。

C#3支持的某些标准查询操作符通过查询表达式内置到语言中。

linq to object的扩展包morelinq可以通过nuget下载。还有Reactive Extensions。

本章的实例数据

要开始实践本章的内容需要有一个示例数据,这个示例数据在C# in depth的网站上有,不过,你可以通过百度找到这个样例数据。这里就不会放出来了,太占篇幅。

简单的开始:选择元素

static void Main(string[] args)
        {
           // ApplicationChooser.Run(typeof(Program), args);
            var query = from user in SampleData.AllUsers
                select user;
            foreach (User user in query)
            {
                Console.WriteLine(user);
            }

            Console.ReadKey();
        }
注意:SampleData.AllUsers这个是示例数据
查询表达式就是用粗体标出的那部分。
这个例子没什么用,我们可以在foreach中直接用SampleData.AllUsers。————我们会用这个例子来引出两个概念:①转译②范围变量。
首先看转译
编译器把查询表达式转译为普通的C#代码, 这是支持C#3查询表达式的基础。转译的时候他不会检查错误,也不会检查有效,就是机械的去转译。上面的例子被转译如下:
var query = SampleData.AllUsers.Select(user => user);

可以看出转译的目标是一个方法调用。C#3的编译器进一步的便宜代码之前,会先将查询表达式转译成这个样子。特别的,它不会检查到底使用Enumerable.Select,还是用List<T>.Select,这会在转译后有编译器进一步的去决定。转译只关注后续的编译器能否正常的编译转译后的代码——它只负责这个环节。重要之处在于,lmabda能够被转换成委托和表达式树,这个是后续编译器做的事情的基础。稍后,在我介绍某些由编译器调用的方法的签名时, 记住在LINQ to Objects中只进行一种调用—— 任何时候,参数(大部分) 都是委托类型, 编译器将用Lambda表达式作为实参, 并尽量查找具有合适签名的方法。 还必须记住, 在转译执行之后, 不管Lambda 表达式中的普通变量(比如方法内部的局部变量) 在哪出现, 它都会以我们在前面的章节看到的方式转换成捕获变量。 这只是普通Lambda表达式的行为—— 不过除非你理解哪些变量将被捕获, 否则很容易被查询结果弄糊涂。

查询表达式实现原理

class Dummy<T>
    {
        public Dummy<T> Select<T>(Func<T, bool> predicate)
        {
            Console.WriteLine("Select called");
            return new Dummy<T>();
        }
    }

    static class Extenssion
    {
        public static Dummy<T> Where<T>(this Dummy<T> dummy, Func<T, bool> predicate)
        {
            Console.WriteLine("Where called");
            return dummy;
        }
    }

static void Main(string[] args)
{
var source = new Dummy<string>();
var query = from dummy in source
where dummy.ToString() == "Ignored"
select "Anything";

Console.ReadKey();
}

上面的代码印证了我们一开始所说的,转译就是这么工作的,我们随便在某一个类型上面定义一些实例方法和扩展方法然后就可以用查询表达式来编写查询,转译的时候根本不在乎你是不是使用了基于IEnumerable的一些扩展方法。所以,他会被转以为下面的代码:

var query = source.Where(dummy => dummy.ToString() == "Ignored").Select(dummy => "anything");

注意在查询表达式select中使用的是”Anything"而不是dummy这是因为select dummy这种特殊的情况会被转译后删除。我的理解是加不加都没啥用,不影响。

注意Dummy这个类实际上并没有实现IEnumerable<T>,这说明了转译并不依赖具体的类型而是依赖具体的方法名称和参数,这也是一种鸭子类型,C#的很多地方都是鸭子类型,比如枚举器能够枚举的根本原因是要找到类型中是否包含一个GetEnumerator的方法,还有async和await也是,这个在后面在做说明。

然后再来看另一个概念:范围变量

技术分享图片

还是上面的那个查询表达式,上下文关键字很容易解释—— 它们明确告知编译器我们要对数据进行的处理。 同样,数据源表达式也仅仅是普通的C#表达式—— 在这个例子中是一个属性,不过它也可以是一个简单的方法调用或变量。

这里较难理解的是范围变量声明和投影表达式。范围变量不像其他种类的变量。在某些方面,它根本就不是变量。 它们只能用于查询表达式中, 实际代表了从一个表达式传递给另外一个表达式的上下文信息。 它们表示了特定序列中的一个元素,而且它们被用于编译器 转译中,以便把其他表达式轻易地转译为Lambda表达式。

我们已经知道最初的查询表达式会转换为如下形式:

SampleData.AllUsers.Select(user => user)

lambda表达式的左边,就是范围变量,而右边,就是select子句的逻辑,转译的过程就是这么简单。

在更复杂的转译过程中,比如SampleData.AllUsers.Select(user => user.Name),也是依赖于C#3更加完善的类型推断,他把所有的类型参数看作一个整体,可以根据一个类型参数来推断出另外一个类型参数,而这也是lmabda表达式允许使用隐式类型的原因。一切都归功于C#3更加强大和完善的类型推断。(其实在前面的章节中有描述)。

到目前为止,我们都实在一个强类型的集合上面使用查询操作符,但是,还有一些弱类型的集合,比如ArrayList和object[],这个时候,Cast和OfType操作符就排上用场了。

static void Main(string[] args)
        {

            ArrayList list = new ArrayList() { "first", "second", "third" };
            IEnumerable<string> strings = list.Cast<string>();
            foreach (string item in strings)
            {
                Console.WriteLine(item);//依次输出"first","second","third"
            }
            ArrayList anotherList = new ArrayList()
           {
               1,"first",3,"fourth"
           };
            IEnumerable<int> ints = anotherList.OfType<int>();
            foreach (int item in ints)
            {
                Console.WriteLine(item);//依次输出1,3
            }
            Console.ReadKey();
        }

在将这种弱类型的集合转换成强类型的集合时,Cast和OfType的机制有所不同,Case会尝试转换每一个元素,遇到不支持的类型时,就会报错,但注意报错的时机:只有在输出1之后,才进行报错,因为Cast和OfType都对序列进行流处理。而OfType会尝试去转换每一个元素,跳过那些不合格的元素。

Cast和OfType只允许一致性、拆箱和装箱转换。List<int>和List<short>之间的转换会失败——Cast会报异常,OfType不会。

而在查询表达式中,显示的声明范围变量的类型和Cast的执行绑定到了一起:如果在一个弱类型的集合中显示的声明范围变量的类型:

static void Main(string[] args)
        {

            ArrayList list = new ArrayList() { "first", "second", "third" };
            var query = from string oneString in list
                select oneString.Substring(0, 3);
            foreach (string item in query)
            {
                Console.WriteLine(item);
            }
                  Console.ReadKey();
        }

这个被转译后就会编程这样

  var anotherQuery = list.Cast<string>().Select(li => li.Substring(0, 3));

没有这个类型转换(Cast)我们根本就不能调用Select————因为Select是只能用于IEnumerable<T>而不能用于IEnumerable。。

当然,除了在弱类型的集合中使用显式声明的范围类型变量,在强类型中也会这样使用。比如,List<接口>中你可能想使用显式类型为”接口实现“声明的范围类型,因为你知道这个List中装的都是”接口实现“而不是”接口“。

接下来阐述一些重要的概念:

  • LINQ以数据序列为基础, 在任何可能的地方都进行流处理。
  • 创建一个查询并不会立即执行它:大部分操作都会延迟执行。
  • C#3的查询表达式包括一个把表达式转换为普通C#代码的预处理阶段,接着使用类型推断、重载、Lambda表达式等这些常规的规则来恰当地对转换后的代码进行编译。
  • 在查询表达式中声明的变量的作用:它们仅仅是范围变量,通过它们你可以在查询表达式内部一致地引用数据。

对序列进行过滤和排序

where

这个介绍了很多变的过滤功能的操作符为我们揭开了一些秘密,比如流式传输。编译器把这个子句转译为带有Lambda表达式的Where方法调用,它使用合适的范围变量作为这个Lambda表达式的参数, 而以过滤表达式作为主体。过滤表达式当作进入数据流的每个元素的 谓词,只有返回true的元素才能出现在结果序列中。使用多个where子句, 会导致多个链接在一起的Where调用——只有满足所有谓词的元素才能进入结果序列。

 static void Main(string[] args)
        {
            User tim = SampleData.Users.TesterTim;
            var query = from defect in SampleData.AllDefects
                where defect.Status != Status.Closed
                where defect.AssignedTo == tim
                select defect.Summary;
            foreach (string item in query)
            {
                Console.WriteLine(item);
            }
            Console.ReadKey();
        }

上面这个包含两个where的查询表达式会被转译成这样:

 var anotherQurty = SampleData.AllDefects
                .Where(de => de.Status != Status.Closed)
                .Where(de => de.AssignedTo == tim)
                .Select(de => de.Summary);

我们当然可以将两个where合并成一个,这或许能够提高一些性能,但也要考虑可读性。

查询表达式的退格

退格的意思是,如果一个select操作符什么都不做,只是返回给定序列的相同序列,那么转译后的代码中就会删除select的相关的调用:

var myFirstQuery = from def in SampleData.AllDefects select def;

上面这段代码在转译后编译器会故意加一个select的操作符在后面,不要以为我说粗了,等我全部表述完了,你就明白了:

var mySecondQuery = SampleData.AllDefects.Select(de => de);

在上面增加一个Select和不增加还有有根本的区别的,Select方法表达的是返回一个新的序列的意思,意思是说我们并没有在原始数据上面进行任何CRUD操作,只是返回一个新的数据源,在这个数据源上面进行操作,是不会对原始数据造成任何影响的。

当有其他操作存在的时候, 就不用为编译器保留一个“空操作” 的select子句了。 例如,假设我们把上面where下面的那个代码块中的查询表达式改为选取整个缺陷而不仅仅是姓名:

User tim = SampleData.Users.TesterTim;
            var query = from defect in SampleData.AllDefects
                where defect.Status != Status.Closed
                where defect.AssignedTo == tim
                select defect;

现在我们不需要select的调用,转译后的代码如下:

var anotherQuery = SampleData.AllDefects.Where(defec => defec.Status != Status.Closed) .Where(defec => defec.AssignedTo == tim);

使用orderby子句进行排序

...
 User tim = SampleData.Users.TesterTim;
            var query = from defect in SampleData.AllDefects
                        where defect.Status != Status.Closed
                        where defect.AssignedTo == tim
                        orderby defect.Severity descending
                        select defect;
....

如果你下载了本章的样例代码,那么会返回下面的结果:

Showstopper-Webcam makes me look bald
Major-Subtitles only work in Welsh
Major-Play button points the wrong way
Minor-Network is saturated when playing WAV file
Trivial-Installation is slow

可以看到已经返回两个Major,但是这两个Major如何进行排序呢?我们进一步改造代码:

....
 User tim = SampleData.Users.TesterTim;
            var query = from defect in SampleData.AllDefects
                        where defect.Status != Status.Closed
                        where defect.AssignedTo == tim
                        orderby defect.Severity descending,defect.LastModified
                        select defect;
....

我们在根据defect.Severity descending进行排序后,又根据defect.LastModified进行类排序。

这个语句被转译为下面的代码:

....
var anotherQeury = SampleData.AllDefects
                .Where(de => de.Status != Status.Closed)
                .Where(de => de.AssignedTo == tim)
                .OrderByDescending(de => de.Severity)
                .ThenBy(de => de.LastModified);
....

可以看到”orderby defect.Severity descending,defect.LastModified“这句是被翻译成了OrderBy...ThenBy的形式。同时select被去掉了,原因上面有解。

下面来总结一下orderby子句的原理:

  • 它们基本上是上下文关键字orderby,后面跟一个或多个排序规则。
  • 一个排序规则就是一个表达式(可以使用范围变量),后面可以紧跟ascending或descending关键字, 它的意思显而易见(默认规则是升序。)
  • 对于主排序规则的转译 就是调用OrderBy或OrderByDescending,而其他子排序规则通过调用ThenBy或ThenByDescending来进行转换,正如我们例子中看到的。OrderBy和ThenBy的不同之处非常简单:OrderBy假设它对排序规则起决定作用,而ThenBy可理解为对之前的一个或多个排序规起 辅助作用。
  • 可以使用多个orderby子句,但是只有最后那个才会”胜利“,也就是说前面的那几个都没用了。
  • 应用排序规则要求所有数据都已经载入(至少对于LINQ to Objecs是这样的)——例如,你就不能对一个无限序列进行排序。这个原因是显而易见的,比如,在你看到 所有元素之前,你不知道你看到的某些东西是否出现在结果序列的开头。

 

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

标签:sel   post   直接   复习   写代码   含义   ted   show   active   

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

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!