Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In PowerShell, why is $null -lt 0 = $true? Is that reliable?

Consider the following PowerShell code:

> $null -gt 0
False
> $null -ge 0
False
> $null -eq 0
False
> $null -le 0
True
> $null -lt 0
True

Of course the same is true for a $variable explicitly set to $null or for a non-existent variable.

  1. Why is that? It doesn't make a lot of sense to me. I feel like $null by definition doesn't have a value that could be tested as such, or at the very least, that it would evaluate to zero in such tests. But other than that I guess I don't know what behavior I would actually expect. Googling (or searching SO) for e.g. "Why is null less than zero in Powershell" doesn't seem to yield any results, though I do see relevant questions and answers for several other languages.
  2. Can and should this result be relied on?
  3. Aside from testing the variable with GetType(), or various implementations of "IsNumeric", "IsNullOrEmpty", etc. what is a 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? Or is one of those methods considered pretty standard?

Thanks for your time. Apologies in advance if this is too "squishy" of a question for this venue.

P.S. For what it's worth my usual environment is PowerShell v5.1.

like image 900
mmseng Avatar asked Feb 27 '21 07:02

mmseng


Video Answer


1 Answers

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 to 0 ([int] by default); e.g. $null + 0 is 0
  • * 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.
  • 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.

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.

like image 119
mklement0 Avatar answered Sep 25 '22 02:09

mklement0