Цикл очереди ForEach, выдающий InvalidOperationException

Раньше я не использовал Queues<T> в какой-либо степени, поэтому я мог упустить что-то очевидное. Я пытаюсь повторить Queue<EnemyUserControl> следующим образом (каждый кадр):

foreach (var e in qEnemy)
{
     //enemy AI code
}

Когда враг умирает, пользовательский элемент управления врагом вызывает событие, на которое я подписался, и я делаю это (первый враг в очереди удален по замыслу):

void Enemy_Killed(object sender, EventArgs e)
{      
     qEnemy.Dequeue();

     //Added TrimExcess to check if the error was caused by NULL values in the Queue (it wasn't :))
     qEnemy.TrimExcess();
}

Однако после вызова метода Dequeue я получаю InvalidOperationException в цикле foreach. Когда я вместо этого использую Peek, ошибок нет, поэтому он должен что-то делать с изменением самой очереди, поскольку Dequeue удаляет объект. Мое первоначальное предположение состоит в том, что он жалуется на то, что я изменяю коллекцию, которая повторяется Enumerator, но удаление из очереди выполняется вне цикла?

Любые идеи, что может быть причиной этой проблемы?

Спасибо


person keyboardP    schedule 04.06.2011    source источник
comment
Вы должны использовать while(queue.Any()) queue.Dequeue();   -  person Telemat    schedule 14.02.2015


Ответы (6)


Вы изменяете очередь внутри цикла foreach. Вот что вызывает исключение.
Упрощенный код для демонстрации проблемы:

var queue = new Queue<int>();
queue.Enqueue(1);
queue.Enqueue(2);

foreach (var i in queue)
{
    queue.Dequeue();
}

Возможное решение — добавить ToList(), например:

foreach (var i in queue.ToList())
{
    queue.Dequeue();
}
person Alex Aza    schedule 04.06.2011
comment
О, немного фейспалмового момента. Один из методов в коде ИИ вызывает метод Movement, который, в свою очередь, вызывает событие kill (я думал, что оно было вызвано каким-то кодом вне цикла), поэтому удаление из очереди выполняется внутри цикла. Метод ToList() работает отлично. Спасибо! - person keyboardP; 04.06.2011

Я знаю, что это старый пост, но как насчет следующего:

var queue = new Queue<int>();
queue.Enqueue(1);
queue.Enqueue(2);

while (queue.Count > 0)
{
  var val = queue.Dequeue();
}

Ваше здоровье

person DarkUrse    schedule 25.11.2013
comment
Я бы рекомендовал немного изменить это на while/do вместо do/while, чтобы вы выполняли проверку .Count перед попыткой первого .Dequeue(), если очередь пуста. - person DaveD; 04.11.2015
comment
В остальном отличный ответ! Только что отредактировал в соответствии с опасениями @DaveD. - person user919426; 14.03.2019

Старый пост, но я подумал, что дам лучший ответ:

var queue = new Queue<int>();
queue.Enqueue(1);
queue.Enqueue(2);


while (queue?.Count > 0))
{
  var val = queue.Dequeue();
}

Поскольку в исходном ответе DarkUrse использовалось do/while, и это могло вызвать исключение, если очередь пуста при попытке исключить из очереди пустую очередь, также добавлена ​​​​защита от нулевой очереди.

person JohnChris    schedule 10.07.2018
comment
queue.Count › 0 не генерирует никаких исключений, если очередь пуста. Только если он нулевой. И в этом случае queue.Any() также вызовет исключение NullReferenceException. Итак, что вы имеете в виду? - person Alexey Khrenov; 13.11.2019
comment
@AlexeyKhrenov, поэтому, если вы посмотрите на историю редактирования ответов DarkUrse, вы увидите, что раньше это было do/while, и если очередь была пустой, при попытке удаления из очереди возникало исключение. Вы в чем-то правы, поскольку мой ответ должен содержать предыдущий ответ, который я исправлял, поскольку чье-то редактирование теперь делает мой ответ недействительным... Я отредактирую его, а также добавлю защиту от нулевой очереди - person JohnChris; 15.11.2019
comment
@AlexeyKhrenov мой ответ был обновлен, чтобы лучше объяснить его первоначальное намерение ... а также добавил нулевую проверку, которую вы подняли. надеюсь все прояснил, спасибо - person JohnChris; 15.11.2019

Это типичное поведение нумераторов. Большинство перечислителей предназначены для правильной работы только в том случае, если базовая коллекция остается статической. Если коллекция изменена во время перечисления коллекции, то следующий вызов MoveNext, который вводится для вас блоком foreach, сгенерирует это исключение.

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

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

Если вы хотите придерживаться Queue, вам придется удалить из очереди каждый элемент и условно повторно поставить в очередь те элементы, которые не следует удалять. Вам по-прежнему понадобится вторая коллекция, чтобы отслеживать элементы, которые можно исключить из повторной постановки в очередь.

person Brian Gideon    schedule 04.06.2011

Вы не можете удалять элементы из коллекции во время итерации по ним.

Лучшее решение, которое я нашел, — это использовать «Список‹> toDelete» и добавить в этот список все, что вы хотите удалить. После завершения цикла foreach вы можете удалить элементы из целевой коллекции, используя ссылки в списке toDelete следующим образом:

foreach (var e in toDelete)
    target.Remove(e);
toDelete.Clear();

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

person June Rhodes    schedule 04.06.2011
comment
Вы можете просто перебрать очередь и очистить ее. В этом случае эффект будет таким же, как при использовании списка, поэтому нет смысла использовать для этого очередь (если вам не нужно исключать из очереди по отдельности). - person arni; 11.12.2014

Неважно, где вы изменяете коллекцию. Если коллекция изменяется, когда вы перечисляете ее элементы, вы получаете исключение. Вы можете использовать блокировки и убедиться, что коллекция не изменяется при повторении или, если вы используете .NET 4.0, замените Queue на ConcurrentQueue.

person Xaqron    schedule 04.06.2011