Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 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?

like image 823
Sameer Singh Avatar asked Dec 29 '22 04:12

Sameer Singh


2 Answers

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.

like image 59
Sean Avatar answered Dec 30 '22 17:12

Sean


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.

like image 23
Roman Kuzmin Avatar answered Dec 30 '22 17:12

Roman Kuzmin