使用Where-Object时出现意外行为

4

我刚刚碰到了一个意外的行为 Where-Object 的行为,我找不到任何解释:

$foo = $null | Where-Object {$false}
$foo -eq $null
> True


($null, 1 | Measure-Object).Count
> 1

($foo, 1 | Measure-Object).Count
> 1


($null, $null, 1 | Measure-Object).Count
> 1

($foo, $foo, 1 | Measure-Object).Count
> 0

如果Where-Object的条件为false,$foo应该是$null(这似乎是正确的)。

然而,在将任何值传递到管道之前,至少将$foo管道化两次似乎会破坏它。

是什么原因导致了这种情况?


其他不一致之处:

($foo, $null, 1 | Measure-Object).Count
> 1

($foo, $null, $foo, 1 | Measure-Object).Count
> 0

($null, $foo, $null, 1 | Measure-Object).Count
> 1

($foo, 1, $foo, $foo | Measure-Object).Count
> 1

($null, $foo, $null, $foo, 1 | Measure-Object).Count
> 0

4
$foo并不完全等同于$null,可以通过检查$foo.psbase来验证(真正的$null.psbase中没有任何内容)。这是运行时对包装对象进行奇怪处理的结果。 - Jeroen Mostert
3
也许更清晰的是 $foo -is [psobject],对于 ... | Where-Object {$False}$True,对于 $null$False。它是一个 PSObject,可以隐式转换为 $null,但并不完全透明。 - Jeroen Mostert
请参阅此SO问答。https://dev59.com/er_qa4cB1Zd3GeqPMqua#66397251 - postanote
2个回答

5

简述:

  • 并非所有表面上的$null值都相同,正如Jeroen Mostert的评论所示: PowerShell有两种null,它们在不同情况下的行为是不同的 - 请参见下一节。

  • 此外,您会看到也许令人惊讶的Measure-Object行为和一个流水线漏洞 - 请参阅底部部分。

    • 最好从测试命令中消除Measure-Object,直接在数组上调用.Count;例如,(在问题中创建null类型的最简单方法是:$foo = & {}):

      • ($foo, $null, 1).Count得到3
      • ($null, $foo, $null, $foo, 1).Count得到5
    • 如您所见,这两种null类型(如下所述)都可以成为数组的元素


PowerShell中有两种不同类型的null值:

  • 有真正的标量 null(例如在C#中对应的null)。

    • 这个null包含在自动的$null变量中。
    • .NET方法可以返回它。 (虽然PowerShell代码也可以输出它,但最好避免这样做)。
  • 还有可枚举"集合null"(也称为"AutomationNull",基于其类名),它在技术上是System.Management.Automation.Internal.AutomationNull.Value单例,它本身是一个[psobject]实例。

    • PowerShell命令(二进制cmdlet和PowerShell脚本/函数)没有产生输出时,此值技术上是由管道输出的。
    • 获取此值的最简单方法是使用& {},即执行一个空脚本块;当然,您也可以明确使用[System.Management.Automation.Internal.AutomationNull]::Value)。

很遗憾,截至PowerShell 7.2版本,区分集合null值和标量null值是非常困难的:

  • GitHub问题#13465提议在未来的PowerShell版本中允许通过$var -is [AutomationNull]检测集合null。

  • 目前,有几种解决方法用于测试给定值$var是否包含集合null;也许最简单的方法(但不明显)是:

    • $null -eq $var -and $var -is [psobject]仅当$var包含集合null值时才为$true,因为只有集合null才是技术上的一个对象。

行为差异

表达式上下文和参数绑定中,集合null会隐式转换为$null,两者没有区别。
需要注意的是,这意味着您无法将集合null作为参数传递 - 请参见GitHub问题#9150中的讨论。
在表达式的上下文中,支持集合作为LHS的运算符的异常情况:它们将集合null视为一个空集合,因此计算结果为一个空数组@()),而不是$null
例如,$var -replace 'foo' | ForEach-Object { 'hi' }仅在$var为标量$null时打印'hi',而不是集合null,因为-replace操作然后输出一个空数组,这个空数组不会通过管道发送任何东西。请参见GitHub问题#3866
管道中:
标量$null通过管道发送 - 它的行为类似于单个对象:$null | ForEach-Object { '$_ is $null? ' + ($null -eq $_) }打印'$_ is $null? True'
集合null不会通过管道发送 - 它的行为类似于没有元素的集合,就像@() | ForEach-Object { 'hi' }(发送一个空数组),& {} | ForEach-Object { 'hi' }不会通过管道发送任何东西,因为没有什么可以枚举,因此永远不会输出'hi'
奇怪的是,在foreach循环语句(与ForEach-Object cmdlet相对)中,标量$null不会被枚举,并且在以下示例中循环体不会执行(集合null同样如此):
foreach ($i in $null) { 'hi' }

Measure-Object和管道问题:

  • Measure-Object通常忽略$null,可能是有意设计的。

    • 这在GitHub问题#10905中讨论,建议引入-IncludeNull开关以支持基于“可选加入”的方法考虑$null值。(默认行为不会更改,以不破坏向后兼容性。)
  • 然而,你已经发现了PowerShell在处理涉及集合空值多对象输入方面存在一个明显的漏洞(截至PowerShell 7.1.2),仅由Measure-Object 显示,如你自己所指出的:

    • 在多对象输入中遇到第二个集合空值时,通过管道发送对象时,意外地停止

      • 例如:(1, (& {}), 2, (& {}), 3, 4, 5 | Measure-Object).Count只得到2:仅计算了12 (集合空值本身没有通过管道发送),因为第二个集合空值意外地停止了枚举,导致剩下的对象-345-甚至没有发送到Measure-Object
    • 请参见GitHub问题#14920


0

除了mklement0非常详细和受到赞赏的答案,我想分享一下我使用的解决方法:

$numbers = 3, 42, 7, 69, 13
$no1 = $numbers | Where-Object {$_ -eq 1}
$no2 = $numbers | Where-Object {$_ -eq 2}
$no3 = $numbers | Where-Object {$_ -eq 3}

不要直接将变量传递给 ForEach-Object,因为这样不会产生任何输出... :

$no1, $no2, $no3 | ForEach-Object {$_}
> 

...将变量names传递到ForEach-Object,并利用Get-Variable获取所需结果:

'no1', 'no2', 'no3' | ForEach-Object {(Get-Variable $_).Value}
> 3

1
谢谢;一个更简单但也更晦涩的解决方法是:[array] $no1 + $no2 + $no3 | ForEach-Object {$_}。通过使用数组连接,集合中的空值被有效地消除了。 - mklement0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接