In PowerShell, why is $null -lt 0 = $true? Is that reliable?
Why is that?
The behavior is counterintuitive:
Operators -lt
, -le
, -gt
, -ge
, even though they can also have numeric meaning, seemingly treat a $null
operand as if it were the empty string (''
), i.e. they default to string comparison, as the sample commands in postanote's helpful answer imply.
That is, $null -lt 0
is in effect evaluated the same as '' -lt '0'
, which explains the $true
result, because in lexical comparison the condition is met.
While you can conceive of $null -eq 0
as '' -eq '0'
too, the -eq
case is special - see below.
Additionally, placing the 0
on the LHS still acts like a string comparison (except with -eq
see below) - even though it is normally the type of the LHS that causes the RHS to be coerced to the same type.
That is, 0 -le $null
too seems to act like '0' -le ''
and therefore returns $false
.
While such behavior is to be expected in operators that are exclusively string-based, such as -match
and -like
, it is surprising for operators that also support numbers, especially given that other such operators - as well as those that are exclusively numeric - default to numeric interpretation of $null
, as 0
.
+
,-
, and/
do force a LHS$null
to0
([int]
by default); e.g.$null + 0
is0
*
does not; e.g.,$null * 0
is again$null
.
Of these, -
and /
are exclusively numeric, whereas +
and *
also work in string and array contexts.
There is an additional inconsistency: -eq
never performs type coercion on a $null
operand:
$null -eq <RHS>
is only ever$true
if<RHS>
is also$null
(or "automation null" - see below), and is currently the only way to reliably test a value for being$null
. (To put it differently:$null -eq ''
is not the same as'' -eq ''
- no type coercion takes place here.)- GitHub PR #10704, which has unfortunately stalled, aims to implement a dedicated syntax for
$null
tests, such as<LHS> -is $null
.
- GitHub PR #10704, which has unfortunately stalled, aims to implement a dedicated syntax for
Similarly,
<LHS> -eq $null
also performs no type coercion on$null
and returns$true
only with$null
as the LHS;- However, with an array-valued
<LHS>
,-eq
acts as filter (as most operators do), returning the subarray of elements that are$null
; e.g.,1, $null, 2, $null, 3 -eq $null
returs 2-element array$null, $null
. - This filtering behavior is the reason that only
$null -eq <RHS>
- with$null
as the scalar LHS - is reliable as a test for (scalar)$null
.
- However, with an array-valued
Note that the behaviors equally apply to the "automation null" value that PowerShell uses to express the (non-)output from commands (technically, the [System.Management.Automation.Internal.AutomationNull]::Value
singleton), because this value is treated the same as $null
in expressions; e.g. $(& {}) -lt 0
is also $true
- see this answer for more information.
Similarly, the behaviors also apply to instances of nullable value types that happen to contain $null
(e.g., [System.Nullable[int]] $x = $null; $x -lt 0
is also $true
)Thanks, Dávid Laczkó., though note that their use in PowerShell is rare.
Can and should this result be relied on?
Since the behavior is inconsistent across operators, I wouldn't rely on it, not least because it's also hard to remember which rules apply when - and there's at least a hypothetical chance that the inconsistency will be fixed; given that this would amount to a breaking change, however, that may not happen.
If backward compatibility weren't a concern, the following behavior would remove the inconsistencies and make for rules that are easy to conceptualize and remember:
When a (fundamentally scalar) binary operator is given a $null
operand as well as a non-$null
operand - irrespective of which is the LHS and which is the RHS:
For operators that operate exclusively on numeric / Boolean / string operands (e.g.
/
/-and
/-match
): coerce the$null
operand to the type implied by the operator.For operators that operate in multiple "domains" - both textual and numeric (e.g.
-eq
) - coerce the$null
operand to the other operand's type.
Note that this would then additionally require a dedicated $null
test with different syntax, such as the -is $null
from the above-mentioned PR.
Note: The above does not apply to the collection operators, -in
and -contains
(and their negated variants -notin
and -notcontains
), because their element-wise equality comparison acts like -eq
and therefore never applies type coercion to $null
values.
what is the best (i.e. most concise, best performing, etc.) way to reliably test for integer values (or other types for that matter) in a variable that might have a value of $null?
The following solutions force a $null
operand to 0
:
- Note:
(...)
around the LHS of the-lt
operations below is used for conceptual clarity, but isn't strictly necessary - see about_Operator_Precedence.
In PowerShell (Core) 7+, use ??
, the null-coalescing operator, which works with operands of any type:
# PowerShell 7+ only($null ?? 0) -lt 0 # -> $false
In Windows PowerShell, where this operator isn't supported, use a dummy calculation:
# Windows PowerShell(0 + $null) -lt 0 # -> $false
While something like [int] $null -lt 0
works too, it requires you to commit to a specific numeric type, so if the operand happens to be higher than [int]::MaxValue
, the expression will fail; [double] $null -lt 0
would minimize that risk, though could at least hypothetically result in loss of accuracy.
The dummy addition (0 +
) bypasses this problem and lets PowerShell apply its usual on-demand type-widening.
As an aside: This automatic type-widening can exhibit unexpected behavior too, because an all-integer calculation whose result requires a wider type than either operand's type can fit is always widened to [double]
, even when a larger integer type would suffice; e.g. ([int]::MaxValue + 1).GetType().Name
returns Double
, even though a [long]
result would have sufficed, resulting in potential loss of accuracy - see this answer for more information.
Test $null comparison results
(0).GetType()('').GetType()(' ').GetType()($null).GetType()# Results<#IsPublic IsSerial Name BaseType-------- -------- ---- --------True True Int32 System.ValueTypeTrue True String System.ObjectTrue True String System.ObjectYou cannot call a method on a null-valued expression.#>Measure-Object -InputObject (0).GetType()Measure-Object -InputObject ('').GetType()Measure-Object -InputObject (' ').GetType()Measure-Object -InputObject ($null).GetType()# Results<#Count : 1Average : Sum : Maximum : Minimum : Property : Count : 1Average : Sum : Maximum : Minimum : Property : Count : 1Average : Sum : Maximum : Minimum : Property : You cannot call a method on a null-valued expression.#>$Null -eq '' [string]$Null -eq ''$Null -eq [string]''[string]$Null -eq [string]''# Results<#FalseTrueFalseTrue#>$Null -eq '' [bool]$Null -eq ''$Null -eq [bool]''[bool]$Null -eq [bool]''# Results<#FalseTrueFalseTrue#>$Null -eq '' [int]$Null -eq ''$Null -eq [int]''[int]$Null -eq [int]''# Results<#FalseTrueFalseTrue#>$Null -eq '' [double]$Null -eq ''$Null -eq [double]''[double]$Null -eq [double]''# Results<#FalseTrueFalseTrue#>Clear-Host 0, $null | ForEach { ('#')*40 "`nTest `$null as default" $null -gt $PSItem $null -ge $PSItem $null -eq $PSItem $null -le $PSItem $null -lt $PSItem "`n" ('#')*40 "Using $PSItem" "`nTest `$null as string" "Left Side`tRight Side`tBoth Sides" Write-Host ([string]$null -gt $PSItem) -NoNewline Write-Host "`t|`t" ($null -gt [string]$PSItem) -NoNewline Write-Host "`t|`t" ([string]$null -gt [string]$PSItem) Write-Host ([string]$null -ge $PSItem) -NoNewline Write-Host "`t|`t" ($null -ge [string]$PSItem) -NoNewline Write-Host "`t|`t" ([string]$null -ge [string]$PSItem) Write-Host ([string]$null -eq $PSItem) -NoNewline Write-Host "`t|`t" ($null -eq [string]$PSItem) -NoNewline Write-Host "`t|`t" ([string]$null -eq [string]$PSItem) Write-Host ([string]$null -le $PSItem) -NoNewline Write-Host "`t|`t" ($null -le [string]$PSItem) -NoNewline Write-Host "`t|`t" ([string]$null -le [string]$PSItem) Write-Host ([string]$null -lt $PSItem) -NoNewline Write-Host "`t|`t" ($null -lt [string]$PSItem) -NoNewline Write-Host "`t|`t" ([string]$null -lt [string]$PSItem) "`n" ('#')*40 "Using $PSItem" "`nTest `$null as boolean" "Left Side`tRight Side`tBoth Sides" Write-Host ([boolean]$null -gt $PSItem) -NoNewline Write-Host "`t|`t" ($null -gt [boolean]$PSItem) -NoNewline Write-Host "`t|`t" ([boolean]$null -gt [boolean]$PSItem) Write-Host ([boolean]$null -ge $PSItem) -NoNewline Write-Host "`t|`t" ($null -ge [boolean]$PSItem) -NoNewline Write-Host "`t|`t" ([boolean]$null -ge [boolean]$PSItem) Write-Host ([boolean]$null -eq $PSItem) -NoNewline Write-Host "`t|`t" ($null -eq [boolean]$PSItem) -NoNewline Write-Host "`t|`t" ([boolean]$null -eq [boolean]$PSItem) Write-Host ([boolean]$null -le $PSItem) -NoNewline Write-Host "`t|`t" ($null -le [boolean]$PSItem) -NoNewline Write-Host "`t|`t" ([boolean]$null -le [boolean]$PSItem) Write-Host ([boolean]$null -lt $PSItem) -NoNewline Write-Host "`t|`t" ($null -lt [boolean]$PSItem) -NoNewline Write-Host "`t|`t" ([boolean]$null -lt [boolean]$PSItem) "`n" ('#')*40 "Using $PSItem" "`nTest `$null as int" "Left Side`tRight Side`tBoth Sides" Write-Host ([int]$null -gt $PSItem) -NoNewline Write-Host "`t|`t" ($null -gt [int]$PSItem) -NoNewline Write-Host "`t|`t" ([int]$null -gt [int]$PSItem) Write-Host ([int]$null -ge $PSItem) -NoNewline Write-Host "`t|`t" ($null -ge [int]$PSItem) -NoNewline Write-Host "`t|`t" ([int]$null -ge [int]$PSItem) Write-Host ([int]$null -eq $PSItem) -NoNewline Write-Host "`t|`t" ($null -eq [int]$PSItem) -NoNewline Write-Host "`t|`t" ([int]$null -eq [int]$PSItem) Write-Host ([int]$null -le $PSItem) -NoNewline Write-Host "`t|`t" ($null -le [int]$PSItem) -NoNewline Write-Host "`t|`t" ([int]$null -le [int]$PSItem) Write-Host ([int]$null -lt $PSItem) -NoNewline Write-Host "`t|`t" ($null -lt [int]$PSItem) -NoNewline Write-Host "`t|`t" ([int]$null -lt [int]$PSItem) "`n" ('#')*40 "Using $PSItem" "`nTest `$null as double" "Left Side`tRight Side`tBoth Sides" Write-Host ([double]$null -gt $PSItem) -NoNewline Write-Host "`t|`t" ($null -gt [double]$PSItem) -NoNewline Write-Host "`t|`t" ([double]$null -gt [double]$PSItem) Write-Host ([double]$null -ge $PSItem) -NoNewline Write-Host "`t|`t" ($null -ge [double]$PSItem) -NoNewline Write-Host "`t|`t" ([double]$null -ge [double]$PSItem) Write-Host ([double]$null -eq $PSItem) -NoNewline Write-Host "`t|`t" ($null -eq [double]$PSItem) -NoNewline Write-Host "`t|`t" ([double]$null -eq [double]$PSItem) Write-Host ([double]$null -le $PSItem) -NoNewline Write-Host "`t|`t" ($null -le [double]$PSItem) -NoNewline Write-Host "`t|`t" ([double]$null -le [double]$PSItem) Write-Host ([double]$null -lt $PSItem) -NoNewline Write-Host "`t|`t" ($null -lt [double]$PSItem) -NoNewline Write-Host "`t|`t" ([double]$null -lt [double]$PSItem)}# Results<#########################################Test $null as defaultFalseFalseFalseTrueTrue########################################Using 0Test $null as stringLeft Side Right Side Both SidesFalse | False | FalseFalse | False | FalseFalse | False | FalseTrue | True | TrueTrue | True | True########################################Using 0Test $null as booleanLeft Side Right Side Both SidesFalse | False | FalseTrue | False | TrueTrue | False | TrueTrue | True | TrueFalse | True | False########################################Using 0Test $null as intLeft Side Right Side Both SidesFalse | False | FalseTrue | False | TrueTrue | False | TrueTrue | True | TrueFalse | True | False########################################Using 0Test $null as doubleLeft Side Right Side Both SidesFalse | False | FalseTrue | False | TrueTrue | False | TrueTrue | True | TrueFalse | True | False########################################Test $null as defaultFalseTrueTrueTrueFalse########################################Using Test $null as stringLeft Side Right Side Both SidesTrue | False | FalseTrue | False | TrueFalse | False | TrueFalse | True | TrueFalse | True | False########################################Using Test $null as booleanLeft Side Right Side Both SidesTrue | False | FalseTrue | False | TrueFalse | False | TrueFalse | True | TrueFalse | True | False########################################Using Test $null as intLeft Side Right Side Both SidesTrue | False | FalseTrue | False | TrueFalse | False | TrueFalse | True | TrueFalse | True | False########################################Using Test $null as doubleLeft Side Right Side Both SidesTrue | False | FalseTrue | False | TrueFalse | False | TrueFalse | True | TrueFalse | True | False#>