Matthew
Matthew

Reputation: 1166

PowerShell Function parameters - by reference or by value?

So, I tried looking up the answer to this question, and found the generally available answer is that PowerShell passes parameters by value. These generally accepted solutions all post sample code to prove their assertions, similar to the following:

Function add1 ($parameter)
{
    Write-Host "    In Function: `$parameter = $parameter"
    Write-Host "    In Function: `$parameter += 1"
    $parameter += 1
    Write-Host "    In Function: `$parameter = $parameter"
}

cls
$a = 1
Write-Host "Before function: `$a = $a"
add1 $a
Write-Host " After function: `$a = $a"

This gives the results:

Before function: Run Command: $a = 1
    In Function: $parameter: 1
    In Function: Run Command: $parameter += 1
    In Function: $parameter: 2
 After function: $a: 1

Thus proving that parameters are passed by value, right? Well, I was having a heck of a time troubleshooting a function I was writing. The function added a couple of additional NoteProperty items to a PSCustomObject I pass in to the function, and my program would throw all sorts of errors saying that the NoteProperty already existed, even though I had not modified the original object in the parent scope, only inside the function.

So, I set up a version of the above code to test using parameter of type [PSCustomObject], like so:

Function F1($Obj)
{
    'Function F1: Run command: $Obj.FirstValue = 11'
    $Obj.FirstValue = 11
    "             `$Obj.Name: $($StartObject.Name)"
    "             `$Obj.FirstValue: $($StartObject.FirstValue)"
    "             `$Obj.SecondValue: $($StartObject.SecondValue)"
}

Function F2($Obj)
{
    'Function F2: Run command: $Obj | Add-Member -MemberType NoteProperty -Name SecondValue -Value 33'
    $obj | Add-Member -MemberType NoteProperty -Name SecondValue -Value 33
    "             `$Obj.Name: $($StartObject.Name)"
    "             `$Obj.FirstValue: $($StartObject.FirstValue)"
    "             `$Obj.SecondValue: $($StartObject.SecondValue)"
}

cls
Remove-Variable StartObject
"Main script: Run command: `$StartObject = [PSCustomObject]@{Name='Original';FirstValue=22}"
$StartObject = [PSCustomObject]@{Name='Original';FirstValue=22}
"             `$StartObject.Name: $($StartObject.Name)"
"             `$StartObject.FirstValue: $($StartObject.FirstValue)"
"             `$StartObject.SecondValue: $($StartObject.SecondValue)"
'Run command: F1 $StartObject'
" "
F1 $StartObject
" "
"Main script: `$StartObject.Name: $($StartObject.Name)"
"             `$StartObject.FirstValue: $($StartObject.FirstValue)"
"             `$StartObject.SecondValue: $($StartObject.SecondValue)"
"Run command: F2 $StartObject"
" "
F2 $StartObject
" "
"Main script: `$StartObject.Name = $($StartObject.Name)"
"             `$StartObject.FirstValue = $($StartObject.FirstValue)"
"             `$StartObject.SecondValue = $($StartObject.SecondValue)"

This messy piece of programming produces the following output:

Main script: Run command: $StartObject = [PSCustomObject]@{Name='Original';FirstValue=22}
             $StartObject.Name: Original
             $StartObject.FirstValue: 22
             $StartObject.SecondValue: 
Run command: F1 $StartObject

Function F1: Run command: $Obj.FirstValue = 11
             $Obj.Name: Original
             $Obj.FirstValue: 11
             $Obj.SecondValue: 

Main script: $StartObject.Name: Original
             $StartObject.FirstValue: 11
             $StartObject.SecondValue: 
Run command: F2 @{Name=Original; FirstValue=11}

Function F2: Run command: $Obj | Add-Member -MemberType NoteProperty -Name SecondValue -Value 33
             $Obj.Name: Original
             $Obj.FirstValue: 11
             $Obj.SecondValue: 33

Main script: $StartObject.Name = Original
             $StartObject.FirstValue = 11
             $StartObject.SecondValue = 33

These results clearly show that when [PSCustomObject] parameters are used, any modifications within the function take place on the passed object, thus pass by reference. This behavior happens regardless of defining my parameters as [PSCustomObject]$Obj, or leaving them untyped. This is not a huge problem in and of itself, but the problem is that I was unable to find this little gem of information in any of the documentation I looked through. I checked a few tutorial sites and Microsoft's own documentation on Function Parameters, but did not see this exception.

So, my question boils down to this: Has anyone found any documentation to support my theory that while most parameters default to passing by value, they are passed by reference when objects are concerned?

