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 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 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With