延迟的LINQ查询执行如何实际工作?

最近我遇到了这样一个问题: What numbers will be printed considering the following code:

class Program
{
    static void Main(string[] args)
    {
        int[] numbers = { 1, 3, 5, 7, 9 };
        int threshold = 6;
        var query = from value in numbers where value >= threshold select value;

        threshold = 3;
        var result = query.ToList();

        result.ForEach(Console.WriteLine);
        Console.ReadLine();
    }
}

答案: 3, 5, 7, 9

这对我来说很令人惊讶。 我认为threshold值将在查询构造和后来的执行时间被放到堆栈上,这个数字将被撤回并在没有发生的情况下使用。

另一种情况( numbers在执行之前设置为null ):

    static void Main(string[] args)
    {
        int[] numbers = { 1, 3, 5, 7, 9 };
        int threshold = 6;
        var query = from value in numbers where value >= threshold select value;

        threshold = 3;
        numbers = null;
        var result = query.ToList();
        ...
    }

似乎对查询没有影响。 它打印出与前面的例子完全相同的答案。

任何人都可以帮助我理解幕后的真实情况吗? 为什么更改threshold会影响查询执行,而更改numbers不会影响?


您的查询可以在方法语法中这样写:

var query = numbers.Where(value => value >= threshold);

要么:

Func<int, bool> predicate = delegate(value) {
    return value >= threshold;
}
IEnumerable<int> query = numbers.Where(predicate);

这些代码片段(包括您在查询语法中的查询)都是等价的。

当你像这样展开查询时,你会发现predicate是一个匿名方法, threshold是该方法中的一个闭包。 这意味着它将在执行时承担价值。 编译器将生成一个真正的(非匿名)方法来处理这个问题。 该方法在声明时不会被执行,而是在枚举query时执行的每个项目(延迟执行)。 由于枚举发生在threshold值的threshold被改变(并且threshold是闭包)之后,所以使用新的值。

当您将numbers设置为null ,您将引用设置为无处,但该对象仍然存在。 Where (并在query引用)返回的IEnumerable仍然引用它,并且现在初始引用为null并没有关系。

这就解释了这种行为: numbersthreshold在延迟执行中扮演着不同的角色。 numbers是对枚举数组的引用,而threshold是局部变量,其作用域被“转发”到匿名方法。

扩展,第1部分:在枚举期间修改封闭

当您更换线路时,您可以更进一步...

var result = query.ToList();

...有:

List<int> result = new List<int>();
foreach(int value in query) {
    threshold = 8;
    result.Add(value);
}

你正在做的是在你的数组迭代期间改变threshold值的threshold 。 当你第一次点击循环主体时( value 3),你将阈值改为8,这意味着值5和7将被跳过,下一个要添加到列表中的值是9.原因是在每次迭代中再次评估threshold值的threshold ,然后使用有效值。 而且由于阈值已经改变为8,所以数字5和7不会被评估为更大或相等。

扩展,第2部分:实体框架不同

为了使事情更加复杂,当你使用LINQ提供程序从原始创建不同的查询并执行它时,情况会有所不同。 最常见的例子是实体框架(EF)和LINQ2SQL(现在主要被EF取代)。 这些提供程序在枚举之前从原始查询创建一个SQL查询。 由于此时闭包的值只被计算一次(实际上它不是闭包,因为编译器生成表达式树而不是匿名方法),枚举期间threshold变化对结果没有影响。 查询提交到数据库后发生这些更改。

从这个教训是,你必须始终知道你正在使用的LINQ的味道,并且对其内部工作的一些理解是一个优势。


最容易的是看看编译器会产生什么。 你可以使用这个网站:https://sharplab.io

using System.Linq;

public class MyClass
{
    public void MyMethod()
    {
        int[] numbers = { 1, 3, 5, 7, 9 };

        int threshold = 6;

        var query = from value in numbers where value >= threshold select value;

        threshold = 3;
        numbers = null;

        var result = query.ToList();
    }
}

这里是输出:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;

[assembly: AssemblyVersion("0.0.0.0")]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[module: UnverifiableCode]
public class MyClass
{
    [CompilerGenerated]
    private sealed class <>c__DisplayClass0_0
    {
        public int threshold;

        internal bool <MyMethod>b__0(int value)
        {
            return value >= this.threshold;
        }
    }

