这两个 $null 值为什么不同以及如何不同?

Why and how are these two $null values different?

显然,在 PowerShell(版本 3)中并非所有 $null 都是相同的:

    >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

那么 $l_t2$l_t3 有何不同?为什么?特别是 $l_t2 真的是 $null 吗?请注意 $l_t2 不是一个空数组($l_t1 是,而 $l_t1 -eq $null returns 没有,正如预期的那样),但它也不是真正的 $null,如 $l_t3。特别是 $l_t2.count returns 0 而不是错误,此外,将 $l_t2 添加到 $l_t 的行为就像添加一个空数组,而不是添加 $null。为什么 $l_t2 在作为参数传入函数 addToArray 时突然变成了 "more $null" ???????

任何人都可以解释这种行为,或者给我指出可以解释它的文档吗?

编辑: 下面 PetSerAl 的回答是正确的。 I have also found this Whosebug post on the same issue.

Powershell 版本信息:

    >$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                                                                                          

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

$l_t2 不是 $null,而是 [System.Management.Automation.Internal.AutomationNull]::Value。它是 PSObject 的一个特殊实例。当管道 returns 零个对象时返回。这就是您可以检查它的方式:

$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

我通过反射调用 ReferenceEquals 以防止 PowerShell 从 AutomationNull 转换为 $null。

$l_t1 -eq $null returns nothing

对我来说,它 returns 一个空数组,正如我所期望的那样。

$l_t2.count returns 0

这是一个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???????

在某些情况下,PowerShell 似乎会将 AutomationNull 转换为 $null,例如调用 .NET 方法。在 PowerShell v2 中,即使将 AutomationNull 保存到变量,它也会转换为 $null.

当您从 PowerShell 函数 return 集合时,默认情况下 PowerShell 确定 return 值的数据类型如下:

  • 如果集合有多个元素,return结果是一个数组。请注意,return 结果的数据类型是 System.Array,即使被 returned 的对象是不同类型的集合。
  • 如果集合只有一个元素,return结果是该元素的值,而不是一个元素的集合,return结果的数据类型是数据该元素的类型。
  • 如果集合为空,return结果为$null

$l_t = @() 将空数组分配给 $l_t

$l_t2 = emptyArray$null 赋值给 $l_t2,因为函数 emptyArray return 是一个空集合,因此 return 结果是 $null.

$l_t2$l_t3 均为 null,并且它们的行为方式相同。由于您已将 $l_t 预先声明为空数组,因此当您添加 $l_t2$l_t3 到它,使用 += 运算符或 addToArray 函数, 一个值为 **$null* 的元素被添加到数组中。

如果您想强制函数保留您正在 returning 的集合对象的数据类型,请使用逗号运算符:

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

注意:函数声明中emtpyArray后面的空括号是多余的。如果您使用它们来声明参数,则只需要在函数名称后加上括号。


需要注意的一个有趣的一点是,逗号运算符 并不一定 必须使 return 值成为数组。

回想一下,正如我在第一个要点中提到的,默认情况下,具有多个元素的集合的 return 结果的数据类型是 System.Array 无论集合的实际数据类型如何。例如:

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

注意这个集合的数据类型是List`1,不是System.Array

但是,如果您 return 它来自一个函数,在函数中 $list 的数据类型是 List`1,但它被 return 编辑为包含相同元素的 System.Array

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

如果您希望 return 结果是与您正在 returning 的函数中的数据类型相同的数据类型的集合,逗号运算符将实现这一点:

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

这不限于类似数组的集合对象。据我所知,任何时候 PowerShell 更改您正在 returning 的对象的数据类型,并且您希望 return 值保留对象的原始数据类型,您都可以这样做通过在被 returned 的对象前面加上逗号。我第一次遇到这个问题是在编写查询数据库并 returned DataTable 对象的函数时。 return 结果是哈希表数组而不是数据表。将 return $my_datatable_object 更改为 return ,$my_datatable_object 使函数 return 成为实际的 DataTable 对象。

实用摘要补充:

  • 恰好产生无输出的命令不会return$null,而是[System.Management.Automation.Internal.AutomationNull]::Value单例, 可以将其视为“数组值 $null”,或者,创造一个术语,空枚举。它有时也被称为“AutomationNull”,因为它的类型名称。

    • 请注意,由于 PowerShell 对集合的展开,即使是显式输出空集合对象的命令(例如 @())也有 no 输出(除非显式枚举阻止,例如 Write-Output -NoEnumerate).
  • 简而言之,这个特殊值 标量 上下文中表现得像 $null,并且像 枚举上下文中的空数组,特别是管道中的空数组,如下例所示。

注意事项:

  • [System.Management.Automation.Internal.AutomationNull]::Value作为cmdlet/函数传递参数值总是转换它到 $null.

  • PSv3+中,即使是实际(标量)$null也是不是foreach循环中枚举;它 在管道中枚举,但是 - 请参阅底部。

  • PSv2-中,变量中保存一个空枚举悄悄地把它转换成$null$null 也在 foreach 循环中被枚举 (不仅仅是在管道中) - 见底部。

# 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.
   

[System.Management.Automation.Internal.AutomationNull]::Value 文档指出:

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 与 PSv3+,以及普遍的不一致

PSv2 对于存储在 variables:

中的值没有区分 [System.Management.Automation.Internal.AutomationNull]::Value$null
  • foreach语句/管道中直接使用无输出命令did 按预期工作 - 没有通过管道发送任何内容/未输入 foreach 循环:

      Get-ChildItem nosuchfiles* | ForEach-Object { 'hi' }
      foreach ($f in (Get-ChildItem nosuchfiles*)) { 'hi' }
    
  • 相比之下,如果将无输出命令保存在变量中或使用显式$null,行为 不同:

      # 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: 所有以上命令输出一个以[=34开头的字符串=],因为$null 通过管道发送/被foreach枚举:
      与 PSv3+ 不同,[System.Management.Automation.Internal.AutomationNull]::Value 分配给变量 [=184 时 转换为 $null =],而 $null 在 PSv2.

      中总是
    • PSv3+行为在 PSv3 中发生了变化,无论是好是坏:

      • 更好没有通过管道发送枚举$resultforeach循环进入,因为[System.Management.Automation.Internal.AutomationNull]::Value保留 分配给变量时 ,与 PSv2 不同。

      • 可能更糟: foreach 不再枚举 $null(无论指定为文字或存储在变量中),因此 foreach ($f in $null) { 'hi4' } 可能会令人惊讶地产生 no 输出。
        从好的方面来说,新行为不再枚举 未初始化的变量 ,其计算结果为 $null(除非与 Set-StrictMode 一起阻止)。
        但是,一般来说,不枚举 $null 在 PSv2 中更合理,因为它无法将空集合值存储在变量中。

摘要中,PSv3+行为:

  • 取消了在 foreach 语句

    的上下文中区分 $null[System.Management.Automation.Internal.AutomationNull]::Value 的能力
  • 因此引入了与管道行为的不一致,其中这种区别

为了向后兼容,无法更改当前行为。 This comment on GitHub 针对不需要向后兼容的(假设的)潜在的未来 PowerShell 版本提出了解决这些不一致的方法。