Яку перевагу отримав реалізація 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 прийняла це, здавалося б, дуже обдумане рішення?


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, але з the IEnumerable<T> взагалі інтерфейсом.

Це різниця між File.ReadAllLines і File.ReadLines . Один прочитає весь файл в оперативній пам’яті, а інший передасть вам його по черзі, так що ви можете працювати з великими файлами (доки вони мають перерви рядків).

Ви можете легко кешувати все, що ви хочете кешувати, матеріалізуючи свою послідовність виклику або на, .ToList()або .ToArray()на неї. Але ті з нас, хто не хоче це зробити кеш, у нас є шанс цього не зробити зробити.

І на пов'язаній замітці: як ви кешуєте наступне?

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

Ви не можете. Ось чому IEnumerable<T>існує як є.


2
Ваш останній приклад був би більш переконливим, якби це був фактично нескінченний ряд (наприклад, Fibonnaci), а не просто нескінченна нитка нулів, що не особливо цікаво.
Роберт Харві

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 безпосередньо), перелічуваних був збирається міняти , так як це те, що бази даних роблять .

Крім цього, існує єдиний Принцип відповідальності. Набагато простіше зробити якийсь код запиту, який працює і створити кешування над ним, ніж створити код, який запитує та кешує, але потім видалити кешування.


3
Можливо, варто згадати, що ICollectionіснує, і, ймовірно, веде себе так, як очікує IEnumerableсебе
ОП

Якщо ви використовуєте 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 підрахували результати одразу, у нас було б три колекції:

  • Де результат
  • Виберіть результат
  • Результат групи

З яких нас хвилює лише останній. Немає сенсу зберігати середні результати, тому що ми не маємо доступу до них, і ми хочемо знати лише про вже відфільтровані та згруповані машини за роками.

Якщо виникла потреба зберегти будь-який з цих результатів, рішення просте: розірвіть дзвінки і зателефонуйте .ToList()на них та збережіть їх у змінній.


Як бічна примітка, в JavaScript методи Array насправді повертають результати негайно, що може призвести до збільшення споживання пам'яті, якщо не бути обережним.


3

По суті, цей код - введення Guid.NewGuid ()внутрішньої Selectзаяви - є дуже підозрілим. Це, звичайно, якийсь запах коду!

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

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

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


0

Відкладене виконання дозволяє тим, хто пише LINQ-код (бути точним, використовуючи IEnumerable<T> ), чітко вибрати, чи результат буде негайно обчислено і збережено в пам'яті, чи ні. Іншими словами, це дозволяє програмістам вибирати час обчислення порівняно з обміном місця для зберігання, що найбільш відповідає їх застосуванню.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.