繁体   English   中英

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

[英]How does deferred LINQ query execution actually work?

最近我遇到了这样一个问题: 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值为闭包)之后发生,因此使用新值。

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 当您第一次点击循环体时(当value 3时),您将阈值更改为8,这意味着将跳过值5和7,并且要添加到列表中的下一个值为9。是在每次迭代时再次评估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));
        }
    }

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

变量“numbers”是查询已经实例化并对其进行处理的变量。 它保留了设置查询时的值。 当执行查询时,在谓词中使用“threshold”valiable,这在ToList()中。 此时,谓词在trashhold上找到值。

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

我认为理解它的最简单的方法就是每行查看它并考虑执行什么和何时执行,而不是只在内存中声明。

//this line declares numbers array
 int[] numbers = { 1, 3, 5, 7, 9 };

//that one declares value of threshold and sets it to 6
 int threshold = 6;

//that line declares the query which is not of the type int[] but probably IQueryable<int>, but never executes it at this point
//To create IQueryable it still iterates through numbers variable, and kind of assign lambda function to each of the items.
 var query = from value in numbers where value >= threshold select value;

//that line changes threshold value to 6
 threshold = 3;

//that line executes the query defined easier, and uses current value value of threshold, as it is only reference
 var result = query.ToList();

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

该机制为您提供了一些很好的功能,例如在多个位置构建查询,并在每次运行准备就绪后执行它。

numbers变量值设置为null将不会更改立即调用的结果,以进行枚举。

您的LINQ查询不会返回请求的数据,它会返回获取可以逐个访问数据元素的内容的可能性。

在软件术语中:LINQ语句的值是IEnumerable<T> (或IQueryable<T>此处不再进一步讨论)。 此对象不包含您的数据。 事实上,你不能用IEnumerable<T>做很多事情。 它唯一能做的就是生成另一个实现IEnumerator<T>对象。 (注意区别:IEnumerable vs IEnumerator)。 这个`GetEnumerator()'函数是我的第一句话中的“ 获取可以访问的内容...... ”的一部分。

您从IEnumerable<T>.GetEnumerator()获得的对象实现了IEnumerator。 此对象也不必保存您的数据。 它只知道如何生成数据的第一个元素(如果有的话),如果它有一个元素,它知道如何获取下一个元素(如果有的话)。 这是“我可以逐句访问您数据的元素 ”。

因此, IEnumerable<T>Enumerator<T>都不(必须)保存您的数据。 它们只是帮助您按定义的顺序访问数据的对象。

在早期,当我们没有List<T>或实现IEnumerable<T>类似集合类时,实现IEnumerable<T>IEnumerator<T>函数ResetCurrentMoveNext是非常麻烦的。 实际上,现在很难找到实现IEnumerator<T>例子,它们不使用同样实现IEnumerator<T>

关键字Yield的引入Yield缓解了IEnumerable<T>IEnumerator<T>的实现。 如果函数包含Yield return ,则返回IEnumerable<T>

IEnumerable<double> GetMySpecialNumbers()
{   // returns the sequence: 0, 1, pi and e
    yield return 0.0;
    yield return 1.0;
    yield return 4.0 * Math.Atan(1.0);
    yield return Math.Log(1.0)
}

请注意,我使用术语序列。 它不是List而不是Dictionary,你只能通过询问第一个元素来访问元素,并反复询问下一个元素。

您可以使用IEnumerable<T>.GetEnumerator()IEnumerator<T>的三个函数来访问序列的元素。 这种方法很少再使用:

IEnumerable<double> myNumbers = GetMySpecialNumbers();
IEnumerator<double> enumerator = myNumbers.GetEnumerator();
enumerator.Reset();

// while there are numbers, write the next one
while(enumerator.MoveNext())
{   // there is still an element in the sequence
    double valueToWrite = enumerator.Current();
    Console.WriteLine(valueToWrite);
}

随着foreach的引入,这变得更加容易:

foreach (double valueToWrite in GetMySpecialNumbers())
    Console.WriteLine(valueToWrite);

在内部,这将执行GetNumerator()Reset() / MoveNext() / Current()

所有通用集合类(如List,Array,Dictionary,HashTable等)都实现IEnumerable。 大多数情况下,函数返回一个IEnumerable,你会发现它在内部使用其中一个集合类。

yieldforeach之后的另一个伟大发明是引入扩展方法。 看到解密的扩展方法

扩展方法使您可以使用您无法更改的类,例如List<T> ,并仅使用您有权访问的函数为其编写新功能。

这是LINQ的推动力。 它使我们能够为所有内容编写新功能:“嘿,我是一个序列,你可以要求我的第一个元素和我的下一个元素”(=我实现IEnumerable)。

如果你看一下LINQ源代码,你会发现像Where / Select / First / Reverse / ...等LINQ函数被写成IEnumerable的扩展函数。 他们中的大多数使用泛型集合类(HashTable,Dictionary),其中一些使用yield return,有时你甚至会看到基本的IEnumerator函数,如Reset / MoveNext

通常,您可以通过连接LINQ函数来编写新功能。 但是,请记住,有时yield会使您的函数更容易理解,从而更容易重用,调试和维护。

示例:假设您有一系列生产的Products 每个Product都有一个DateTime属性ProductCompletedTime ,表示产品的生产完成时间。

假设您想知道两个完成的产品之间有多少时间。 问题:无法计算第一个产品。

有了收益,这很容易:

public static IEnumerable<TimeSpan> ToProductionTimes<Product>
    (this IEnumerable<Product> products)
{
    var orderedProducts = product.OrderBy(product => product.ProductionTime;
    Product previousProduct = orderedProducts.FirstOrDefault();
    foreach (Product product in orderedProducts.Skip(1))
    {
        yield return product.ProductCompletedTime - previouseProduct.ProductCompletedTime;
        previousProduct = product;
    }
}

尝试在Linq中执行此操作,要了解发生的情况将更加困难。

结论 IEnumerable不保存您的数据,它只保留逐个访问您数据的潜力。

访问数据最常用的方法是foreach,ToList(),ToDictionary,First等。

每当你需要编写一个返回困难IEnumerable<T>的函数时,至少考虑编写一个yield return函数。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM