Home > Net >  Why does an array with a single empty array have the length of 0?
Why does an array with a single empty array have the length of 0?

Time:06-23

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?

CodePudding user response:

  • @(...), 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).

    • 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 and convenient 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.

  • Related