PowerShell ForEach-Object 消除

PowerShell ForEach-Object elimination

让我们考虑一个集合的集合,以及需要在管道内对内部集合的每个元素执行的操作。

为了简单起见,就让它成为数组的数组吧,操作就是简单的打印到屏幕上。为了表达我的问题,让我们也有一个元素不是集合的数组:

$Array = "A", "B", "C"
$ArrayOfArrays = (1, 2, 3), (4, 5, 6), (7, 8, 9)

我们知道管道会将集合分解为元素,如下所示:

$Array | & {process {Write-Host $_}}
$ArrayOfArrays | & {process {Write-Host $_}}

现在,令我惊讶的是,当我 运行 这样做时,它并没有将内部数组分解为其元素:

$ArrayOfArrays | % -process {Write-Host $_} (1)

这都不是:

$ArrayOfArrays | % -process {% -process {Write-Host $_}} (2)

(但是后者似乎是不必要的尝试,看到 (1) 没有那样做,但我试过了...)
我希望尝试 (1) 这样做,因为我认为管道会进行一次分解,并且当 ForEach-Object 接收到一个元素时,如果它是一个集合,它将进一步分解它。

我只能用内部管道解决它:

$ArrayOfArrays | % -process {$_ | % -process {Write-Host $_}} (3)

但是通过这种方法我可以消除 ForEach-Object,当然:

$ArrayOfArrays | & {process {$_ | & {process {Write-Host $_}}}} (4)

所以我的两个问题是:

1,

How to access an element of a collection that is in the collection in a pipeline, other than tries (3) and (4), or is this the only way to do that?

2,

If the only way to do what question 1 is asking is tries (3) and (4), then what is a valid use case of ForEach-Object, where it can not be eliminated? I mean it can be a logical case, but also performance vs a script block. The fact that it is nicer than a script block with one pair of braces less is just not really enough for me...

.
Manuel Batsching 回答后编辑:

作为ForEach-Objectreturns一个collection的元素经过处理后,我们可以这样做(我放弃了Write-Host,可能不是一个很好的任意操作,所以让它是 GetType):

$ArrayOfArrays | % -process {$_} | & {process {$_.GetType()}}

但我们也知道,如果某些东西 returns 是管道中的一个新对象,如果它被进一步管道化并且它是一个集合,它将触发故障。所以要进行细分,我们可以再次消除 ForEach-Object 并执行此操作:

$ArrayOfArrays | & {process {$_}} | & {process {$_.GetType()}}

如果我像这样定义一个过滤器,这个虚拟操作可以在语法上减少:

Filter §
{
    param (
            [Parameter (Mandatory = $True, ValueFromPipeline = $True)]
            [Object]
            $ToBeTriggeredForBreakDown
    ) # end param

    $ToBeTriggeredForBreakDown

}

并像这样使用它:

$Array | § | & {process {$_.GetType()}}
$ArrayOfArrays | § | & {process {$_.GetType()}}

$ArrayOfArraysOfArrays = ((1, 2), (3, 4)), ((5, 6), (7, 8))
$ArrayOfArraysOfArrays | § | & {process {$_.GetType()}}
$ArrayOfArraysOfArrays | § | § | & {process {$_.GetType()}}

所以当我使用 ForEach-Object 时,我仍然很难看到它,在我看来它完全没用 - 除了我在问题中寻找的原因。

.
研究后编辑:

一些集合提供了它们自己的方法,例如因为 v4 arrays 有一个 ForEach 方法,所以除了 (3) 和 (4) 之外,还可以这样做(同样是一个虚拟操作,但代码更少):

$ArrayOfArrays.ForEach{$_} | & {process {$_.GetType()}}

所以这部分涵盖了问题 1。

根据我的理解,一旦数组沿着管道向下传递或传递到输出流,数组的展开就完成了。

您将通过以下所有方法看到此行为:

$ArrayOfArrays | % -process { $_ }
$ArrayOfArrays | & { process { $_ } }
foreach ($arr in $ArrayOfArrays) { $arr }

现在破坏您示例中的展开的是 Write-Host cmdlet。由于此 cmdlet 不是写入输出流而是写入您的控制台,因此它将输入对象转换为 [string]。这就是为什么您会在控制台上看到内部数组的字符串表示形式。

Write-Host替换为Write-Output,内部数组将被正确展开:

 PS> $ArrayOfArrays | % -process { Write-Output $_ }
1
2
3
4
5
6
7
8
9

编辑:

您可以使用调试器来准确确定解包完成的位置。例如,在 VSCode:

中使用以下代码
$ArrayOfArrays = (1, 2, 3), (4, 5, 6), (7, 8, 9)
$foo = $null
$foo = $ArrayOfArrays | % { Write-Output $_ }

