Pass Class Property by Reference in PowerShell

The primary purpose of the [ref] class (it is not a keyword) is to facilitate calling .NET APIs that have ref and out parameters.

[ref] is rarely used in pure PowerShell code and best avoided there, because it deviates from how parameters are usually passed, is syntactically cumbersome, and has pitfalls, such as the one at hand.

In a nutshell:

  • [ref] only works meaningfully with a PowerShell variable, where it truly creates an alias name for the given variable object, so that getting and setting the variable value targets the very same variable object, irrespective of whether you use the original name or the alias.

  • While PowerShell lets you cast any expression to [ref], with anything other than a variable it functions like a regular assignment, and is therefore ineffective.[1]

This answer has more in-depth information about [ref].

Simplified examples:

Correct use of [ref]: with a variable:

  • Illustration without the use of a function:
PS> $foo = 42; $bar = [ref] $foo; ++$bar.Value; $foo
43  # OK, incrementing the .Value of $bar updated the value of $foo

Syntax note: The variable to alias must be cast directly to [ref]. Therefore, trying to use it as a variable type constraint does not work: [ref] $bar = $foo

  • Real-world example with a .NET API, [int]::TryParse(), whose second parameter is an out parameter:
# Declare a variable to use with [ref]
# Note: No need to type the variable.
$intVal = $null

# Pass the variable via [ref] to receive the out parameter value
# of [int]::TryParse()
$null = [int]::TryParse('42', [ref] $intVal)

# $intVal now contains 42

Pointless use of [ref]: with any other expression, such as an object property:[1]

PS> $foo = [pscustomobject] @{ prop = 42 }; $bar = [ref] $foo.prop; ++$bar.Value; $foo.prop
42  # !! $foo's value was NOT updated.

Therefore your options are:

  • If Add-Five cannot be modified, use an auxiliary variable to act as the by-reference argument.

      class MyClass{ $a = 0 }
    
      Function Add-Five {
        param([ref]$value)
        $value.Value+=5
      }
    
      $myObj = [MyClass]::new()
    
      # Create and use an aux. variable.
      $aux = $myObj.a
      Add-Five ([ref] $aux)
    
      # Update the property via the updated aux. variable.
      $myObj.a = $aux
      # myObj.a now contains 5
    
  • Otherwise:

    • Let Add-Five output (return) the new value and assign it to the property:

      class MyClass{ $a = 0 }
      
      Function Add-Five {
        param($value) # regular parameter (variable)
        $value + 5    # output the new value
      }
      
      $myObj = [MyClass]::new()
      
      # Pass the property value and assign back to it.
      $myObj.a = Add-Five $myObj.a 
      # myObj.a now contains 5
      
    • As Mathias R. Jessen suggests, pass a [MyClass] instance as a whole to Add-Five and let it update the property there:

      class MyClass{ $a = 0 }
      
      Function Add-Five {
        param([MyClass] $MyObj) # expect a [MyClass] instance as a whole
        $MyObj.a += 5           # update the property directly
      }
      
      $myObj = [MyClass]::new()
      
      Add-Five $myObj
      # myObj.a now contains 5
      

[1] There's another - albeit exotic - use of [ref] shown in the conceptual about_Ref help topic, but it doesn't relate to creating a reference to a value stored elsewhere; instead, it uses a [ref] instance as an alternative to a regular variable to allow it to be updated from descendant scopes in a syntactically more convenient manner (with a regular variable, you'd have to use the Get-Variable / Set-Variable cmdlets).