Попередження про обробку для можливого багаторазового перерахування IEnumerable


358

У моєму коді потрібно IEnumerable<>кілька разів використовувати таким чином помилку Resharper "Можливе багаторазове перерахування IEnumerable".

Приклад коду:

public List<object> Foo(IEnumerable<object> objects)
{
    if (objects == null || !objects.Any())
        throw new ArgumentException();

    var firstObject = objects.First();
    var list = DoSomeThing(firstObject);        
    var secondList = DoSomeThingElse(objects);
    list.AddRange(secondList);

    return list;
}
  • Я можу змінити objectsпараметр, який буде, Listа потім уникнути можливого багаторазового перерахування, але тоді я не отримую найвищого об'єкта, з яким я можу працювати.
  • Інша справа , що я можу зробити , це перетворити IEnumerableв Listпочатку методу:

 public List<object> Foo(IEnumerable<object> objects)
 {
    var objectList = objects.ToList();
    // ...
 }

Але це просто незручно .

Що б ви зробили в цьому сценарії?

Відповіді:


470

Проблема з прийняттям IEnumerableпараметру полягає в тому, що він повідомляє абонентам "Я хочу це перерахувати". Це не говорить їм, скільки разів ви хочете перерахувати.

Я можу змінити параметр об'єктів на List, а потім уникнути можливого багаторазового перерахування, але тоді я не отримую найвищого об'єкта, з яким я можу працювати .

Мета зайняти найвищий об’єкт - благородна, але це залишає місце для занадто багатьох припущень. Ви дійсно хочете, щоб хтось передав LINQ запит SQL до цього методу, тільки ви перерахували його вдвічі (отримуючи потенційно різні результати щоразу?)

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

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

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

Прикро. NET не має інтерфейсу, який є IEnumerable + Count + Indexer, без методів Add / Remove etc., що, як я підозрюю, вирішив би цю проблему.


31
Чи відповідає ReadOnlyCollection <T> бажаним вимогам інтерфейсу? msdn.microsoft.com/en-us/library/ms132474.aspx
Ден перебуває у світлі вогняним світлом

73
@DanNeely Я б запропонував IReadOnlyCollection(T)(новий з .net 4.5) як найкращий інтерфейс для передачі думки про те, що це той, IEnumerable(T)який повинен бути перерахований кілька разів. Як зазначено у цій відповіді, вона IEnumerable(T)сама настільки загальна, що навіть може посилатися на неперезахисний вміст, який неможливо перерахувати знову без побічних ефектів. Але IReadOnlyCollection(T)передбачає повторну ітерабельність.
бінкі

1
Якщо ви робите це .ToList()на початку методу, спочатку потрібно перевірити, чи не параметр є нульовим. Це означає, що ви отримаєте два місця, куди ви кинете ArgumentException: якщо параметр недійсний і якщо у нього немає жодних елементів.
comecme

Я прочитав цю статтю про семантику IReadOnlyCollection<T>vs IEnumerable<T>і потім опублікував питання про використання IReadOnlyCollection<T>в якості параметра і в яких випадках. Я думаю, що висновок, до якого я зрештою дійшов, - це логіка. Що його слід використовувати там, де очікується перерахування, не обов'язково там, де може бути багаторазове перерахування.
Нео

32

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

if(objects == null) throw new ArgumentException();
using(var iter = objects.GetEnumerator()) {
    if(!iter.MoveNext()) throw new ArgumentException();

    var firstObject = iter.Current;
    var list = DoSomeThing(firstObject);  

    while(iter.MoveNext()) {
        list.Add(DoSomeThingElse(iter.Current));
    }
    return list;
}

Зауважте, я трохи змінив семантику DoSomethingElse, але це головним чином для показу непрограмованого використання. Наприклад, можна повторно загорнути ітератор. Ви також можете зробити його блоком ітератора, що може бути непогано; то немає list- і ви б yield returnдодавали елементи, як їх отримуєте, а не додавали до списку, який потрібно повернути.


4
+1 Добре, що це дуже приємний код, але я втрачаю чіткість та читаність коду.
gdoron підтримує Моніку

5
@gdoron не все досить; p Не знаю, що вище, не читається. Особливо, якщо він зроблений в ітераторний блок - досить мило, ІМО.
Марк Гравелл