$foo = $null行设置断点,将变量$foo$_添加到监视列表,按F5启动调试器并观察变量变化,同时按F11 进入各个处理步骤。

  • $_ 将显示管道中的当前元素的内部数组。
  • $foo 管道执行结束后将只接收解包的元素

在 PowerShell 7 中,Foreach-Object 具有用于并行执行的 -Parallel 开关。这不一定对所有类型的处理都快。您将不得不对此进行试验。

Foreach-Object-Process 参数采用脚本块数组。因此,您可以在技术上对每个管道对象执行不同的处理脚本。

1,2,3 | Foreach-Object -begin {"First loop iteration"} -process {$_ + 1},{$_ + 2},{$_ + 3} -End {"Last loop iteration"}
First loop iteration
2
3
4
3
4
5
4
5
6
Last loop iteration

# Example of already having script blocks defined
$sb1,$sb2,$sb3 = { $_ + 1 },{$_ + 2},{$_ + 3}
1,2,3 | Foreach-Object -begin {"Starting the loop"} -process $sb1,$sb2,$sb3 -end {"the loop finished"}
Starting the loop
2
3
4
3
4
5
4
5
6
the loop finished

Foreach-Object也支持运算语句。从技术上讲,您无需执行任何操作,但可以说 1,2,3 | Foreach ToString1,2,3 | & { process { $_.ToString() }}.

更具可读性

Foreach-Object 也有 -InputObject 参数,您可以在其中将整个对象作为一个项目处理。这是它防止您在管道中看到的阵列展开的方式。你可以用你的方法做到这一点,但你必须在发送到管道之前像 ,@(1,2,3) 一样对自己进行模糊的数组包装。

# Single pipeline object
$count = 1
ForEach-Object -InputObject 1,2,3 -Process {"Iteration Number: $count"; $_; $count++}
Iteration Number: 1
1
2
3

# array unwrapping down pipeline

$count = 1
1,2,3 | ForEach-Object -Process {"Iteration Number: $count"; $_; $count++}
Iteration Number: 1
1
Iteration Number: 2
2
Iteration Number: 3
3

由于 Foreach-Object 是一个 cmdlet,您可以访问 Common Parameters。因此,您可以利用 -PipelineVariable 将此命令的输出用于更深层次管道中的命令。

# Using OutVariable
1,2,3 | Foreach-Object {$_ + 100} -OutVariable numbers |
    Foreach-Object -process { "Current Number: $_"; "Numbers Processed So Far: $numbers" }
Current Number: 101
Numbers Processed So Far: 101
Current Number: 102
Numbers Processed So Far: 101 102
Current Number: 103
Numbers Processed So Far: 101 102 103

# Using PipeLineVariable
1,2,3 | Foreach-Object {$_ + 100} -PipeLineVariable first |
    Foreach-Object {$_ * 2} -PipelineVariablesecond |
        Foreach-Object {"First number is $first"; "second number is $second"; "final calculation is $($_*3)" }
First number is 101
second number is 202
final calculation is 606
First number is 102
second number is 204
final calculation is 612
First number is 103
second number is 206
final calculation is 618

我的测试用例表明 data | & { process {}} 方法比 data | foreach-object -process {} 方法快。因此,关于您想从中得到什么,这似乎是一种权衡。

Measure-Command {1..100000 | & { process {$_}}}


Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 0
Milliseconds      : 107
Ticks             : 1074665
TotalDays         : 1.24382523148148E-06
TotalHours        : 2.98518055555556E-05
TotalMinutes      : 0.00179110833333333
TotalSeconds      : 0.1074665
TotalMilliseconds : 107.4665


Measure-Command {1..100000 | Foreach-Object {$_}}


Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 0
Milliseconds      : 768
Ticks             : 7686545
TotalDays         : 8.89646412037037E-06
TotalHours        : 0.000213515138888889
TotalMinutes      : 0.0128109083333333
TotalSeconds      : 0.7686545
TotalMilliseconds : 768.6545

当 运行 宁 Foreach-Object 时,所有代码都 运行 在当前调用者的范围内,包括脚本块的内容。 & 运行 子作用域中的代码以及该作用域中的任何更改在返回父作用域(调用作用域)时可能不会反映出来。您将需要使用 . 在当前范围内调用。

# Notice $a outputs nothing outside of the loop
PS > 1,2,3 | & { begin {$a = 100} process { $_ } end {$a}}
1
2
3
100
PS > $a

PS >

# Notice with . $a is updated
PS > 1,2,3 | . { begin {$a = 100} process { $_ } end {$a}}
1
2
3
100
PS > $a
100
PS >

# foreach updates current scope (used a different variable, because  
# $a was already added by the previous command)
PS > 1,2,3 | foreach-object -begin {$b = 333} -process {$_} -end {$b}
1
2
3
333
PS > $b
333
PS >