Учитывая следующее:
var nums = GetNums();
Console.WriteLine(nums.Any());
Console.WriteLine(string.Join(", ", nums));
static IEnumerable<int> GetNums()
{
yield return 1;
yield return 2;
yield return 3;
}
ReSharper помечает это как «Возможное множественное перечисление». Однако его запуск производит
True
1, 2, 3
доказывая, что ни один элемент не был пропущен. Всегда ли так будет при вызове Any без предиката? Или есть случаи, когда это приведет к нежелательным побочным эффектам?
Это полностью зависит от того, что поддерживает IEnumerable<T>
и как оно работает, но вам придется повторять это несколько раз. Некоторые перечислимые значения при каждом перечислении будут давать разные результаты, некоторые — нет (как в вашем примере).
Например, BlockingCollection<T>
позволяет вам получить «потребляющее перечисляемое», которое дает элементы только один раз.
Пример кода:
var blockingCollection = new BlockingCollection<int>();
blockingCollection.Add(1);
blockingCollection.Add(2);
blockingCollection.Add(3);
blockingCollection.CompleteAdding();
var nums = blockingCollection.GetConsumingEnumerable();
Console.WriteLine(nums.Any());
Console.WriteLine(string.Join(", ", nums));
Console.WriteLine(nums.Any());
Выход:
True
2, 3
False
Другая возможная проблема заключается в том, что второе перечисление nums
может выполнить (потенциально тяжелый) запрос к базе данных во второй раз, если IEnumerable<T>
представляет результаты запроса к базе данных. Это может привести к дополнительной ненужной работе, связывающей ненужные ресурсы вашего приложения и сервера базы данных.
Данные также могут меняться между запросами. Представьте себе такой сценарий:
nums.Any()
, который делает один запрос и указывает на наличие соответствующей записи.string.Join(", ", nums)
, но теперь он пуст, потому что при втором запуске запроса в базе данных нет записей.Если вы собираетесь использовать перечисляемое несколько раз или опасаетесь, что данные могут измениться между перечислениями, вы можете материализовать их в массив, список или другую коллекцию в памяти. Обратите внимание, что .ToList()
будет перебирать перечисляемое для создания списка.
Например:
var blockingCollection = new BlockingCollection<int>();
blockingCollection.Add(1);
blockingCollection.Add(2);
blockingCollection.Add(3);
blockingCollection.CompleteAdding();
var nums = blockingCollection.GetConsumingEnumerable()
.ToList(); // put the values in a list
Console.WriteLine(nums.Any());
Console.WriteLine(string.Join(", ", nums));
Console.WriteLine(nums.Any());
Выход:
True
1, 2, 3
True
Any
перечислит IEnumerable<T>
.
В этом случае GetNums
просто возвращает объект IEnumerable<int>
, метод GetEnumerator()
которого возвращает новый перечислитель, который возвращается в исходное состояние.
Вот несколько выдержек из декомпилированного кода, который генерирует https://sharplab.io/, что очень ясно дает понять, что происходит.
// GetNums returns an instance of type <GetNums>d__0, with a state of -2
internal static IEnumerable<int> GetEnums()
{
return new <GetNums>d__0(-2);
}
// After Any() consumes the first element of the enumerator, the state is no longer -2
// so GetEnumerator in <GetNums>d__0 returns 'new <GetNums>d__0(0)', which has a state of 0.
// this is as if GetEnums has not been executed at all
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
{
<>1__state = 0;
return this;
}
return new <GetNums>d__0(0);
}
Читая спецификацию, я не вижу, где указано это поведение, поэтому другая реализация вполне может отказаться от такого поведения.
Если бы GetNums
не была чистой функцией, а вместо этого yield return SomeSideEffect()
SomeSideEffect()
вызывалась бы дважды и во второй раз могла бы получить другое значение.
Чтобы показать, что Any()
действительно вызывает MoveNext
в перечислителе, вы можете написать свой собственный тип, реализующий IEnumerable<T>
и IEnumerator<T>
, а в методе GetEnumerator
вернуть this
(в отличие от того, что компилятор генерирует для блока итератора).
class OneToFive: IEnumerable<int>, IEnumerator<int> {
public IEnumerator<int> GetEnumerator() => this;
IEnumerator IEnumerable.GetEnumerator() => this;
public int Current { get; set; }
object IEnumerator.Current => Current;
public bool MoveNext() {
if (Current > 4) return false;
Current++;
return true;
}
public void Reset() { Current = 0; }
public void Dispose() {}
}
var nums = new OneToFive();
Console.WriteLine(nums.Any()); // True
Console.WriteLine(string.Join(", ", nums)); // 2, 3, 4, 5
Resharper предупреждает вас, что код до
yield return 1
будет выполнен дважды. Это может быть дорого, иметь другие побочные эффекты или даже давать разные результаты.