2

There's an array of objects, where each has a collection of objects, where each has a string property. When I do a nested iteration:

$TheArray | %{$_.TheCollection | %{$_.TheProperty}}

it seems like I end up not with an array of string arrays, but with a 1D array of strings. Is that by design? That is the desired behavior in the first place, but utterly unexpected.

Seva Alekseyev
  • 59,826
  • 25
  • 160
  • 281

2 Answers2

1

Yes, that output makes sense to me, at least on an intuitive level. I can't explain in accurate technical detail, but the only object written to the pipeline in your expression

$TheArray | %{$_.TheCollection | %{$_.TheProperty} }

is the inner-most

$_.TheProperty

Since this evaluates to a String, a number of Strings are accumulated in the pipeline and returned in an array.

Here's some sample code that mocks-up what you've described:

class HasProperty {
    [String] $TheProperty;
    HasProperty ([String] $prop){
        $this.TheProperty = $prop
    }
}

class SomeObject {
    [HasProperty[]] $TheCollection
    SomeObject ([HasProperty[]] $array) {
        $this.TheCollection = $array
    }
}

[SomeObject[]]$TheArray = @()

$TheArray = foreach ($i in (0..9)) {
    [HasProperty[]]$tempArray = foreach ($n in (0..3)) { [HasProperty]::new("Property$i-$n") }
    [SomeObject]::new($tempArray)
}
$TheArray | %{$_.TheCollection | %{$_.TheProperty} }

PowerShell's object-oriented pipeline makes it easy to extract values from some collection of objects. I've used it to get the group membership of a collection of users to determine how their memberships overlap, for instance.

veefu
  • 2,820
  • 1
  • 19
  • 29
1
  • PowerShell by default enumerates (unrolls) collections[1] when outputting them to the pipeline: that is, instead of outputting the collection itself, its elements are output, one by one.

  • PowerShell collects all output objects from a pipeline in a flat array (of type [object[]], i.e. an array whose elements can be of any type (System.Object)), if two or more objects are output (a single object is collected as itself).

Therefore, even outputting multiple arrays creates a single, flat output array by default, which is the concatenation of these arrays.

A simpler example:

# Output 2 2-element arrays.
PS> 1..2 | % { @(1, 2) } | Measure-Object | % Count
4  # i.e., @(1, 2, 1, 2).Count

In order to produce nested arrays, you must suppress enumeration, which can be achieved in two ways:

  • Simplest option: Wrap the output array in an auxiliary, transient single-element array so that when PowerShell enumerates the aux. array its one and only element - the original array - is output as a whole, as a single object. This can be achieved with the unary form of , the array-constructor ("comma") operator:
# Use unary , to wrap the RHS in a transient single-element array,
# which ensures that @(1, 2) is output *as a whole*.
PS> 1..2 | % { , @(1, 2) } | Measure-Object | % Count
2  # i.e., @(@(1, 2), @(1, 2)).Count
  • Alternative, using Write-Output -NoEnumerate (PSv4+; this is slower, but conceptually more explicit):
PS> 1..2 | % { Write-Output -NoEnumerate @(1, 2) } | Measure-Object | % Count
2  # i.e., @(@(1, 2), @(1, 2)).Count

Note:

  • While use of @(...) is not strictly necessary to create array literals (as used above) - for literals, separating elements with , is sufficient[2] - you still need @(...) to ensure that output from enclosed expressions or commands is treated as an array, in case only a single object happens to be output.

[1] Specifically, types that implement the System.Collections.IEnumerable interface or its generic counterpart are enumerated, but there are a few exceptions, notably strings and dictionaries. See the bottom section of this answer for details.

[2] E.g., 1, 2 creates a two-element array. In the case of nesting such an array via unary ,, you do need an enclosure, however, to clarify precedence, for which the general-purpose (...) (grouping operator) would suffice: , (1, 2) - though you may prefer , @(1, 2) for conceptual clarity.

mklement0
  • 382,024
  • 64
  • 607
  • 775