Обработка предупреждения о возможном множественном перечислении IEnumerable - PullRequest
331 голосов
/ 23 ноября 2011

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

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

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

Ответы [ 7 ]

444 голосов
/ 23 ноября 2011

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

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

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

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

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

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

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

30 голосов
/ 23 ноября 2011

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

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 голосов
/ 23 июля 2018

Использование 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> всегда совместимо.)

Как всегда, это не является абсолютным, если вы пишете большой объем базы данных, где случайныймногократное перечисление будет катастрофой, вы можете предпочесть другой компромисс.

4 голосов
/ 22 июня 2015

Я обычно перегружаю свой метод 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 позаботиться об этом.

4 голосов
/ 23 ноября 2011

Если цель действительно состоит в том, чтобы предотвратить множественные перечисления, то ответ, который читает Марк Гравелл, но с сохранением той же семантики, вы можете просто удалить избыточные вызовы 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 не являются универсальными или, по крайней мере, ограничены ссылочным типом.

0 голосов
/ 07 ноября 2018

Если вам нужно проверить только первый элемент, вы можете посмотреть его, не просматривая всю коллекцию:

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;
            }
        }
    }
}
0 голосов
/ 23 ноября 2011

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...