Home > Software engineering >  Cast $PSBoundParameters to [HashTable] to enable .Clone()
Cast $PSBoundParameters to [HashTable] to enable .Clone()

Time:10-23

I have been adding $psBoundParameters to a hash table like this.

$seedTaskState = @{
   arguments = $PSBoundParameters
}

Later, I need to use those same arguments to call the next function, with one of the values changed. So I used this

$nestedTaskParameters = $seedTaskState.arguments

and then changed the one value I needed to change. Which... doesn't work because complex types are by reference so I was changing the original bound parameters, which causes all sorts of issues.

I can just initialize $nestedTaskParameters as a hash table and loop through the bound parameters and add them, like so.

$nestedTaskParameters = @{}
foreach ($key in $seedTaskState.arguments.Keys) {
    $nestedTaskParameters.Add($key, $seedTaskState.arguments.$key)
}

And I had thought that this might work, and be more elegant

$nestedTaskParameters = $seedTaskState.arguments.Clone()

but .Clone() is only available with a hashtable, not a bound parameters dictionary.

So I tried casting $seedTaskState.arguments to a hash table first, then cloning, like this.

$nestedTaskParameters = ([Hashtable]$seedTaskState.arguments).Clone()

That seems to work, but is also well outside my comfort zone, so I wonder if there is some sort of gotcha with this approach?

CodePudding user response:

Splatting works with any dictionary-like type that implements the System.Collections.Generic.IDictionary`2 .NET interface or its non-generic counterpart, System.Collections.IDictionary.

Caveat:

  • The resulting hashtable is a collection of entries that is separate from $PSBoundParameters, i.e. you can add or remove entries or assign new values to its entries independently, without affecting $PSBoundParameters.

  • However, if $PSBoundParameters entry values happen to be instances of .NET reference types, modifying these values from either dictionary affects both dictionaries, because the corresponding entries in both dictionaries "point to" (reference) the very same object instance. That is, what the [hashtable] constructor created were what are called shallow copies of the entries in $PSBoundParameters, and the same would apply to calling .Clone() on an existing hashtable.

    • If you need to avoid that, you'll have to manually create deep copies of such values, which may be nontrivial; to determine whether a given value is an instance of .NET reference type, use -not $someValue.GetType().IsValueType

    • See this answer for more information.

# Sample function.
function Foo {

  param(
    $bar,
    $baz
  )
  
  # Effectively copy the entries from $PSBoundParameters into a 
  # new hashtable.
  $hashtable = [hashtable] $PSBoundParameters

  # Add new entries to the hashtable.
  $hashtable.new1 = 'stuff1'
  $hashtable.new2 = 'stuff2'
  # Remove one.
  $hashtable.Remove('bar')

  # Modify the instance of the .NET reference type stored in the entry
  # with key 'baz'.
  # THIS AFFECTS THE ORIGINAL ENTRY IN $PSBoundParameters TOO.
  $hashtable.baz.prop = 2

  # Output the two dictionaries.
  '-- $PSBoundParameters'
  $PSBoundParameters
  "`n-- hashtable`n"
  $hashtable

}

# Call the function with an instance of a value type and a reference type.
Foo 42 ([pscustomobject] @{ prop = 1 })

The above yields the following, which shows that directly modifying the entry whose value is an instance of a .NET reference type affected both dictionaries, while adding and removing entries did not:

-- $PSBoundParameters

Key      Value
---      -----
bar         42
baz  @{prop=2}

-- hashtable

new2    stuff2
new1    stuff1
baz  @{prop=2}
  • Related