Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I prevent a string argument changing from null to empty when bound to a parameter?

Consider the following code:

function f {
    param (
        [AllowNull()]
        [string]
        $x
    )
    return $x
}

$r = f -x $null

$null is converted to [string]::Empty by the time return is reached. $null is different from [string]::Empty and I'd like to preserve this distinction. I'd also prefer to keep $x as type [string] because $x only has meaning as a string and the interface is used elsewhere.

  1. How can I make $x come out as $null when it is passed $null?
  2. Is there some other way I can tell that $x was passed $null not [string]::Empty from inside f?

Update 1

What I am trying to do works for other types. Here is the same concept for [int]:

function f { 
    param( 
        [System.Nullable[int]]$x 
    )
    return $x 
}

$r = f -x $null

In that case $r is indeed $null. $x can be either $null or [int] but nothing else. It seems strange to me to have to allow any object just so I can pass a $null or [int].

[System.Nullable[string]] produces an error that boils down to [System.Nullable[T]] requires that [T] is a value type. [string] is a reference type, so that doesn't work.


Update 2

It seems to be possible to pass $null without causing conversion to a parameter of any type except [string]. I've tested the following:

function f { param([System.Nullable[int]]$x) $x }
function f { param([System.Nullable[System.DayOfWeek]]$x) $x }
function f { param([hashtable]$x) $x }
function f { param([array]$x) $x }
function f { param([System.Collections.Generic.Dictionary[string,int]]$x) $x }
function f { param([System.Collections.ArrayList]$x) $x }
function f { param([System.Collections.BitArray]$x) $x }
function f { param([System.Collections.SortedList]$x) $x }
function f { param([System.Collections.Queue]$x) $x }
function f { param([System.Collections.Stack]$x) $x }

Passing $null to any of these functions outputs $null. The only parameter type I haven't found a way to which to pass $null without conversion is [string].


Update 3

PowerShell's behavior in this regard is also inconsistent with C#. The corresponding function in C# is as follows:

public string f(string x)
{
    return x;
}

Calling f(null) returns null.


Update 4

Apparently [NullString]::Value was intended to address this problem. I seems to work to pass null to string parameters in C# APIs. However, [NullString]::Value gets converted to [string]::empty in PowerShell the same as $null. Consider the following code:

function f {
    param (
        [AllowNull()]
        [string]
        $x
    )
    return $x
}

$r = f -x ([NullString]::Value)
$r.GetType().Name

Executing that code outputs String. $r is [string]::Empty despite that [NullString]::Value was passed to $x.


Update 5

The PowerShell team has indicated that this was by design:

This is by design and ... changing the behavior would be a massive breaking change.

That thread involved an interesting discussion about the reasoning behind it. I suspect that some of ramifications of this behavior were not understood when the decision was made as the behavior directly contravenes PowerShell cmdlet "Strongly Encouraged Design Guideline" SD03 which reads in part as follows:

If your parameter needs to differentiate between 3 values: $true, $false and “unspecified”, then define a parameter of type Nullable. The need for a 3rd, "unspecified" value typically occurs when the cmdlet can modify a Boolean property of an object. In this case "unspecified" means to not change the current value of the property.

like image 490
alx9r Avatar asked Aug 16 '17 17:08

alx9r


1 Answers

To summarize and complement the information from the question, answers, and comments:

tl;dr:

