Чи лінк ефективніший, ніж на поверхні?


13

Якщо я напишу щось подібне:

var things = mythings
    .Where(x => x.IsSomeValue)
    .Where(y => y.IsSomeOtherValue)

Це те саме, що:

var results1 = new List<Thing>();
foreach(var t in mythings)
    if(t.IsSomeValue)
        results1.Add(t);

var results2 = new List<Thing>();
foreach(var t in results1)
    if(t.IsSomeOtherValue)
        results2.Add(t);

Або є якась магія під обкладинками, яка працює приблизно так:

var results = new List<Thing>();
foreach(var t in mythings)
    if(t.IsSomeValue && t.IsSomeOtherValue)
        results.Add(t);

Або взагалі щось зовсім інше?


4
Ви можете переглянути це в ILSpy.
ChaosPandion

1
Це більше схоже на другий приклад, ніж перший, але другий відповідь ChaosPandion про те, що ILSpy - твій друг.
Майкл

Відповіді:


27

LINQ-запити ледачі . Це означає код:

var things = mythings
    .Where(x => x.IsSomeValue)
    .Where(y => y.IsSomeOtherValue);

робить дуже мало. Оригінальний перелічувальний ( mythings) перераховується лише тоді, коли отримане число ( things) споживається, наприклад, foreachциклом .ToList(), або .ToArray().

Якщо ви зателефонували things.ToList(), він приблизно еквівалентний вашому останньому коду, маючи, можливо, деякі (зазвичай незначні) накладні витрати від перелічників.

Так само, якщо ви використовуєте цикл foreach:

foreach (var t in things)
    DoSomething(t);

Він за характеристиками схожий на:

foreach (var t in mythings)
    if (t.IsSomeValue && t.IsSomeOtherValue)
        DoSomething(t);

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

Якщо нумерація перерахована лише частково, це особливо важливо. Розглянемо цей код:

things.First();

Те, як реалізується LINQ, mythingsбуде перераховано лише до першого елемента, який відповідає вашим умовам. Якщо цей елемент на початку списку, це може бути величезним підвищенням продуктивності (наприклад, O (1) замість O (n)).


1
Одна різниця в продуктивності між LINQ і еквівалентним кодом, що використовується, foreachполягає в тому, що LINQ використовує виклики делегата, які мають деякі накладні витрати. Це може бути суттєво, коли умови виконуються дуже швидко (що вони часто роблять).
svick

2
Ось що я мав на увазі під перерахунком накладних витрат. Це може бути проблемою в деяких (рідкісних) випадках, але, на мій досвід, це не дуже часто - зазвичай час, який потрібно, дуже малий для початку, або він значно переважає інші операції, які ви робите.
Блакитна риба

Потворне обмеження лінивої оцінки Лінка полягає в тому, що немає способу зробити «знімок» перерахування, крім методів, таких як ToListабо ToArray. Якби така річ була належним чином вбудована IEnumerable, можна було б попросити список "зробити знімок" будь-яких аспектів, які можуть змінитися в майбутньому без необхідності створювати все.
supercat

7

Наступний код:

var things = mythings
    .Where(x => x.IsSomeValue)
    .Where(y => y.IsSomeOtherValue);

Еквівалент ні до чого, через ледачу оцінку нічого не станеться.

var things = mythings
    .Where(x => x.IsSomeValue)
    .Where(y => y.IsSomeOtherValue)
    .ToList();

Інакше, тому що оцінка буде запущена.

Кожен пункт mythingsбуде наданий першому Where. Якщо він пройде, він буде відданий другому Where. Якщо він пройде, він буде частиною виводу.

Так це виглядає приблизно так:

var results = new List<Thing>();
foreach(var t in mythings)
{
    if(t.IsSomeValue)
    {
        if(t.IsSomeOtherValue)
        {
            results.Add(t);
        }
    }
}

7

Відкладене виконання убік (що інші відповіді вже пояснюють, я лише зазначу ще одну деталь), це більше схоже на ваш другий приклад.

Давайте просто уявіть , що ви дзвоните ToListна things.

Реалізація Enumerable.Whereприбутку a Enumerable.WhereListIterator. Коли ви зателефонуєте Whereна це WhereListIterator(він же ланцюжок- Whereдзвінки), ви більше не дзвоните Enumerable.Where, але Enumerable.WhereListIterator.Where, що фактично поєднує предикати (використовуючи Enumerable.CombinePredicates).

Так це більше схоже if(t.IsSomeValue && t.IsSomeOtherValue).


"повертає Enumerable.WhereListIterator" змусив мене натиснути. Можливо, дуже проста концепція, але це те, що я не помітив у ILSpy. Спасибі
ConditionRacer

Дивіться про реалізацію цієї оптимізації Джоном Скітом, якщо вас цікавить більш глибокий аналіз.
Сервіс

1

Ні, це не те саме. У вашому прикладі things- це an IEnumerable, який на даний момент є лише ітератором, а не фактичним масивом чи списком. Більше того, оскільки thingsне використовується, цикл ніколи навіть не оцінюється. Цей тип IEnumerableдозволяє перебирати елементи yield, введені вказівками Linq, і обробляти їх далі додатковими інструкціями, що означає, що зрештою у вас є лише один цикл.

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

Дивіться це пов’язане питання ТА: /programming/2789389/how-do-i-implement-ienumerable

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