Home > front end >  Powershell array's ForEach doesn't return a System.array?
Powershell array's ForEach doesn't return a System.array?

Time:12-31

I noticed that when running ForEach on an array object and capturing the output to a new variable, the new variable is not of type System.array:

PS D:\Playground> $Arr = 1, 2, 3
PS D:\Playground> $Arr2 = $Arr.ForEach({$_})
PS D:\Playground> $Arr2.Gettype()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Collection`1                             System.Object

Rather, it is of type Collection'1.

What is this type? Is it equivalent to an array?

BTW, this is not the same as with ForEach-Object:

PS D:\Playground> $Arr3 = $($Arr | ForEach-Object { $_ })
PS D:\Playground> $Arr3.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

CodePudding user response:

Let me build on Jeroen Mostert's excellent comment:

  • The .ForEach() array method, and its sister method, .Where(), return [System.Collections.ObjectModel.Collection[psobject]] collection instances rather than regular PowerShell arrays ([object[]]).

    • Unlike the related ForEach-Object / Where-Object cmdlets, these methods always return a collection, even with only a single object:

      # .ForEach() method:
      # Collection result even with single object.
      @(1).ForEach({ $_ }).GetType().Name # -> Collection`1
      
      # ForEach-Object cmdlet:
      # Single output object: received as-is.
      (@(1) | ForEach { $_ }).GetType().Name # -> Int32
      # Two or more output objects: array result (if captured / used in expression)
      (1, 2 | ForEach { $_ }).GetType().Name # -> Object[]
      
    • Note: These methods are examples of intrinsic members, i.e. properties and methods PowerShell exposes on all objects, irrespective of their type (unless a type-native member of the same name exists, which takes precedence).

  • In essence, this collection type behaves like an array in PowerShell (due to implementing the [System.Collections.Generic.IList[psobject]] interface):

    • Its elements are enumerated in the pipeline, just as an array's elements are.
    • Positional indexing (e.g. [0]) is supported, just as with arrays.
    • Unlike an array, however:
      • It is resizable; that is, its instances allow you to add (.Add()) and remove (.Remove()) elements.
      • Its element type is [psobject] (not [object]), the usually invisible helper type capable of wrapping any .NET object, which PowerShell employs (largely) behind the scenes.
        • Typically, this difference won't matter, but - unfortunately - there are edge cases where it does - see GitHub issue #5579.

The .ForEach() method vs. the ForEach-Object cmdlet:

Note: The following applies analogously to .Where() vs. Where-Object.

  • Use ForEach-Object on command output, in order to benefit from the streaming behavior of the PowerShell pipeline (one-by-one processing, as input is being received, no need for up-front collection of input); e.g.:

    Get-ChildItem -Name *.txt| ForEach-Object { "[$_]" }
    
  • Use .ForEach() on arrays (collections) that are already are / can be collected in memory as a whole first, if faster processing is called for; e.g.:

    ('foo.txt', 'bar.txt').ForEach({ "[$_]" })
    

Beware of the differences in single-object behavior and output-collection type discussed above, however.

See this answer for a detailed juxtaposition of .ForEach(), ForEach-Object, the foreach statement, as well as member enumeration.

  • Related