为什么此过滤器无法从管道中接受某些 $null 值?

Why does this filter fail to accept certain $null values from the pipeline?

上下文

考虑以下辅助函数:

Filter If-Null(
   [Parameter(ValueFromPipeline=$true)]$value,
   [Parameter(Position=0)]$default
) {
   Write-Verbose "If ($value) {$value} Else {$default}"
   if ($value) {$value} else {$default}
}

它基本上是作为管道函数实现的 null-coalescing operator。它应该像这样工作:

PS> $myVar = $null
PS> $myVar | If-Null "myDefault" -Verbose
VERBOSE: If () {} Else {myDefault}
myDefault

但是,当我将 $myVar 设置为空数组中的第一个元素时...

PS> $myVar = @() | Select-Object -First 1

...实际上应该与 $null...

相同
PS> $myVar -eq $null
True
PS> -not $myVar
True

...然后管道不再工作:

PS> $myVar | If-Null "myDefault" -Verbose

根本没有输出。甚至没有冗长的打印。这意味着 If-Null 甚至没有被执行。

问题

所以看起来 @() | select -f 1,虽然是 -eq$null,但 有点不同 $null 不知何故破坏管道?

任何人都可以解释这种行为吗?我错过了什么?

附加信息

PS> (@() | select -f 1).GetType()
You cannot call a method on a null-valued expression.
At line:1 char:1
+ (@() | select -f 1).GetType()
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

PS> (@() | select -f 1) | Get-Member
Get-Member : You must specify an object for the Get-Member cmdlet.
At line:1 char:23
+ (@() | select -f 1) | Get-Member
+                       ~~~~~~~~~~
    + CategoryInfo          : CloseError: (:) [Get-Member], InvalidOperationException
    + FullyQualifiedErrorId : NoObjectInGetMember,Microsoft.PowerShell.Commands.GetMemberCommand
PS> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      5.0.10586.117
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.10586.117
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1

解决方案

is correct (a better explanation can be found in 到重复的问题)。我只是想分享我对问题的解决方案。

我修复了 If-Null,即使没有任何处理,它也 returns $default:

Function If-Null(
    [Parameter(ValueFromPipeline = $true)]$value, 
    [Parameter(Position = 0)]$default
) {
    Process { 
        $processedSomething = $true
        If ($value) { $value } Else { $default } 
    }

    # This makes sure the $default is returned even when the input was an empty array or of
    # type [System.Management.Automation.Internal.AutomationNull]::Value (which prevents
    # execution of the Process block).
    End { If (-not $processedSomething) { $default }}
}

此版本现在可以正确处理空管道结果:

PS> @() | select -f 1 | If-Null myDefault
myDefault

数组通过管道展开,因此每个数组元素都单独传递。如果将一个空数组传递到管道中,它实际上将展开为空,这意味着永远不会调用下游 cmdlet,从而留下一个空变量。

您可以通过将 $null@() 传递到一个只为每个输入项回显一个字符串的循环中来观察此行为:

PS C:\> @() | % { 'foo' }    # no output here!
PS C:\> $null | % { 'foo' }  # output: foo
foo

根据上下文,这与带有 "value" $null 的变量不同。尽管在大多数情况下 PowerShell 会自动将 "empty" 变量转换为 $null 的值(如您的检查所示),但在将变量传递到管道时不会这样做。在那种情况下,您仍然不会将任何内容传递到管道中,因此永远不会调用您的过滤器。