Objects with no '.Count' Property - use of @() (array subexpression operator) vs. [Array] cast
In PSv3+, with its unified handling of scalars and collections, any object - even $null
- should have a .Count
property (and, with the exception of $null
, should support indexing with [0]
).
Any occurrence of an object not supporting the above should be considered a bug.
For instance, [pscustomobject]
instances not playing by these rules was a known bug, fixed in 2017.
Since I don't know if said bug is related to the [Microsoft.Management.Infrastructure.CimInstance#ROOT/Microsoft/Windows/Storage/MSFT_Disk]
instances that Get-Disk
outputs, and since Get-Disk
- at least currently - is only available in Windows PowerShell, I encourage you to file a separate bug on uservoice.com.
Use of array-subexpression operator @(...)
is only necessary:
as a workaround for the bug above.
in case a scalar object happens to have its own
.Count
property.
Generally, if you do need to ensure that something is an array, use @(...)
rather than [Array] ...
/ [object[]] ...
- @()
is PowerShell-idiomatic, more concise, and syntactically easier.
That said, given that @()
technically creates a (shallow) copy of an existing array, you may prefer [Array]
when dealing with potentially large arrays.
Additionally, @(...)
and [Array] ...
are not generally equivalent, as PetSerAl's helpful examples in a comment on the question demonstrate; to adapt one of his examples:
@($null)
returns a single-item array whose one and only element is $null
, whereas [Array] $null
has no effect (stays $null
).
This behavior of @()
is consistent with its purpose (see below): since $null
is not an array, @()
wraps it in one (resulting in a [System.Object[]]
instance with $null
as the only element).
In PetSerAl's other examples, @()
's behavior with New-Object
-created arrays and collections - may be surprising - see below.
The purpose of @(...)
and how it works:
The purpose of @()
, the array-subexpression operator, is, loosely speaking, to ensure that the result of an expression/command is treated as an array, even if it happens to be a scalar (single 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.@(...)
is never needed for array literals (in v5.1+ it is optimized away), but it is useful in two cases:to create an empty array:
@()
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 just1, 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
is1
Command calls that output a single array as a whole result in a nested array:
@(Write-Output -NoEnumerate 1, 2).Count
is1
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[]'
Read on for more detailed information, if needed.
Details:
@()
behaves as follows: Tip of the hat to PetSerAl for his extensive help.
In PSv5.1+ (Windows PowerShell 5.1 and PowerShell [Core] 6+), using an expression that directly constructs an array using
,
, the array constructor operator, optimizes@()
away:E.g.,
@(1, 2)
is the same as just1, 2
, and@(, 1)
is the same as just, 1
.In the case of an array constructed with just
,
- which yields aSystem.Object[]
array - this optimization is helpful, because it saves the unnecessary step of first unrolling that array and then repackaging it (see below).
Presumably, this optimization was prompted by the widespread and previously inefficient practice of using@( ..., ..., ...)
to construct arrays, stemming from the mistaken belief that@()
is needed to construct an array.However, in Windows PowerShell v5.1 only, the optimization is unexpectedly also applied when constructing an array with a specific type using a cast, such as
[int[]]
(the behavior has been corrected in PowerShell [Core] 6+ and older Windows PowerShell versions are not affected); e.g.,
@([int[]] (1, 2)).GetType().Name
yieldsInt32[]
. This is the only situation in which@()
returns something other thanSystem.Object[]
, and assuming that it always does can lead to unexpected errors and side effects; e.g.:
@([int[]] (1, 2))[-1] = 'foo'
breaks.
$a = [int[]] (1, 2); $b = @([int[]] $a)
unexpectedly doesn't create a new array - see this GitHub issue.
Otherwise: If the (first) statement inside
@(...)
is an expression that happens to be a collection, that collection is enumerated; a command's (typically one-by-one streaming) output is collected as-is; in either case the resulting count of objects determines the behavior:If the result is a single item / contains no items, the result is wrapped in a single-element / empty array of type
[System.Object[]]
.E.g.,
@('foo').GetType().Name
yieldsObject[]
and@('foo').Count
yields1
(though, as stated, in PSv3+, you can use'foo'.Count
directly).
@( & { } ).Count
yields0
(executing an empty script block outputs a "null collection" ([System.Management.Automation.Internal.AutomationNull]::Value
)Caveat:
@()
around aNew-Object
call that creates an array / collection outputs that array/collection wrapped in a single-element outer array.@(New-Object System.Collections.ArrayList).Count
yields1
- the empty array list is wrapped in a single-elementSystem.Object[]
instance.The reason is that
New-Object
, by virtue of being a command (such as a cmdlet call) is not subject to enumeration (unwrapping), causing@()
to see only a single item (which happens to be an array/collection), which it therefore wraps in a single-item array.What may be confusing is that this does not happen when you use an expression to construct an array / a collection, because the expression's output is enumerated (unwrapped, unrolled):
@([system.collections.arraylist]::new()).Count
yields0
; the expression outputs an empty collection that is enumerated, and since there's noting to enumerate,@()
creates an emptySystem.Object[]
array.
Note that, in PSv3+, simply using an extra set of parentheses ((...)
) withNew-Object
- which converts theNew-Object
command to an expression - would yield the same result:
@((New-Object System.Collections.ArrayList)).Count
yields0
too.
If the result comprises multiple items, these items are returned as a regular PowerShell array (
[System.Object[]]
); e.g.:- With a command:
$arr = @(Get-ChildItem *.txt)
collects the one-by-one streaming output fromGet-ChildItem
in aSystem.Object[]
array
- With an expression:
$arr = [int[]] (1, 2); @($arr)
enumerates[int[]]
array$arr
and then repackages the elements as aSystem.Object[]
array.Note the inefficiency and potential loss of type fidelity of this process: the original array is enumerated and collected in a new array that is always of type
System.Object[]
; the efficient alternative is to cast to[array]
(which also works with commands):[array] $result = ...
- With a command: