IEnumerable <T>хранит функцию, которая будет вызвана позже? - PullRequest
0 голосов
/ 16 октября 2018

Недавно я столкнулся с кодом, который не ведет себя так, как я ожидал.

1: int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8 };
2: IEnumerable<int> result = numbers.Select(n => n % 2 == 0 ? n : 0);
3: 
4: int a = result.ElementAt(0);
5: numbers[0] = 10;
6: int b = result.ElementAt(0);

Когда я прошел этот код в Visual Studio, я был удивлен, увидев, что выделение желтого цвета выпрыгнуло из строки4 обратно к лямбда-выражению в строке 2, затем снова из строки 6 к лямбде в строке 2.

Кроме того, значение a после выполнения этого кода равно 0, а значение b равно10.

Исходный код, который заставил меня понять, что это могло / могло произойти, включал вызов метода внутри Select(), а доступ к любому свойству или определенному элементу IEnumerable приводил к тому, что метод в Select() былвызывается снова и снова.

// The following code prints out:
// Doing something... 1
// Doing something... 5
// Doing something... 1
// Doing something... 2
// Doing something... 3
// Doing something... 4
// Doing something... 5

using System;
using System.Linq;
using System.Collections.Generic;

class Program
{
    static void Main(string[] args)
    {
        int[] numbers = { 1, 2, 3, 4, 5 };
        IEnumerable<int> result = numbers.Select(DoSomething);

        int a = result.ElementAt(0);
        int b = result.ElementAt(4);
        int c = result.Count();
    }

    static int DoSomething(int x)
    {
        Console.WriteLine("Doing something... " + x);
        return x;
    }
}

Мне кажется, что теперь я понимаю, как будет вести себя код (и я нашел в Интернете другие вопросы, которые являются результатом такого поведения).Однако, что именно точно заставляет код внутри Select() вызываться из более поздних строк?

Ответы [ 3 ]

0 голосов
/ 16 октября 2018

Давайте разберем это по частям, пока не поймем.Доверьтесь мне;не торопитесь и прочитайте это, и это станет для вас откровением в понимании Enumerable типов и ответе на ваш вопрос.

Посмотрите на интерфейс IEnumerable, который является основой IEnumerable<T>.Он содержит один метод;IEnumerator GetEnumerator();.

Перечислители - хитрое чудовище, потому что они могут делать все, что хотят.Все, что действительно имеет значение, - это вызов GetEnumerator(), который происходит автоматически в цикле foreach;или вы можете сделать это вручную.

Что делает GetEnumerator()?Он возвращает другой интерфейс, IEnumerator.

Это магия.IEnumerator имеет 1 свойство и 2 метода.

object Current { get; }
bool MoveNext();
void Reset();

Давайте разберем магию.

Сначала позвольте мне объяснить, каковы они, как правило, и я говорю обычно, потому что, как я уже упоминалможет быть хитрый зверь.Вам разрешено реализовывать это, однако вы выбираете ... Некоторые типы не соответствуют стандартам.

object Current { get; } очевидно.Получает текущий объект в IEnumerator;по умолчанию это может быть ноль.

bool MoveNext(); Возвращает true, если в IEnumerator есть другой объект, и ему следует установить значение Current для этого нового объекта.

void Reset(); указывает тип начать с начала.

Теперь давайте реализуем это.Пожалуйста, найдите время, чтобы просмотреть этот тип IEnumerator, чтобы вы поняли его.Поймите, что когда вы ссылаетесь на тип IEnumerable, вы даже не ссылаетесь на IEnumerator (это);однако вы ссылаетесь на тип, который возвращает IEnumerator через GetEnumerator()

Примечание: Будьте осторожны, чтобы не перепутать имена.IEnumerator отличается от IEnumerable.

IEnumerator

public class MyEnumerator : IEnumerator
{
    private string First => nameof(First);
    private string Second => nameof(Second);
    private string Third => nameof(Third);
    private int counter = 0;

    public object Current { get; private set; }

