When checking variables and collections of variables for nullity, comparison operators seem to enumerate collections of size 2 or more:
> if ( @( $null, $null ) -eq $null ) { $True } else { $False }
True
But they do not for collections of size 1:
> if ( @( $null ) -eq $null ) { $True } else { $False }
False
I'm aware that it's best practice to null-compare using the left-hand side ($null -eq @( $null )
), but can someone explain what's happening here? I suspect there's something more subtle happening that impacts other code that I write.
Why are these two results different?
Use comparison operators ( -eq , -ne , -gt , -lt , -le , -ge ) to compare values and test conditions. For example, you can compare two string values to determine whether they are equal. The comparison operators also include operators that find or replace patterns in text.
To check to see if one object is equal to another object in PowerShell is done using the eq operator. The eq operator compares simple objects of many types such as strings, boolean values, integers and so on. When used, the eq operator will either return a boolean True or False value depending on the result.
The Like operator is a Powershell comparison operator that uses wildcards.
PowerShell includes the following comparison operators: Equality. -eq , -ieq , -ceq - equals. -ne , -ine , -cne - not equals. -gt , -igt , -cgt - greater than.
tl;dr
In PowerShell conditionals / implicit Boolean contexts:
Single-element arrays are treated like scalars: that is, their one and only element itself is interpreted as a Boolean.[1]
2+-element arrays are always $true
, irrespective of their content.
With an array as the LHS, array-aware operators such as -eq
invariably also output an array.
Since your array elements are all $null
and you compare to $null
, your comparison is an effective no-op - e.g., @( $null ) -eq $null
results in @( $null )
- and your conditionals are equivalent to:
[bool] @( $null, $null ) # -> $true - array with 2+ elements is always $True
[bool] @( $null ) # -> $false(!) - treated like: [bool] $null
Perhaps surprisingly, the implicit Boolean logic applies pipeline logic to an array:
That is, a single-element array is (conceptually) unwrapped and its element is interpreted as a Boolean.
Therefore, [bool] @( $null )
is treated the same as [bool] $null
, which is $false
.
Generally, @( <one-and-only-element> )
(or , <one-and-only-element>
) is treated the same as <one-and-only-element>
in a Boolean context.
By contrast, if an array has 2 or more elements, it is always $true
in a Boolean context, even if all its elements would individually be considered $false
.
Workaround for testing whether an arbitrary array is empty:
Base your conditional on the .Count
property:
if ( (<array>).Count ) { $true } else { $false }
You could append -gt 0
, but that's not strictly necessary, because any nonzero value is implicitly $true
.
Applied to your example:
PS> if ( ( @($null) -eq $null ).Count ) { $true } else { $false }
True
Testing an arbitrary value for being a (scalar) $null
:
if ($null -eq <value>) { $true } else { $false }
Note how $null
must be used as the LHS in order to prevent the array-filtering logic from taking effect, should <value>
be an array.
That's also the reason why Visual Studio Code with the PowerShell extension advises "$null should be on the left side of comparisons" if you write something like $var -eq $null
.
[1] To-Boolean conversion summary:
The following are implicitly $false
:
''
/""
(empty string)
0
(of any numeric type).
$null
Pitfall: Comparing $null
to a Boolean explicitly with -eq
is always $false
, even with $null
as the RHS (despite the RHS normally getting coerced to the type of the LHS):
$false -eq $null # !! $false - unlike `$false -eq [bool] $null`
Pitfall: Any non-empty string evaluates to $true
e.g., [bool] 'False'
is $true
Note that this differs from explicit string parsing: [bool]::Parse('false')
does return$false
(and $true
for 'true'
, but recognizes nothing else).
Instances of any other (non-collection) type are implicitly $true
, including of type [pscustomobject]
and [hashtable]
(which PowerShell treats as a single object, not as a collection of entries).
[bool]
.NET conversion operators, meaning that these operators are - mostly - not honored; see this answer.IList
interface - see the source code):
Empty collections are always $false
, as is the special "null collection" value indicating the absence of output from a command, [System.Management.Automation.Internal.AutomationNull]::Value
.
Pitfall: Single-element collections evaluate to:
$true
if it has at least 1 element (irrespective of what that element is).2+-element collections are always $true
.
The following items evaluate to $false
:
@()
0
$null
$false
''
In your first example:
@($null, $null) -eq $null
This evaluates to $null, $null
which is a non-zero collection, so it is $true
. You can observe this with the following:
[bool]($null, $null)
In your second example, what you're observing is filtering of an array like the first case, but returning a scalar (instead of an array) since only one item of the array matched the filter:
@($null) -eq $null
This evaluates to @($null)
but powershell is evaluating it as a scalar in a boolean context, so it returns $false
, observed by:
[bool]@($null)
Footnote: in powershell v2, there was a bug with $null
filtering which spawned the left-hand $null
comparison. This bug caused if/else
blocks to be skipped entirely.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With