Подтвердить что ты не робот

Как выполняется отсроченное выполнение запроса 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?

4b9b3361

Ответ 1

Ваш запрос может быть написан следующим образом в синтаксисе метода:

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

Или:

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

Эти фрагменты кода (включая ваш собственный запрос в синтаксисе запроса) эквивалентны.

Когда вы разворачиваете такой запрос, вы видите, что predicate является анонимным методом и threshold является closure в этом методе. Это означает, что он будет принимать значение во время выполнения. Компилятор будет генерировать фактический (не анонимный) метод, который позаботится об этом. Метод не будет выполняться, когда он будет объявлен, но для каждого элемента, когда перечисляется query (исполнение отложено). Поскольку перечисление происходит после изменения значения thresholdthreshold является закрытием), используется новое значение.

Когда вы устанавливаете numbers в null, вы устанавливаете ссылку в никуда, но объект все еще существует. IEnumerable, возвращаемый Where (и упоминаемый в query), все еще ссылается на него, и не имеет значения, что начальная ссылка теперь null.

Это объясняет поведение: numbers и threshold играют разные роли в отложенном исполнении. 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: структура Entity Framework отличается

Чтобы усложнить ситуацию, когда вы используете поставщиков LINQ, которые создают другой запрос из вашего оригинала и затем выполняют его, все немного отличается. Наиболее распространенными примерами являются Entity Framework (EF) и LINQ2SQL (теперь они в значительной степени заменены EF). Эти поставщики создают SQL-запрос из исходного запроса перед перечислением. С этого момента значение замыкания оценивается только один раз (фактически это не замыкание, потому что компилятор генерирует дерево выражений, а не анонимный метод), изменения в threshold во время перечисления не влияют на результат. Эти изменения происходят после запроса запроса в базу данных.

Урок этого заключается в том, что вы должны всегда знать, какой вкус LINQ вы используете, и что некоторое понимание его внутренней работы является преимуществом.

Ответ 2

Проще всего видеть, что будет создано компилятором. Вы можете использовать этот сайт: 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));
        }
    }

Поэтому он просто просто ссылается на наш исходный объект в закрытом поле.

Ответ 3

Переменная "числа" - это та, на которой был создан запрос и работает на нем. Он сохраняет значение, которое оно имело при задании запроса. Если в предикате используется "пороговое значение", когда запрос выполняется, то есть в ToList(). В этот момент предикаты обнаруживают значение в trashhold.

В любом случае это не ясный код...

Ответ 4

Я думаю, что самый простой способ понять это просто посмотреть на строку в строке и подумать о том, что и когда выполняется, как против только объявленного в памяти.

//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 не изменит результат так, как он был немедленно вызван, для перечисления.

Ответ 5

Запрос 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> Reset, Current и MoveNext. На самом деле, в настоящее время трудно найти примеры реализации IEnumerator<T>, которые не используют класс, который также реализует IEnumerator<T>. Пример

Введение ключевого слова 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)
}

Обратите внимание, что я использую последовательность терминов. Это не список, не словарь, вы можете получить доступ к элементам, запросив первый, и повторно просить следующий.

Вы можете получить доступ к элементам последовательности с помощью 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, вы обнаружите, что внутри она использует один из этих классов коллекции.

Еще одно замечательное изобретение после Yield и foreach - введение методов расширения. См. методы расширения demystified.

Методы расширения позволяют вам выбрать класс, который вы не можете изменить, например List<T>, и написать для него новые функции, используя только те функции, к которым у вас есть доступ.

Это стало стимулом для LINQ. Это позволило нам написать новую функциональность для всего, что говорило: "Эй, я последовательность, вы можете попросить мой первый элемент и для моего следующего элемента" (= я реализую IEnumerable).

Если вы посмотрите на исходный код LINQ,, вы увидите, что функции LINQ, такие как Where/Select/First/Reverse/... и т.д., записываются как функции расширения IEnumerable. Большинство из них используют общие классы коллекций (HashTable, Dictionary), некоторые из них используют возврат доходности, а иногда вы даже видите основные функции 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.