Here is a simple scenario in C#:
var intList = new List<int>();
intList.Add(4);
intList.Add(7);
intList.Add(2);
intList.Add(9);
intList.Add(6);
foreach (var num in intList)
{
if (num == 9)
{
intList.Remove(num);
Console.WriteLine("Removed item: " + num);
}
Console.WriteLine("Number is: " + num);
}
This throws an InvalidOperationException
because I am modifying the collection while enumerating it.
Now consider similar PowerShell code:
$intList = 4, 7, 2, 9, 6
foreach ($num in $intList)
{
if ($num -eq 9)
{
$intList = @($intList | Where-Object {$_ -ne $num})
Write-Host "Removed item: " $num
}
Write-Host "Number is: " $num
}
Write-Host $intList
This script actually removes the number 9 from the list! No exceptions thrown.
Now, I know the C# example uses a List object while the PowerShell example uses an array, but how does PowerShell enumerate a collection that will be modified during the loop?
The answer is already given by @Sean, I am just providing the code which shows that the original collection is not changed during foreach
: it enumerates through the original collection and there is no contradiction therefore.
# original array
$intList = 4, 7, 2, 9, 6
# make another reference to be used for watching of $intList replacement
$anotherReferenceToOriginal = $intList
# prove this: it is not a copy, it is a reference to the original:
# change [0] in the original, see the change through its reference
$intList[0] = 5
$anotherReferenceToOriginal[0] # it is 5, not 4
# foreach internally calls GetEnumerator() on $intList once;
# this enumerator is for the array, not the variable $intList
foreach ($num in $intList)
{
[object]::ReferenceEquals($anotherReferenceToOriginal, $intList)
if ($num -eq 9)
{
# this creates another array and $intList after assignment just contains
# a reference to this new array, the original is not changed, see later;
# this does not affect the loop enumerator and its collection
$intList = @($intList | Where-Object {$_ -ne $num})
Write-Host "Removed item: " $num
[object]::ReferenceEquals($anotherReferenceToOriginal, $intList)
}
Write-Host "Number is: " $num
}
# this is a new array, not the original
Write-Host $intList
# this is the original, it is not changed
Write-Host $anotherReferenceToOriginal
Output:
5
True
Number is: 5
True
Number is: 7
True
Number is: 2
True
Removed item: 9
False
Number is: 9
False
Number is: 6
5 7 2 6
5 7 2 9 6
We can see that $intList
is changed when we "remove an item". It only means that this variable now contains a reference to a new array, it is the variable changed, not the array. The loop continues enumeration of the original array which is not changed and $anotherReferenceToOriginal
still contains a reference to it.
The foreach construct evaluates the list to completion and stores the result in a temporary variable before it starts iterating over it. When you do that actual removal you are updating $intList to reference a new list. In other words in actually doing something like this under the hood:
$intList = 4, 7, 2, 9, 6
$tempList=$intList
foreach ($num in $tempList)
{
if ($num -eq 9)
{
$intList = @($intList | Where-Object {$_ -ne $num})
Write-Host "Removed item: " $num
}
Write-Host "Number is: " $num
}
Write-Host $intList
Your call to:
$intList = @($intList | Where-Object {$_ -ne $num})
Actually creates a completely new list with the value removed.
If you change the removal logic to remove the last item in the list (6) then I think you'll find that it's still printed even though you think it's removed because of the temporary copy.
The problem here is that you're not comparing equivalent code samples. In the Powershell sample you are creating a new list vs modifying the list in place as is done in the C# sample. Here is a sample which is closer in functionality to the original C# one
$intList = new-object System.Collections.ArrayList
$intList.Add(4)
$intList.Add(7)
$intList.Add(2)
$intList.Add(9)
$intList.Add(6)
foreach ($num in $intList) {
if ($num -eq 9) {
$intList.Remove($num)
Write-Host "Removed item: " $num
}
Write-Host "Number is: " $num
}
Write-Host $intList
And when run it produces the same error
Number is: 4
Number is: 7
Number is: 2
Removed item: 9
Number is: 9
An error occurred while enumerating through a collection: Collection was modifi
ed; enumeration operation may not execute..
At C:\Users\jaredpar\temp\test.ps1:10 char:8
+ foreach <<<< ($num in $intList)
+ CategoryInfo : InvalidOperation: (System.Collecti...numeratorSi
mple:ArrayListEnumeratorSimple) [], RuntimeException
+ FullyQualifiedErrorId : BadEnumeration
4 7 2 6