12

Apparently, in PowerShell (ver. 3) not all $null's are the same:

    >function emptyArray() { @() }
    >$l_t = @() ; $l_t.Count
0
    >$l_t1 = @(); $l_t1 -eq $null; $l_t1.count; $l_t1.gettype()
0
IsPublic IsSerial Name                                     BaseType                                                         
-------- -------- ----                                     --------                                                         
True     True     Object[]                                 System.Array                                                     
    >$l_t += $l_t1; $l_t.Count
0
    >$l_t += emptyArray; $l_t.Count
0
    >$l_t2 = emptyArray; $l_t2 -eq $null; $l_t2.Count; $l_t2.gettype()
True
0
You cannot call a method on a null-valued expression.
At line:1 char:38
+ $l_t2 = emptyArray; $l_t2 -eq $null; $l_t2.Count; $l_t2.gettype()
+                                      ~~~~~~~~~~~~~~~
  + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
  + FullyQualifiedErrorId : InvokeMethodOnNull
    >$l_t += $l_t2; $l_t.Count
0
    >$l_t3 = $null; $l_t3 -eq $null;$l_t3.gettype()
True
You cannot call a method on a null-valued expression.
At line:1 char:32
+ $l_t3 = $null; $l_t3 -eq $null;$l_t3.gettype()
+                                ~~~~~~~~~~~~~~~
  + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
  + FullyQualifiedErrorId : InvokeMethodOnNull
    >$l_t += $l_t3; $l_t.count
1
    >function addToArray($l_a, $l_b) { $l_a += $l_b; $l_a.count }
    >$l_t = @(); $l_t.Count
0
    >addToArray $l_t $l_t1
0
    >addToArray $l_t $l_t2
1

So how and why is $l_t2 different from $l_t3? In particular, is $l_t2 really $null or not? Note that $l_t2 is NOT an empty array ($l_t1 is, and $l_t1 -eq $null returns nothing, as expected), but neither is it truly $null, like $l_t3. In particular, $l_t2.count returns 0 rather than an error, and furthermore, adding $l_t2 to $l_t behaves like adding an empty array, not like adding $null. And why does $l_t2 suddenly seem to become "more $null" when it gets passed in the the function addToArray as a parameter???????

Can anyone explain this behaviour, or point me to documentation that would explain it?

Edit: The answer by PetSerAl below is correct. I have also found this stackOverflow post on the same issue.

Powershell version info:

    >$PSVersionTable
Name                           Value                                                                                        
----                           -----                                                                                        
WSManStackVersion              3.0                                                                                          
PSCompatibleVersions           {1.0, 2.0, 3.0}                                                                              
SerializationVersion           1.1.0.1                                                                                      
BuildVersion                   6.2.9200.16481                                                                               
PSVersion                      3.0                                                                                          
CLRVersion                     4.0.30319.1026                                                                               
PSRemotingProtocolVersion      2.2                                                                                          
mklement0
  • 382,024
  • 64
  • 607
  • 775
