To complement the existing, helpful answers by addressing your specific questions:
is it better to use foreach ($item in $collection)
instead? [from the title]
Yes: foreach
by far performs best among the PowerShell enumeration techniques and is the only one that directly supports stopping the enumeration on demand, with break
.
See below for a detailed discussion of performance as well as syntax considerations.
...what is the proper way to continue
when using .ForEach({})
?
The proper way is to use return
:
In script blocks ({ ... }
), such as ones passed to the .ForEach()
method or to the ForEach-Object
cmdlet, return
exits that block only.
In the context of enumerating commands such as .ForEach()
/ ForEach-Cmdlet
, this means that processing continues with the next input object, i.e. it resumes the ongoing enumeration.
This makes it analogous to the continue
keyword, which only applies to a select few language statements, such as foreach
- see this answer for details.
Note that even though language statements use { ... }
blocks too, they aren't stand-alone script-block objects - the {
and }
purely act as delimiters for the enclosed code, so that a return
in such a block exits the enclosing function or script file (or stand-alone script block) instead; a quick illustration of the difference:
# Prints only 1 - the `return` exists the enclosing script block,
# not (just) the `foreach` statement.
& { foreach ($i in 1..2) { $i; return }; 'never get here' }
However, I suspect you are instead looking for an analog to the break
keyword, which stops processing further input on demand, i.e. stops the ongoing enumeration.
As with continue
, break
only meaningfully works with a select few language statements - used outside such a context, PowerShell looks up the call stack for an enclosing loop / switch
statement anywhere and breaks out of that; in the absence of one, the entire script (runspace) is exited.
As of PowerShell (Core) 7.4.0, there is NO break
analog for .ForEach()
or ForEach-Object
/ the pipeline in general.
Future considerations:
This asymmetry between loop-like statements and the pipeline is unfortunate:
Stopping an enumeration on demand can be an important performance optimization.
Outside of loop-like statements, this is currently only supported in two very specific scenarios:
As iRon points out, .ForEach()
's companion function, the intrinsic .Where()
method, has an optional second parameter to which 'First'
may be passed to stop enumeration once the first match has been found.
In the pipeline, Select-Object
's -First
parameter is capable of stopping the enumeration after a given number of objects have been received.[2]
There's a long-standing feature request that asks for user code to be able to stop upstream cmdlets on demand:
GitHub issue #3821
No decision as to how to implement this has been made as of this writing, but the two basic choices are:
- Provide a new cmdlet.
- Provide a new keyword (note that applying
break
and continue
to script blocks in pipelines too is not an option, as it would break backward compatibility).
Neither approach is a good fit for also bringing support for on-demand enumeration stoppage to the .ForEach()
method, although a keyword-based approach, if named abstractly, would work better conceptually, e.g. breakenum
or, to avoid confusion with break
, stopenum
Performance considerations:
Given Santiago's pipeline-performance optimization based on a process
block, the performance ranking is actually as follows, based on an input collection that is already in memory, in full:
The foreach
statement - the fasted by far.
The pipeline with the process
block workaround, to compensate for the inefficient implementation of ForEach-Object
, roughly one order of magnitude slower.
The .ForEach()
method, roughly two orders of magnitude slower.
The ForEach-Object
cmdlet, roughly two orders of magnitude slower, and around 50% slower than .ForEach()
.
Note:
The rough relative performance qualifiers are based on experiments with processing 1 million input objects and capturing the output in a variable, in PowerShell (Core) v7.4.0 (built on .NET 8), on both macOS and Windows. (In Windows PowerShell, foreach
is noticeably slower than in PowerShell (Core), though still about 5 times faster than the process
block solution).
There is one case in which .ForEach()
is slower than ForEach-Object
, but it is atypical: if you output results directly to the host (console).
Memory-usage / output-timing considerations - collect-in-full vs. streaming behavior:
Reasons to use or avoid .ForEach()
/ .Where()
:
Given the faster alternatives, are there still benefits to .ForEach()
and .Where()
?
Downsides:
Both .ForEach()
and .Where()
always emit a collection,[3] even if there's only one output object.
Given PowerShell's automatic enumeration behavior in the pipeline and its member-access enumeration feature, that will often not matter in practice, though it is more likely to be a problem with .Where()
in combination with 'First'
; e.g.:
# Breaks, because the collection returned cannot bind
# to the [int] parameter; requires [0]
& {
param([int] $Number)
} (1, 5, 10).Where({ $_ -ge 5 }, 'First')
Due to the specific type of the collection,[3] its elements are wrapped in typically invisible [psobject]
instances, which is not only unnecessary, but can have side effects.
Again, it will often not matter in practice and never should, but these meant-to-be-invisible wrappers do situationally result in different behavior - see GitHub issue #5579.
Method syntax - i.e., the need to use (...)
around the argument list and to separate arguments with ,
- is not a natural fit for PowerShell (which uses shell-like invocation syntax), though for users with a programming background that is less likely to be an issue.
Upsides:
As method calls, they can directly act as or take part in expressions. This allows you to do things such as:
# Direct use as a command argument.
Write-Output (1..3).ForEach({ $_ + 1 })
# Direct use as pipeline input.
(1..3).ForEach({ $_ + 1 }) | Write-Output
Note:
Language statements such as foreach
can only act as as expressions in an assignment, and only stand-alone, e.g., $results = foreach ($i in 1..3) { $i + 1 }
works, but direct use of foreach
in the examples above would not.
To make language statements work as expressions in general, wrap them in $(...)
or @(...)
for up-front collection of their outputs, or in & { ... }
or . { ... }
to stream their output (something that .ForEach()
and .Where()
cannot do).
Additional features: Both .ForEach()
and .Where()
have features that their cmdlet counterparts, ForEach-Object
and Where-Object
do not support.
.Where()
notably supports stopping after the first or only returning the last match, as well as splitting the input collection in two; e.g.:
# Returns right away, because 'First' stops enumeration after the
# first match. 'Last' is available too.
(1..1e6).Where({ $_ -eq 2 }, 'First')
# Returns *two* collections
$odd, $even = (1..10).Where({ $_ % 2 -eq 1 }, 'Split')
- Bringing these powerful features to the
Where-Object
cmdlet too is the subject of GitHub issue #13834.
.ForEach()
's additional features are less compelling, as expression-mode alternatives exists, but one feature is useful when member-access enumeration isn't available: the availability to efficiently collect property values from all elements of a collection; e.g.:
# Returns a collection of all file lengths (sizes).
# Note that (Get-ChildItem -File).Length - i.e. member-access enumeration -
# is NOT an option here, because the array used to collect the
# command output *itself* has a .Length property.
(Get-ChildItem -File).ForEach('Length')
[1] See GitHub issue #10982.
[2] It does this via a non-public exception type, which is why user code cannot take advantage of it.
[3]
Of type System.Collections.ObjectModel.Collection`1
, with [psobject]
-typed elements.
.ForEach
is not faster thanforeach
, in fact.ForEach
is the worst of those three methods because it doesn't stream output. – Marybethmaryellen.ForEach
always produces output, no matter what as shown in my answer ;) – Marybethmaryellen