1

Using PowerShell ISE on Windows 11

PS C:\Users\malcolm> $PSVersionTable

Name                           Value                                                              
----                           -----                                                              
PSVersion                      5.1.22000.282                                                      
PSEdition                      Desktop                                                            
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}                                            
BuildVersion                   10.0.22000.282                                                     
CLRVersion                     4.0.30319.42000                                                    
WSManStackVersion              3.0                                                                
PSRemotingProtocolVersion      2.3                                                                
SerializationVersion           1.1.0.1  

I need to pass a class property as reference in PowerShell.

For example like this:

Function Add-Five
{
    param([ref]$value)
    $value.Value+=5
}

$a = 5
Add-Five -value ([ref]$a)
Write-Host "A = $a"

As expected this outputs 10

However, if I do this:

class MyClass
{
    $a
}
Function Add-Five
{
    param([ref]$value)
    $value.Value+=5
}
    
$class = New-Object MyClass
$class.a = 5
Add-Five -value ([ref]$class.a)
Write-Host "A = $($class.a)"

The output is 5.

As a workaround I can do this :

Function Add-FiveSpecial
{
    param($className,$propertyName)
    (Get-Variable -Name $className).Value.$propertyName += 5    
}

Add-FiveSpecial -className "class" -propertyName "a"
Write-Host "A = $($class.a)"

Which outputs 10

Is there a way to get [ref] to work with class properties?

Malcolm McCaffery
  • 2,468
  • 1
  • 22
  • 43
  • Can the class property be of the type `ref` ? It should work that way. Also, why not have a instance method to do what the function is doing ? – Santiago Squarzon Jan 18 '22 at 00:41
  • Using ref as class property does work, but then as far as I can tell I can't set the type of data in the class property if I want. In my actual program it is an instance method, but I have the same issue there. When not a class property I can still have the value as type Int32 for example and pass as reference, but not as class property. – Malcolm McCaffery Jan 18 '22 at 01:15
  • Having `Add-Five` as an instance method of your class is not an option? – Santiago Squarzon Jan 18 '22 at 01:55
  • 2
    Why not pass the `[MyClass]` instance as the parameter? – Mathias R. Jessen Jan 18 '22 at 01:59

1 Answers1

2

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).

mklement0
  • 382,024
  • 64
  • 607
  • 775