没有“.Count”的对象 属性 - @()(数组子表达式运算符)与 [Array] 转换的使用

Objects with no '.Count' Property - use of @() (array subexpression operator) vs. [Array] cast

我正在尝试执行一些简单的 if 语句,但是所有基于 [Microsoft.Management.Infrastructure.CimInstance] 的较新的 cmdlet 似乎都没有公开 .count 方法?

$Disks = Get-Disk
$Disks.Count

return 什么都没有。我发现我可以将其转换为 [array],这使得它 return 成为预期的 .NET .count 方法。

[Array]$Disks = Get-Disk
$Disks.Count

无需直接将其转换为先前 cmdlet 的数组即可工作:

(Get-Services).Count

解决此问题的推荐方法是什么?

一个不起作用的例子:

$PageDisk = Get-Disk | Where {($_.IsBoot -eq $False) -and ($_.IsSystem -eq $False)}
  If ($PageDisk.Count -lt 1) {Write-Host "No suitable drives."; Continue}
   Else If ($PageDisk.Count -gt 1) {Write-Host "Too many drives found, manually select it."}
   Else If ($PageDisk.Count -eq 1) { Do X }

选项 A(转换为数组):

[Array]$PageDisk = Get-Disk | Where {($_.IsBoot -eq $False) -and ($_.IsSystem -eq $False)}
  If ($PageDisk.Count -lt 1) {Write-Host "No suitable drives."; Continue}
   Else If ($PageDisk.Count -gt 1) {Write-Host "Too many drives found, manually select it."}
   Else If ($PageDisk.Count -eq 1) { Do X }

选项 B(使用数组索引):

 $PageDisk = Get-Disk | Where {($_.IsBoot -eq $False) -and ($_.IsSystem -eq $False)}
  If ($PageDisk[0] -eq $Null) {Write-Host "No suitable drives."; Continue}
   Else If ($PageDisk[1] -ne $Null) {Write-Host "Too many drives found, manually select it."}
   Else If (($PageDisk[0] -ne $Null) -and (PageDisk[1] -eq $Null)) { Do X }

选项 C(数组)-感谢@PetSerAl:

$PageDisk = @(Get-Disk | Where {($_.IsBoot -eq $False) -and ($_.IsSystem -eq $False)})
  If ($PageDisk.Count -lt 1) {Write-Host "No suitable drives."; Continue}
   Else If ($PageDisk.Count -gt 1) {Write-Host "Too many drives found, manually select it."}
   Else If ($PageDisk.Count -eq 1) { Do X }

基于 CIM 的 cmdlet 不公开 .Count 方法的原因是什么?推荐的处理方法是什么?选项 B 对我来说似乎令人费解,而且难以阅读。选项 A 有效,但 powershell 不应该将其转换为我的数组吗?我是不是用完全错误的方式来解决这个问题?

在 PSv3+ 中,通过统一处理标量和集合,any 对象 - 甚至 $null - should 有一个 .Count 属性(并且,除了 $null,应该支持使用 [0] 进行索引)。

任何对象 支持上述内容的任何事件都应被视为 bug;例如:

  • 使用此 intrinsic (engine-supplied) .Count property unexpectedly fails when Set-StrictMode-Version 2 or higher is in effect, which is a long-standing bug reported in GitHub issue #2798,从 PowerShell 7.2 开始仍然存在(而 type-native .Count 属性,例如在数组上,可以安全地访问)。

  • [pscustomobject] 不遵守这些规则的实例在 2017 年是 known bug, fixed)。