I am perfectly willing to believe that I missed some documentation somewhere, so please...point it out and show me the error of my ways! :)

Thanks very much

Upvotes: 5

Views: 10032

Answers (1)

mklement0
mklement0

Reputation: 437933

Note: The following also applies to assigning one variable to another: $b = $a ...
* makes $b reference the very same object that $a does if $a's value is an instance of a reference type,
* makes $b receive an independent copy of $a's value if the latter is an instance of a value type.

  • PowerShell uses by-(variable)-value passing by default; that is, the content of a variable is passed, not a reference to the variable itself.

    • Extra effort is needed if you want by-(variable)-reference passing, i.e. if you want to pass a reference to a variable itself, allowing the callee to both get the variable's content and to assign new content; in the simplest form, you can use a [ref]-typed parameter (akin to ref parameters in C#). However, note that this technique is rarely necessary in PowerShell.
  • Whether that content is a copy of what the caller sees or a reference to the very same object depends on the data type of the content:

    • If the content happens to be an instance of a .NET reference type - as [pscustomobject] is - that content is an object reference, and the callee can therefore potentially modify that object, by virtue of seeing the very same object as the caller.

      • If you want to pass a copy (clone) of a reference-type instance, note that there is no universal mechanism for creating one:
        • You can create copies of instances of types if they implement the System.ICloneable interface by calling their .Clone() method, but note that it is up to the implementing type whether to perform shallow or deep cloning[1]; it is for that reason that use of this interface is discouraged; in practice, types that do implement it typically perform shallow cloning, notably arrays, array lists (System.Collections.ArrayList) and hashtables (but note that an [ordered] hashtable (System.Collections.Specialized.OrderedDictionary) doesn't implement ICloneable at all.
        • Additionally, in PowerShell, you can call .psobject.Copy() on instances of type [pscustomobject] to create a shallow copy. (Do not use this method on objects of any other type, where it will effectively be a no-op.) Similarly, individual .NET types may implement custom cloning methods.
    • If, by contrast, that content is an instance of a .NET value type - e.g., [int] - or a string[2], an independent copy of that instance is passed.

    • This distinction is fundamental to .NET, not specific to PowerShell; it is also how arguments are passed in C#, for instance.

To determine whether a given variable's value is an instance of a value type or a reference type, use something like the following:

1, (Get-Date), (Get-Item /) |  # sample values
  foreach {
    '{0} is of type {1}; is it a value type? {2}' -f $_, 
                                                     $_.GetType(),
                                                     $_.GetType().IsValueType
  }

You'll see something like:

1 is of type System.Int32; is it a value type? True
4/30/2020 12:37:01 PM is of type System.DateTime; is it a value type? True
/ is of type System.IO.DirectoryInfo; is it a value type? False

If you look up the documentation for a given .NET type, say System.DateTime, the inheritance information will start with Object -> ValueType for value types; in C# terms, a value type is either a struct or an enum, whereas a reference type is a class.


Terminology and concepts:

There are two unrelated concepts at play here, and the fact that they both use the terms (by-)value and (by-)reference can get confusing:

  • By-(variable)-value vs. by-(variable)-reference parameter-passing is a data-holder (variable) concept:

    • It describes whether, on parameter passing, a variable's value is passed (by value) or a reference to the variable itself[3] (by reference).
  • Reference types vs. value types is purely a data concept:

    • That is, for technical reasons, any object in .NET is either an instance of a value type (stored on the stack) or a reference type (stored on the heap). Instances of the former are directly stored in variables, whereas the latter are stored by way of a reference. Therefore, copying a variable value - e.g., in the context of by-value parameter-passing - means:
      • either: making a copy of a value-type instance itself, resulting in an independent data copy.
      • or: making a copy of a reference-type instance reference; a copy of a reference still points to the same object, however, which is why even by-variable-value passed reference-type instances are directly seen by the callee (by way of their reference copy).

[1] Shallow cloning means that property values that are reference-type instances are copied as-is - as references - which means that the clone's property value again references the very same object as the original. Deep cloning means that such property values are cloned themselves, recursively. Deep cloning is expensive and isn't always possible.

[2] A string ([string]) is technically also an instance of a reference type, but, as an exception, it is treated like a value type; see this answer for the rationale behind this exception.

[3] Another way of thinking about it: a reference (pointer) to the location where the variable stores its value is passed. This allows the callee to not only access the variable's value, but also to assign a (new) value.

Upvotes: 13

Related Questions