一般来说,管道命令的所有参数在管道启动之前绑定一次(甚至在第一个 begin{} 块运行之前!)。当整个管道再次运行时(例如在循环中),参数也将再次绑定。
这仅适用于当前范围级别。当您在管道中创建新范围(脚本块)时,例如。 G。通过使用ForEach-Object,这个新范围内的参数将为脚本块的每次调用绑定。
此外,接受管道输入的参数可以通过传递脚本块进行后期绑定。
关于您的第一个代码示例...
$myObjects | Add-Member -NotePropertyName 'MasterHash' -NotePropertyValue @{x='y'}
...在管道启动之前创建一个单独的哈希表,然后通过引用传递给Add-Member。因此$myObjects 的所有数组元素都会将此引用存储到同一个哈希表。
关于您的第二个和第三个代码示例...
$myObjects | ForEach-Object { Add-Member -InputObject $_ -NotePropertyName 'MyHash' -NotePropertyValue @{x='y'} }
...我写的关于新范围的内容适用,因此将为每个数组元素创建一个新的哈希表实例。
为了更好地理解,我们可以通过编写我们自己的管道兼容函数来做一些实验来进行一些日志记录。
测试管道函数
在下文中,函数Test-Producer 简单地将两个字母“A”和“B”作为单独的元素输出,它们被Test-Consumer 使用。 Add-Counter 函数为每次调用返回一个递增的数字,我们将使用它作为管道函数的参数。
$script:counter = 1
Function Add-Counter {
Write-Host "Add-Counter returns $script:counter"
($script:counter++) # output, then increment
}
Function Test-Producer {
[CmdletBinding()]
param (
[Parameter()]
[int] $counter
)
begin {
Write-Host "Test-Producer begin: { counter: $counter }"
}
process {
Write-Host "Test-Producer process: { counter: $counter }"
'A', 'B' # Outputs two individual elements, not an array
}
end {
Write-Host "Test-Producer end: { counter: $counter }"
}
}
Function Test-Consumer {
[CmdletBinding()]
param (
[Parameter(ValueFromPipeline)]
[String] $inputObject,
[Parameter()]
[String] $counter
)
begin {
Write-Host "Test-Consumer begin: { inputObject: $inputObject; counter: $counter }"
}
process {
Write-Host "Test-Consumer process: { inputObject: $inputObject; counter: $counter }"
}
end {
Write-Host "Test-Consumer end: { inputObject: $inputObject; counter: $counter }"
}
}
实验 1 - 常规管道参数
管道:
Test-Producer -counter (Add-Counter) | Test-Consumer -counter (Add-Counter)
输出:
Add-Counter returns 1
Add-Counter returns 2
Test-Producer begin: { counter: 1 }
Test-Consumer begin: { inputObject: ; counter: 2 }
Test-Producer process: { counter: 1 }
Test-Consumer process: { inputObject: A; counter: 2 }
Test-Consumer process: { inputObject: B; counter: 2 }
Test-Producer end: { counter: 1 }
Test-Consumer end: { inputObject: B; counter: 2 }
我们可以看到Add-Counter 在开始时被调用了两次,确认管道参数确实很早就绑定了,在管道开始之前。因此,每个命令的counter 参数在整个管道执行过程中都保持在初始值。
实验 2 - ForEach-Object
管道:
Test-Producer -counter (Add-Counter) | ForEach-Object {
$_ | Test-Consumer -counter (Add-Counter)
}
输出:
Add-Counter returns 1
Test-Producer begin: { counter: 1 }
Test-Producer process: { counter: 1 }
Add-Counter returns 2
Test-Consumer begin: { inputObject: ; counter: 2 }
Test-Consumer process: { inputObject: A; counter: 2 }
Test-Consumer end: { inputObject: A; counter: 2 }
Add-Counter returns 3
Test-Consumer begin: { inputObject: ; counter: 3 }
Test-Consumer process: { inputObject: B; counter: 3 }
Test-Consumer end: { inputObject: B; counter: 3 }
Test-Producer end: { counter: 1 }
这个管道的输出看起来有点不同,因为我们现在在一个新的范围内调用了Test-Consumer(ForEach-Object 的脚本块)。
Add-Counter 在管道开始之前只被调用一次(对于Test-Producer 参数)。然后它会被调用两次,在每次调用 Test-Consumer 之前调用一次。实际上Add-Counter是在子管道$_ | Test-Consumer运行之前调用的,所以管道参数绑定的规则仍然适用于这个子管道。
实验 3 - 脚本参数
正如我在开头所写的,接受管道输入的参数可以是后期绑定的。为了测试这一点,我们通过指定ValueFromPipelineByPropertyName 属性来修改Test-Consumer 的counter 参数:
Function Test-Consumer2 {
[CmdletBinding()]
param (
[Parameter(ValueFromPipeline)]
[String] $inputObject,
[Parameter(ValueFromPipelineByPropertyName)]
[String] $counter
)
begin {
Write-Host "Test-Consumer2 begin: { inputObject: $inputObject; counter: $counter }"
}
process {
Write-Host "Test-Consumer2 process: { inputObject: $inputObject; counter: $counter }"
}
end {
Write-Host "Test-Consumer2 end: { inputObject: $inputObject; counter: $counter }"
}
}
管道:
Test-Producer -counter (Add-Counter) | Test-Consumer2 -counter { Add-Counter }
注意与第一个实验相比的不同之处 - 现在我们将 Add-Counter 包裹在花括号中以创建一个脚本块。如果我们在第一个实验中使用常规大括号,什么都不会改变。
输出:
Add-Counter returns 1
Test-Producer begin: { counter: 1 }
Test-Consumer2 begin: { inputObject: ; counter: }
Test-Producer process: { counter: 1 }
Add-Counter returns 2
Test-Consumer2 process: { inputObject: A; counter: 2 }
Add-Counter returns 3
Test-Consumer2 process: { inputObject: B; counter: 3 }
Test-Producer end: { counter: 1 }
Test-Consumer2 end: { inputObject: B; counter: 3 }
现在Test-Consumer2 的process{} 块为counter 参数获取不同的值,类似于使用ForEach-Object 的第二个实验。与第二个示例的不同之处在于Add-Counter 调用与process 块交错,因为Test-Consumer2 中的begin 和end 只需要调用一次,因此效率更高。在第二个实验中,必须为每个输入元素调用它们。
请注意,这不适用于Add-Member。您不能将脚本块传递给 NotePropertyValue 参数,因为它不接受管道输入。
结论
要为每个数组元素创建一个新的哈希表实例,您可以使用ForEach-Object,但这不是最有效的方法。
最有效的方法是使用数组方法.ForEach,因为它不涉及管道开销:
$myObjects = [PSCustomObject]@{}, [PSCustomObject]@{}
$myObjects.ForEach{
Add-Member -InputObject $_ -NotePropertyName 'MyHash' -NotePropertyValue @{x='y'}
}
# Compare references to confirm they are different
$myObjects[0].MyHash -eq $myObjects[1].MyHash
这会输出false,因为每个数组元素都包含一个新的哈希表实例。 -eq 运算符只比较不同的引用。