2

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
YoavKlein
  • 2,005
  • 9
  • 38
  • 2
    I started writing an answer but stopped again, because I'm not really sure what a satisfying answer would look like. When you ask "What is this type?", what exactly are you hoping to learn? It's namespace? Finding the documentation? – Mathias R. Jessen Dec 30 '21 at 15:19
  • 5
    This uses the "magic" `ForEach` method that returns a [`Collection`](https://learn.microsoft.com/dotnet/api/system.collections.objectmodel.collection-1) (which is a very weird type to use, since it's intended as a base class -- but since it's not abstract, it is indeed possible to use it directly). No, this is not equivalent to an array, but because PowerShell mostly only cares if things are enumerable (not what the exact type is) and since `Collection` is indexable just like an array, that doesn't usually matter much. – Jeroen Mostert Dec 30 '21 at 15:20
  • 1
    `$i = { 'asd' }.Invoke()` => `$i.GetType()` => _Collection\`1_ => `$i[0].GetType()` => _string_ – Santiago Squarzon Dec 30 '21 at 15:23

1 Answers1

5

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-access enumeration.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • I learnt the `foreach` method can be used on any object just a few days ago, also didn't know `.foreach({ begin {...} process {...} end {...} })` was possible. Another reason to not use `ForEach-Object` ever again lol – Santiago Squarzon Dec 30 '21 at 19:45
  • 2
    @Santiago, and I didn't know that something like `(1, 2).foreach({ begin { 0 } process { "[$_]" } end { 3 } })` works - thanks! Note that for streaming (memory-throttling) processing you'll still need `ForEach-Object`. Also, `ForEach-Object` and `Where-Object` could be much less of an execution-speed concern _if they were implemented efficiently_ - see [GitHub feature request #10982](https://github.com/PowerShell/PowerShell/issues/10982). – mklement0 Dec 30 '21 at 19:58