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
  • 58,089
  • 23
  • 157
  • 265

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,700
  • 1
  • 18
  • 29
0
  • PowerShell by default enumerates (unrolls) collections 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.

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.
> 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 aux. outer array so that enumerating the outer array yields the embedded one as a single output object:
# Use unary , to wrap the RHS in a single-element array.
> 1..2 | % { , @(1, 2) } | Measure-Object | % Count
2  # i.e., @(@(1, 2), @(1, 2)).Count
  • Alternative, using Write-Output -NoEnumerate (PSv4+):
> 1..2 | % { Write-Output -NoEnumerate @(1, 2) } | Measure-Object | % Count
2  # i.e., @(@(1, 2), @(1, 2)).Count

Note: While use of @(...) is not necessary to create array literals (as used above) - for literals, separating elements with , is sufficient - 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.

mklement0
  • 312,089
  • 56
  • 508
  • 622