1

I'm making a PowerShell script and came across a weird problem (for my world view at least :)) Here's the object $Source with 1 property and some integer values:

$Source

Priority
--------
   43.37
   26.51
   23.69
    6.43

I would like to create a new variable and copy the content of $Source into it:

$ChangedSource = $Source

Okay, now i would like to change the values of $ChangedSource a bit, without affecting $Source:

$ChangedSource | % {$_.Priority = 100}

So, let's check if it worked:

$ChangedSource

Priority
--------
     100
     100
     100
     100

It worked! But let's make sure $Source hasn't been affected by this change:

$Source

Priority
--------
     100
     100
     100
     100

Wait, what?

Can someone explain to me why $Source changes if I change $ChangedSource? Is $ChangedSource nothing more but a reference to $Source? If so, how could I detach $ChangedSource from $Source?

eightcore
  • 13
  • 2

1 Answers1

5

You're right about the $ChangedSource being nothing more than a reference to the $Source object. For what you want, you could simply make a copy of the $Source object by doing

$ChangedSource = $Source | Select-Object *

Example:

$Source= [PsCustomObject]@{'Priority' = 43.37}, 
         [PsCustomObject]@{'Priority' = 26.51},
         [PsCustomObject]@{'Priority' = 23.69},
         [PsCustomObject]@{'Priority' =  6.43}


$ChangedSource = $Source | Select-Object *
$ChangedSource | ForEach-Object {$_.Priority = 100}

Write-Host '$Source' -ForegroundColor Yellow
$Source | Format-Table

Write-Host '$ChangedSource' -ForegroundColor Yellow
$ChangedSource |  Format-Table

Output:

$Source

Priority
--------
   43.37
   26.51
   23.69
    6.43


$ChangedSource

Priority
--------
     100
     100
     100
     100

This works, because the Priority values are just numbers.
However, if the $source object contains other objects, and you want to clone this into another object, you will still end up with references to the same objects inside the source and the copy. If you want to be able to manipulate the copy while keeping the source intact, you'll need to 'Deep-Clone' the source object.

For that you can use below function:

function Clone-Object ([object]$obj, [switch]$DeepClone) {
    if ($DeepClone) {
        # create a deep-clone of an object
        $ms = New-Object System.IO.MemoryStream
        $bf = New-Object System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
        $bf.Serialize($ms, $obj)
        $ms.Position = 0
        $clone = $bf.Deserialize($ms)
        $ms.Close()
    }
    else {
        # create a shallow copy of same type
        $clone = New-Object -TypeName $($obj.GetType().FullName)
        foreach ($pair in $obj.GetEnumerator()) { $clone[$pair.Key] = $pair.Value }
    }

    return $clone
}


Update

As mklement0 commented, the above function has limitations.

Here a new version that (hopefully) does a better job.

With the -DeepClone switch, the function now tries to clone the object using [System.Management.Automation.PSSerializer]::Serialize() if the source objects type attributes does not have the 'Serializable' flag set.
If that too fails, an error will be written out.

Without the -DeepClone switch, a test is done first to make sure the source object implements the IEnumerable Interface. If that is the case, it tries to create a shallow clone to return an object of the same type.

Otherwise, a copy of the object is made using $clone = $obj | Select-Object * which has the properties from the source object, but will be of a different type.

Otherwise it tries to create a shallow clone to return an object of the same type.

Please feel free to improve it.

function Clone-Object ([object]$obj, [switch]$DeepClone) {
    if ($DeepClone) {
        # create a deep-clone of an object
        # test if the object implements the IsSerializable Interface
        if ([bool]($obj.GetType().IsSerializable)) {      # or: if ([bool]($obj.GetType().Attributes -band 'Serializable')) {
            $ms = New-Object System.IO.MemoryStream
            $bf = New-Object System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
            $bf.Serialize($ms, $obj)
            $ms.Position = 0
            $clone = $bf.Deserialize($ms)
            $ms.Close()
        }
        else {
            # try PSSerializer that serializes to CliXml
            # source: https://stackoverflow.com/a/32854619/9898643
            try {
                $clixml = [System.Management.Automation.PSSerializer]::Serialize($obj, 100)
                $clone  = [System.Management.Automation.PSSerializer]::Deserialize($clixml)
            }
            catch {
                Write-Error "Could not Deep-Clone object of type $($obj.GetType().FullName)"
            }
        }
    }
    else {
        # create a shallow copy of the same type
        # if the object has a Clone() method
        if ($obj -is [System.ICloneable]) {
            $clone = $obj.Clone()
        }
        # test if the object implements the IEnumerable Interface
        elseif ($obj -is [System.Collections.IEnumerable]) {
            try {
                $clone = New-Object -TypeName $($obj.GetType().FullName) -ErrorAction Stop
                foreach ($pair in $obj.GetEnumerator()) { $clone[$pair.Key] = $pair.Value }
            }
            catch {
                Write-Error "Could not Clone object of type $($obj.GetType().FullName)"
            }        
        }
        else {
            # this returns an object with the properties copied, 
            # but it is NOT OF THE SAME TYPE as the source object
            $clone = $obj | Select-Object *
        }
    }

    return $clone
}
Theo
  • 57,719
  • 8
  • 24
  • 41
  • 1
    `+1`, you were quicker then me, some references I was about to include: [Value Types and Reference Types](https://learn.microsoft.com/dotnet/visual-basic/programming-guide/language-features/data-types/value-types-and-reference-types), [Deep copying a PSObject](https://stackoverflow.com/questions/9204829/deep-copying-a-psobject) – iRon Sep 07 '19 at 12:28
  • @iRon Thanks. Since you beat me yesterday with [this one](https://stackoverflow.com/a/57823901/9898643) I've returned the favor. – Theo Sep 07 '19 at 13:00
  • 1
    Helpful, but please update your answer to note the important constraints that apply to `Clone-Object`: _Without_ `-DeepClone`, objects can only be cloned if they (a) have a parameterless constructor and (b) if they implement the `IEnumerable` interface (honestly, these are severe limitations that call the utility of this function into question). _With_ `-DeepClone`, objects can only be cloned if their type's `.Serializable` attribute reports `$true`. – mklement0 Sep 08 '19 at 03:36
  • @mklement0 Thanks for this info. I have posted an updated version of the function that (hopefully) overcomes the issues you mentioned. – Theo Sep 08 '19 at 13:17
  • Hey Theo, I think I encountered a problem with your $obj.GetType().IsSerializable test -- if $obj is an array full of non-serializable objects, then the test returns $true. In this case, the later .Serialize() method will return an error... or at least it is for me. – Frank Lesniak Jan 22 '20 at 00:46