Какое преимущество было получено от реализации LINQ таким образом, чтобы не кэшировать результаты?

20

Это известная ловушка для людей, которые промокают ноги с помощью LINQ:

public class Program
{
    public static void Main()
    {
        IEnumerable<Record> originalCollection = GenerateRecords(new[] {"Jesse"});
        var newCollection = new List<Record>(originalCollection);

        Console.WriteLine(ContainTheSameSingleObject(originalCollection, newCollection));
    }

    private static IEnumerable<Record> GenerateRecords(string[] listOfNames)
    {
        return listOfNames.Select(x => new Record(Guid.NewGuid(), x));
    }

    private static bool ContainTheSameSingleObject(IEnumerable<Record>
            originalCollection, List<Record> newCollection)
    {
        return originalCollection.Count() == 1 && newCollection.Count() == 1 &&
                originalCollection.Single().Id == newCollection.Single().Id;
    }

    private class Record
    {
        public Guid Id { get; }
        public string SomeValue { get; }

        public Record(Guid id, string someValue)
        {
            Id = id;
            SomeValue = someValue;
        }
    }
}

Это выведет «False», потому что для каждого имени, предоставленного для создания исходной коллекции, функция select продолжает переоцениваться, и результирующий Recordобъект создается заново. Чтобы это исправить, ToListв конце можно добавить простой вызов GenerateRecords.

Какое преимущество Microsoft надеется получить, внедрив его таким образом?

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

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

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

В чем здесь преимущество, и почему Microsoft приняла это, казалось бы, очень обдуманное решение?

Panzercrisis
источник
1
Просто вызовите ToList () в вашем методе GenerateRecords (). return listOfNames.Select(x => new Record(Guid.NewGuid(), x)).ToList(); Это дает вам «кэшированную копию». Проблема решена.
Роберт Харви
1
Я знаю, но мне было интересно, почему они сделали это необходимым в первую очередь.
Panzercrisis
11
Поскольку ленивая оценка имеет существенные преимущества, не последним из которых является «о, кстати, эта запись изменилась с момента последнего запроса, вот новая версия», что и является тем, что иллюстрирует ваш пример кода.
Роберт Харви
Могу поклясться, что прочитал почти одинаково сформулированный вопрос здесь за последние 6 месяцев, но сейчас я его не нахожу. Самый близкий, который я могу найти, был от 2016 года на stackoverflow: stackoverflow.com/q/37437893/391656
Mr.Mindor
29
У нас есть имя для кэша без политики истечения срока действия: «утечка памяти». У нас есть имя для кэша без политики аннулирования: «ферма ошибок». Если вы не собираетесь предлагать всегда правильную политику истечения срока действия и аннулирования, которая работает для каждого возможного запроса LINQ, то ваш вопрос вроде бы отвечает сам.
Эрик Липперт

Ответы:

51

Какое преимущество было получено от реализации LINQ таким образом, чтобы не кэшировать результаты?

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

Это не имеет ничего общего с LINQ, но с IEnumerable<T>интерфейсом в целом.

Это разница между File.ReadAllLines и File.ReadLines . Один будет считывать весь файл в ОЗУ, а другой будет передавать его вам построчно, чтобы вы могли работать с большими файлами (если они имеют разрывы строк).

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

И на заметку: как вы кешируете следующее?

IEnumerable<int> AllTheZeroes()
{
    while(true) yield return 0;
}

Тебе нельзя. Вот почему IEnumerable<T>существует так, как есть.

nvoigt
источник
2
Ваш последний пример был бы более убедительным, если бы это был фактический бесконечный ряд (такой как Фибоначчи), а не просто бесконечная цепочка нулей, что не особенно интересно.
Роберт Харви
23
@RobertHarvey Это правда, я просто подумал, что легче заметить, что это бесконечный поток нулей, когда нет никакой логики для понимания.
nvoigt
2
int i=1; while(true) { i++; yield fib(i); }
Роберт Харви
2
Пример, о котором я думал, был Enumerable.Range(1,int.MaxValue)- очень легко определить нижнюю границу того, сколько памяти будет использоваться.
Крис
4
Другая вещь, которую я видел по линии, while (true) return ...состояла в том, while (true) return _random.Next();чтобы генерировать бесконечный поток случайных чисел.
Крис
24