    public void MyMethod()
    {
        MyClass.<>c__DisplayClass0_0 <>c__DisplayClass0_ = new MyClass.<>c__DisplayClass0_0();
        int[] expr_0D = new int[5];
        RuntimeHelpers.InitializeArray(expr_0D, fieldof(<PrivateImplementationDetails>.D603F5B3D40E40D770E3887027E5A6617058C433).FieldHandle);
        int[] source = expr_0D;
        <>c__DisplayClass0_.threshold = 6;
        IEnumerable<int> source2 = source.Where(new Func<int, bool>(<>c__DisplayClass0_.<MyMethod>b__0));
        <>c__DisplayClass0_.threshold = 3;
        List<int> list = source2.ToList<int>();
    }
}
[CompilerGenerated]
internal sealed class <PrivateImplementationDetails>
{
    [StructLayout(LayoutKind.Explicit, Pack = 1, Size = 20)]
    private struct __StaticArrayInitTypeSize=20
    {
    }

    internal static readonly <PrivateImplementationDetails>.__StaticArrayInitTypeSize=20 D603F5B3D40E40D770E3887027E5A6617058C433 = bytearray(1, 0, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 7, 0, 0, 0, 9, 0, 0, 0);
}

正如你所看到的,如果你改变threshold变量,你真的会改变auto-generated类中的字段。 因为您可以随时执行查询,所以不可能引用位于堆栈上的字段 - 因为当您退出方法时, threshold将从堆栈中移除 - 因此编译器会将此字段更改为带field自动生成的类属于同一类型。

第二个问题:为什么null有效(在代码中不可见)

当你使用: source.Where它调用这个扩展方法时:

   public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) {
        if (source == null) throw Error.ArgumentNull("source");
        if (predicate == null) throw Error.ArgumentNull("predicate");
        if (source is Iterator<TSource>) return ((Iterator<TSource>)source).Where(predicate);
        if (source is TSource[]) return new WhereArrayIterator<TSource>((TSource[])source, predicate);
        if (source is List<TSource>) return new WhereListIterator<TSource>((List<TSource>)source, predicate);
        return new WhereEnumerableIterator<TSource>(source, predicate);
    }

正如你所看到的,它将参考传递给:

WhereEnumerableIterator<TSource>(source, predicate);

这里是where iterator源代码:

    class WhereEnumerableIterator<TSource> : Iterator<TSource>
    {
        IEnumerable<TSource> source;
        Func<TSource, bool> predicate;
        IEnumerator<TSource> enumerator;

        public WhereEnumerableIterator(IEnumerable<TSource> source, Func<TSource, bool> predicate) {
            this.source = source;
            this.predicate = predicate;
        }

        public override Iterator<TSource> Clone() {
            return new WhereEnumerableIterator<TSource>(source, predicate);
        }

        public override void Dispose() {
            if (enumerator is IDisposable) ((IDisposable)enumerator).Dispose();
            enumerator = null;
            base.Dispose();
        }

        public override bool MoveNext() {
            switch (state) {
                case 1:
                    enumerator = source.GetEnumerator();
                    state = 2;
                    goto case 2;
                case 2:
                    while (enumerator.MoveNext()) {
                        TSource item = enumerator.Current;
                        if (predicate(item)) {
                            current = item;
                            return true;
                        }
                    }
                    Dispose();
                    break;
            }
            return false;
        }

        public override IEnumerable<TResult> Select<TResult>(Func<TSource, TResult> selector) {
            return new WhereSelectEnumerableIterator<TSource, TResult>(source, predicate, selector);
        }

        public override IEnumerable<TSource> Where(Func<TSource, bool> predicate) {
            return new WhereEnumerableIterator<TSource>(source, CombinePredicates(this.predicate, predicate));
        }
    }

所以它只是简单地在私有字段中引用我们的源对象。


变量“数字”是查询已实例化并对其进行处理的变量。 它保留了查询设置时的价值。 在执行查询时谓词中使用“threshold”valiable,该值位于ToList()中。 在这一点上谓词找到了在trashhold上的价值。

无论如何,这不是一个明确的代码...

链接地址: http://www.djcxy.com/p/51087.html

上一篇: How does deferred LINQ query execution actually work?

下一篇: The foreach identifier and closures