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

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

Но это просто неудобно.

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

Ответ 1

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

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

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

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

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

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

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

Ответ 2

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

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

Ответ 3

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

Ответ 4

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

Ответ 5

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

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

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

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