    public bool MoveNext()
    {
        if (counter > 2) return false;

        counter++;
        switch (counter)
        {
            case 1:
                Current = First;
                break;
            case 2:
                Current = Second;
                break;
            case 3:
                Current = Third;
                break;                    
        }
        return true;
    }

    public void Reset()
    {
        counter = 0;
    }
}

Теперь давайте создадим тип IEnumerable и используем этот IEnumerator.

IEnumerable

public class MyEnumerable : IEnumerable
{
    public IEnumerator GetEnumerator() => new MyEnumerator();
}

Это что-то, чтобы впитать ... Когда вы делаете вызов, как numbers.Select(n => n % 2 == 0 ? n : 0), вы не выполняете никаких элементов ...Вы возвращаете тип, очень похожий на приведенный выше..Select(…) возвращает IEnumerable<int>.Посмотрите выше ... IEnumerable не что иное, как интерфейс, который вызывает GetEnumerator().Это происходит всякий раз, когда вы входите в циклическую ситуацию, или это можно сделать вручную.Итак, имея это в виду, вы уже можете видеть, что итерация никогда не начинается, пока вы не вызовете GetEnumerator(), и даже тогда она никогда не начнется, пока вы не вызовете MoveNext() метод результата GetEnumerator(), который является типом IEnumerator.

Итак ...

Другими словами, у вас просто есть ссылка на IEnumerable<T> в вашем вызове и ничего более.Итераций не было.Вот почему код переходит обратно в ваш, потому что он, наконец, выполняет итерацию в методе ElementAt и затем просматривает выражение lamba.Оставайтесь со мной, и позже я обновлю пример, чтобы завершить этот урок, но сейчас давайте продолжим наш простой пример:

Давайте теперь создадим простое консольное приложение для тестирования наших новых типов.

Консольное приложение

class Program
{
    static void Main(string[] args)
    {
        var myEnumerable = new MyEnumerable();

        foreach (var item in myEnumerable)
            Console.WriteLine(item);

        Console.ReadKey();
    }

    // OUTPUT
    // First
    // Second
    // Third
}

Теперь давайте сделаем то же самое, но сделаем его общим.Я не буду так много писать, но буду внимательно следить за кодом на предмет изменений, и вы получите его.

Я собираюсь скопировать и вставить все это в одном.

Все консольное приложение

using System;
using System.Collections;
using System.Collections.Generic;

namespace Question_Answer_Console_App
{
    class Program
    {
        static void Main(string[] args)
        {
            var myEnumerable = new MyEnumerable<Person>();

            foreach (var person in myEnumerable)
                Console.WriteLine(person.Name);

            Console.ReadKey();
        }

        // OUTPUT
        // Test 0
        // Test 1
        // Test 2
    }

    public class Person
    {
        static int personCounter = 0;
        public string Name { get; } = "Test " + personCounter++;
    }

    public class MyEnumerator<T> : IEnumerator<T>
    {
        private T First { get; set; }
        private T Second { get; set; }
        private T Third { get; set; }
        private int counter = 0;

        object IEnumerator.Current => (IEnumerator<T>)Current;
        public T Current { get; private set; }

        public bool MoveNext()
        {
            if (counter > 2) return false;

            counter++;
            switch (counter)
            {
                case 1:
                    First = Activator.CreateInstance<T>();
                    Current = First;
                    break;
                case 2:
                    Second = Activator.CreateInstance<T>();
                    Current = Second;
                    break;
                case 3:
                    Third = Activator.CreateInstance<T>();
                    Current = Third;
                    break;
            }
            return true;
        }

        public void Reset()
        {
            counter = 0;
            First = default;
            Second = default;
            Third = default;
        }

        public void Dispose() => Reset();
    }

    public class MyEnumerable<T> : IEnumerable<T>
    {
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
        public IEnumerator<T> GetEnumerator() => new MyEnumerator<T>();
    }
}

Итак, давайте повторим ... IEnumerable<T> - это тип, у которого есть метод, который возвращает тип IEnumerator<T>,Тип IEnumerator<T> имеет свойство T Current { get; }, а также методы IEnumerator.

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

Консольное приложение

