【问题标题】:Why is PowerShell applying the predicate of a `Where` to an empty list为什么 PowerShell 将“Where”的谓词应用于空列表
【发布时间】:2019-04-08 14:15:49
【问题描述】:

如果我在 PowerShell 中运行它,我希望看到输出 0(零):

Set-StrictMode -Version Latest

$x = "[]" | ConvertFrom-Json | Where { $_.name -eq "Baz" }
Write-Host $x.Count

相反,我得到了这个错误:

The property 'name' cannot be found on this object. Verify that the     property exists and can be set.
At line:1 char:44
+     $x = "[]" | ConvertFrom-Json | Where { $_.name -eq "Baz" }
+                                            ~~~~~~~~~~~~~~~
+ CategoryInfo          : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : PropertyAssignmentException

如果我在"[]" | ConvertFrom-Json 周围加上大括号,它会变成这样:

$y = ("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" }
Write-Host $y.Count

然后它“工作”。

在引入括号之前有什么问题?

解释“works”周围的引号 - 设置严格模式 Set-StrictMode -Version Latest 表示我在 $null 对象上调用 .Count。这可以通过包装@()来解决:

$z = @(("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" })
Write-Host $z.Count

我觉得这很不满意,但这是对实际问题的补充。

【问题讨论】:

  • 首先=不是-eq!第二个变体“有效”,因为从未评估过Where(集合为空)。将"[]" 替换为"[{}]" 以获得更多信息。至于为什么这同样不适用于第一个变体(即为什么有一个管道,并且应用了Where)——这更有趣,并且可能与ConvertFrom-Json 的微妙之处有关。 ..
  • 投票结束为错字。问题在于=,仅此而已。
  • @TheIncorrigible1 错字已修复。还取消了设置严格模式,我省略了。问题依然存在。
  • 如果您尝试访问不存在的属性,严格模式会导致引发异常。如果您想避免这种情况,您应该使用其他参数集之一,例如:| ? Name -eq Baz
  • 为了消除对这个问题的潜在混淆:问题是为什么 JSON 输入在 PowerShell 中通过 ConvertFrom-Json 变成 空数组 出人意料地仍然通过管道并执行 Where-Object 脚本块,而如果您直接使用空数组 (@() | Where ...),则不会执行此操作。

标签: arrays powershell enumeration powershell-core convertfrom-json


【解决方案1】:

为什么 PowerShell 将 Where 的谓词应用于空列表?

因为ConvertFrom-Json 告诉Where-Object 不要尝试枚举其输出。

因此,PowerShell 尝试访问空数组本身的 name 属性,就像我们要这样做:

$emptyArray = New-Object object[] 0
$emptyArray.name

当您将ConvertFrom-Json 括在括号中时,powershell 会将其解释为一个单独 管道,该管道执行并结束之前任何输出都可以发送到Where-Object 和@因此 987654329@ 无法知道 ConvertFrom-Json 希望它这样对待数组。


我们可以通过使用-NoEnumerate 开关参数集显式调用Write-Output 在powershell 中重新创建此行为:

# create a function that outputs an empty array with -NoEnumerate
function Convert-Stuff 
{
  Write-Output @() -NoEnumerate
}

# Invoke with `Where-Object` as the downstream cmdlet in its pipeline
Convert-Stuff | Where-Object {
  # this fails
  $_.nonexistingproperty = 'fail'
}

# Invoke in separate pipeline, pass result to `Where-Object` subsequently
$stuff = Convert-Stuff
$stuff | Where-Object { 
  # nothing happens
  $_.nonexistingproperty = 'meh'
}

Write-Output -NoEnumerate 内部调用Cmdlet.WriteObject(arg, false),这反过来又导致运行时在参数绑定期间枚举arg 值与下游cmdlet(在您的情况下为Where-Object


为什么会这样?

在解析 JSON 的特定上下文中,这种行为可能确实是可取的:

$data = '[]', '[]', '[]', '[]' |ConvertFrom-Json

既然我向它传递了 5 个有效的 JSON 文档,我是否应该期待来自 ConvertFrom-Json 的正好 5 个对象? :-)