It's best not to fight PowerShell's design of not allowing [string] variables to be $null, and to limit use of [NullString]::Value to calls to .NET methods.


  • PowerShell converts $null to '' (the empty string) when it is assigned to [string]-typed [parameter] variables, and parameter variables also default to ''.

    • The only exception is the use of uninitialized [string] properties in PSv5+ custom classes, as alxr9 (the OP) points out: class c { [string] $x }; $null -eq ([c]::new()).x indeed yields $True implying that property .x contains $null. However, this exception is likely accidental and probably a bug, given that when you initialize the property with $null or assign $null to it later, the conversion to '' again kicks in; similarly, using return $null from a [string]-typed method outputs ''.

    • The exception aside, PowerShell's behavior differs from C# string variables / parameters, to which you can assign / pass null directly, and which default to null in certain contexts. string is a .NET reference type, and this behavior applies to all reference types.
      (Since reference type instances can inherently contain null, there is no need for a separate nullable wrapper via System.Nullable`1, which is indeed not supported (it works for value types only).)

  • As noted in the question (update 5), PowerShell's departure from C#'s behavior is by (design, and it's changing it is not an option for reasons of backward compatibility alone.

  • [NullString]::Value was introduced in v3 specifically to allow passing null to string parameters of .NET methods - and while use in pure PowerShell code wasn't explicitly discouraged or prevented, the unexpected behavior in update 4 and the comments by a core PowerShell team member (see below) suggest that such uses weren't anticipated.

    • Caveat: While it is possible to use [NullString]::Value in pure PowerShell code, there may be pitfalls beyond the one discussed below, given that use of [NullString]::Value was never intended outside the context of calling .NET methods; to quote a core member of the PowerShell team:

Parameters to C# methods was the target scenario for [NullString]::Value, and I will say that might be the only reasonable scenario.

  • A workaround is to type your (parameter) variable as [object] or to not type-constrain it at all, which amounts to the same. Such variables happily accept $null, but note that you may have to stringify (convert to [string]) non-$null values yourself (although PowerShell does that for you automatically in explicit or implied string contexts) - see the penultimate code example below.

If, despite the advice above, you do need a [string] parameter variable that you can pass $null to via [NullString]::Value, as in update 4 in your question, there is an - obscure - workaround for the optimization bug that prevents your code from working, thanks to sleuthing by PetSerAl:

function f {
    param (
        [string] $x
    )
    # Workaround; without this, even passing [NullString]::Value 
    # returns '' rather than $null            
    if ($False) { Remove-Variable } 
    return $x
}

$r = f -x ([NullString]::Value)
$r.GetType().Name  # now fails, because $r is $null

Note that when assigning / passing [NullString]::Value to a [string]-typed [parameter] variable, it is instantly converted to $null (in the case of a parameter variable, only if the bug gets fixed or with the workaround in place). However, once $null has been successfully stored in the variable this way, it can apparently be passed around as such (again, only if the bug gets fixed or with the workaround in place).


If you don't want to rely on the workaround / wait for the fix and/or don't want to burden the caller with having to pass [NullString]::Value instead of $null, you can build on the answers by Curios and Jason Schnell, which rely on using an untyped (implicitly [object]-typed) or explicitly [object]-typed parameter, which can accept $null as-is:

function f {
    param (
        [AllowNull()] # Explicitly allow passing $null.
                      # Note: Strictly speaking only necessary with [Parameter(Mandatory=$True)]
        $x # Leave the parameter untyped (or use [object]) so as to retain $null as-is
    )

    # Convert $x to a type-constrained [string] variable *now*:
    if ($null -eq $x) {
        # Make $x contain $null, despite being [string]-typed
        [string] $x = [NullString]::Value
    } else {
        # Simply convert any other type to a string.
        [string] $x = $x
    }

    # $x is now a bona fide [string] variable that can be used
    # as such even in .NET method calls.

    return $x
}

It's somewhat cumbersome, but enables the caller to pass $null directly (or any string, or a type of any other instance that will be converted to a string).

A slight down-side is that this approach doesn't allow you to define positional parameters in the same position via different parameter sets that are selected by the parameters' specific types.


Finally, it's worth mentioning that if it's sufficient to detect when a (non-mandatory) parameter was omitted, you can check $PSBoundParameters:

function f {
    param (
        [string] $x
    )

    if ($PSBoundParameters.ContainsKey('x')) { # Was a value passed to parameter -x?
        "-x argument was passed: $x"
    } else {
        "no -x argument passed."
    }
}

As stated, this only works for the omission case (and therefore doesn't work for mandatory parameters at all). If you pass $null, the usual conversion to '' kicks in, and you won't be able to distinguish between passing $null and ''.
(Though if you added the above workaround / waited for the bug fix, you could again pass [NullString]::Value to effectively pass $null, or even use [NullString]::Value as the parameter default value.)

like image 116
mklement0 Avatar answered Oct 22 '22 00:10

mklement0