Reputation: 19
enter code here
I have a powershell script with an arrary formed like this:
$trace = [PSCustomObject]@{
number = -1
times = @()
address = ""
ip = ""
}
This is in a loop so there are numerous values assigned as above. Then, a larger array is formed:
$traces += $trace
Later, in the script, I see that $traces.number contents have unexpectedly changed. So I output its contents in a series using
Write-Host "200 traces.number" $traces.number
This results in
200 traces.number 1 2 3 4 5 6 7 8 9 10
200 traces.number 1 2 3 4 5 6 7 8 9 10
200 traces.number 1 2 3 4 5 6 6 7 8 9
200 traces.number 1 2 3 4 5 6 6 7 8 9
The size of the array remains at 10 but the value "6" is repeated. The line of code preceding this change is:
for ($j3 = 0; $j3 -lt $routecountless1; $j3++) { $route3[$j3].number = $j3 + 1 }
$route3 is an entirely different array so it seems there would be no action on $traces. There are 5 other lines of code that do the same thing on $route1 ... $route6 and nothing like this appears to be happening with any of those. The indexing variables are all different $j1, $j2....
It looks like the $traces.number array is being overwritten. But how to figure that out and how to prevent?
Upvotes: -1
Views: 369
Reputation: 437062
$route3
is an entirely different array so it seems there would be no action on$traces
.
In .NET, which PowerShell is built on, it comes down to reference types vs. value types; [string]
is an exception in that is treated like a value type, even though it is technically a reference type.
[pscustomobject]
is a reference type, so any number of variables / array elements may "point" (store a reference to) to a given, single instance of this type, so that updates to this instance are invariably reflected in all those variables / array elements.
For example:
# Create a (single-element) array that contains (references)
# a [pscustomobject] instance.
$trace = , [pscustomobject] @{ number = 42 }
# Create another array that references the *same* [pscustomobject] instance.
$route3 = , $trace[0]
# Updating the .number property of the [pscustomobject] instance
# affects *both* arrays.
$route3[0].number = 43
# Print the updated [pscustomobject] instance referenced from both arrays.
# -> 43, 43
$trace[0].number, $route3[0].number
how to figure that out?
To test if a given value is an instance of a reference type, use the negated return value of the type's .IsValueType
property; e.g. (building on the above):
# $trace[0] contains a [pscustomobject] instance.
$trace[0].GetType().IsValueType # -> $false -> is reference-type instance
To test if two values reference the very same instance of a reference type, use [object]::ReferenceEquals()
:
[object]::ReferenceEquals($trace[0], $route3[0]) # -> $true
how to prevent?
In order to get an independent copy of a reference-type instance, you must clone it, i.e. create a new instance of it that contains the same data.
The challenge is that there is no universal cloning mechanism, especially if the cloning must be deep (recursive) in order to ensure that nested values are independent copies too.
Copying an instance's members as-is (member-wise cloning, a.k.a shallow cloning) only creates independent values for those members that happen to contain value-type instances. That is, those properties in the original object that contain (reference) reference-type instances will have the clone's properties reference the very same instances.
While .NET does provide an ICloneable
interface, its behavior is ill-specified (shallow vs. deep cloning, cloning all members as-is vs. selective modifications) and even not recommended for use in public APIs.
The root of .NET's type hierarchy, System.Object
, has a protected (i.e., non-public) .MemberWiseClone()
method, which facilitates member-wise cloning, which classes can take advantage of if they choose to publicly expose cloning functionality. The linked documentation page contains a more detailed discussion of shallow vs. deep cloning.
In practice, two .NET types frequently used in PowerShell offer methods that perform shallow cloning:
[pscustomobject]
(a.k.a [psobject]
) has a .Copy()
instance method, which must be accessed via the intrinsic .psobject
property.
[array]
has a .Clone()
method (which is therefore available on arrays of any type, including the [object[]]
arrays PowerShell constructs by default).
[object[]]
array is to use @(...)
, the array-subexpression operator - that is, it enumerates the elements of a given collection and collects them in an [object[]]
array.A modified version of the example above with a manually cloned [pscustomobject]
instance:
# Create a (single-element) array that contains (references) a
# [pscustomobject] instance.
$trace = , [pscustomobject] @{ number = 42 }
# Create another array that references
# a *shallow clone* of that [pscustomobject] instance.
$route3 = , $trace[0].psobject.Copy()
# Updating the .number property of the [pscustomobject] instance
# in the other array now only affects that array.
$route3[0].number = 43
# Print the updated [pscustomobject] instance referenced from both arrays,
# which are now DIFFERENT numbers.
# -> 42, 43
$trace[0].number, $route3[0].number
A limited deep-cloning implementation for [pscustomobject]
graphs can be found in this answer:
It limits deep-cloning to [pscustomobject]
instances and arrays / collections therefore (if a collection isn't an array, it is cloned as an array). This is sufficient for object graphs returned from ConvertFrom-Json
, for instance, but not for arbitrary object graphs containing instances of other reference types.
The linked helper function, Copy-PSCustomObject
, when invoked with -Deep
, automatically provides the functionality shown in this simplified example:
$obj = [pscustomobject] @{ numProp = 42; arrayProp = 'foo', 'bar' }
# Create a *shallow* clone, using .psobject.Copy()
# !! This alone is NOT enough, because .arrayProp in the
# !! copy points to the very same array as $obj.arrayProp.
# !! You would see this if you did $obj.arrayProp[0] = 'new', for instance.
$copy = $obj.psobject.Copy()
# Now make the copy a *deep* clone by (shallow-)cloning the array.
# !! *Shallow*-cloning the array is enough in *this case*, because
# !! all array elements are *strings*, which are treated like value types.
# !! If (other) reference-type instances were among the elements,
# !! *recursion* would be needed, which Copy-PSCustomObject -Deep
# !! does, but only with respect to [pscustomobject] and nested collections.
# !! Now, $obj.arrayProp[0] = 'new2' wouldn't affect $copy.arrayProp[0] anymore.
$copy.arrayProp = @($obj.arrayProp)
Upvotes: 1