【讨论】:

  • 有趣! ConvertFrom-Json "告诉" Where-Object 如何不尝试枚举它的输出?
  • 感谢更新的答案!为什么这是可取的?不允许它似乎会破坏管道,但我认为有充分的理由说明为什么我们不想希望稍后在管道中枚举一些东西?
  • ...我想如果我想将生成的 json 解码对象通过管道传输到其他对象,如果它恰好是一个数组而被分解为多个对象,那将是一件痛苦的事情.这将导致对管道的下一个阶段进行多次调用,通常我们会期望调用一次。好的,再次感谢 Mathias,如果我想错了,请大喊 :)
  • ConvertFrom-Json 令人惊讶的“类似 PowerShell”的默认行为在 github.com/PowerShell/PowerShell/issues/3424 中进行了讨论,并可能引入了一个开关,例如 -[No]Enumerate,以提供对枚举行为的控制。
  • Quib​​ble: Where-Object 从来不知道前面的管道段是如何产生输出的——它只是对提供的输入进行操作:一个对象恰好是一个带有直接 @987654345 的数组@调用,以及包含在(...)中时对该数组的强制枚举。
【解决方案2】:

使用空数组作为直接管道输入,什么都没有通过管道发送,因为数组是枚举,并且由于没有要枚举的内容 - 因为空数组没有元素 - Where (Where-Object) 脚本块永远不会执行:

Set-StrictMode -Version Latest

# The empty array is enumerated, and since there's nothing to enumerate,
# the Where[-Object] script block is never invoked.
@() | Where { $_.name -eq "Baz" } 

相比之下,在最高 v6.x 的 PowerShell 版本中"[]" | ConvertFrom-Json 生成一个空数组作为单个输出对象,而不是枚举其(不存在的)元素,因为 ConvertFrom-Json 在这些版本中枚举它输出的数组的元素;它相当于:

Set-StrictMode -Version Latest

# Empty array is sent as a single object through the pipeline.
# The Where script block is invoked once and sees $_ as that empty array.
# Since strict mode is in effect and arrays have no .name property
# an error occurs.
Write-Output -NoEnumerate @() | Where { $_.name -eq "Baz" }

ConvertFrom-Json 的行为在 PowerShell 上下文中令人惊讶 - cmdlet 通常枚举 多个输出 - 但 是可防御的在 JSON 解析的上下文中;毕竟,如果ConvertFrom-Json 枚举了空数组,则信息将丢失,因为您将无法将其与空 JSON 输入 ("" | ConvertFrom-Json )。

一致认为两个用例都是合法的,用户应该通过开关在两种行为(枚举或不枚举)之间做出选择 (相关讨论请参见this GitHub issue)。

因此,从 PowerShell [Core] 7.0 开始

  • 枚举现在默认执行。

  • 可以通过新的 -NoEnumerate 开关选择加入 行为。

PowerShell 6.x- 中,如果需要枚举,则 - 晦涩 - 解决方法是 强制枚举,只需将 ConvertFrom-Json 调用包含在(...)grouping operator(将其转换为表达式,并且表达式在管道中使用时始终枚举命令的输出):

# (...) around the ConvertFrom-Json call forces enumeration of its output.
# The empty array has nothing to enumerate, so the Where script block is never invoked.
("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" }

至于你尝试了什么:你尝试访问.Count属性和你使用@(...)

$y = ("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" }
$y.Count # Fails with Set-StrictMode -Version 2 or higher

ConvertFrom-Json 调用包含在 (...) 中,您的整体命令返回“无”:松散地说,$null,但更准确地说,是一个“数组值空”,即 [System.Management.Automation.Internal.AutomationNull]::Value 单例表示命令没有输出。 (在大多数情况下,后者被视为与$null 相同,但在用作管道输入时尤其如此。)

[System.Management.Automation.Internal.AutomationNull]::Value 没有.Count 属性,这就是为什么如果Set-StrictMode -Version 2 或更高版本有效,您将收到The property 'count' cannot be found on this object. 错误。

通过将整个管道包装在@(...)array subexpression operator 中,您可以确保将输出视为一个数组,它使用数组值为空的输出创建一个空数组 - 这确实有一个.Count 属性。

请注意,应该能够在 $null[System.Management.Automation.Internal.AutomationNull]::Value 上调用 .Count,因为 PowerShell 将 .Count 属性添加到 每个对象,如果不存在的话 - 包括标量,在统一集合和标量的处理方面做出了值得称赞的努力。

也就是说,将Set-StrictMode 设置为-Off(默认值)或-Version 1,以下确实工作并且 - 明智地 - 返回0

# With Set-StrictMode set to -Off (the default) or -Version 1:

# $null sensibly has a count of 0.
PS> $null.Count
0

# So does the "array-valued null", [System.Management.Automation.Internal.AutomationNull]::Value 
# `. {}` is a simple way to produce it.
PS> (. {}).Count # `. {}` outputs 
0

上述当前不适用于Set-StrictMode -Version 2 或更高版本(从 PowerShell [Core] 7.0 开始),应被视为一个错误,正如 this GitHub issue 中所报告的那样(由 Jeffrey Snover 撰写,不少于)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多