1

The Length property works as expected on all arrays that I test except one weird case:

PS> @(@()).Length
0

It's not that empty arrays are generally omitted though:

PS> @(@(), @()).Length
2

PS> @(@(), @(), @()).Length
3

What's going on?

4
  • 3
    In the 1st case, PowerShell unrolls the inner array, which is empty. Thus, the outer array becomes empty too. This is just how the array sub-expression operator @() works. Another example to confirm: @(@(1,2,3)).Length outputs 3 instead of 1. Commented Jun 22, 2022 at 13:33
  • 1
    Following on @zett42's comment, this is why using the unary form of the comma operator, you get your expected results: @(,@()).Count Commented Jun 22, 2022 at 13:34
  • The keyword here is "array unrolling". This is a source of a lot of question and there is a lot on SO and elsewhere on the topic and all the gotchas. As a simple rule of me, generically speaking, always wrap results of function that return a collection in an array (eg: $Stuff = @(Get-Collection) so that you get an array no matter what. If you don't do that, then empty collections get unrolled into $null, single item collections get unrolled in their single element type and collection with more elements get automatically unrolled in the outer collection, making all ths a bit more consistent Commented Jun 22, 2022 at 13:43
  • See; PowerShell doesn't return an empty array as an array Commented Jun 22, 2022 at 14:49

1 Answer 1

4
  • @(...), the array-subexpression operator is not an array constructor, it is an array "guarantor" (see next section), and nesting @(...) operations is pointless.

    • @(@()) is in effect the same as @(), i.e. an empty array of type [object[]].
  • To unconditionally construct arrays, use ,, the array constructor operator.

    • To construct an array wrapper for a single object, use the unary form of ,, as Abraham Zinala suggests:

      # Create a single-element array whose only element is an empty array.
      # Note: The outer enclosure in (...) is only needed in order to 
      #       access the array's .Count property.
      (, @()).Count # -> 1
      

Note that I've used .Count instead of .Length above, which is more PowerShell-idiomatic; .Count works across different collection types. Even though System.Array doesn't directly implement .Count, it does so via the ICollection interface, and PowerShell allows access to interface members without requiring a cast.


Background information:

  • @(...)'s primary purpose is to ensure that output objects collected from - invariably pipeline-based - commands (e.g, @(Get-ChildItem *.txt)) are always collected as an array (invariably of type [object[]]) - even if ... produces only one output object.

    • If getting an array is desired, use of @(...) is necessary because collecting output that happens to contain just one object would by default be collected as-is, i.e. not wrapped in an array (this also applies when you use $(...), the subexpression operator); only for multiple output objects is an array used, which is always [object[]]-typed.

    • Note that PowerShell commands (typically) do not output collections; instead, they stream a (usually open-ended) number of objects one by one to the pipeline; capturing command output therefore requires collecting the streamed objects - see this answer for more information.

  • @(...)'s secondary purpose is to facilitate defining array literals, e.g. @('foo', 'bar')

    • Note:

      • Using @(...) for this purpose was not by original design, but such use became so prevalent that an optimization was implemented in version 5 of PowerShell so that, say, 1, 2 - which is sufficient to declare a 2-element array - may also be expressed as @(1, 2) without unnecessary processing overhead.

      • On the plus side, @(...) is visually distinctive in general and syntactically convenient specifically for declaring empty (@()) or single-element arrays (e.g. @(42)) - without @(...), these would have to expressed as [object[]]:new() and , 42, respectively.

      • However, this use of @(...) invites the misconception that it acts as an unconditional array constructor, which isn't the case; in short: wrapping extra @(...) operations around a @(...) operation does not create nested arrays, it is an expensive no-op; e.g.:

        @(42)    # Single-element array
        @(@(42)) # !! SAME - the outer @(...) has no effect.
        
    • When @(...) is applied to a (non-array-literal) expression, what this expression evaluates to is sent to the pipeline, which causes PowerShell to enumerate it, if it considers it enumerable;[1] that is, if the expression result is a collection, its elements are sent to the pipeline, one by one, analogous to a command's streaming output, before being collected again in an [object[]] array.

      # @(...) causes the [int[]]-typed array to be *enumerated*,
      # and its elements are then *collected again*, in an [object[]] array.
      $intArray = [int[]] (1, 2)
      @($intArray).GetType().FullName # -> !! 'System.Object[]'
      
      • To prevent this enumeration and re-collecting:

        • Use the expression as-is and, if necessary, enclose it just in (...)

        • To again ensure that an array is returned, an efficient alternative to @(...) is to use an [array] cast; the only caveat is that if the expression evaluates to $null, the result will be $null too ($null -eq [array] $null):

          # With an array as input, an [array] cast preserves it as-is.
          $intArray = [int[]] (1, 2)
          ([array] $intArray).GetType().FullName # -> 'System.Int32[]'
          
          # With a scalar as input, a single-element [object[]] array is created.
          ([array] 42).GetType().FullName # -> 'System.Object[]'
          

[1] See the bottom section of this answer for an overview of which .NET types PowerShell considers enumerable in the pipeline.

Sign up to request clarification or add additional context in comments.

2 Comments

Okay, that not only answered my question, but also justified the logic behind. Would it be right to think that @(1,2,3) is actually an array 1,2,3 arrayified again with @()?
@radrow, historically speaking, yes - until v5 came along, which, loosely speaking, optimizes the @(...) away in this scenario (strictly speaking, @(...) is still parsed in this case, but special-cased to avoid re-creating the array). However, the re-creation does happen with non-literal operands; e.g: $a = 1, 2; @($a)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.