How can I prevent a string argument changing from null to empty when bound to a parameter?
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, usingreturn $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 tonull
in certain contexts.string
is a .NET reference type, and this behavior applies to all reference types.
(Since reference type instances can inherently containnull
, there is no need for a separate nullable wrapper viaSystem.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 passingnull
tostring
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:
- Caveat: While it is possible to use
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.)
function f {
param (
[AllowNull()]$x
)
return $x
}
$r = f -x $null
By removing the [string]
and using [AllowNull()]
the above function will now allow you to pass in a null object or an empty string. You can check for the type using $x.GetType
with an if statement and determining if $x
is null or an empty string.
By default, [string] assigns default value as [string]::Empty
, so the parameter definition will convert it whenever enters function f.
a. You can change the parameter as [object]$x
[object]$newparamnull -eq $null
[string]$newparamstring -eq [string]::Empty
b. The previous change will do the job:
function f {
param (
[AllowNull()]
[object]
$x)
if($x -eq $null) {
write-output "null"
}
elseif($x -eq [string]::empty){
write-output "empty"
}
else {"other"}
}
Test:
f -x $null
f -x [string]::empty
f -x "aaa"