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

9

我正试图执行一些简单的if语句,但是所有基于[Microsoft.Management.Infrastructure.CimInstance]的新命令都似乎没有暴露.count方法?

$Disks = Get-Disk
$Disks.Count

没有任何返回值。我发现可以将它转换为[array],这样就像预期那样返回了.NET .count方法。

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

这个命令可以在不直接将其转换为数组的情况下正常工作:

(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基于cmdlets为什么不公开.Count方法?应该如何处理?选项B似乎很复杂,难以阅读。选项A可行,但PowerShell不应该将其强制转换为数组吗?我的做法完全错了吗?

2
$Result = @(Your command here) - user4003407
4
(Get-Services).Count 的工作原理是因为 Get-Services 返回多个对象。如果需要一个数组(可能包含0或1个对象),则可以使用数组子表达式运算符(@(...)),正如 @PetSerAl 上面建议的那样。 - Mathias R. Jessen
@PetSerAl 谢谢。这不就是和 [Array]$Result = Command_Here 完全一样吗? - CobyCode
@MathiasR.Jessen 谢谢!我一直以为PowerShell中的几乎所有内容都是设置为处理多个对象的?即使"(10*2).Count"也可以工作。是什么决定了构建的对象是否可以处理多个或单个对象? - CobyCode
5
不,它们是不同的:[Array]$a = &{}; $b = @(&{}); $a.GetType(); $b.GetType()[Array]$a = New-Object Object[] 10; $b = @(New-Object Object[] 10); $a.Count; $b.Count[Array]$a = New-Object Collections.Generic.List[Object]; $b = @(New-Object Collections.Generic.List[Object]); $a.Count; $b.Count - user4003407
@PetSerAl 我花了一段时间才明白!我还从这个问题[链接] (https://dev59.com/GlgQ5IYBdhLWcg3wsGGZ) 中找到了一些进一步的信息,看起来您也参与了回答。我可以看出它们是不同的,但仍然不完全确定区别在哪里。.GetType()显示两者都有一个系统数组的基类型。还有:你能写个答案让我给你点赞吗? - CobyCode
2个回答

15
在PSv3+中,由于统一处理标量和集合,任何对象 - 甚至包括$null - 都应该具有.Count属性(除了$null之外,应该支持使用[0]进行索引)。 不支持上述操作的任何对象都应被视为错误,例如:

由于我不知道所说的错误是否与[Microsoft.Management.Infrastructure.CimInstance#ROOT/Microsoft/Windows/Storage/MSFT_Disk]实例相关,并且因为Get-Disk目前仅在Windows PowerShell中可用, 我建议您在uservoice.com上提交单独的错误报告。

只有在以下情况下才需要使用数组子表达式运算符@(...):

  • 解决手头的错误/ Set-StrictMode -Version 2错误。

  • 当标量对象恰好拥有自己的.Count属性时.


一般来说,

如果您确实需要确保从命令中捕获的输出以数组形式捕获,那么与[Array] ... / [object[]] ...相比,@(...)是PowerShell习惯用语,更简洁和语法更容易的形式。
相比之下,从表达式中捕获输出可能需要使用[Array] ... / [object[]] ...,以避免枚举已经是一个数组并将其收集在新数组中的表达式输出的潜在低效率 - 有关详细信息,请参见下文。
对于任何类型的输出,与您的问题一样,您可以将[Array]作为变量的类型约束(放置在左侧),以确保稍后的赋值也被视为数组;一个假设的例子:[Array] $a = 11存储在(单元素)[object[]]数组中,并且稍后的$a = 2赋值会隐式地为2执行相同的操作。
此外,@(...)[Array] ... 通常是等效的,但并非总是如此,正如PetSerAl在评论中提供的有用示例所示;为了适应他的其中一个示例: @($null) 返回一个包含一个元素的单项数组,其唯一的元素是 $null,而 [Array] $null 没有任何效果(仍然是 $null)。 @() 的这种行为与其目的一致(请参见下文):由于 $null 不是数组,因此 @() 将其封装在一个数组中(导致一个 [System.Object[]] 实例,其中 $null 是唯一的元素)。
在 PetSerAl 的其他示例中,@() 与使用 New-Object 创建的数组和集合的行为可能会令人惊讶,请参见下文。

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

@(),即 数组子表达式运算符,其目的是确保表达式/命令的结果被视为一个数组,即使它实际上是一个标量(单个对象)。

  • @(...) collects an enclosed command's output as-is / an enclosed expression's enumerated output in an - always new - [object[]] array, even if there's only a single output object.

    • As such, @() is merely a slight modification - for the single-object output case - of PowerShell's default behavior with respect to collecting command and expression output from its success output stream - see this answer 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 for helping to clarify.
  • @(...) is never needed for array literals (in v5.1+ it is optimized away) - use of ,, the array constructor operator by itself is generally sufficient, e.g., 'foo', 'bar' instead of @('foo', 'bar') - but you may prefer it for visual clarity; it is also useful in the following cases:

    • to create an empty array: @())

    • to create a single-element array - e.g. @('foo') - which is easier to read than the unary form of , that would otherwise be required - e.g. , 'foo'

    • for syntactic convenience: to spread what is conceptually an array literal across multiple lines without having to use , to separate the elements and without having to enclose commands in (...); e.g.:

      @(
        'one'
        Write-Output two
      )
      
  • Pitfalls:

    • @() is not an array constructor, but a "guarantor": therefore, @(@(1,2)) does not create a nested array:

      • @(@(1, 2)) is effectively the same as @(1, 2) (and just 1, 2). In fact, each additional @() is an expensive no-op, because it simply creates a copy of the array output by the previous one.
      • Use the unary form of , the array constructor operator, to construct nested arrays:
        , (1, 2)
    • $null is considered a single object by @(), and therefore results in a single-element array with element $null:

      • @($null).Count is 1
    • Command calls that output a single array as a whole result in a nested array:

      • @(Write-Output -NoEnumerate 1, 2).Count is 1
    • In an expression, wrapping a collection of any type in @() enumerates it and invariably collects the elements in a (new) [object[]] array:

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

如果需要更详细的信息,请继续阅读。


详情:

@()的行为如下: 向PetSerAl致敬,感谢他的广泛帮助。

PSv5.1+(Windows PowerShell 5.1和PowerShell [Core] 6+)中,使用使用,直接构造数组的表达式,数组构造运算符会优化掉@()
  • 例如,@(1, 2)1, 2相同,@(, 1), 1相同。
  • 对于仅使用,构造的数组 - 它产生一个System.Object[]数组 - 这种优化是有帮助的,因为它节省了先展开该数组然后重新打包它的不必要步骤。
    假定,这种优化是由广泛而以前低效的使用@( ..., ..., ...)来构造数组的做法所促成的,源于错误的信念,即需要@()来构造数组。
  • 然而,在仅限Windows PowerShell v5.1的情况下,当使用强制转换构造具有特定类型的数组时,如[int[]](此行为已在PowerShell [Core] 6+中进行了更正,旧版的Windows PowerShell不受影响);例如:
    @([int[]] (1, 2)).GetType().Name会得到Int32[]。这是唯一一种@()返回的结果不是System.Object[]的情况,假定它总是这样做可能会导致意外的错误和副作用;例如:
    @([int[]] (1, 2))[-1] = 'foo'会出错。
    $a = [int[]] (1, 2); $b = @([int[]] $a)出乎意料地没有创建一个新的数组 - 参见GitHub issue #4280
否则:如果@(...)中的(第一个)语句是恰好是可枚举的表达式,[1]则枚举它,即其元素逐个发送到成功输出流;命令(通常是逐个流式处理)的输出按原样收集;在任何情况下,对象计数决定行为
  • 如果结果是单个项目/不包含项目,则结果被包装在类型为[System.Object[]]的单个元素/空数组中

    • 例如,@('foo').GetType().Name会得到Object[],而@('foo').Count会得到1(尽管如上所述,在PSv3+中,您可以直接使用'foo'.Count)。
      @( & { } ).Count会得到0(执行空脚本块会输出"null collection" ([System.Management.Automation.Internal.AutomationNull]::Value))

    • 注意@()包围一个创建数组/集合的New-Object调用会将该数组/集合输出包装在单个元素的外部数组中

      • @(New-Object System.Collections.ArrayList).Count会得到


        [1] 对于 PowerShell 认为可枚举的类型的摘要 - 这些类型既排除了实现 IEnumerable 接口的某些类型,也包括一个不实现该接口的类型 - 请参见 this answer 的底部部分。


谢谢!这很有道理。但是那么绕过这个问题的首选方法是什么?如果我使用 $a = @(...),稍后当我将 $a 的值用作某些其他 cmldet 上期望 system.object[] 的输入时,它不起作用...所以 '$disk = @(get-disk); new-volume $disk' 将失败。我想我可以使用 'new-volume $disk[0]' 或 '$disk=disk[0]; new-volume $disk'.... - CobyCode
另外需要注意的是,我似乎对所有基于[Microsoft.Management.Infrastructure.CimInstance]构建的cmdlet都会出现这个问题。 - CobyCode
1
@CobyCode: $disk = @(get-disk) 确实返回一个 [System.Object[]] 实例,你可以通过 Get-Member -InputObject $disk 进行验证。据我所知,New-Volume 的第一个位置参数是一个 标量 类型,因此尝试传递一个 [System.Object[]] 实例是预期会失败的;请参阅 New-Volume -? - mklement0
1
@mklement0 这可能是我见过的最清晰明了的评论(除了您在此处的另一个答案链接)。谢谢!根据您的建议,我将提交给uservoice。 - CobyCode
@SlawomirBrzezinski,为了结束这次交流:感谢您鼓励我改进答案,并澄清@()与默认成功输出流行为的关系。考虑到本答案的重点,我认为我的更新是反映这一点的适当方式(我刚刚更新了一下并向您致以小小的致意)。如果您同意,我建议我们清理一下这里的评论(我已经删除了我的先前评论)。 - mklement0
显示剩余3条评论

2

@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

谢谢,说得好 - 我也更新了我的答案。这个漏洞已经存在很长时间了,并且被 Jeffrey Snover 本人提出 - 请参见 GitHub issue #2798 - mklement0

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