Search code examples
powershellnull

What is the difference between $null and the empty output of Where-Object?


Consider the following:

$psversiontable # 7.4.6

$a = $null
$null -eq $a # Returns $true
$a.GetType() # You cannot call a method on a null-valued expression.
$b = $a.PSObject.Copy() # You cannot call a method on a null-valued expression.

$c = Get-Process | Where-Object { $_.Name -eq "oiawmij3209j23oifnoeicn" } # Matches nothing, should return nothing
$null -eq $c # Returns $true
$c.GetType() # You cannot call a method on a null-valued expression.
$d = $c.PSObject.Copy() # Works
$d.GetType() # PSCustomObject

My questions:

  • What is the difference between $a and $c in this situation? They both apparently equate to $null.
  • Why does $c.PSObject.Copy() work when $a.PSObject.Copy() doesn't?
  • Why does $c.PSObject.Copy() work when $c.GetType() doesn't?
  • What causes this? Is it something to do with Where-Object, or to do with PSObject.Copy(), or something else?
  • How can I/should I reliably differentiate between/test for these cases?

Thanks for your time.

Edit: a comment asked why I'm using the PSObject property. It's to avoid this problem:

# $hash2 is a "copy" of $hash1, but both variables actually just contain references to the same object in memory, so modifying $hash2 also modifies $hash1
$hash1 = @{ foo = "apple" }
$hash2 = $hash1
$hash2.bar = "banana"
Write-Host $hash1.bar # Returns "banana"

# $hash4 is a "real" copy of $hash3, i.e. each variable contains a reference to a different object in memory, thus modifying $hash4 does NOT also modify $hash3
$hash3 = @{ foo = "apple" }
$hash4 = $hash3.PSObject.Copy()
$hash4.bar = "banana"
Write-Host $hash3.bar # Returns nothing

In the relevant production code there may be better ways to work around this, but that's how I'm currently doing it, and what spawned the question.


Solution

  • This question was originally (and reasonably) closed as a duplicate of the following questions:

    However @mklement0 and others provide a lot of details in comments in this question which deserve to be captured in a more visible and collected answer, though they may be repeated somewhat in mklement0's answers to the other questions. As I (OP) am the learner in this situation, feel free to edit the answer/provide corrections.

    The core of this question (and the linked duplicates) revolves around the technical differences between $null and "automation null" ([System.Management.Automation.Internal.AutomationNull]::Value), a.k.a. "the enumerable null".

    To answer the direct questions in the OP:

    • What is the difference between $a and $c in this situation?

      • $a is $null, while $c is automation null. Automation null is returned via a pipeline that has no results, which can be achieved in different ways, as demonstrated by the different questions:
        • Get-Process | Where-Object { $_.Name -eq "oiawmij3209j23oifnoeicn" }
        • 1,2,3,4 | ? { $_ -ge 5 }
        • & { }
    • Why does $c.PSObject.Copy() work when $a.PSObject.Copy() doesn't?

      • Because $c, as automation null is technically a [PSObject] with a PSObject property, while $a is just $null.
      • However the fact that $c.PSObject can be accessed is arguably a bug, see further context below.
    • Why does $c.PSObject.Copy() work when $c.GetType() doesn't?

      • In the former context you are simply accessing an existing property of $c.
      • The latter context is an expression, where $c is effectively being evaluated to $null, and is thus equivalent to $null.GetType().
    • What causes this? Is it something to do with Where-Object, or to do with PSObject.Copy(), or something else?

      • As noted, automation null is being returned by the pipeline where Where-Object returns no results.
    • How can I/should I reliably differentiate between/test for these cases?

      • In the current implementation, automation null is exposed as a [PSObject], and so detecting it, and differentiating it from $null can be achieved with ($null -eq $value) -and ($value -is [PSObject]).
      • Alternatively, you can probe the enumerability like so: ($null -eq $value) -and (@($value).length -eq 0), or ($null -eq $value) -and (@($value).count -eq 0).
        • The length/count of an array containing automation null will be 0, while that of @($null) will be 1 (for reasons someone more experienced would have to explain).
      • However see below about potential future improvements, which would presumably break one or both of these detection methods.

    Further context:

    • Currently, automation null behaves as $null in expressions, but acts differently in enumeration contexts such as the pipeline, where it acts like an empty collection and therefore enumerates nothing.
    • The fact that the enumerable null even surfaces as an object in PowerShell code - enabling things like (& {}).psobject and (& {}) -is [psobject] - is arguably a bug; a leaky implementation of an abstraction which reveals the fact that it is technically an object (of type [psobject] a.k.a. [System.Management.Automation.PSObject], namely the aforementioned [System.Management.Automation.Internal.AutomationNull]::Value singleton).
    • A green-lit enhancement that has yet to be implemented (as of PowerShell 7.5.0) will enable a conceptually appropriate test for whether a given value is the enumerable null: $value -is [System.Management.Automation.Null].
    • As noted, see the duplicates linked above for more detail in mklement0's various answers.