class Program
{
    static void Main(string[] args)
    {
        IEnumerable<Person> enumerable = new MyEnumerable<Person>();
        IEnumerator<Person> enumerator = enumerable.GetEnumerator();

        while (enumerator.MoveNext())
            Console.WriteLine(enumerator.Current.Name);

        Console.ReadKey();
    }
    // OUTPUT
    // Test 0
    // Test 1
    // Test 2
}

К вашему сведению: Одна вещь, на которую следует обратить внимание, в ответе выше есть две версии Linq.Linq в EF или Linq-to-SQL содержит методы расширения, отличные от обычных linq.Основное отличие состоит в том, что выражение запроса в Linq (при обращении к базе данных) вернет IQueryable<T>, который реализует интерфейс IQueryable, который создает выражения SQL, которые запускаются и повторяются.Другими словами ... что-то вроде предложения .Where(…) не запрашивает всю базу данных, а затем выполняет итерацию по ней.Превращает это выражение в выражение SQL.Вот почему такие вещи, как .Equals(), не будут работать в этих конкретных лямбда-выражениях.

0 голосов
/ 16 октября 2018

Сохраняет ли IEnumerable<T> функцию, которая будет вызвана позже?

Да.IEnumerable - это именно то, что он говорит.Это то, что может быть перечислено в будущем.Вы можете думать об этом как о настройке конвейера операций.

Только когда он фактически перечислен (IE вызывает foreach, .ElementAt(), ToList() и т. Д.), Что любая из этих операций на самом делевызывается.Это называется отложенное выполнение .

что именно вызывает код внутри Select () для вызова из более поздних строк?

Когда вы вызываетеSomeEnumerable.Select(SomeOperation), результатом является IEnumerable, который является объектом, представляющим тот "конвейер", который вы настроили.Реализация этого IEnumerable хранит функцию, которую вы ему передали.Фактический источник для этого (для ядра .net) здесь .Вы можете видеть, что SelectEnumerableIterator, SelectListIterator и SelectArrayIterator все имеют Func<TSource, TResult> в качестве частного поля.Здесь хранится та функция, которую вы указали для дальнейшего использования.Итераторы массива и списка просто предоставляют некоторые ярлыки, если вы знаете, что выполняете итерацию конечной коллекции.

0 голосов
/ 16 октября 2018

У вас есть ссылка на запрос LINQ, который оценивается столько раз, сколько вы его перебираете.

Из документов (вы можете видеть, что это называется Отложенное выполнение ):

Как указывалось ранее, только сама переменная запросахранит команды запроса.Фактическое выполнение запроса откладывается до тех пор, пока вы не выполните итерацию по переменной запроса в операторе foreach.Эта концепция называется отложенным выполнением

...

Поскольку переменная запроса сама по себе никогда не содержит результаты запроса, вы можете выполнять ее так часто, как захотите.Например, у вас может быть база данных, которая постоянно обновляется отдельным приложением.В вашем приложении вы можете создать один запрос, который извлекает самые последние данные, и вы можете выполнять его несколько раз с некоторым интервалом, чтобы каждый раз получать разные результаты.

Итак, когда у вас есть

IEnumerable<int> result = numbers.Select(DoSomething);

У вас есть ссылка на запрос, который преобразует каждый элемент в numbers в результат DoSomething.
Итак, вы можете сказать, что следующее:

int a = result.ElementAt(0);

повторяетсяresult до первого элемента.То же самое происходит для ElementAt(4), но в этот раз оно повторяется до пятого элемента.Обратите внимание, что вы видите только напечатанный Doing something... 5, потому что .Current оценивается один раз.Вызов не состоялся бы, если бы запрос в тот момент не мог произвести 5 элементов.
Вызов .Count снова повторяет запрос result и возвращает количество элементов в этот момент.

Если вместо сохранения ссылки на запрос вы сохранили ссылку на результаты, например:

IEnumerable<int> result = numbers.Select(DoSomething).ToArray();
// or
IEnumerable<int> result = numbers.Select(DoSomething).ToList();

Вы увидите только эти выходные данные:

// Doing something... 1
// Doing something... 2
// Doing something... 3
// Doing something... 4
// Doing something... 5
...