1

I have a variable that changes it's values by me updating the values from a different variable.

I'm trying to create my own object and was experimenting with some code I found online and I stumbled on this issue and can't wrap my head on how this happens.

Preparations:

$a = "" | select First,Last #This just creates a custom object with two columns.
$b = @() #This is just an empty array to fill later on with the values of '$A'

$a.first = "Alpha"
$a.last = "Bravo"
$b += $a

$a.first = "Charlie"
$a.last = "Delta"
$b += $a

What is supposed to happen:

First   Last
-----   ----
Alpha   Bravo
Charlie Delta

But the actual results are:

First   Last
-----   ----
Charlie Delta
Charlie Delta

What is wrong here?

Ansgar Wiechers
  • 193,178
  • 25
  • 254
  • 328
Yeahish
  • 69
  • 1
  • 7
  • 4
    you are not adding COPIES of `$A` ... you are adding a reference to `$A`.[*grin*] so when it changes the _references that point to it_ will also change. either use `.Clone()` or make new objects to add to your collection. – Lee_Dailey Apr 30 '19 at 22:08
  • I wouldn't use clone personally, I'd make your own clone method within the object which constructs a new object from the values that are initialized in the old one and returns that new object. From what I've learned in my old computer science classes `.Clone()` doesn't necessarily make a deep copy... but doesn't necessarily make a shallow copy as well. – Riley Carney Apr 30 '19 at 22:11
  • @Lee_Dailey in a custom object there is no .clone method. Also can you please explain what you meant by make a new object? i'm asking because I used an custom object instead of an array to store the collection but it also stores the first object as a reference. – Yeahish Apr 30 '19 at 22:52
  • @Yeahish - i forgot that custom objects have no `.Copy()` or `.Clone()` methods. [*blush*] in that case, you will need to make a new one each time. since the `Select-Object` method is _SLOW_, you likely otta use the proper method = `[PSCustomObject]@{}`. – Lee_Dailey Apr 30 '19 at 22:58
  • @Lee_Dailey: Agreed re `[pscustomobject] @{ ... }`, but note that you _can_ shallow-clone `[pscustomobject]`, namely via `.psobject.Copy()`. – mklement0 May 01 '19 at 04:56
  • @mklement0 - ooo! thanks for the reminder [*grin*] ... i had [once again] forgotten about the hidden `.PSObject` property. – Lee_Dailey May 01 '19 at 12:51
  • @Lee_Dailey, I'm quite new to this in depth powershell, can you please explain why the select-object method to create a custom object is slower then [pscustomobject], don't they both just do the same thing? – Yeahish May 01 '19 at 13:48
  • 1
    @Yeahish - the Select-Object method has a GREAT DEAL more code involved to allow it to handle many different use cases. plus, it requires one to send things across a pipeline ... which is always slower than the more direct methods. the `PSCO` technique is simpler since it is dedicated to making objects AND it does not require one to use the pipeline. – Lee_Dailey May 01 '19 at 15:45

1 Answers1

3

As Lee_Dailey points out, you're adding references to the custom object ([pscustomobject][1]) instance stored in $a to array $b (given that [pscustomobject] is a reference type), and you're updating the same custom object, so that you ended up with:

  • 2 array elements that point to the very same object...
  • ... whose property values are the last ones you assigned.

The most immediate fix is to create a copy of $a before assigning new property values, which can be done with .psobject.Copy(), which creates a - shallow - clone:

...
# Create a copy of $a to create the next array element.
$a = $a.psobject.Copy()

$a.first = "Charlie"
$a.last = "Delta"
$b += $a

Of course, you could avoid the problem and use custom-object literals (PSv3+) instead, which creates a new instance every time:

$b = @()

$b += [pscustomobject] @{ 
  first = "Alpha"
  last = "Bravo"
}

$b += [pscustomobject] @{ 
  first = "Charlie"
  last = "Delta"
}

As an aside: growing arrays with += is inefficient, because a new array must be created behind the scenes every time; this may not be noticeable with only a few additions / iterations, but with a larger number it's better to use a list data type such as [System.Collections.Generic.List[object]]:

$b = New-Object System.Collections.Generic.List[object]

$b.Add([pscustomobject] @{ 
  first = "Alpha"
  last = "Bravo"
})

$b.Add([pscustomobject] @{ 
  first = "Charlie"
  last = "Delta"
})

[1] Custom objects in PowerShell:

[pscustomobject] (which is effectively the same as [psobject]) is PowerShell's "property bag" type that allows you to construct objects ad hoc without requiring a distinct .NET type declared ahead of time; from the perspective of .NET, a given custom object's type is always System.Management.Automation.PSCustomObject, though the specific, dynamically attached properties can differ, by design.

Select-Object (select) too outputs [pscustomobject] instances, though they report a custom type name, via PowerShell's ETS (Extended Type System), as their primary type name:

PS> ("" | select First,Last).pstypenames
Selected.System.String  # custom type name to reflect the type of the *input*
System.Management.Automation.PSCustomObject # the true type name
System.Object # the name of the base type

The above method of creating custom objects is obsolete, however, and PSv3+ supports literal [pscustomobject] @{ ... } syntax for direct construction, which has the added advantages of being able to initialize the properties as part of the same statement and being faster than Select-Object (and also New-Object):

# PSv3+ equivalent of the above 
# (except for the custom 'Selected.System.String' type name),
# allowing you to also initialize the properties.
[pscustomobject] @{
   First = $null  
   Last = $null 
}

Note: In PSv2 you could use New-Object PSCustomObject -Property @{ ... } to similar effect, but the order in which the properties are attached will typically not reflect the definition order.

Note that you can always instantiate regular .NET types in PowerShell as well, with the New-Object cmdlet or, in PSv5+, alternatively with syntax [<type>]::new([...]). You use the same methods to instantiate PSv5+ custom classes declared with the class keyword.

mklement0
  • 382,024
  • 64
  • 607
  • 775