David I. McIntosh
  • 2,038
  • 4
  • 23
  • 45
  • 1
    `$l_t3.count` returns `0` and not an error too. You just don't have `$l_t3.count` in your snippet. You skip it and go straight to `$l_t3.gettype()` which does error. `$l_t2` is `$null` because powershell unrolls arrays in function return values so your empty array becomes an empty list of returned values. I'm not sure about the other points but powershell is funny with arrays and null in my experience as well (as well as in a number of other places). – Etan Reisner May 03 '15 at 18:12
  • @etanReisner: Correct about `$l_t3.count` - I missed that. But `$l_t2` is still not the same as `$l_t3`, as can be seen by the results of adding it to `$l_t`. Such difference in behaviour can make things fail in unexpected ways. I don't really know what `$l_t2` is. – David I. McIntosh May 03 '15 at 18:25
  • Yeah, I'm not sure what the deal with that is. There are probably ways to introspect what is stored there but I don't know what they are. In my, admittedly limited, experience powershell is just occasionally confusing and inscrutable especially regarding arrays, function returns and `$null`. – Etan Reisner May 03 '15 at 19:05
  • This is caused by the way PowerShell handles return results from a function. The data type of the return value isn't necessarily the same as the data type of the object you're returning. Your **emptyArray** function's return value is **$null**, not an empty array. I'll write up a more complete answer. – Adi Inbar May 03 '15 at 19:24
  • Possible duplicate of [Why is an empty PowerShell pipeline not the same as null?](http://stackoverflow.com/questions/22343187/why-is-an-empty-powershell-pipeline-not-the-same-as-null) – Nat Jan 26 '16 at 21:45
  • @Nat That was mentioned in the "Edit" at the end. – David I. McIntosh Jan 28 '16 at 02:01

3 Answers3

19

In particular, is $l_t2 really $null or not?

$l_t2 is not $null, but a [System.Management.Automation.Internal.AutomationNull]::Value. It is a special instance of PSObject. It is returned when a pipeline returns zero objects. That is how you can check it:

$a=&{} #shortest, I know, pipeline, that returns zero objects
$b=[System.Management.Automation.Internal.AutomationNull]::Value

$ReferenceEquals=[Object].GetMethod('ReferenceEquals')

$ReferenceEquals.Invoke($null,($a,$null)) #returns False
$ReferenceEquals.Invoke($null,($a,$b))    #returns True

I call ReferenceEquals thru Reflection to prevent conversion from AutomationNull to $null by PowerShell.

$l_t1 -eq $null returns nothing

For me it returns an empty array, as I expect from it.

$l_t2.count returns 0

It is a new feature of PowerShell v3:

You can now use Count or Length on any object, even if it didn’t have the property. If the object didn’t have a Count or Length property, it will will return 1 (or 0 for $null). Objects that have Count or Length properties will continue to work as they always have.

PS> $a = 42 
PS> $a.Count 
1

 

And why does $l_t2 suddenly seem to become "more $null" when it gets passed in the the function addToArray as a parameter???????

It seems that PowerShell converts AutomationNull to $null in some cases, like calling .NET methods. In PowerShell v2, even when saving AutomationNull to a variable it gets converted to $null.

user4003407
  • 21,204
  • 4
  • 50
  • 60
  • You are correct that `$l_t -eq $null' returns an empty array. I was sloppy in my language. When I said "it returns nothing", I meant it returns nothing to display on the output stream (indicating it is an empty array, as you point out). Thanks for this post, btw. Very interesting info. – David I. McIntosh May 04 '15 at 17:57
  • BTW, how can one see that `$l_t2` is this `[System.Management.Automation.Internal.AutomationNull]::Value` ? I did this: `$l_t4 = [System.Management.Automation.Internal.AutomationNull]::Value`, and then `$l_t4` behaves identically to `$l_t2`, so what you say certainly bears up under testing. – David I. McIntosh May 04 '15 at 18:36
  • @DavidI.McIntosh I add some info about checking that `$l_t2` is `AutomationNull`. – user4003407 May 04 '15 at 19:03
  • You can distinguish between AutomationNull.Value and $null by wrapping your value in array enclosures. No reflection is required. For example: ```PowerShell $x = $null; $y = [System.Management.Automation.Internal.AutomationNull]::Value; foreach ($item in 'x','y') { $value = Get-Variable -Name $item -ValueOnly; if ($value -eq $null) { if (@($value).Count -eq 0) { "`$${item} is [System.Management.Automation.Internal.AutomationNull]::Value" } else { "`$${item} is `$null" } } } ``` – Kirk Munro May 30 '17 at 20:06
  • @KirkMunro Actually I use `ReferenceEquals` not as way to distinguish `$null` from `AutomationNull`, but as way to proof, that `&{}` return `AutomationNull` but not something else. – user4003407 May 31 '17 at 17:49
  • @PetSerAl My point is that you can do the same proof like this: ```@(& {}).Count```. If that returns 0, you got back AutomationNull. Otherwise, you got back something else. – Kirk Munro Jun 02 '17 at 21:02
  • 1
    @KirkMunro Then you first need to proof that no other value can produce the same result. – user4003407 Jun 02 '17 at 21:12
  • @PetSerAl Well technically you can get the same result from an empty array. e.g. ```@(@()).Count```. Not much of a problem since PowerShell unwraps arrays, but if you really want to distinguish between AutomationNull and everything else, you can just expand on what I consider the quick test, like this: ```$value = @{}; $value -eq $null -and @($value).Count -eq 0 # If true, $value is AutomationNull```. – Kirk Munro Jun 05 '17 at 01:46
  • 1
    That is true only if `$null` and `AutomationNull` are the only two values, which can pass `$value -eq $null` condition. For example: `$value = ($null, $null).GetEnumerator(); $value -eq $null -and @($value).Count -eq 0`. Yes, I can come with the set of simple rules, which reliable filter out `AutomationNull` only, but then I will need to provide the proof, that my set of rules actually doing exactly that and not allow some rogue value to slip in. I just think, that direct reference comparison with `AutomationNull` is better suited for this task. @KirkMunro – user4003407 Jun 06 '17 at 05:01
13

To complement PetSerAl's great answer with a pragmatic summary:

  • Commands that happen to produce no output do not return $null, but the [System.Management.Automation.Internal.AutomationNull]::Value singleton, which can be thought of as an "array-valued $null" or, to coin a term, null enumeration. It is sometimes also called "AutomationNull", for its type name.

    • Note that, due to PowerShell's automatic enumeration of collections, even a command that explicitly outputs an empty collection object such as @() has no output (unless enumeration is explicitly prevented, such as with Write-Output -NoEnumerate).
  • In short, this special value behaves like $null in scalar contexts, and like an empty array in enumeration contexts, notably in the pipeline, as the examples below demonstrate.

    • Given that $null and the null enumeration situationally behave differently, distinguishing between the two via reflection may be necessary, which is currently far from trivial; GitHub issue #13465 proposes implementing a test that would allow you to use $someValue -is [AutomationNull].
      As of PowerShell 7.3.0, the following, obscure test is required:
      $null -eq $someValue -and $someValue -is [psobject]

Caveats:

  • Passing [System.Management.Automation.Internal.AutomationNull]::Value as a cmdlet / function parameter value invariably converts it to $null.

  • In PSv3+, even an actual (scalar) $null is not enumerated in a foreach loop; it is enumerated in a pipeline, however - see bottom.

  • In PSv2-, saving a null enumeration in a variable quietly converted it to $null and $null was enumerated in a foreach loop as well (not just in a pipeline) - see bottom.

# A true $null value:
$trueNull = $null  

# An operation with no output returns
# the [System.Management.Automation.Internal.AutomationNull]::Value singleton,
# which is treated like $null in a scalar expression context, 
# but behaves like an empty array in a pipeline or array expression context.
$automationNull = & {}  # calling (&) an empty script block ({}) produces no output

# In a *scalar expression*, [System.Management.Automation.Internal.AutomationNull]::Value 
# is implicitly converted to $null, which is why all of the following commands
# return $true.
$null -eq $automationNull
$trueNull -eq $automationNull
$null -eq [System.Management.Automation.Internal.AutomationNull]::Value
& { param($param) $null -eq $param } $automationNull

# By contrast, in a *pipeline*, $null and
# [System.Management.Automation.Internal.AutomationNull]::Value
# are NOT the same:

# Actual $null *is* sent as data through the pipeline:
# The (implied) -Process block executes once.
$trueNull | % { 'input received' } # -> 'input received'

# [System.Management.Automation.Internal.AutomationNull]::Value is *not* sent 
# as data through the pipeline, it behaves like an empty array:
# The (implied) -Process block does *not* execute (but -Begin and -End blocks would).
$automationNull | % { 'input received' } # -> NO output; effectively like: @() | % { 'input received' }

# Similarly, in an *array expression* context
# [System.Management.Automation.Internal.AutomationNull]::Value also behaves
# like an empty array:
(@() + $automationNull).Count # -> 0 - contrast with (@() + $trueNull).Count, which returns 1.

# CAVEAT: Passing [System.Management.Automation.Internal.AutomationNull]::Value to 
# *any parameter* converts it to actual $null, whether that parameter is an
# array parameter or not.
# Passing [System.Management.Automation.Internal.AutomationNull]::Value is equivalent
# to passing true $null or omitting the parameter (by contrast,
# passing @() would result in an actual, empty array instance).
& { param([object[]] $param) 
    [Object].GetMethod('ReferenceEquals').Invoke($null, @($null, $param)) 
  } $automationNull  # -> $true; would be the same with $trueNull or no argument at all.
   

The [System.Management.Automation.Internal.AutomationNull]::Value documentation states:

Any operation that returns no actual value should return AutomationNull.Value.

Any component that evaluates a Windows PowerShell expression should be prepared to deal with receiving and discarding this result. When received in an evaluation where a value is required, it should be replaced with null.


PSv2 vs. PSv3+, and general inconsistencies:

PSv2 offered no distinction between [System.Management.Automation.Internal.AutomationNull]::Value and $null for values stored in variables:

  • Using a no-output command directly in a foreach statement / pipeline did work as expected - nothing was sent through the pipeline / the foreach loop wasn't entered:

      Get-ChildItem nosuchfiles* | ForEach-Object { 'hi' }
      foreach ($f in (Get-ChildItem nosuchfiles*)) { 'hi' }
    
  • By contrast, if a no-output commands was saved in a variable or an explicit $null was used, the behavior was different:

      # Store the output from a no-output command in a variable.
      $result = Get-ChildItem nosuchfiles* # PSv2-: quiet conversion to $null happens here
    
      # Enumerate the variable.
      $result | ForEach-Object { 'hi1' }
      foreach ($f in $result) { 'hi2' }
    
      # Enumerate a $null literal.
      $null | ForEach-Object { 'hi3' }
      foreach ($f in $null) { 'hi4' }
    
    • PSv2: all of the above commands output a string starting with hi, because $null is sent through the pipeline / being enumerated by foreach:
      Unlike in PSv3+, [System.Management.Automation.Internal.AutomationNull]::Value is converted to $null on assigning to a variable, and $null is always enumerated in PSv2.

    • PSv3+: The behavior changed in PSv3, both for better and worse:

      • Better: Nothing is sent through the pipeline for the commands that enumerate $result: The foreach loop is not entered, because the [System.Management.Automation.Internal.AutomationNull]::Value is preserved when assigning to a variable, unlike in PSv2.

      • Possibly Worse: foreach no longer enumerates $null (whether specified as a literal or stored in a variable), so that foreach ($f in $null) { 'hi4' } perhaps surprisingly produces no output.
        On the plus side, the new behavior no longer enumerates uninitialized variables, which evaluate to $null (unless prevented altogether with Set-StrictMode).
        Generally, however, not enumerating $null would have been more justified in PSv2, given its inability to store the null-collection value in a variable.

In summary, the PSv3+ behavior:

  • takes away the ability to distinguish between $null and [System.Management.Automation.Internal.AutomationNull]::Value in the context of a foreach statement

  • thereby introduces an inconsistency with pipeline behavior, where this distinction is respected.

For the sake of backward compatibility, the current behavior cannot be changed. This comment on GitHub proposes a way to resolve these inconsistencies for a (hypothetical) potential future PowerShell version that needn't be backward-compatible.

mklement0
  • 382,024
  • 64
  • 607
  • 775
3

When you return a collection from a PowerShell function, by default PowerShell determines the data type of the return value as follows:

  • If the collection has more than one element, the return result is an array. Note that the data type of the return result is System.Array even if the object being returned is a collection of a different type.
  • If the collection has a single element, the return result is the value of that element, rather than a collection of one element, and the data type of the return result is the data type of that element.
  • If the collection is empty, the return result is $null

$l_t = @() assigns an empty array to $l_t.

$l_t2 = emptyArray assigns $null to $l_t2, because the function emptyArray returns an empty collection, and therefore the return result is $null.

$l_t2 and $l_t3 are both null, and they behave the same way. Since you've pre-declared $l_t as an empty array, when you add either $l_t2 or $l_t3 to it, either with the += operator or the addToArray function, an element whose value is **$null* is added to the array.

If you want to force the function to preserve the data type of the collection object you're returning, use the comma operator:

PS> function emptyArray {,@()}
PS> $l_t2 = emptyArray
PS> $l_t2.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

PS> $l_t2.Count
0

Note: The empty parentheses after emtpyArray in the function declaration is superfluous. You only need parentheses after the function name if you're using them to declare parameters.


An interesting point to be aware of is that the comma operator doesn't necessarily make the return value an array.

Recall that as I mentioned in the first bullet point, by default the data type of the return result of a collection with more than one element is System.Array regardless of the actual data type of the collection. For example:

PS> $list = New-Object -TypeName System.Collections.Generic.List[int]
PS> $list.Add(1)
PS> $list.Add(2)
PS> $list.Count
2
PS> $list.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     List`1                                   System.Object

Note that the data type of this collection is List`1, not System.Array.

However, if you return it from a function, within the function the data type of $list is List`1, but it's returned as a System.Array containing the same elements.

PS> function Get-List {$list = New-Object -TypeName System.Collections.Generic.List[int]; $list.Add(1); $list.Add(2); return $list}
PS> $l = Get-List
PS> $l.Count
2
PS> $l.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

If you want the return result to be a collection of the same data type as the one within the function that you're returning, the comma operator will accomplish that:

PS> function Get-List {$list = New-Object -TypeName System.Collections.Generic.List[int]; $list.Add(1); $list.Add(2); return ,$list}
PS> $l = Get-List
PS> $l.Count
2
PS> $l.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     List`1                                   System.Object

This isn't limited to array-like collection objects. As far as I've seen, any time PowerShell changes the data type of the object you're returning, and you want the return value to preserve the object's original data type, you can do that by preceding the object being returned with a comma. I first encountered this issue when writing a function that queried a database and returned a DataTable object. The return result was an array of hashtables instead of a DataTable. Changing return $my_datatable_object to return ,$my_datatable_object made the function return an actual DataTable object.

Adi Inbar
  • 12,097
  • 13
  • 56
  • 69
  • 1
    Lots of interesting information here, but you are wrong on some key points. The point of my post is that `$l_t2` is _not_ a `$null`. As evidence that `$l_t2` is `$null`, you state "when you add ... `$l_t2` ... to `$l_t` ... an element whose value is ... `$null` ... is added to the array" This is what one would expect, but it is precisely this that is NOT true and the point of my entire post. `$l_t += $l_t2; $l_t.Count` returns 0, not 1, where-as `$l_t += $l_t3; $l_t.Count` returns 1, as expected, not 0. The behaviour is different, ergo `$l_t2` and `$l_t3` are different. `$l_t2` is NOT `$null`. – David I. McIntosh May 04 '15 at 17:50
  • Indeed there is an interesting information here, but it is incidental to the question, and the part that does touch on the question describes behavior that seemingly only applied _up to PSv2_ (whereas the question is about v3). As it stands, this answer contradicts the (truthfully reported) PSv3+ behavior described in the question. @AdiInbar: Can I suggest you add a preamble noting these points to your answer? – mklement0 Jan 10 '17 at 14:08