Powershell: [ref] doesn't work with class members

Created on 31 Aug 2016  路  17Comments  路  Source: PowerShell/PowerShell

PROBLEM
Using the [ref] keyword with a class member silently fails, also using with parentheses makes it fail (hard to troubleshoot)

IMPACT
APIs that use [ref] parameters cannot be used with class members.

REPRO

Function TryGetResolvedAndExpandedPath([ref] $result) {
    $result.Value = 'Replaced'
}

class Foo {
    [string] $classVar
}

Describe "[ref] not working with class members" {

    $foo = [Foo]::new()
    $foo.classVar = 'StartVal'
    TryGetResolvedAndExpandedPath ([ref]($foo.classVar))

    It "should be replaced" {
        $foo.classVar | Should Be "Replaced"
    }

    TryGetResolvedAndExpandedPath ([ref]$foo.classVar)

    It "should be replaced" {
        $foo.classVar | Should Be "Replaced"
    }

    [string] $scriptVar = 'StartVal'
    TryGetResolvedAndExpandedPath ([ref] $scriptVar)

    It "should be replaced" {
        $scriptVar | Should Be "Replaced"
    }

    [string] $scriptVar = 'StartVal'
    TryGetResolvedAndExpandedPath ([ref]($scriptVar))

    It "should be replaced" {
        $scriptVar | Should Be "Replaced"
    }
}

PSEdition Core
CLRVersion
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 3.0.0.0
GitCommitId v6.0.0-alpha.9
PSRemotingProtocolVersion 2.3
PSVersion 6.0.0-alpha
WSManStackVersion 3.0
SerializationVersion 1.1.0.1

Resolution-External WG-Language

Most helpful comment

I suggest we document that [ref] is intended for COM interop and the need for in/out params are handled by different means

All 17 comments

It has never been supposed to work that way. See documentation (Remarks, there's no saying about using it with a class member, whether it's a field, property or anything else). Moreover, reading the code of PSReference reveals the reason -- it is supposed to support variables and values only.

Moreover, PowerShell does need marshalling when you call CLR methods with ref -- the value is not directly stored into the variable as the method executes, instead, it is stored after the call completes (this is different from CLR method call). For example:

Add-Type -TypeDefinition 'public class Foo
{
    public string ClassField;
    public static void EditStringAndThrow(ref string str) { str = "Edited"; ((object)null).ToString(); }
    public void EditStringAndThrow() { EditStringAndThrow(ref ClassField); }
}';
$str = 'Initial';
[Foo]::EditStringAndThrow($str);
$str; <# Gives Initial. #>
$foo = [Foo]::new(); $foo.ClassField = 'Initial';
$foo.EditStringAndThrow();
$foo.ClassField; <# Gives Edited. #>

To explain why parentheses fail the ref, just note that with a pair of parentheses, the expression is no longer a variable, it's a compound expression therefore the constructor PSReference(object value) is used -- the ref has nothing to do with that variable, it's a value reference. This is a feature so that you don't really have to receive the value produced by a cmdlet/function if you don't want it.

Perhaps this is just a doc issue

We should do something - ideally support the feature or report a parse error.

[ref] really only exists for COM. With COM, there are cases where you need to pass a value _in_ by reference so the example with the object property is not an error. It's by design.

@lzybkr unluckily that'll be a breaking change. For those examples here that doesn't do the "ref a variable" thing, they create a value ref instead, and is a feature, by design.

Imagine a function:

Function Use-Ref
{
Param ([ref]$OutputRef)
Write-Output $OutputRef.Value;
$OutputRef.Value = 42;
}

The following things are pretty equivalent in terms of calling Use-Ref:

Use-Ref ([ref]1)
# create a ref from value 1
# the ref is not used after the call
$a = 1
Use-Ref ([ref]($a))
# use the value of $a as the parameter's
# initial value but do not store the notified value back.
$a = New-Object PSObject -Property @{ Prop = 1 }
Use-Ref ([ref] $a.Prop)
# evaluate the expression $a.Prop
# and create a value ref.

It is crucial to understand that ([ref]$a) is special -- the only syntax to create a variable ref, which means PowerShell engine will write the value back to that variable after the cmdlet/function finishes.

GeeLaw's assessment is pretty accurate. [ref] is "special" with only limited scenarios being covered. If it wasn't for COM, [ref] wouldn't exist. So If you're using [ref] and not doing COM you're probably doing it wrong.

@BrucePay weirdly, I dont really see any common cmdlets using ref. I encounter most of my refs in a method call.

I suggest we document that [ref] is intended for COM interop and the need for in/out params are handled by different means

Is this User Voice the same issue ?

BUG : [Class] Can't override a C# method with an argument of type out
https://windowsserver.uservoice.com/forums/301869-powershell/suggestions/13191009-bug-class-can-t-override-a-c-method-with-an-a

@fmichaleczek different, but related. Thank you for adding it to the discussion!

@SteveL-MSFT Going back to your original issue, you should be aware that [ref] can be applied to values as well as variables:

PS[1] (70) > [ref] 1
Value
-----
    1

This is what's happening in your example. When you do [ref] ($foo.classvar) you are actually creating a reference to the value returned by classvar:

PS[1] (71) > class Foo {
>>     [string] $classVar
>> }
PS[1] (72) > $x = [foo]::new()
PS[1] (73) > $x.classVar -eq $null
True
PS[1] (74) > $r = [ref] ($x.classVar)
PS[1] (75) > $r.Value -eq $null
True

So [ref] is working as intended.

I have needed to use [ref] when working with a few different soap based web services via New-WebServiceProxy that required [ref] variables for things like error strings.

The functions would error without being called with a [ref] variable passed in for that parameter making [ref] required as far as I could figure out to use the web service.

Closing this since we have a doc issue opened

I tripped over this today, docs issue is still open :-(

We also don't have a proper way to handle in/out variables, judging from the discussion above and the lack of anything other than [ref] being available. 馃槙

Yes, I think we should definitely open a feature request issue for this and other things.
Today I wrote a local Update-ConnectionString helper function and instead of calling it as $configObject.connectionstrings[$index] = Update-ConnectionString $configObject.connectionstrings[$index] -otherparams $foo, it would've been much more elegant to call it as Update-ConnectionString ([ref]$configObject.connectionstrings[$index]) -otherparams $foo

it would've been much more elegant to call it as Update-ConnectionString ([ref]$configObject.connectionstrings[$index]) -otherparams $foo

I'd love that so much, but I'm not really sure how that would be doable. You can't safely store a managed pointer on the heap, so a general purpose [ref] that works in all places where ref/in/out work doesn't seem possible. Maybe the compiler could have special handling and create a PSReference with an instance/array and a field name/index? Not sure if enough people would use it unfortunately :/

Was this page helpful?
0 / 5 - 0 ratings