Я можу просто написати: "var objectList = objects.ToList ();" і збережіть всю обробку перелік.
gdoron підтримує Моніку

10
@gdoron у запитанні, яке ви прямо сказали, ви не хотіли цього робити; p Також підхід до ToList () може не відповідати, якщо дані дуже великі (можливо, нескінченні - послідовності не потрібно бути кінцевими, але можуть ще ітерація). Зрештою, я лише представляю варіант. Тільки ви знаєте повний контекст, щоб побачити, чи є це обґрунтованим. Якщо ваші дані можна повторювати, ще одним правильним варіантом є: нічого не змінювати! Натомість ігноруйте R # ... все залежить від контексту.
Marc Gravell

1
Я думаю, що рішення Марка є досить приємним. 1. Цілком зрозуміло, як ініціалізується "список", дуже зрозуміло, як список ітералізований, і дуже ясно, над якими членами списку оперують.
Кіт Гофман

8

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

Однак у них є величезний мінус, який спричинить проблеми, якщо ви спробуєте перефактурувати код, щоб використовувати інтерфейси, наприклад, щоб зробити його більш випробуваним та привітнішим до динамічного проксі. Ключовим моментом є те, IList<T>що не успадковується відIReadOnlyList<T> інших колекцій та відповідних інтерфейсів, доступних лише для читання. (Словом, це тому, що .NET 4.5 хотів зберегти сумісність ABI з попередніми версіями. Але вони навіть не скористалися можливістю змінити це в ядрі .NET. )

Це означає, що якщо ви отримаєте IList<T>якусь частину програми і хочете передати її іншій частині, яка очікує IReadOnlyList<T>, ви не можете! Тим не менш, ви можете передати IList<T>як ідентифікаторIEnumerable<T> .

Зрештою, IEnumerable<T>це єдиний інтерфейс , доступний лише для читання, який підтримується всіма колекціями .NET, включаючи всі інтерфейси колекції. Будь-яка інша альтернатива повернеться, щоб вкусити вас, коли ви зрозумієте, що ви відключились від деяких архітектурних рішень. Тому я думаю, що це правильний тип використання у функціях підписів, щоб висловити, що ви просто хочете колекцію лише для читання.

(Зауважте, що ви завжди можете записати IReadOnlyList<T> ToReadOnly<T>(this IList<T> list)метод розширення, який простий запускає, якщо базовий тип підтримує обидва інтерфейси, але ви повинні додавати його вручну скрізь, коли рефакторинг, де як IEnumerable<T>завжди сумісний.)

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


2
+1 Зайти на стару публікацію та додати документацію про нові способи вирішення цього питання з новими можливостями платформи .NET (тобто IReadOnlyCollection <T>)
Кевін Авіньйон,

4

Якщо мета справді - запобігти численному перерахуванню, ніж відповідь Марка Гравелла - це прочитати, але зберігаючи ту саму семантику, ви можете просто зняти надлишки Anyта Firstвиклики та перейти з:

public List<object> Foo(IEnumerable<object> objects)
{
    if (objects == null)
        throw new ArgumentNullException("objects");

    var first = objects.FirstOrDefault();

    if (first == null)
        throw new ArgumentException(
            "Empty enumerable not supported.", 
            "objects");

    var list = DoSomeThing(first);  

    var secondList = DoSomeThingElse(objects);

    list.AddRange(secondList);

    return list;
}

Зауважте, що це передбачає, що ви IEnumerableне є загальним або, принаймні, обмежуєтесь бути еталонним типом.


2
objectsвсе ще передається DoSomethingElse, який повертає список, тому можливість, що ви все ще перераховуєте двічі, все ще існує. У будь-якому випадку, якщо колекція була запитом LINQ до SQL, ви двічі потрапляєте в БД.
Пол Стовелл

1
@PaulStovell, Прочитайте це: "Якщо мета справді - запобігти численному перерахуванню, ніж відповідь Марка Гравелла - це прочитати" =)
gdoron підтримує Моніку

Це було запропоновано в деяких інших публікаціях stackoverflow про викидання виключень для порожніх списків, і я пропоную це тут: навіщо кидати виняток у порожній список? Отримати порожній список, дати порожній список. Як парадигма, це досить просто. Якщо абонент не знає, що вони передають порожній список, це проблема.
Кіт Гофман

