Deep copying arrays and objects in PowerShell

From Svendsen Tech PowerShell Wiki
Jump to: navigation, search

I discovered what seems like a simple and clever way of creating a deep copy of an array in PowerShell. It should definitely work on arrays containing so-called value types. I've only tested (successfully) with integers and strings (strings are reference types) in a generic array (System.Object[]).

PowerShell Objects (PSObjects) are apparently not deep copied this way, but I talk more about deep copying objects below. For creating a (sort of) deep copy of a PSObject, I found this on Stack Overflow, which uses a temporary hash, and calls the .Clone() method on it. As someone points out, the .Clone() method actually creates a shallow copy, but this can work in some cases. I mention a couple of tricks using ConvertTo-Csv, ConvertFrom-Csv, Export-CliXml, and Import-CliXml below.

The method I discovered for arrays is simply piping the current array through ForEach-Object, as I found myself able to demonstrate that this creates a deep copy of what's passed through (tested on int32 and System.String).

In case you're wondering what a deep copy is: creating a deep copy means creating a separated copy of a value, for instance an array element. By default .NET and PowerShell use reference types. If you create an array $a and create a copy with $b = $a, when you change $a, the changes are also reflected in $b. A deep copy makes sure the new item is not connected to the old item.

Contents




This is likely more easily understood through an example.

PS C:\> $Array = @('a', 'b') # Create array with two elements

PS C:\> $ArrayCopy = $Array # Regular assignment (reference) copy

PS C:\> $ArrayDeepCopy = $Array | foreach { $_ } # My array deep copy trick

PS C:\> $Array[1] = 'xxx' # Change the original array

PS C:\> $ArrayCopy[1] # Observe that the reference copy changes too
xxx

PS C:\> $ArrayDeepCopy[1] # But the deep copy did not change
b

Be aware that this method will "flatten" arrays. To preserve nesting, use a comma as a unary operator (only one argument), in front of the current object. Study the following example for further enlightenment.

PS C:\> $Array = @( @(1..3), @(4..6) )

PS C:\> $ArrayCopy = $Array

PS C:\> $ArrayCopy.Length
2

PS C:\> $Array[0] = @(7..9)

PS C:\> $Array[0]
7
8
9

PS C:\> $ArrayCopy[0] # this "regular" copy changed too
7
8
9

PS C:\> $ArrayDeepCopy = $Array | foreach { , $_ }

PS C:\> $ArrayDeepCopy.Length # multi-dimensional array preserved
2

PS C:\> $ArrayDeepCopyFlat = $Array | foreach { $_ } # no comma

PS C:\> $ArrayDeepCopyFlat.Length # flattened into 6 elements
6

PS C:\> $Array[0] = @(1..3)

PS C:\> $ArrayDeepCopy[0] # no changes despite the line above
7
8
9
PS C:\>

I also found this which mentions the [array] class' static method Copy().

This also works, as per the demonstration below, but it is a bit more work. You need to "prepare" a target array, that's of the same length as the array you're copying, and to specify the length when you call Copy().

PS C:\> $Array = @(1..3)

PS C:\> $ArrayCopy = @(1..$Array.Count) | % { $_ * 2 } # prepare

PS C:\> $ArrayCopy # different from $Array
2
4
6

PS C:\> [array]::Copy($Array, $ArrayCopy, $Array.Count)

PS C:\> $ArrayCopy # now the same as $Array
1
2
3

PS C:\> $Array[0] = 5

PS C:\> $Array[0]
5

PS C:\> $ArrayCopy[0] # unchanged, despite changing $Array
1

PSObjects

For PSObjects that don't have nested properties you can also serialize and get rid of the connection to the original object (the "$Object" variable in the example below), by converting to CSV and back via the PS cmdlets built into PSv2 and up: ConvertTo-Csv and ConvertFrom-Csv.

PS C:\temp> $Object = New-Object -TypeName PSObject -Property @{ key = 'value' }

PS C:\temp> $ObjectCopy = $Object # create a regular (reference) copy

PS C:\temp> $Object.key = 'ChangedValue' # change original value

PS C:\temp> $ObjectCopy.key # observe that the copy changes too
ChangedValue

PS C:\temp> $Object, $ObjectCopy | foreach { $_.GetType().FullName }
System.Management.Automation.PSCustomObject
System.Management.Automation.PSCustomObject

PS C:\temp> $ObjectDeepCopy = $Object | ConvertTo-Csv -NoTypeInformation | ConvertFrom-Csv

PS C:\temp> $ObjectDeepCopy.GetType().FullName
System.Management.Automation.PSCustomObject

PS C:\temp> $Object.key = "AnotherChange" # change original object

PS C:\temp> $ObjectCopy.key
AnotherChange

PS C:\temp> $ObjectDeepCopy.key # observe that deep copy does not change
ChangedValue

For complex objects, you can use Export-CliXml and Import-CliXml, with a file as an intermediary step.

PS C:\temp> $Obj = [pscustomobject] @{ key = 'array', 'of', 'values' }

PS C:\temp> $Obj

key
---
{array, of, values}

PS C:\temp> $Obj | ConvertTo-Csv | ConvertFrom-Csv # fails

key
---
System.Object[]

PS C:\temp> $Obj | Export-Clixml -LiteralPath .\serialized.xml

PS C:\temp> $ObjDeepCopy = Import-Clixml .\serialized.xml

PS C:\temp> $obj.key = 1..3

PS C:\temp> $obj.key
1
2
3

PS C:\temp> $ObjDeepCopy

key
---
{array, of, values}

PS C:\temp> $ObjDeepCopy.key
array
of
values

PS C:\temp> $ObjDeepCopy.key.Count
3

A serialization method is mentioned here, also on Stack Overflow. Currently (2016-10-09) the only answer there is that serialization. The foreach method and these other methods I described here is certainly a lot easier in some cases.

Side note and small rant: I tried answering the question above on the SO site, and spent 10 minutes writing a concise code example, but apparently I've been banned from answering. Likely because I posted 5-6 links to this site as answers without knowing it was considered bad form, in a short time span, and they were subsequently deleted by moderators, their algorithm started hating me, and now I'm stuck in limbo with only two possible (trivial and crap) answers for people to upvote, and I don't have enough reputation points to even comment (50 needed). That didn't work out so well. So, instead I wrote this article ...