Фільтрування циклів foreach за умови, коли умова проти продовжують захисні пропозиції


24

Я бачив, як деякі програмісти використовують це:

foreach (var item in items)
{
    if (item.Field != null)
        continue;

    if (item.State != ItemStates.Deleted)
        continue;

    // code
}

замість того, де я зазвичай використовую:

foreach (var item in items.Where(i => i.Field != null && i.State != ItemStates.Deleted))
{
    // code
}

Я навіть бачив комбінацію обох. Мені дуже подобається читати з «продовжувати», особливо при більш складних умовах. Чи є навіть різниця у продуктивності? За запитом до бази даних, я припускаю, що це буде. А як щодо звичайних списків?


3
Для звичайних списків це звучить як мікрооптимізація.
апокаліпсис

2
@zgnilec: ... але насправді, який із двох варіантів є оптимізованою версією? З цього приводу я, звичайно, маю думку, але з того, що просто подивитися на код, це не всім зрозуміло.
Док Браун

2
Звичайно, тривати буде швидше. Використання linq. Де ви створюєте додатковий ітератор.
апокаліпсис

1
@zgnilec - хороша теорія. Потрібно розмістити відповідь, пояснюючи, чому ви так вважаєте? Обидві відповіді, які існують зараз, говорять протилежне.
Бобсон

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

Відповіді:


64

Я б вважав це відповідним місцем для використання розділення команд / запитів . Наприклад:

// query
var validItems = items.Where(i => i.Field != null && i.State != ItemStates.Deleted);
// command
foreach (var item in validItems) {
    // do stuff
}

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

Під час налагодження ви можете зробити перерву раніше, foreachщоб швидко перевірити, чи відповідає вміст validItemsтому, що ви очікуєте. Вам не доведеться вступати в лямбду, якщо не потрібно. Якщо вам потрібно вступити в лямбда, то я пропоную розділити її на окрему функцію, а потім замість цього перейти.

Чи є різниця у продуктивності? Якщо запит підтримується базою даних, то версія LINQ може запускатись швидше, оскільки SQL-запит може бути більш ефективним. Якщо це LINQ для об'єктів, то ви не побачите жодної реальної різниці в продуктивності. Як завжди, профіліруйте свій код і виправляйте фактично вузькі місця, а не намагаючись передбачити оптимізації заздалегідь.


1
Чому надзвичайно великий набір даних має значення? Тільки тому, що зрештою мізерні кошти лямбдасів складуть?
BlueRaja - Danny Pflughoeft

1
@ BlueRaja-DannyPflughoeft: Так, ти маєш рацію, цей приклад не передбачає додаткової алгоритмічної складності за межами оригінального коду. Я видалив фразу.
Крістіан Хейтер

Це не призводить до двох ітерацій над колекцією? Природно, другий коротший, враховуючи, що в ньому знаходяться лише дійсні елементи, але все одно потрібно зробити це двічі, один раз відфільтрувати елементи, другий раз працювати з дійсними елементами.
Енді

1
@DavidPacker порядку № IEnumerableвезуть по foreachєдиною петлі.
Бенджамін Ходжсон

2
@DavidPacker: саме це і робиться; більшість методів LINQ to Objects реалізовані за допомогою блоків ітератора. Приклад коду вище буде повторюватися через колекцію рівно один раз, виконуючи Whereлямбда та тіло циклу (якщо лямбда повертає істину) один раз на елемент.
Крістіан Хейтер

7

Звичайно, є різниця у продуктивності, що .Where()призводить до виклику делегата для кожного окремого елемента. Однак я б зовсім не хвилювався про продуктивність:

  • Цикли тактових частот, що використовуються для виклику делегата, є незначними порівняно з циклами тактових годин, використовуваними рештою коду, який ітералізує колекцію та перевіряє умови.

  • Штраф за ефективність виклику делегата складає близько кількох тактових циклів, і, на щастя, ми вже давно минули дні, коли нам довелося турбуватися про окремі цикли годин.

Якщо з якихось причин продуктивність дійсно важлива для вас на рівні тактового циклу, використовуйте List<Item>замість цього IList<Item>, щоб компілятор міг використовувати прямі (і нездійсненні) виклики замість віртуальних викликів, і щоб ітератор List<T>, який насправді є a struct, не має бути в коробці. Але це справді дрібниці.

Запит на базу даних - це інша ситуація, оскільки існує (принаймні теоретично) можливість надсилання фільтра до RDBMS, що значно покращує продуктивність: лише відповідні рядки зроблять подорож від RDBMS до вашої програми. Але для цього я думаю, що вам доведеться використовувати linq, я не думаю, що цей вираз може бути надісланий RDBMS таким, яким він є.

Ви дійсно побачите переваги if(x) continue;моменту, коли вам доведеться налагоджувати цей код: Одноразовий перехід через if()s і continues прекрасно працює; одномоментний ступінь до фільтруючого делегата - це біль.


Тобто, коли щось не так, і ви хочете переглянути всі елементи та перевірити в налагоджувачі, які з них мають Field! = Null, а які - State! = Null; це може бути важко неможливим за допомогою пропонування ... де.
gnasher729

Хороший момент з налагодженням. Перейти до місця, де не так вже й погано у Visual Studio, але ви не можете перезаписати лямбда-вирази під час налагодження без повторної компіляції, і цього ви уникаєте при використанні if(x) continue;.
Паприк

Власне кажучи, визивається .Whereлише один раз. Те , що можна посилатися на кожній ітерації фільтра делегат (а MoveNextі Currentна лічильнику, коли вони не отримують оптимізованими)
CodesInChaos

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