4

I've seen this, but neither answer actually answers the question using named argument in the call to the function

This is for Powershell 7. No workflow. And really Named Parameters this time! Also not covered in the standard documentation here .. Starting to think that this isn't possible.

this works, but it is requires brackets with positional arguments, not named arguments

Function MyFunction ([ref][string]$MyRefParam) {
    $MyRefParam.Value = "newValue";
}
$myLocal = "oldValue"
Write-Host $myLocal  # Outputs: oldValue
MyFunction ([ref]$myLocal);
Write-Host $myLocal # Outputs: newValue

Of course we all know that the best way to call a Powershell function is with named argument MyFunction -Arg1 23 rather than positional arguments MyFunction 23 or MyFunction(23) . These would not get through our Pull Requests!

But this doesn't work

Function MyFunction ([ref][string]$MyRefParam) {
    $MyRefParam.Value = "newValue";
}
$myLocal = "oldValue"
Write-Host $myLocal  # Outputs: oldValue
MyFunction -MyRefParam ([ref]$myLocal) # Outputs Cannot process argument transformation on parameter 'MyRefParam'. Reference type is expected in argument.
Write-Host $myLocal # Outputs: oldValue

Is there another way to provide the ref type in this syntax? So far I've tried combinations of [ref] and [ref][string] in both the param definition and in the call - I can't get anything to let Powershell see that I am really passing a Ref

Thanks for any help!

Brett
  • 719
  • 1
  • 7
  • 19
  • So the only purpose of using [ref] is to force function calls to use the actual parameter name to ease you code reviews? – Dennis Sep 24 '21 at 20:30

1 Answers1

7

Use only a [ref] type constraint in your parameter declaration, i.e. remove the additional [string] type constraint (it doesn't do anything, and arguably shouldn't even be allowed - see bottom section):

Function MyFunction ([ref] $MyRefParam) {
    $MyRefParam.Value = "newValue"
}

$myLocal = 'oldVaue'
MyFunction -MyRefParam ([ref] $myLocal)
$myLocal # -> 'newvalue'

You cannot type a [ref] instance: [ref] isn't a keyword that modifies a parameter declaration (as ref would be in C#), it is a type in its own right, System.Management.Automation.PSReference, and its value-holding property, .Value is of type object, i.e. it can hold any type of object.

The upshot:

  • You cannot enforce a specific data type when a value is passed via [ref].[1]

  • Conversely, you're free to assign a value of any type to the .Value property of the [ref] instance received.

Taking a step back:

  • [ref] is primarily intended for supporting calls to .NET APIs with ref / out parameters.

  • Its use in pure PowerShell code is unusual and best avoided, not least due to the awkward invocation syntax.


That said, the difference in behavior with your double type constraint between positional and named parameter binding is certainly curious - see Mathias R. Jessen's excellent explanation in the comments.

However, the real problem is that you're even allowed to define a multi-type constraint (unusual in itself) that involves [ref], because there appears to be an intentional check to prevent that, yet it doesn't surface in parameter declarations.

You can make it surface as follows, however (run this directly at the command line):

# ERROR, because [ref] cannot be combined with other type constraints.
PS> [ref] [string] $foo = 'bar'

Cannot use [ref] with other types in a type constraint

That the same check isn't enforced in parameter declarations has been reported in GitHub issue #16146.


[1] Behind the scenes, a generic type that is non-public is used when an actual value is passed with a [ref] cast (System.Management.Automation.PSReference<T>), which is instantiated with whatever type the value being cast is. However, this generic type's .Value property is still [object]-typed, allowing for modifications to assign any type.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 3
    While counter-intuitive, it's not so much a bug as a case of one feature ([type coercion of explicitly bound parameter arguments](https://github.com/PowerShell/PowerShell/blob/master/src/System.Management.Automation/engine/ParameterBinderController.cs#L1286)) "infringing" on the facility for `[ref]` parameters. That being said, I've never encountered any use case that actually required `[ref]` parameters in PowerShell functions :) – Mathias R. Jessen Sep 23 '21 at 20:27
  • 3
    The argument passed in both cases is of type `[PSReference[string]]`. When the argument is explicitly bound by name, the binder _always_ attempts to coerce to the target type, in this case converting `[PSReference[string]]` to `[string]`. It no longer satisfies `[ref]`. When the argument is passed positionally, the binder does _not_ attempt to transform the argument - and since `[PSReference[string]]` is _assignable_ to `[ref]`, no further errors occur. – Mathias R. Jessen Sep 23 '21 at 20:52
  • 2
    Take a look at `$refValue = [ref]""; Trace-Command { MyFunction $refValue; MyFunction -MyRefParam $refValue } -Name ParameterBinding,TypeConversion` - type conversion tracer never makes a peep during the first invocation, because PowerShell never actually attempts to coerce implicitly bound optionals :) – Mathias R. Jessen Sep 23 '21 at 20:58
  • 1
    Thanks. Oops, was sure I tried pure [ref], but clearly not but great explanation. The reason I need it is because I want to track the number of times exceptions are handled in certain areas of code (but not in others), so incrementing an int in the catch and passing back as a ref seemed better form than using a global variable. Any other options? – Brett Sep 23 '21 at 22:03
  • 1
    @Brett, good question, but I don't think I fully understand your use case; I suggest asking a _new_ question focused just on that - one that isn't focused on one particular approach, so as to avoid an [XY problem](http://meta.stackexchange.com/a/66378/248777). – mklement0 Sep 23 '21 at 22:55
  • 1
    I am not able to adequately answer to this as I am on holidays. Nevertheless, your (as usual) nice answer gives a lot background information on why `[ref]` isn't very wel delivered in native PowerShell. I have been a few times in the same situation, and in the end, I think that creating a general custom (hash) table (where every property is a reference) is the best semantical "solution" to this inquiry. Something like: `$Ref = @{}; Function MyFunction ($MyRefParam) { $Ref[$MyRefParam] = "newValue" }; MyFunction -MyRefParam 'MyRef'` – iRon Sep 24 '21 at 10:38