Msgstr "Возможна множественная перечисление IEnumerable" vs "Параметр может быть объявлен с базовым типом"
В Resharper 5 следующий код привел к предупреждению "Параметр может быть объявлен с базовым типом" для list
:
public void DoSomething(List<string> list)
{
if (list.Any())
{
// ...
}
foreach (var item in list)
{
// ...
}
}
В Resharper 6 это не так. Однако, если я изменю метод на следующий, я все равно получаю это предупреждение:
public void DoSomething(List<string> list)
{
foreach (var item in list)
{
// ...
}
}
Причина в том, что в этой версии список перечисляется только один раз, поэтому его изменение на IEnumerable<string>
не будет автоматически вводить другое предупреждение.
Теперь, если я изменил первую версию вручную, чтобы использовать IEnumerable<string>
вместо List<string>
, я получу это предупреждение ( "Возможное множественное перечисление IEnumerable" ) в обоих случаях list
в теле метода:
public void DoSomething(IEnumerable<string> list)
{
if (list.Any()) // <- here
{
// ...
}
foreach (var item in list) // <- and here
{
// ...
}
}
Я понимаю, почему, но мне интересно, как решить это предупреждение, предполагая, что для метода действительно нужен только IEnumerable<T>
, а не List<T>
, потому что я просто хочу перечислить элементы, и я не хотите изменить список.
Добавление list = list.ToList();
в начале метода заставляет предупреждение уходить:
public void DoSomething(IEnumerable<string> list)
{
list = list.ToList();
if (list.Any())
{
// ...
}
foreach (var item in list)
{
// ...
}
}
Я понимаю, почему это заставляет предупреждение уходить, но это выглядит как взломать меня...
Любые предложения, как лучше решить это предупреждение и использовать наиболее общий тип в сигнатуре метода?
Для хорошего решения должны быть решены следующие проблемы:
- В методе нет вызова
ToList()
, поскольку он имеет влияние на производительность.
- Нет использования
ICollection<T>
или даже более специализированных интерфейсов/классов, поскольку они изменяют семантику метода, как видно из вызывающего.
- Нет многократных итераций над
IEnumerable<T>
и, следовательно, риск доступа к базе данных несколько раз или аналогичных.
Примечание. Я знаю, что это не проблема Resharper, и поэтому я не хочу подавлять это предупреждение, но исправляю основную причину, поскольку это предупреждение является законным.
UPDATE:
Пожалуйста, не волнуйте Any
и foreach
. Мне не нужна помощь в объединении этих операторов, чтобы иметь только один перечисление перечислимого.
В этом методе действительно может быть что угодно, перечисляющее перечислимое множество раз!
Ответы
Ответ 1
Вообще говоря, вам нужен какой-то объект состояния, в который вы можете PUSH элементы (внутри цикла foreach), и из которых вы получите окончательный результат.
Недостатком перечисленных операторов LINQ является то, что они активно перечисляют источник вместо того, чтобы принимать элементы, которые им нажимаются, поэтому они не соответствуют вашим требованиям.
Если вы, например, просто нужно минимальное и максимальное значения последовательности из 1'000'000 целых чисел, которые стоят времени в процессорном времени на сумму 1 000 долларов, вы в конечном итоге пишете что-то вроде этого:
public class MinMaxAggregator
{
private bool _any;
private int _min;
private int _max;
public void OnNext(int value)
{
if (!_any)
{
_min = _max = value;
_any = true;
}
else
{
if (value < _min) _min = value;
if (value > _max) _max = value;
}
}
public MinMax GetResult()
{
if (!_any) throw new InvalidOperationException("Sequence contains no elements.");
return new MinMax(_min, _max);
}
}
public static MinMax DoSomething(IEnumerable<int> source)
{
var aggr = new MinMaxAggregator();
foreach (var item in source) aggr.OnNext(item);
return aggr.GetResult();
}
Фактически, вы только что повторили логику операторов Min() и Max(). Конечно, это легко, но они являются лишь примерами для произвольной сложной логики, которую вы могли бы легко выразить в методе LINQish.
Решение пришло ко мне на вчерашней ночной прогулке: нам нужно ПОКУПАТЬ... РЕАКТИВНО! Все любимые операторы также существуют в реактивной версии, построенной для парадигмы толчка. Они могут быть привязаны по желанию к любой сложности, в которой вы нуждаетесь, так же, как их перечислимые коллеги.
Итак, пример min/max сводится к:
public static MinMax DoSomething(IEnumerable<int> source)
{
// bridge over to the observable world
var connectable = source.ToObservable(Scheduler.Immediate).Publish();
// express the desired result there (note: connectable is observed by multiple observers)
var combined = connectable.Min().CombineLatest(connectable.Max(), (min, max) => new MinMax(min, max));
// subscribe
var resultAsync = combined.GetAwaiter();
// unload the enumerable into connectable
connectable.Connect();
// pick up the result
return resultAsync.GetResult();
}
Ответ 2
Этот класс даст вам возможность разделить первый элемент с перечислением, а затем включить IEnumerable для остальной части перечисления, не давая вам двойного перечисления, что позволит избежать потенциально неприятного удара производительности. Это использование похоже на это (где T - любой тип, который вы перечисляете):
var split = new SplitFirstEnumerable(currentIEnumerable);
T firstItem = split.First;
IEnumerable<T> remaining = split.Remaining;
Вот сам класс:
/// <summary>
/// Use this class when you want to pull the first item off of an IEnumerable
/// and then enumerate over the remaining elements and you want to avoid the
/// warning about "possible double iteration of IEnumerable" AND without constructing
/// a list or other duplicate data structure of the enumerable. You construct
/// this class from your existing IEnumerable and then use its First and
/// Remaining properties for your algorithm.
/// </summary>
/// <typeparam name="T">The type of item you are iterating over; there are no
/// "where" restrictions on this type.</typeparam>
public class SplitFirstEnumerable<T>
{
private readonly IEnumerator<T> _enumerator;
/// <summary>
/// Constructor
/// </summary>
/// <remarks>Will throw an exception if there are zero items in enumerable or
/// if the enumerable is already advanced past the last element.</remarks>
/// <param name="enumerable">The enumerable that you want to split</param>
public SplitFirstEnumerable(IEnumerable<T> enumerable)
{
_enumerator = enumerable.GetEnumerator();
if (_enumerator.MoveNext())
{
First = _enumerator.Current;
}
else
{
throw new ArgumentException("Parameter 'enumerable' must have at least 1 element to be split.");
}
}
/// <summary>
/// The first item of the original enumeration, equivalent to calling
/// enumerable.First().
/// </summary>
public T First { get; private set; }
/// <summary>
/// The items of the original enumeration minus the first, equivalent to calling
/// enumerable.Skip(1).
/// </summary>
public IEnumerable<T> Remaining
{
get
{
while (_enumerator.MoveNext())
{
yield return _enumerator.Current;
}
}
}
}
Это предполагает, что IEnumerable имеет хотя бы один элемент для запуска. Если вы хотите больше выполнить настройку типа FirstOrDefault, вам нужно поймать исключение, которое иначе было бы создано в конструкторе.
Ответ 3
Вероятно, вы должны принять IEnumerable<T>
и игнорировать предупреждение с несколькими итерациями.
Это сообщение предупреждает вас, что если вы передадите свой метод lazy (например, итератор или дорогостоящий запрос LINQ), части итератора будут выполняться дважды.
Ответ 4
Нет идеального решения, выберите один в соответствии с ситуацией.
- enumerable.ToList, вы можете оптимизировать его, сначала попробовав "перечислимый как List", если вы не изменяете список
- Повторяйте два раза над IEnumerable, но очистите его для вызывающего (document it)
- Разделить двумя способами
- Возьмите список, чтобы избежать стоимости "как" /ToList и потенциальной стоимости двойного перечисления.
Первое решение (ToList), вероятно, является наиболее "правильным" для общедоступного метода, который может работать с любым Enumerable.
Вы можете игнорировать проблемы Resharper, предупреждение является законным в общем случае, но может быть неправильным в вашей конкретной ситуации. Особенно, если метод предназначен для внутреннего использования, и у вас есть полный контроль над абонентами.
Ответ 5
Существует общее решение для обращения к предупреждениям Resharper: отсутствие гарантии повторяемости IEnumerable и базового класса List (или потенциально дорогого метода ToList()).
Создайте специализированный класс, I.E "RepeatableEnumerable", реализующий IEnumerable, с "GetEnumerator()", реализованный со следующим логическим контуром:
-
Допустим все элементы, уже собранные до сих пор из внутреннего списка.
-
Если обернутый перечислитель имеет больше элементов,
-
Отметьте внутренний счетчик как не имеющий больше элементов.
Добавьте методы расширения и соответствующие оптимизации, где завернутый параметр уже повторяется. Resharper больше не будет помечать указанные предупреждения в следующем коде:
public void DoSomething(IEnumerable<string> list)
{
var repeatable = list.ToRepeatableEnumeration();
if (repeatable.Any()) // <- no warning here anymore.
// Further, this will read at most one item from list. A
// query (SQL LINQ) with a 10,000 items, returning one item per second
// will pass this block in 1 second, unlike the ToList() solution / hack.
{
// ...
}
foreach (var item in repeatable) // <- and no warning here anymore, either.
// Further, this will read in lazy fashion. In the 10,000 item, one
// per second, query scenario, this loop will process the first item immediately
// (because it was read already for Any() above), and then proceed to
// process one item every second.
{
// ...
}
}
С небольшой работой вы также можете включить функцию RepeatableEnumerable в LazyList, полную реализацию IList. Однако это выходит за рамки этой конкретной проблемы.:)
UPDATE: реализация кода, запрошенная в комментариях, - не знаю, почему исходного PDL недостаточно, но в любом случае следующее верно реализует предложенный мной алгоритм (моя собственная реализация реализует полный интерфейс IList, это немного вне рамки, которую я хочу выпустить здесь...:))
public class RepeatableEnumerable<T> : IEnumerable<T>
{
readonly List<T> innerList;
IEnumerator<T> innerEnumerator;
public RepeatableEnumerable( IEnumerator<T> innerEnumerator )
{
this.innerList = new List<T>();
this.innerEnumerator = innerEnumerator;
}
public IEnumerator<T> GetEnumerator()
{
// 1. Yield all items already collected so far from the inner list.
foreach( var item in innerList ) yield return item;
// 2. If the wrapped enumerator has more items
if( innerEnumerator != null )
{
// 2A. while the wrapped enumerator can move to the next item
while( innerEnumerator.MoveNext() )
{
// 1. Get the current item from the inner enumerator.
var item = innerEnumerator.Current;
// 2. Add the current item to the inner list.
innerList.Add( item );
// 3. Yield the current item
yield return item;
}
// 3. Mark the inner enumerator as having no more items.
innerEnumerator.Dispose();
innerEnumerator = null;
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
// Add extension methods and appropriate optimizations where the wrapped parameter is already repeatable.
public static class RepeatableEnumerableExtensions
{
public static RepeatableEnumerable<T> ToRepeatableEnumerable<T>( this IEnumerable<T> items )
{
var result = ( items as RepeatableEnumerable<T> )
?? new RepeatableEnumerable<T>( items.GetEnumerator() );
return result;
}
}
Ответ 6
Я понимаю, что этот вопрос старый и уже отмечен как ответ, но я был удивлен, что никто не предлагал вручную перебирать перечислитель:
// NOTE: list is of type IEnumerable<T>.
// The name was taken from the OP code.
var enumerator = list.GetEnumerator();
if (enumerator.MoveNext())
{
// Run your list.Any() logic here
...
do
{
var item = enumerator.Current;
// Run your foreach (var item in list) logic here
...
} while (enumerator.MoveNext());
}
Кажется намного более простым, чем другие ответы здесь.
Ответ 7
Почему бы и нет:
bool any;
foreach (var item in list)
{
any = true;
// ...
}
if(any)
{
//...
}
Обновление: Лично я бы не стал резко изменять код, чтобы обойти предупреждение, подобное этому. Я бы просто отключил предупреждение и продолжил. Предупреждение предлагает вам изменить общий поток кода, чтобы он стал лучше; если вы не улучшаете код (и, возможно, ухудшаете его), чтобы устранить это предупреждение; то точка предупреждения будет пропущена.
Например:
// ReSharper disable PossibleMultipleEnumeration
public void DoSomething(IEnumerable<string> list)
{
if (list.Any()) // <- here
{
// ...
}
foreach (var item in list) // <- and here
{
// ...
}
}
// ReSharper restore PossibleMultipleEnumeration
Ответ 8
UIMS * - По сути, нет большой проблемы. IEnumerable < Т > была "самой простой вещью, которая представляет собой кучу вещей того же типа, поэтому использовать ее в методе sigs правильно". Теперь он стал "вещью, которая может оцениваться за кулисами, и может занять некоторое время, поэтому теперь вам всегда нужно беспокоиться об этом".
Как будто IDictionary неожиданно был расширен для поддержки ленивой загрузки значений через свойство LazyLoader типа Func < TKey, TValue > . На самом деле это было бы аккуратно, но не так аккуратно, чтобы быть добавленным в IDictionary, потому что теперь каждый раз, когда мы получаем IDictionary, мы должны беспокоиться об этом. Но это где мы.
Таким образом, казалось бы, что "если метод принимает IEnumerable и анализирует его дважды, всегда усиливайте eval через ToList(), это лучшее, что вы можете сделать. И приятная работа Jetbrains, чтобы дать нам это предупреждение.
* (Если я не пропустил что-то... только что сделал, но это кажется полезным)
Ответ 9
Будьте осторожны при принятии перечислений в вашем методе. "Предупреждение" для базового типа - это только подсказка, предупреждение о перечислении является истинным предупреждением.
Однако ваш список будет перечислить как минимум два раза, потому что вы делаете что-то, а затем - foreach. Если вы добавите ToList()
, ваш перечисление будет перечислить три раза - удалите ToList().
Я бы предложил установить подсказки предупреждения для базового типа для подсказки. Таким образом, у вас все еще есть подсказка (зеленая подчеркивание) и возможность ее быстрого ее устранения (alt + enter) и никаких "предупреждений" в вашем файле.
Вам следует позаботиться, чтобы перечисление IEnumerable было дорогостоящим действием, таким как загрузка чего-либо из файла или базы данных, или если у вас есть метод, который вычисляет значения и использует возврат доходности. В этом случае сначала ToList()
или ToArray()
загружать/вычислять все данные только ONCE.
Ответ 10
Вы можете использовать ICollection<T>
(или IList<T>
). Он менее специфичен, чем List<T>
, но не страдает от проблемы множественного перечисления.
Тем не менее, я бы использовал IEnumerable<T>
в этом случае. Вы также можете рассмотреть возможность реорганизации кода для перечисления только один раз.
Ответ 11
Используйте IList как ваш тип параметра, а не IEnumerable. IEnumerable имеет другую семантику для List, тогда как IList имеет тот же
IEnumerable может быть основан на потоке, недоступном для поиска, поэтому вы получаете предупреждения
Ответ 12
Вы можете выполнять итерацию только один раз:
public void DoSomething(IEnumerable<string> list)
{
bool isFirstItem = true;
foreach (var item in list)
{
if (isFirstItem)
{
isFirstItem = false;
// ...
}
// ...
}
}
Ответ 13
Есть что-то, о чем никто раньше не говорил (@Zebi). Any() уже итерации пытается найти элемент. Если вы вызываете ToList(), он также будет выполнять итерацию, чтобы создать список. Первоначальная идея использования IEnumerable заключается только в повторении, что-то еще вызывает итерацию для выполнения. Вы должны попробовать, внутри одного цикла, сделать все.
И включите в него свой метод .Any().
если вы передадите список действий в своем методе, у вас будет очищенное повторение после кода
public void DoSomething(IEnumerable<string> list, params Action<string>[] actions)
{
foreach (var item in list)
{
for(int i =0; i < actions.Count; i++)
{
actions[i](item);
}
}
}