Unexpected results when reusing a custom object fo

2019-07-09 05:07发布

问题:

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?

回答1:

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