在LINQ中,延迟执行有哪些好处?

53
LINQ使用延迟执行模型,这意味着当调用Linq操作符时,不会立即返回结果序列,而是返回一个对象。只有在枚举此对象时才会产生序列元素。
虽然我理解延迟查询的原理,但我有些难以理解延迟执行的好处:
1)我读到过,只有在实际需要结果时执行延迟查询可以带来很大的好处。那么这个好处是什么?
2)延迟查询的另一个优点是,如果您定义了一个查询,那么每次枚举结果时,如果数据发生更改,您将获得不同的结果。
a)但是,从下面的代码中可以看出,即使不使用延迟查询,我们也能够实现相同的效果(因此每次枚举资源时,如果数据发生更改,我们会得到不同的结果)。
List<string> sList = new List<string>( new[]{ "A","B" });

foreach (string item in sList)
    Console.WriteLine(item); // Q1 outputs AB

sList.Add("C");

foreach (string item in sList)
    Console.WriteLine(item); // Q2 outputs ABC

3) 还有哪些延迟执行的好处?


1
你对延迟执行的工作原理有误解。即使每个阶段都返回IEnumerable<T>,序列也不会每次枚举。 - Reed Copsey
1
@jlafay,你为什么删除了我的编辑? - user702769
因为评论和答案不应该成为问题的一部分。请阅读我的编辑以了解说明。 - Jeff LaFay
3个回答

61
主要好处在于这使得过滤操作(LINQ的核心)变得更加高效。(实际上,这是您提到的第一项优点)。
例如,考虑以下LINQ查询:
 var results = collection.Select(item => item.Foo).Where(foo => foo < 3).ToList();

使用延迟执行,上述代码对集合进行了一次迭代,在迭代期间每次请求项目时,执行映射操作、筛选,然后使用结果构建列表。

如果让LINQ完全执行,则每个操作(Select / Where)都必须遍历整个序列。这将使链接操作非常低效。

个人而言,我认为你上面提到的第二点更像是一种副作用,而不是LINQ的好处——虽然有时候有益,但有些时候会导致混淆,所以我认为应该把它视为“需要理解的内容”,而不是夸大其作用,将其作为LINQ的优点。


针对你的编辑:

  

在你的特定示例中,在两种情况下,Select都会迭代集合并返回类型为item.Foo的IEnumerable I1。Where()然后枚举I1并返回item.Foo类型的IEnumerable<> I2。然后I2将被转换为List。

这不是正确的 - 延迟执行可以防止这种情况发生。

在我的示例中,返回类型为IEnumerable<T>,这意味着它是一个可以枚举的集合,但由于延迟执行,它实际上没有被枚举。

当你调用ToList()时,整个集合被枚举。结果在概念上看起来更像是(尽管当然不同):

List<Foo> results = new List<Foo>();
foreach(var item in collection)
{
    // "Select" does a mapping
    var foo = item.Foo; 

    // "Where" filters
    if (!(foo < 3))
         continue;

    // "ToList" builds results
    results.Add(foo);
}

延迟执行会导致序列自身只被枚举(foreach)一次,在使用 ToList() 时进行。如果没有延迟执行,它的实现将更像是(概念上):

// Select
List<Foo> foos = new List<Foo>();
foreach(var item in collection)
{
    foos.Add(item.Foo);
}

// Where
List<Foo> foosFiltered = new List<Foo>();
foreach(var foo in foos)
{
    if (foo < 3)
        foosFiltered.Add(foo);
}    

List<Foo> results = new List<Foo>();
foreach(var item in foosFiltered)
{
    results.Add(item);
}

1
+1,但可能需要一个不同的示例,因为ToList实际上会迭代整个序列。 - Davy8
2
@user702769:我进行了编辑,展示给你看区别了吗? - Reed Copsey
2
@user702769:嗯,它有些不同,但是IEnumerable<T>只允许每个项逐个返回。这意味着"ToLists"枚举序列"穿过"值,并且每个运算符单独操作值。实际的枚举/遍历仅发生一次。这就是所谓的"延迟执行"的含义。 - Reed Copsey
2
正如我所说,我上面所做的只是概念性的 - 它实际上并没有将代码合并在一起(在LINQ to Objects中 - IQueryable<T>不同,并且有点这样做) - 而是逐个通过运算符提取项目,因此“集合”仅完全枚举一次。 - Reed Copsey
有人能为这个提供文档吗:“延迟执行会导致序列本身只被枚举(foreach)一次”? - BobbyA
显示剩余7条评论

38

延迟执行的另一个好处是它允许您处理无限序列。例如:

public static IEnumerable<ulong> FibonacciNumbers()
{
    yield return 0;
    yield return 1;

    ulong previous = 0, current = 1;
    while (true)
    {
        ulong next = checked(previous + current);
        yield return next;
        previous = current;
        current = next;

    }
}

(来源: http://chrisfulstow.com/fibonacci-numbers-iterator-with-csharp-yield-statements/)

然后你可以执行以下操作:

var firstTenOddFibNumbers = FibonacciNumbers().Where(n=>n%2 == 1).Take(10);
foreach (var num in firstTenOddFibNumbers)
{
    Console.WriteLine(num);
}

输出:

1
1
3
5
13
21
55
89
233
377

如果没有延迟执行,你将会得到一个 OverflowException 异常,或者如果操作没有被 checked 检查,它会无限制地运行,因为它会回绕(如果你在上面调用了 ToList 方法,最终会导致 OutOfMemoryException)。


有所区别。非常好的例子。 - Farhad Jabiyev
太好了!!谢谢 :) - Rahul Singh
1
为什么不先计算所有的斐波那契数,然后返回一个列表呢? - Mateen Ulhaq
3
抱歉回复迟了,但是你所说的“所有斐波那契数”是什么意思呢?这个列表是无限的。如果你是指我知道需要10个,为什么不事先计算,那是因为有时候直到后来你才知道需要多少个。有时候你不需要前10个,也许你需要进行分页并请求第11至20个值。也许你需要将其过滤以仅获取素数值。重点是你可以在代码中稍后决定如何过滤它,而该函数不需要知道它将如何被过滤。 - Davy8
1
@Davy8 对不起,那只是个很糟糕的笑话。;) - Mateen Ulhaq

11

延迟执行的一个重要好处是可以获得最新的数据。虽然这可能会对性能造成影响(特别是处理庞大的数据集时),但同样的,当您原始查询返回结果时,数据可能已经发生了变化。在数据库快速更新的情况下,延迟执行确保您将获得最新的信息。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接