因为我不知道所说的错误是否与 Get-Disk 输出的 [Microsoft.Management.Infrastructure.CimInstance#ROOT/Microsoft/Windows/Storage/MSFT_Disk] 实例有关,并且因为 Get-Disk - 至少目前 - 仅在 Windows PowerShell,我鼓励您在 uservoice.com.

上提交一个单独的错误

array-subexpression运算符@(...)的使用是必要的:

  • 作为解决方法手头的错误/Set-StrictMode -Version 2错误。

  • 万一 标量 对象碰巧有它的 自己的 .Count 属性.


一般,

  • 如果您确实需要确保命令的输出被捕获为数组,@(...) 是 PowerShell-idiomatic,与 [Array] ... / [object[]] ...

    相比,更简洁、语法更简单的形式
  • 相比之下,从 表达式 捕获 输出可能需要 [Array] ... / [object[]] ...,以避免 enumerating 表达式输出的潜在低效率,该表达式输出已经 一个数组并将其收集在 new 数组 - 详情见下文。

  • 对于任何一种输出类型,如您的问题,您可以使用 [Array] 作为 类型约束 变量(放在它的左边),以确保后面的赋值也被视为数组;一个人为的例子:[Array] $a = 11 存储在一个 (single-element) [object[]] 数组中,而稍后的 $a = 2 赋值隐式地为 2 执行此操作.

此外,@(...)[Array] ... 通常,但总是 等价,正如 PetSerAl 在对该问题的评论中提供的有用示例;改编他的例子之一:

@($null) returns 一个 single-item 数组,其唯一元素是 $null,而 [Array] $null 无效(保持 $null ).

@() 的这种行为与其目的一致(见下文):由于 $null 不是数组,@() 将其包装在一个数组中(导致 [System.Object[]]$null 作为唯一元素的实例)。

在 PetSerAl 的其他示例中,@() 使用 New-Object 创建的数组和集合的行为 - 可能令人惊讶 - 见下文。


@(...) 的目的及其工作原理:

@()array-subexpression operator 目的,笼统地说,是 确保 expression/command 被视为 array,即使它恰好是 scalar(单个对象)。:

  • @(...) 收集一个封闭的 命令的 输出 as-is / 封闭的 表达式的 枚举 输出 - always new - [object[]] array,即使只有一个 单个 输出对象。

    • 因此,@() 只是一个轻微的修改 - 对于 single-object 输出案例 - 的 PowerShell 关于从其成功输出流收集命令和表达式输出的默认行为 - 请参阅 for more information. In short, single-object output is by default collected as-is, whereas @() wraps it in an array (by contrast, multiple output objects always get collected in an array, in both cases, of necessity).Tip of the hat to Slawomir Brzezinski以帮助澄清。
  • @(...) 永远 不需要 数组 literals(在 v5.1+ 中它被优化掉了)- 使用 ,,通常 array constructor operator 本身就足够了,例如 'foo', 'bar'而不是 @('foo', 'bar') - 但您 可能更喜欢它,因为 视觉清晰度 ;它在以下情况下也有用:

    • 创建一个数组:@())

    • 创建一个 single-element 数组 - 例如@('foo') - 这比 一元 形式的 , 更容易阅读,否则将需要 - 例如, 'foo'

    • 为了语法上的便利:将概念上的数组字面量分布在多行中,而不必使用,来分隔元素,也没有必须在 (...) 中包含命令;例如:

      @(
        'one'
        Write-Output two
      )
      
  • 陷阱:

    • @()不是数组构造函数,而是一个“担保人”:因此,@(@(1,2)) 创建嵌套数组:

      • @(@(1, 2)) 实际上与 @(1, 2) 相同(只是 1, 2)。事实上,每个额外的 @() 都是一个 昂贵的 no-op,因为它只是创建一个 copy 的数组输出上一个
      • 使用,array constructor operator的一元形式构造嵌套数组:
        , (1, 2)
    • $null@() 视为 单个对象 ,因此会生成一个 single-element 数组,其中包含元素$null:

      • @($null).Count1
    • Command 调用该输出 整个单个数组 结果是 嵌套 数组:

      • @(Write-Output -NoEnumerate 1, 2).Count1
    • 表达式中,将任何类型的集合包裹在@()[=248=中]枚举它并总是收集(新)[object[]] array:

      中的元素
      • @([System.Collections.ArrayList] (1, 2)).GetType().Name returns 'Object[]'

如果需要,请继续阅读以获取更多详细信息。


详情:

