Обработка предупреждения о возможном множественном перечислении IEnumerable

359

В моем коде нужно использовать 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();
    // ...
 }

Но это просто неловко .

Что бы вы сделали в этом сценарии?

Гдорон поддерживает Монику
источник

Ответы:

471

Проблема с принятием IEnumerableв качестве параметра заключается в том, что он говорит вызывающим абонентам «Я хочу перечислить это». Это не говорит им, сколько раз вы хотите перечислить.

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

Цель взятия самого высокого объекта благородна, но она оставляет место для слишком многих предположений. Вы действительно хотите, чтобы кто-то передавал запрос LINQ to SQL этому методу, только для того, чтобы вы дважды его перечислили (каждый раз получая потенциально разные результаты?)

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

Изменив сигнатуру метода на IList/ ICollection, вы, по крайней мере, проясните для вызывающего абонента, каковы ваши ожидания, и они смогут избежать дорогостоящих ошибок.

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

Жаль, что .NET не имеет интерфейса IEnumerable + Count + Indexer, без методов Add / Remove и т. Д., Что, как я подозреваю, решит эту проблему.

Пол Стовелл
источник
32
Соответствует ли ReadOnlyCollection <T> требуемым интерфейсным требованиям? msdn.microsoft.com/en-us/library/ms132474.aspx
Дэн
74
@DanNeely Я бы предложил IReadOnlyCollection(T)(новый с .net 4.5) лучший интерфейс, чтобы передать идею о том, что он IEnumerable(T)предназначен для перечисления несколько раз. Как говорится в этом ответе, IEnumerable(T)сам по себе настолько универсален, что может даже ссылаться на невосстановимый контент, который не может быть перечислен заново без побочных эффектов. Но IReadOnlyCollection(T)подразумевает повторяемость.
Бинки
1
Если вы делаете .ToList()в начале метода, вы должны сначала проверить, не является ли параметр нулевым. Это означает, что вы получите два места, где вы генерируете ArgumentException: если параметр имеет значение null и если у него нет элементов.
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 Ну, это очень хороший код, но я теряю красивость и удобочитаемость кода.
Гдорон поддерживает Монику
5
@gdoron не все так красиво, но, пожалуй, вышеизложенное нечитаемо. Особенно, если он превращен в блок итератора - довольно мило, IMO.
Марк Гравелл
Я могу просто написать: "var objectList = objects.ToList ();" и сохраните всю обработку перечислителя.
Гдорон поддерживает Монику
10
@gdoron в вопросе, который вы прямо сказали, что на самом деле не хотите этого делать; p Кроме того, подход ToList () может не подойти, если данные очень больших (потенциально бесконечных) последовательностей не обязательно должны быть конечными, но могут еще будет повторяться). В конечном счете, я просто представляю вариант. Только вы знаете полный контекст, чтобы увидеть, если это оправдано. Если ваши данные повторяемы, другой допустимый вариант: ничего не меняйте! Вместо этого игнорируйте R # ... все зависит от контекста.
Марк Гравелл
1
Я думаю, что решение Марка довольно хорошее. 1. Очень ясно, как инициализируется «список», очень ясно, как итерируется список, и очень ясно, с какими членами списка оперируют.
Кит Хоффман
9

Использование 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 to SQL, вы дважды попадаете в БД.
Пол Стовелл
1
@PaulStovell, прочитайте это: «Если цель действительно состоит в том, чтобы предотвратить многократные перечисления, то ответ Марка Гравелла - это тот, который нужно прочитать» =)
Гдорон поддерживает Монику
В некоторых других сообщениях 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 или позволить моей перегрузке IEnumerable позаботиться об этом.

Мауро Сампьетро
источник
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;
            }
        }
    }
}
Madruel
источник
-3

Во-первых, это предупреждение не всегда так много значит. Я обычно отключал его, убедившись, что это не горлышко бутылки производительности. Это просто означает, что IEnumerableоценивается дважды, что обычно не является проблемой, если толькоevaluation само себе не занимает много времени. Даже если это займет много времени, в этом случае вы используете только один элемент в первый раз.

В этом сценарии вы также можете использовать более мощные методы расширения linq.

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

В этом случае можно оценить только IEnumerableодин раз с некоторыми трудностями, но сначала проанализировать и посмотреть, действительно ли это проблема.

vidstige
источник
15
Я много работаю с IQueryable, и отключение подобных предупреждений очень опасно.
Гдорон поддерживает Монику
5
Оценивать дважды - плохо в моих книгах. Если метод принимает IEnumerable, у меня может возникнуть соблазн передать ему запрос LINQ to SQL, что приведет к нескольким вызовам БД. Поскольку сигнатура метода не указывает, что он может перечислять дважды, он должен либо изменить сигнатуру (принять IList / ICollection), либо выполнить итерацию только один раз
Пол Стовелл,
4
оптимизация на ранних этапах и ухудшение читаемости, и, следовательно, возможность выполнения оптимизаций, делающих различия позже, намного хуже в моей книге.
Виджет
Когда у вас есть запрос к базе данных, убедитесь, что он оценен только один раз, уже имеет значение. Это не просто теоретическая будущая выгода, это измеримая реальная выгода прямо сейчас. Мало того, что оценка только один раз полезна не только для производительности, но и для правильности. Выполнение запроса к базе данных дважды может привести к разным результатам, так как кто-то мог выполнить команду обновления между двумя оценками запроса.
1
@hvd Я не рекомендовал ничего подобного. Конечно, вы не должны перечислять, IEnumerablesкоторые дают разные результаты каждый раз, когда вы выполняете итерацию, или это занимает очень много времени. См. Ответ Марка Грэйвелса - Он также заявляет, в первую очередь, «возможно, не волнуйтесь». Дело в том, что во многих случаях вы видите, что вам не о чем беспокоиться.
Виджет