4

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?

3 Answers 3

4

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.

Sign up to request clarification or add additional context in comments.

4 Comments

JaredPar's answer indicates that the foreach construct does NOT create a temporary list copy.
@Greg: No, JaredPar uses an ArrayList. There's nothing in the Powershell code to indicate that the list is a List<> or ArrayList, that's an implementation detail.
I'm not familiar enough with Powershell to know: is the "foreach construct" you reference in your answer from the line foreach ($num in $intList) or the line that contains Where-Object {$_ -ne $num}. I had assumed the former.
I believe @Greg was referring to the statement "foreach evaluates the list to completion and stores the result in a temporary variable". For example, $a = 1..3; foreach( $n in $a ) { $a[-1] = -1; $n } will print out 1, 2, -1, not 1, 2, 3 as would appear from a temporary copy.
3

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.

1 Comment

Marking this as answer because it explains things fully. :-)
3

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.