0

I'm finally taking the time to understand why Visual Studio Code nags me to put $null on the left side of equality comparisons.

In playing around with this behavior, I found that when comparing an array containing $null elements to $null with $null on the right side, the "expected unexpected behavior" occurs with arrays containing two or more $null elements but not for arrays containing a single $null element. That is, if ($array -eq $null) { 'It equals $null!' } outputs It equals $null! if $array contains multiple $null elements, but not when $array contains only one. What is it about the number of $null elements that causes this discrepancy?

The following test code demonstrates this behavior...

function TestForNull($description, $value)
{
    $comparisonResult = $value -eq $null;
    $ifEntered = if ($value -eq $null) {
        $true;
    } else {
        $false;
    };

    Write-Host -Object $description;
    Write-Host -Object "`t`$comparisonResult.GetType(): $($comparisonResult.GetType())";
    Write-Host -Object "`t`$comparisonResult.Length: $($comparisonResult.Length)";
    for ($i = 0; $i -lt $comparisonResult.Length; $i++)
    {
        $item = $comparisonResult.GetValue($i);
        $itemText = if ($null -eq $item) {
            '(null)';
        } else {
            $item.ToString();
        };

        Write-Host -Object "`t`$comparisonResult[$i]: $itemText";
    }
    Write-Host -Object "`t`$ifEntered: $ifEntered";
}

TestForNull '0-element array'                          @();
TestForNull '1-element array with all $nulls'          @($null);
TestForNull '2-element array with all $nulls'          @($null, $null);
TestForNull '3-element array with all $nulls'          @($null, $null, $null);
TestForNull '3-element array with one leading $null'   @($null, 2, 3);
TestForNull '3-element array with one inner $null'     @(1, $null, 3);
TestForNull '3-element array with one trailing $null'  @(1, 2, $null);
TestForNull '3-element array with two leading $nulls'  @($null, $null, 3);
TestForNull '3-element array with two boundary $nulls' @($null, 2, $null);
TestForNull '3-element array with two trailing $nulls' @(1, $null, $null);

...and outputs...

0-element array
        $comparisonResult.GetType(): System.Object[]
        $comparisonResult.Length: 0
        $ifEntered: False
1-element array with all $nulls
        $comparisonResult.GetType(): System.Object[]
        $comparisonResult.Length: 1
        $comparisonResult[0]: (null)
        $ifEntered: False
2-element array with all $nulls
        $comparisonResult.GetType(): System.Object[]
        $comparisonResult.Length: 2
        $comparisonResult[0]: (null)
        $comparisonResult[1]: (null)
        $ifEntered: True
3-element array with all $nulls
        $comparisonResult.GetType(): System.Object[]
        $comparisonResult.Length: 3
        $comparisonResult[0]: (null)
        $comparisonResult[1]: (null)
        $comparisonResult[2]: (null)
        $ifEntered: True
3-element array with one leading $null
        $comparisonResult.GetType(): System.Object[]
        $comparisonResult.Length: 1
        $comparisonResult[0]: (null)
        $ifEntered: False
3-element array with one inner $null
        $comparisonResult.GetType(): System.Object[]
        $comparisonResult.Length: 1
        $comparisonResult[0]: (null)
        $ifEntered: False
3-element array with one trailing $null
        $comparisonResult.GetType(): System.Object[]
        $comparisonResult.Length: 1
        $comparisonResult[0]: (null)
        $ifEntered: False
3-element array with two leading $nulls
        $comparisonResult.GetType(): System.Object[]
        $comparisonResult.Length: 2
        $comparisonResult[0]: (null)
        $comparisonResult[1]: (null)
        $ifEntered: True
3-element array with two boundary $nulls
        $comparisonResult.GetType(): System.Object[]
        $comparisonResult.Length: 2
        $comparisonResult[0]: (null)
        $comparisonResult[1]: (null)
        $ifEntered: True
3-element array with two trailing $nulls
        $comparisonResult.GetType(): System.Object[]
        $comparisonResult.Length: 2
        $comparisonResult[0]: (null)
        $comparisonResult[1]: (null)
        $ifEntered: True
Lance U. Matthews
  • 15,725
  • 6
  • 48
  • 68

1 Answers1

2

We already (hopefully) know that $array -eq $null evaluates not to a [Boolean] but to an array containing the elements of $array that are equal to $null. As the condition of an if statement that resulting array must be converted to a [Boolean], and though I could not find any official PowerShell documentation detailing the specifics of how that happens, it is the peculiarities of this conversion that are causing the behavior in question. This is best illustrated not with if statements but simply with cast expressions...

PS> [Boolean] @()
False
PS> [Boolean] @($null)
False
PS> [Boolean] @($null, $null)
True
PS> [Boolean] @($null, $null, $null)
True
PS> [Boolean] @(New-Object -TypeName 'Object')
True
PS> [Boolean] @((New-Object -TypeName 'Object'), (New-Object -TypeName 'Object'))
True
PS> [Boolean] @($true)
True
PS> [Boolean] @($false)
False
PS> [Boolean] @($true, $true)
True
PS> [Boolean] @($true, $false)
True
PS> [Boolean] @($false, $true)
True
PS> [Boolean] @($false, $false)
True
PS> [Boolean] @(0)
False
PS> [Boolean] @(1)
True
PS> [Boolean] @(-1)
True
PS> [Boolean] @('')
False
PS> [Boolean] @('false')
True
PS> [Boolean] @('true')
True
PS> [Boolean] @('1')
True
PS> [Boolean] @('0')
True
PS> [Boolean] @('-1')
True

In case there's no discernible pattern there, the logic is...

  1. Empty arrays are cast to $false.
  2. Arrays with one element are cast to...
    • ...the length of that element as a [Boolean] if that element implements the IList interface.
    • ...the result of casting that element to [Boolean] if that element does not implement the IList interface.
  3. Arrays with multiple elements are cast to $true.

Thus, given that $array contains one $null element (e.g. $array = @($null)), $array -eq $null returns @($null). When converting @($null) to a [Boolean] rule 2 above applies: $null casts to $false, therefore @($null) casts to $false.

Given that $array contains multiple $null elements (e.g. $array = @($null, $null)), $array -eq $null returns an array with the same number of (and, most importantly, multiple) $null elements. The fact that $null casts to $false is irrelevant here because by rule 3 such arrays always cast to $true.

This is why it's significant whether an array contains one or multiple $null elements as far as the pitfall of performing a comparison with $null and passing the array as the first operand.

Additional Reading:

Lance U. Matthews
  • 15,725
  • 6
  • 48
  • 68
  • 1
    @mklement0 I agree that this an exact duplicate (or pretty close to it) of [Why do PowerShell comparison operators not enumerate collections of size 1?](https://stackoverflow.com/q/53107870/150605). In looking for potential duplicates I had included "array", not "collection", in my search terms, which is probably why I did not come across that question. It's interesting to me that so many of the pertinent questions on this matter are all from the last few months. – Lance U. Matthews Dec 22 '18 at 21:30