Why and how do these two $ null values ​​differ?

Apparently, in PowerShell (version 3), not all $null matches:

  >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 NOT an empty array ( $l_t1 is, and $l_t1 -eq $null returns nothing as expected), but this is not true $null , for example $l_t3 . In particular, $l_t2.count returns 0 instead of an error, and in addition, adding $l_t2 to $l_t behaves like adding an empty array, rather than adding $null . And why does $l_t2 suddenly become "more than $null " when it is passed to the addToArray function as a parameter ???????

Can someone explain this behavior or point me to documentation that will explain this?

Edit: The following is the correct answer from PetSerAl. stack overflow

Powershell Version Information:

  >$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 
+7
powershell
source share
3 answers

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

$l_t2 not $null , but << 24>. This is a special instance of PSObject . It returns when the pipeline returns null objects. Here is how you can verify this:

 $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 the conversion from AutomationNull to $ null using PowerShell.

$l_t1 -eq $null returns nothing

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

$l_t2.count returns 0

This is the new PowerShell v3 feature:

Now you can use Count or Length for any object, even if it did not have this property. If the object did not have the Count or Length property, it will return 1 (or 0 for $ null). Objects that have Count or Length properties will continue to work as they always are.

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

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

It seems that PowerShell in some cases converts AutomationNull to $null , for example, calls .NET methods. In PowerShell v2, even when you save the AutomationNull variable, it is converted to $null .

+13
source share

To complement PetSerAl's excellent answer with a pragmatic resume :

  • Commands that do not produce output do not return $null , but [System.Management.Automation.Internal.AutomationNull]::Value singleton which can be thought of as "array-valued $null " or, to generate a member, null collection .

    • Note that due to the expansion of PowerShell collections, even a command that explicitly displays an empty collection object, such as @() , has no way out (unless enumeration is explicitly forbidden, for example, with Write-Output -NoEnumerate ).
  • In short, this special value behaves like $null in scalar contexts and is like an empty array in array / pipeline contexts , as the examples below show.

Cautions :

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

  • In PSv3 +, even the actual (scalar) $null not listed in the foreach ; it is listed in the conveyor, however - see below.

  • PSv2 - , saving the null collection in a variable quietly converted to $null and $null was listed in foreach , as well (not only in the pipeline) - see the bottom.
 # A true $null value: $v1 = $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. $v2 = & {} # 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 $v2 $v1 -eq $v2 $null -eq [System.Management.Automation.Internal.AutomationNull]::Value & { param($param) $null -eq $param } $v2 # 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. $v1 | % { '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). $v2 | % { '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: (@() + $v2).Count # -> 0 - contrast with (@() + $v1).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)) } $v2 # -> $true; would be the same with $v1 or no argument at all. 

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

Any operation that does not return any valid value should return AutomationNull.Value.

Any component that evaluates a Windows PowerShell expression should be prepared to process and discard this result. Upon receipt in an estimate where a value is required, it must be replaced by zero.


PSv2 vs PSv3 + :

PSv2 did not offer a distinction between [System.Management.Automation.Internal.AutomationNull]::Value and $null for values ​​stored in variables:

  • Using the no-output command directly in the foreach / pipeline works as expected - nothing was sent along the pipeline / foreach not entered:

     Get-ChildItem nosuchfiles* | ForEach-Object { 'hi' } foreach ($f in (Get-ChildItem nosuchfiles*)) { 'hi' } 
  • In contrast , if commands without output were stored in a variable, or explicit $null , 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 print a line starting with hi because $null is pipelined / listed by foreach :
      Unlike PSv3 +, [System.Management.Automation.Internal.AutomationNull]::Value converted to $null when a variable is assigned , and $null always listed in PSv2 .

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

      • Better : Nothing is pipelined for commands listing $result : a foreach not entered because [System.Management.Automation.Internal.AutomationNull]::Value saved when a variable is assigned , unlike PSv2.

      • Perhaps worse: foreach no longer lists $null (regardless of whether it is specified as a literal or stored in a variable), so foreach ($f in $null) { 'hi4' } may unexpectedly fail.
        Perhaps, on the positive side, the new behavior no longer lists an uninitialized variable as $null .
        In the general case, however, not listing $null would be more justified in PSv2, given its inability to store the null-collection value in a variable.

In summary, the behavior of PSv3 + :

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

  • thereby introducing inconsistency with the behavior of the pipeline, where this difference is respected.

+4
source share

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 item, the return result is an array. Note that the data type of the return result is System.Array , even if the returned object is a collection of another type.
  • If the collection has one element, the result of the return is the value of this element, and not the collection of one element, and the data type of the return result is the data type of this element.
  • If the collection is empty, the return result is $ null

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

$l_t2 = emptyArray sets $ null to $ l_t2 because the emptyArray function returns an empty collection, so the result of returning $ is zero .

$ l_t2 and $ l_t3 are zero, and they behave the same. Since you previously declared $ l_t as an empty array, adding either $ l_t2 or $ l_t3 to it , or using + = or the addToArray function, an element whose value is ** $ null * is added to the array.

If you want to force the function to save the data type of the returned collection object, 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. Empty parentheses after emtpyArray in the function declaration are redundant. You only need the brackets after the function name if you use them to declare parameters.


An interesting point is that the comma operator does not necessarily return the return value to the array.

Recall that, as I mentioned in the first paragraph, the default data type is the result of returning a collection with more than one System.Array element, 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, inside the function the data type $ list will be List`1 , but it will be 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 in the returned function, the comma operator will do this:

 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 is not limited to massive collection objects. As far as I saw, at any time when PowerShell changes the data type of the returned object and you want the return value to keep the original data type of the object, you can do this before the object is returned with a comma, I first encountered this problem when writing a function. which requested the database and returned a DataTable. The return result was an hashtables array instead of a DataTable. Changing return $my_datatable_object to return ,$my_datatable_object made the function return the actual DataTable.

+2
source share

All Articles