zoukankan      html  css  js  c++  java
  • Linq延迟执行

    LINQ中大部分查询运算符都有一个非常重要的特性:延迟执行。这意味着,他们不是在查询创建的时候执行,而是在遍历的时候执行(换句话说,当enumerator的MoveNext方法被调用时)。让我们考虑下面这个query:

             static void TestDeferredExecution()
    {
    var numbers = new List<int>();
    numbers.Add(1);
    IEnumerable<int> query = numbers.Select(n => n * 10); // Build query

    numbers.Add(2); // Add an extra element after the query
    foreach (int n in query)
    Console.Write(n + "|"); // 10|20|
    }
    可以看出,我们在查询创建之后添加的number也包含在查询结果中了,这是因为直到foreach语句对query进行遍历时,LINQ查询才会执行,这时,数据源numbers已经包含了我们后来添加的元素2,LINQ的这种特性就是延迟执行。除了下面两种查询运算符,所有其他的运算符都是延迟执行的:
    • 返回单个元素或者标量值的查询运算符,如First、Count等。
    • 下面这些转换运算符:ToArray、ToList、ToDictionary、ToLookup。

    上面两种运算符会被立即执行,因为他们的返回值类型没有提供延迟执行的机制,比如下面的查询会被立即执行。

                int matches = numbers.Where(n => (n % 2) == 0).Count();     // 1

    对于LINQ来说,延迟执行时非常重要的,因为它把查询的创建与查询的执行解耦了,这让我们可以向创建SQL查询那样,分成多个步骤来创建我们的LINQ查询。

    重复执行

    延迟执行带来的一个影响是,当我们重复遍历查询结果时,查询会被重复执行:

            static void TestReevaluation()
    {
    var numbers = new List<int>() { 1, 2 };

    IEnumerable<int> query = numbers.Select(n => n * 10); // Build query
    foreach (int n in query) Console.Write(n + "|"); // 10|20|

    numbers.Clear();
    foreach (int n in query) Console.Write(n + "|"); // <nothing>
    }
    有时候,重复执行对我们说可不是一个优点,理由如下:
    • 当我们需要在某一个给定的点保存查询的结果时。
    • 有些查询比较耗时,比如在对一个非常大的sequence进行查询或者从远程数据库获取数据时,为了性能考量,我们并不希望一个查询会被反复执行。

    这个时候,我们就可以利用之前介绍的转换运算符,比如ToArray、ToList来避开重复执行,ToArray把查询结果保存至一个Array,而ToList把结果保存至泛型List<>:

            static void TestDefeatReevaluation()
    {
    var numbers = new List<int>() { 1, 2 };

    List<int> timesTen = numbers
    .Select(n => n * 10)
    .ToList(); // Executes immediately into a List<int>

    numbers.Clear();
    Console.Write(timesTen.Count); // Still 2
    }
    变量捕获

    延迟执行还有一个不好的副作用。如果查询的lambda表达式引用了程序的局部变量时,查询会在执行时对变量进行捕获。这意味着,如果在查询定义之后改变了该变量的值,那么查询结果也会随之改变。

            static void TestCapturedVariable()
    {
    int[] numbers = { 1, 2 };

    int factor = 10;
    IEnumerable<int> query = numbers.Select(n => n * factor);
    factor = 20;
    foreach (int n in query)
    Console.Write(n + "|"); // 20|40|
    }
    这个特性在我们通过foreach循环创建查询时会变成一个真正的陷阱。假如我们想要去掉一个字符串里的所有元音字母,我们可能会写出如下的query:
                  IEnumerable<char> query = "How are you, friend.";

    query = query.Where(c => c != 'a');
    query = query.Where(c => c != 'e');
    query = query.Where(c => c != 'i');
    query = query.Where(c => c != 'o');
    query = query.Where(c => c != 'u');

    foreach (char c in query) Console.Write(c); //Hw r y, frnd.
    尽管程序结果正确,但我们都能看出,如此写出来的程序不够优雅。所以我们会自然而然的想到使用foreach循环来重构上面这段程序:
                IEnumerable<char> query = "How are you, friend.";

    foreach(char vowel in "aeiou")
    query = query.Where(c => c != vowel);

    foreach (char c in query) Console.Write(c); //How are yo, friend.
    结果中只有字母u被过滤了,咋一看,有没有吃一惊呢!但只要仔细一想就能知道原因:因为vowel定义在循环之外,所以每个lambda表达式都捕获了同一变量。当我们的query执行时,vowel的值是什么呢?不正是被过滤的字母u嘛。要解决这个问题,我们只需把循环变量赋值给一个内部变量即可,如下面的temp变量作用域只是当前的lambda表达式。
                IEnumerable<char> query = "How are you, friend.";

    foreach (char vowel in "aeiou")
    {
    char temp = vowel;
    query = query.Where(c => c != temp);
    }     

            foreach (char i in "aeiou")
            { 
              query=query.Where(n => n != i).ToArray();//这样也是可以的
            }


    foreach (char c in query) Console.Write(c); //Hw r y, frnd.
    延迟执行的实现原理

    查询运算符通过返回装饰者sequence(decorator sequence)来支持延迟执行。

    和传统的集合类型如array,linked list不同,一个装饰者sequence并没有自己用来存放元素的底层结构,而是包装了我们在运行时提供的另外一个sequence。此后当我们从装饰者sequence中请求数据时,它就会转而从包装的sequence中请求数据。

    比如调用Where会创建一个装饰者sequence,其中保存了输入sequence的引用、lambda表达式还有其他提供的参数。下面的查询对应的装饰者sequence如图所示:

            IEnumerable<int> lessThanTen = new int[] { 5, 12, 3 }.Where(n => n < 10);

     当我们遍历lessThanTen时,实际上我们是在通过Where装饰者从Array中查找数据。

    而查询运算符链接创建了一个多层的装饰者,每个查询运算符都会实例化一个装饰者来包装前一个sequence,比如下面的query和对应的多层装饰者sequence:

                IEnumerable<int> query = new int[] { 5, 12, 3 }
    .Where(n => n < 10)
    .OrderBy(n => n)
    .Select(n => n * 10);

    在我们遍历query时,我们其实是在通过一个装饰者链来查询最初的array。

    需要注意的是,如果在上面的查询后面加上一个转换运算符如ToList,那么query会被立即执行,这样,单个list就会取代上面的整个对象模型。

  • 相关阅读:
    使用 Dockerfile 定制镜像
    UVA 10298 Power Strings 字符串的幂(KMP,最小循环节)
    UVA 11090 Going in Cycle!! 环平均权值(bellman-ford,spfa,二分)
    LeetCode Best Time to Buy and Sell Stock 买卖股票的最佳时机 (DP)
    LeetCode Number of Islands 岛的数量(DFS,BFS)
    LeetCode Triangle 三角形(最短路)
    LeetCode Swap Nodes in Pairs 交换结点对(单链表)
    LeetCode Find Minimum in Rotated Sorted Array 旋转序列找最小值(二分查找)
    HDU 5312 Sequence (规律题)
    LeetCode Letter Combinations of a Phone Number 电话号码组合
  • 原文地址:https://www.cnblogs.com/SamllBaby/p/4513677.html
Copyright © 2011-2022 走看看