@() 行为如下:感谢 PetSerAl 的广泛帮助。

  • PSv5.1+(Windows PowerShell 5.1和PowerShell [Core] 6+)中,使用表达式直接构造一个数组使用,array constructor operator优化@()离开:

    • 例如,@(1, 2)1, 2 相同,@(, 1), 1.

      相同
    • 在使用 构造数组的情况下,只需 , - 生成 System.Object[] 数组 - 这种优化很有帮助,因为它省去了先展开该数组然后重新打包它的不必要步骤(见下文)。
      据推测,这种优化是由使用 @( ..., ..., ...) 构造数组的广泛且以前低效的做法引起的,源于人们错误地认为需要 @() 来构造数组。

    • 但是,在 Windows PowerShell v5.1 中仅使用 cast[=449= 构造具有 特定类型 的数组时,也应用了优化 意外 ],例如 [int[]](此行为已在 PowerShell [Core] 6+ 和更早的 Windows PowerShell 版本中得到纠正);例如,
      @([int[]] (1, 2)).GetType().Name 产生 Int32[]。这是唯一 @() returns 不同于 System.Object[] 其他 的情况,并且假设它总是 可能导致意外错误和副作用;例如:
      @([int[]] (1, 2))[-1] = 'foo' 休息。
      $a = [int[]] (1, 2); $b = @([int[]] $a) 意外地没有创建 new 数组 - 请参阅 GitHub issue #4280.

  • 否则:如果@(...)中的(第一个)语句是一个表达式是一个 enumerable[1] 它被枚举,即它的元素被 一个一个 发送到成功输出流; 命令的(通常是one-by-one流)输出被收集as-is;在任何一种情况下 最终的对象计数决定了行为:

    • 如果结果是项/包含,结果 包装在 single-element / [System.Object[]].

      类型的空数组中
      • 例如,@('foo').GetType().Name 产生 Object[]@('foo').Count 产生 1(尽管如前所述,在 PSv3+ 中,您可以使用 'foo'.Count 直接)。
        @( & { } ).Count 产生 0(执行空脚本块输出“空集合” ([System.Management.Automation.Internal.AutomationNull]::Value)

      • 警告@() 围绕一个 New-Object 调用创建数组/集合输出 array/collection 包裹在single-element外层数组.

        • @(New-Object System.Collections.ArrayList).Count 产生 1 - 空数组列表包装在 single-element System.Object[] 实例中。

        • 原因是New-Object将列表输出为单个对象(恰好是一个可枚举本身),array/collection),导致 @() 将其包装在 single-item 数组中。

        • 可能令人困惑的是,当您使用表达式构造数组时,不会发生这种情况/一个集合,因为表达式的输出是 enumerated(有时也称为展开或展开的操作):

          • @([system.collections.arraylist]::new()).Count 产量 0;该表达式输出一个枚举的空集合,并且由于没有注意枚举,@() 创建一个空的 System.Object[] 数组。
            请注意,在PSv3+,只需使用一组额外的括号 ((...)) 和 New-Object - 将 New-Object command 表达式 - 会产生相同的结果:
            @((New-Object System.Collections.ArrayList)).Count 也产生 0
    • 如果结果包含多个,这些项将作为常规PowerShell返回数组 ([System.Object[]]);例如:

      • 使用命令:

        • $arr = @(Get-ChildItem *.txt)Get-ChildItem 收集 one-by-one 流输出到 System.Object[] 数组
      • 使用表达式:

        • $arr = [int[]] (1, 2); @($arr) 枚举 [int[]]数组$arr然后重新打包元素作为 System.Object[] 数组。

        • 注意这个过程的效率低下潜在的类型保真度损失:原始数组是枚举并收集在new数组中,该数组始终为System.Object[]类型; 有效的替代方案 将转换为 [array](也适用于 命令): [array] $result = ...


[1] 有关 PowerShell 认为可枚举的类型的摘要 - 既不包括实现 IEnumerable 的 select 类型,也包括不实现的类型 - 请参阅.

的底部

@mklement0 有一个很好的答案,但要补充一件事:如果你的脚本(或调用你的脚本)有 Set-StrictMode,自动 .Count.Length 属性停止工作:

$ Set-StrictMode -off
$ (42).Length
1
$ Set-StrictMode -Version Latest
$ (42).Length
ParentContainsErrorRecordException: The property 'Length' cannot be found on this object. Verify that the property exists.

为了安全起见,您可以在检查长度之前将任何未知变量包装在数组中@(...)

$ $result = Get-Something
$ @($result).Length
1