LINQ何时执行更新的数据列表

本文关键字:数据 列表 更新 何时 执行 LINQ | 更新日期: 2023-09-27 18:04:54

我很好奇何时执行,尤其是在更新数据和第二次调用时。无论何时使用查询变量,例如在foreach语句中,都是这样吗?还是当我更新列表时,比如nums[1]=99?

    int[] nums = { 1, -2, 3, 0, -4, 5 };
    var posNums = from n in nums
                  where n > 0
                  select n;
    foreach (int i in posNums)
        Console.Write("" + i + " ");
    Console.WriteLine();
    nums[1] = 99;
    foreach (int i in posNums)
        Console.Write("" + i + " "); 
    Console.WriteLine(); 

LINQ何时执行更新的数据列表

Linq推迟分析,直到通过Foreach语句或获取迭代器迭代序列。请注意,在后台,.ToArray((和.ToList调用执行这样的迭代。您可以通过使用方法调用版本并按F9来中断传入的lambda来看到这一点。

var posNums = nums
    .Where(n => n > 0);

请注意,因为Linq函数创建枚举器,所以每次迭代序列时,它们也会重新评估所有查询函数,因此,如果您想对查询的结果执行多次(或嵌套!(迭代,使用.ToArray((将集合复制到内存通常是有利的。如果您想对不断变化的数据执行多次迭代,则需要重用相同的Linq结果。

如果你很好奇,你也可以在参考源

中查看.NET框架用于各种Linq语句的源代码

posNums查询将在每次迭代foreach中的结果时执行。

一个简单的方法就是在查询中引入一个副作用。编译器将查询表达式转换为:

var posNums = nums.Where(n => n > 0);

我们可以通过更多的控制台输出来修改您的代码,并准确地查看执行情况:

int[] nums = { 1, -2, 3, 0, -4, 5 };
Console.WriteLine("Before query creation");
var posNums = nums.Where(n => { Console.WriteLine("   Evaluating " + n); return n > 0; });
Console.WriteLine("Before foreach 1");
foreach (int i in posNums)
    Console.WriteLine("   Writing " + i);
Console.WriteLine("Before array modification");
nums[1] = 99;
Console.WriteLine("Before foreach 2");
foreach (int i in posNums)
    Console.WriteLine("   Writing " + i);

输出为:

Before query creation
Before foreach 1
   Evaluating 1
   Writing 1
   Evaluating -2
   Evaluating 3
   Writing 3
   Evaluating 0
   Evaluating -4
   Evaluating 5
   Writing 5
Before array modification
Before foreach 2
   Evaluating 1
   Writing 1
   Evaluating 99
   Writing 99
   Evaluating 3
   Writing 3
   Evaluating 0
   Evaluating -4
   Evaluating 5
   Writing 5
要想准确地了解发生了什么,最简单的方法是实际构建与Where返回的内容等效的东西,并逐步完成它。这里是一个功能等效于Where的实现(至少在源序列迭代和结果的位置(。

我省略了一些性能优化,以保持对重要内容的关注,并为清晰起见写下了一些"长期"操作:

public static IEnumerable<T> Where<T>(
    this IEnumerable<T> source,
    Func<T, bool> predicate)
{
    return new WhereEnumerable<T>(source, predicate);
}
public class WhereEnumerable<T> : IEnumerable<T>
{
    private IEnumerable<T> source;
    private Func<T, bool> predicate;
    public WhereEnumerable(IEnumerable<T> source, Func<T, bool> predicate)
    {
        this.source = source;
        this.predicate = predicate;
    }
    public IEnumerator<T> GetEnumerator()
    {
        return new WhereEnumerator<T>(source.GetEnumerator(), predicate);
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}
public class WhereEnumerator<T> : IEnumerator<T>
{
    private IEnumerator<T> source;
    private Func<T, bool> predicate;
    public WhereEnumerator(IEnumerator<T> source, Func<T, bool> predicate)
    {
        this.source = source;
        this.predicate = predicate;
    }
    public T Current { get; private set; }
    public void Dispose()
    {
        source.Dispose();
    }
    object IEnumerator.Current
    {
        get { return Current; }
    }
    public bool MoveNext()
    {
        while (source.MoveNext())
            if (predicate(source.Current))
            {
                Current = source.Current;
                return true;
            }
        return false;
    }
    public void Reset()
    {
        throw new NotImplementedException();
    }
}

同样值得参考的是,foreach循环等效于什么:

foreach (int i in posNums)
    Console.Write("" + i + " ");

相当于:

using(IEnumerator<int> iterator = posNums.GetEnumerator())
    while(iterator.MoveNext())
    {
        int i = iterator.Current;
        Console.Write("" + i + " ");
    }

因此,现在您可以遍历并查看序列的值何时实际被提取。(我鼓励您使用调试器来遍历这段代码,在您自己的代码中使用这个Where来代替LINQ的Where,看看这里发生了什么。(

在序列上调用Where完全不影响序列,序列的更改也完全不影响Where的结果。当实际调用MoveNext时,可枚举对象开始从底层可枚举对象中提取值,当有foreach循环时调用MoveNext(以及其他可能性(。

我们在这里可以看到的另一件事是,每次我们调用foreach时,我们都会再次调用GetEnumerator,它从Where中获得一个全新的枚举器,它从底层源序列中获得了一个全新的枚举数。这意味着每次调用foreach时,都会从一开始就再次迭代底层序列。