Будет ли LINQ Any() когда-либо выполнять перечисление без аргументов?

Учитывая следующее:

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 без предиката? Или есть случаи, когда это приведет к нежелательным побочным эффектам?

Resharper предупреждает вас, что код до yield return 1 будет выполнен дважды. Это может быть дорого, иметь другие побочные эффекты или даже давать разные результаты.

Jeremy Lakeman 23.07.2024 07:51
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
1
70
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

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

Данные также могут меняться между запросами. Представьте себе такой сценарий:

  1. Кто-то вставляет запись в вашу базу данных.
  2. Вы отмечаете nums.Any(), который делает один запрос и указывает на наличие соответствующей записи.
  3. Эта запись будет удалена.
  4. Вы вызываете 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

Другие вопросы по теме