簡體   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