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?

radrow
  • 6,419
  • 4
  • 26
  • 53
  • 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`. – zett42 Jun 22 '22 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` – Abraham Zinala Jun 22 '22 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 – Sage Pourpre Jun 22 '22 at 13:43
  • See; [PowerShell doesn't return an empty array as an array](https://stackoverflow.com/a/18477004/1701026) – iRon Jun 22 '22 at 14:49

1 Answers1

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.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 2
    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 Jun 22 '22 at 20:20
  • 2
    @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)` – mklement0 Jun 22 '22 at 21:28