@Keith Hoffman, зразок коду підтримує таку ж поведінку, що і код, розміщений в ОП, де використання викиду виключає Firstпорожній список. Я не думаю, що можна сказати, що ніколи не кидайте виняток у порожній список або завжди кидайте виняток у порожній список. Це залежить ...
Жоао Анджело

Досить справедливий Жоао. Більше їжі для роздумів, ніж критики до вашого поста.
Кіт Гофман

3

Я зазвичай перевантажую свій метод IEnumerable та IList у цій ситуації.

public static IEnumerable<T> Method<T>( this IList<T> source ){... }

public static IEnumerable<T> Method<T>( this IEnumerable<T> source )
{
    /*input checks on source parameter here*/
    return Method( source.ToList() );
}

Я подбаю про те, щоб пояснити у зведених коментарях методи, за допомогою яких виклик IEnumerable буде виконувати .ToList ().

Програміст може вибрати .ToList () на більш високому рівні, якщо кілька операцій об'єднуються, а потім викликати перевантаження IList або дозволити моєму IEsumerable перевантаження подбати про це.


20
Це жахливо.
Майк Чемберлен

1
@MikeChamberlain Так, це дійсно жахливо і в той же час абсолютно чисто. На жаль, такий важкий сценарій потребує такого підходу.
Мауро Самп'єтро

1
Але тоді ви будете відтворювати масив кожного разу, коли передаєте його методу IEnumerable параметрів. Я погоджуюся з @MikeChamberlain в цьому плані, це жахлива пропозиція.
Krythic

2
@Krythic Якщо список не потрібно відтворювати, ви виконайте ToList деінде і використовуєте перевантаження IList. У цьому сенс цього рішення.
Мауро Самп'єтро

0

Якщо вам потрібно лише перевірити перший елемент, ви можете зазирнути на нього, не повторюючи всю колекцію:

public List<object> Foo(IEnumerable<object> objects)
{
    object firstObject;
    if (objects == null || !TryPeek(ref objects, out firstObject))
        throw new ArgumentException();

    var list = DoSomeThing(firstObject);
    var secondList = DoSomeThingElse(objects);
    list.AddRange(secondList);

    return list;
}

public static bool TryPeek<T>(ref IEnumerable<T> source, out T first)
{
    if (source == null)
        throw new ArgumentNullException(nameof(source));

    IEnumerator<T> enumerator = source.GetEnumerator();
    if (!enumerator.MoveNext())
    {
        first = default(T);
        source = Enumerable.Empty<T>();
        return false;
    }

    first = enumerator.Current;
    T firstElement = first;
    source = Iterate();
    return true;

    IEnumerable<T> Iterate()
    {
        yield return firstElement;
        using (enumerator)
        {
            while (enumerator.MoveNext())
            {
                yield return enumerator.Current;
            }
        }
    }
}

-3

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

У цьому сценарії ви можете ще більше використовувати потужні методи розширення linq.

var firstObject = objects.First();
return DoSomeThing(firstObject).Concat(DoSomeThingElse(objects).ToList();

Можна лише IEnumerableодин раз оцінити в цьому випадку деякі клопоти, але спершу профайл і побачити, чи справді це проблема.


15
Я багато працюю з IQueryable, а відключення таких попереджень дуже небезпечно.
gdoron підтримує Моніку

5
Оцінювати двічі - це погано в моїх книгах. Якщо метод займає IEnumerable, я можу спокуситись надіслати на нього LINQ запит SQL, що призводить до декількох викликів БД. Оскільки підпис методу не вказує на те, що він може перераховуватися двічі, він повинен або змінити підпис (прийняти IList / ICollection), або лише повторити
повтор

4
оптимізація ранньої та зруйнованої читабельності, а отже, можливість робити оптимізацію, яка зробить різницю пізніше, у моїй книзі значно гірше
vidstige

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

1
@hvd я не рекомендував нічого такого. Звичайно, ви не повинні перераховувати, IEnumerablesщо дає різні результати щоразу, коли ви його повторюєте, або це потребує дуже багато часу для ітерації. Дивіться відповідь Марка Гравелла - Він також в першу чергу заявляє "можливо, не хвилюйтеся". Річ у тім - багато разів, коли ви це бачите, вам нема про що турбуватися.
vidstige
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.