Почему этот фильтр не может принять определенные значения $null из конвейера?

Контекст

Рассмотрим следующую вспомогательную функцию:

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

По сути, это оператор объединения с нулевым значением, реализованный как конвейерная функция. Он должен работать так:

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

Решение

Объяснение Ансгара верно (лучшее объяснение можно найти в ответ mklement0 на повторяющийся вопрос). Я просто хотел поделиться своим решением проблемы.

Я исправил If-Null таким образом, что он возвращает $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

person Good Night Nerd Pride    schedule 10.01.2017    source источник
comment
Я почти уверен, что @() - это массив нулевой длины, поэтому вы наблюдаете такое поведение. $a = @(); $a.GetType(). Я имею в виду, что вы передаете объект, и ваш фильтр правильно возвращает этот объект, который представляет собой массив нулевой длины.   -  person 4c74356b41    schedule 10.01.2017
comment
Но @() | select -f 1 не является массивом нулевого размера. PowerShell говорит мне, что это от -equal до $null.   -  person Good Night Nerd Pride    schedule 10.01.2017
comment
stackoverflow.com/questions/29614171/   -  person    schedule 10.01.2017


Ответы (1)


Массивы развернуты конвейерами, так что каждый элемент массива передается отдельно. Если вы передаете пустой массив в конвейер, он, по сути, превращается в ничто, а это означает, что подчиненный командлет никогда не вызывается, что оставляет вам пустую переменную.

Вы можете наблюдать это поведение, передавая $null и @() в цикл, который просто выводит строку для каждого входного элемента:

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

В зависимости от контекста это отличается от переменной со «значением» $null. Несмотря на то, что в большинстве случаев PowerShell автоматически преобразует «пустую» переменную в значение $null (как видно из ваших проверок), он не делает этого при передаче переменной в конвейер. В этом случае вы все равно ничего не передаете в конвейер, поэтому ваш фильтр никогда не вызывается.

person Ansgar Wiechers    schedule 10.01.2017