开发者

Differences Between PowerShell and C# when Enumerating a Collection

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 enum开发者_Go百科erating 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.


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
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