ForEach-Object 是对管道中的单个对象还是对象集合进行操作?

Does ForEach-Object operate on a single object in the pipeline or on a collection of objects?

我很难理解 PowerShell 管道的工作原理,我意识到很多问题都是由于 ForEach-Object。在我使用过的其他语言中,foreach 对集合进行操作,依次遍历集合中的每个元素。我假设 ForEach-Object 在 PowerShell 管道中使用时会执行相同的操作。然而,我读到的关于管道的所有内容都表明,集合的每个元素都分别通过管道传递,并且重复调用下游 cmdlet,分别对每个元素而不是对整个集合进行操作。

那么 ForEach-Object 是对集合中的单个元素进行操作,而不是对整个集合进行操作吗?从不同的角度来看,管道运算符是将整个集合传递给 ForEach-Object,然后对其进行迭代,还是管道对象对集合进行迭代并将每个元素分别传递给 ForEach-Object

ForEach-Object 遍历集合中的每个项目。当它在当前项目上执行完它的脚本块时,它会沿着管道发送到下一个命令,然后可以立即开始处理它(而 ForEach-Object 正在处理下一个项目,如果有的话)。

您可以在以下示例中看到这一点:

Get-Process | ForEach-Object { Start-Sleep 1; $_ } | Format-Table

Get-Process cmdlet 获取进程列表并立即将每个进程一次一个地发送给 ForEach-ObjectForEach-Object 等待 1 秒,然后输出当前管道元素 $_。这是由 Format-Table 接收的,它输出为 table。你可以看到它并没有等到所有的进程都处理完才输出到屏幕上。

答案是……两者兼而有之。

支持流水线的 PowerShell 函数(高级函数)将 处理 单独通过流水线的每个项目。它还可以定义一个 beginend 块,它们将在流水线阶段只执行一次。也就是说,基本结构是这样的:

function Do-Stuff {
    begin {
         write-output "This will be done once, at the beginning"
    }
    process {
      Write-output "This will be done for each item"
    }
    end {
        Write-output "This will be done once, at the end"
    }
}

1..3 | foreach-Object {Do-Stuff $_} 的输出将是:

This will be done once, at the beginning
This will be done for each item
This will be done for each item
This will be done for each item
This will be done once, at the end

因为Do-Stuff正在写入输出流,如果在这个Foreach-Object之后有额外的管道阶段,每个对象输出将被传递到依次进入下一阶段。如果没有任何进一步的阶段或其他任何东西来捕获输出,输出流将被写入控制台。

例如:

$verbosepreference = "continue";
[int]1..3|foreach-object {write-output $_; write-verbose ($_*-1)}|foreach-object {$_*$_;write-verbose $_} 

给出以下输出:

1
VERBOSE: 1
VERBOSE: -1
4
VERBOSE: 2
VERBOSE: -2
9
VERBOSE: 3
VERBOSE: -3

-X 最后(对于每个项目)输出到 Verbose 流,因为输出被传递到管道的下一阶段并在 之前 处理下一阶段foreach-object 脚本块中的语句已执行。

ForEach-Object cmdlet - 不同于 foreach 语句 - 本身 执行 枚举.

相反,它对通过管道传递的每个项目进行操作(还可以选择在接收第一个项目之前和接收最后一个项目之后执行代码,如果有的话)。

因此,可以说它的命名很糟糕,因为 它是 管道 提供枚举 (默认情况下),并且ForEach-Object 只需为收到的每个项目调用一个脚本块。

以下示例说明了这一点:

# Let the pipeline enumerate the elements of an array:
> 1, 2 | ForEach-Object { "item: [$_]; count: $($_.Count)" }
item: [1]; count: 1
item: [2]; count: 1

# Send the array *as a whole* through the pipeline (PSv4+)
> Write-Output -NoEnumerate 1, 2 | ForEach-Object { "item: [$_]; count: $($_.Count)" }
item: [1 2]; count: 2

请注意,脚本/函数/cmdlet 可以选择是否应枚举它们写入输出流(管道)的集合或作为一个整体(作为单个对象)发送。

在 PowerShell 代码(脚本或函数,无论是否高级(类似于 cmdlet),枚举都是默认设置,但您可以使用 Write-Output -NoEnumerate 选择退出; -NoEnumerate 开关是在 PSv4 中引入的;在此之前,您必须使用 $PSCmdlet.WriteObject(),它仅适用于 advanced 脚本/函数。

另请注意,通过将命令包含在 (...) 中来将命令嵌入 表达式 会强制枚举 :

# Send array as a whole.
> Write-Output -NoEnumerate 1, 2 | Measure-Object

Count: 1
...

# Converting the Write-Output -NoEnumerate command to an expression
# by enclosing it in in (...) forces enumeration
> (Write-Output -NoEnumerate 1, 2) | Measure-Object

Count: 2
...