Какое преимущество Microsoft надеется получить, внедрив его таким образом?

Корректность? Я имею в виду, что перечисляемое ядро ​​может меняться между вызовами. Кэширование приведет к неверным результатам и откроет весь вопрос «когда и как я могу сделать этот кеш недействительным?»

А если учесть , LINQ был первоначально разработан как средство , чтобы сделать LINQ к источникам данных (например , рамки сущности или SQL непосредственно), перечислимое был собирается менять , так как это то, что базы данных делают .

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

Telastyn
источник
3
Возможно, стоит упомянуть, что ICollectionсуществует и, вероятно, ведет себя так, как ожидает IEnumerableот себя
OP
Если вы используете IEnumerable <T> для чтения курсора открытой базы данных, ваши результаты не должны меняться, если вы используете базу данных с транзакциями ACID.
Даг
4

Поскольку LINQ является (и предполагалось с самого начала) универсальной реализацией шаблона Monad, популярного в функциональных языках программирования , и Monad не ограничен тем, чтобы всегда выдавать одни и те же значения при одной и той же последовательности вызовов (фактически, его использование в функциональном программировании популярен именно благодаря этому свойству, которое позволяет избежать детерминированного поведения чистых функций).

Жюль
источник
4

Другой причиной, которая не была упомянута, является возможность объединения различных фильтров и преобразований без создания промежуточных результатов.

Возьмите это к примеру:

cars.Where(c => c.Year > 2010)
.Select(c => new { c.Model, c.Year, c.Color })
.GroupBy(c => c.Year);

Если бы методы LINQ вычислили результаты немедленно, у нас было бы 3 коллекции:

  • Где результат
  • Выберите результат
  • GroupBy результат

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

Если возникла необходимость сохранить какой-либо из этих результатов, решение простое: разбить вызовы на части, вызвать .ToList()их и сохранить в переменной.


Как примечание, в JavaScript методы Array фактически возвращают результаты немедленно, что может привести к большему потреблению памяти, если не соблюдать осторожность.

Артуро Торрес Санчес
источник
3

По сути, этот код - вставка Guid.NewGuid ()внутри Selectутверждения - очень подозрительно. Это, безусловно, какой-то кодовый запах!

Теоретически, мы не обязательно ожидаем, что Selectоператор создает новые данные, но извлекает существующие данные. Несмотря на то, что для Select целесообразно объединять данные из нескольких источников для создания объединенного контента различной формы или даже для вычисления дополнительных столбцов, мы все же можем ожидать, что он будет функциональным и чистым. Помещение NewGuid ()внутрь делает его не функциональным и не чистым.

Создание данных может быть выделено отдельно от выбора и помещено в какую-либо операцию создания, так что выбор может оставаться чистым и повторно используемым, иначе выбор должен быть сделан только один раз и упакован / защищен - это это .ToList ()предложение.

Однако, чтобы быть ясным, проблема кажется мне смешиванием создания внутри выбора, а не отсутствием кэширования. Помещение NewGuid()внутри выбора кажется мне неуместным смешением моделей программирования.

Эрик Эйдт
источник
0

Отложенное выполнение позволяет тем, кто пишет код LINQ (точнее, используя IEnumerable<T>), явно выбирать, будет ли результат немедленно вычислен и сохранен в памяти или нет. Другими словами, он позволяет программистам выбирать компромисс между временем вычисления и объемом памяти, который наиболее подходит для их применения.

Можно утверждать, что большинство приложений сразу же хотят получить результаты, так что это должно было быть поведение по умолчанию LINQ. Но существует множество других API (например List<T>.ConvertAll), которые предлагают такое поведение и делают это с момента создания Framework, тогда как до появления LINQ не было никакого способа отложить выполнение. Что, как показали другие ответы, является необходимым условием для включения определенных типов вычислений, которые в противном случае были бы невозможны (исчерпав все доступное хранилище) при использовании немедленного выполнения.

Ян Кемп
источник