iRon
iRon

Reputation: 23623

Unexpected results when reusing a custom object for the pipeline

A while ago I changed my Join-Object cmdlet which appeared to cause a bug which didn’t reveal in any of my testing.
The objective of the change was mainly code minimizing and trying to improve performance by preparing a custom PSObject and reusing this in the pipeline.
As the Join-Object cmdlet is rather complex, I have created a simplified cmdlet to show the specific issue:
(The PowerShell version is: 5.1.16299.248)

Function Test($Count) {
    $PSObject = New-Object PSObject -Property @{Name = $Null; Value = $Null}
    For ($i = 1; $i -le $Count; $i++) {
        $PSObject.Name = "Name$i"; $PSObject.Value = $i
        $PSObject
    }
}

Directly testing the output gives exactly what I expected:

Test 3 | ft

Value Name
----- ----
    1 Name1
    2 Name2
    3 Name3

Presuming that it shouldn't matter whether I assign the result to a variable (e.g. $a) or not, but it does:

$a = Test 3
$a | ft

Value Name
----- ----
    3 Name3
    3 Name3
    3 Name3

So, apart from sharing this experience, I wonder whether this is programming flaw or a PowerShell bug/quirk?

Upvotes: 2

Views: 317

Answers (1)

mklement0
mklement0

Reputation: 437111

Your original approach is indeed conceptually flawed in that you're outputting the same object multiple times, iteratively modifying its properties.

The discrepancy in output is explained by the pipeline's item-by-item processing:

  • Outputting to the console (via ft / Format-Table) prints the then-current state of $PSObject in each iteration, which gives the appearance that everything is fine.

  • Capturing in a variable, by contrast, reflects $PSObject's state after all iterations have completed, at which point it contains only the last iteration's values, Name3 and 3.


You can verify that output array $a indeed references the very same custom object three times as follows:

[object]::ReferenceEquals($a[0], $a[1]) # $True
[object]::ReferenceEquals($a[1], $a[2]) # $True

The solution is therefore to create a distinct [pscustomobject] instance in each iteration:

PSv3+ offers syntactic sugar for creating custom objects: you can cast a hashtable (literal) to [pscustomobject]. Since this also creates a new instance every time, you can use it to simplify your function:

Function Test($Count) {
  For ($i = 1; $i -le $Count; $i++) {
    [pscustomobject] @{ Name = "Name$i"; Value = $i  }
  }
}

Here's your own PSv2-compatible solution:

Function Test($Count) {
    $Properties = @{}
    For ($i = 1; $i -le $Count; $i++) {
        $Properties.Name = "Name$i"; $Properties.Value = $i
        New-Object PSObject -Property $Properties
    }
}

Upvotes: 3

Related Questions