LINQ 延迟查询的原因

延迟查询

在运行 LINQ 中的某些扩展方法进行集合的查询时,查询不会立即运行。只有当运行至 foreach 对查询结果进行遍历时,或者对查询结果调用 ToList() 方法等情况时,查询才会真正的运行。

我们以 Where() 方法为例进行研究。考察如下代码:

var names = new List<string> {"Nino", "Alberto", "Juan", "Mike", "Phil"};
var namesWithJ = names.Where(n=>n.StartsWith("J")).OrderBy(n => n);

Console.WriteLine("第一次查询");
foreach (string name in namesWithJ)
{
     Console.WriteLine(name);
}
Console.WriteLine();
names.Add("John");
names.Add("Jim");
names.Add("Jack");
names.Add("Denny");
Console.WriteLine("第二次查询");
foreach (string name in namesWithJ)
{
     Console.WriteLine(name);
}

输出结果如下:

第一次查询
Juan

第二次查询
Jack
Jim
John
Juan

我们的查询只定义了一次,两次查询却有不同的输出。这是因为,在遇到 foreach 迭代查询结果时,Where 查询才去从数据源中查询数据,返回给调用者。

现在,我们在查询语句后面加一个 ToList() 方法,那么两次查询输出的都是 Juan 了。

yield return 语句

我们研究一下 Where() 方法,发现它返回的是一个 IEnumerable 对象。它是个枚举器。也就是说,我们的查询语句并没有在内存中产生一个集合,而是一个枚举器。
Where() 方法的内部实现原理如下:

public static IEnumerable<T> Where<T> (this IEnumerable<T> source, Func<T, bool> predicate)
{
    foreach (T item in source)
    {
        if (predicate(item))
            yield return item;
    }
}

yield 语句是 C# 2.0 添加的用于方便创建枚举器的关键字。yield return 语句返回集合的一个元素,并移动到下一个元素上。 从 foreach 中依次访问每一项时,就会调用枚举器进行枚举。这样就可以迭代大量的数据,而无需一次把所有的数据都读入内存中。迭代器方法运行到 yield return 语句时,会返回一个 expression,并保留当前在代码中的位置。 下次调用迭代器函数时,将从该位置重新开始执行

Enumerator 接口(或其泛型形式)定义了一个状态机。它内部拥有一个成员记录当前的状态(当前遍历到的元素 Current),拥有 MoveNext() 方法和 Reset() 方法,这样就可以不断迭代下去。

返回一个枚举器而不是一个集合,这样做有一些好处。例如,如果我们要从数据库中查询一亿个数据,然后将这些数据输出。如果将查询结果放到集合中,然后再输出,那么我们不得不等到这一亿个数据全部查写入内存中后才能输出,这样就有一个阻塞的过程。而如果通过枚举器,那么在执行输出动作的时候,才会真正从数据库中进行查询,并且没查询到一个数据,就可以立即输出它,然后接着从数据库中查询下一条。这样,用户会觉得数据的显示没有延迟。

参考文献

[1] Where 方法
[2] IEnumerator 接口
[3] 迭代器
[4] yield 参考
[5] C#中yield return用法分析

你可能感兴趣的:(C#,.NET,linq)