【问题标题】:Get pipeline value without a Foreach-Object? (add-member)在没有 Foreach-Object 的情况下获取管道值? (添加成员)
【发布时间】:2022-01-30 05:02:13
【问题描述】:

我正在调试一个管道,最后意识到所有对象都引用了同一个哈希表。这是一个惊喜。下面的逻辑对吗?

在下面的管道中,每个实例都有一个属性“MasterHash”,引用同一个哈希表。 NotePropertyValue 仅在管道启动时计算一次。如果没有 Foreach-Object 或 Where-Object,则可以认为所有表达式在任何项被管道传输之前都已被评估。

$myObjects | Add-Member -NotePropertyName 'MasterHash' -NotePropertyValue @{x='y'}

如果每个项目都需要一个唯一的哈希表,则需要一个 Foreach-Object,如下所示。

$myObjects | ForEach-Object { Add-Member -InputObject $_ -NotePropertyName 'MyHash' -NotePropertyValue @{x='y'} }

$myObjects | ForEach-Object { $_ | Add-Member -NotePropertyName 'MyHash' -NotePropertyValue @{x='y'} }

我觉得有点混乱。表达式是否在 begin-process 块中求值?或者也许是在脚本被编译时计算的? Foreach-Object 是访问管道中值的正确方法吗?是否有更好的语法来访问管道值?例如,

$myObjects | Add-Member -NotePropertyName 'NotMasterHash' -NotePropertyValue ???@{x='y'}???

在哪里???是一种神奇的方法,可以让这些计算管道中每个项目的表达式。

【问题讨论】:

  • 很难知道你真正想知道或完成什么,你问题上的 3 行代码正在做完全相同的事情,除了第一行是最有效的......
  • 那么,您有一个 objects 数组,并且您想添加一个名为 MyHash 的新属性,将 Hashtable 存储到它的每个元素,对吗?那么.. 迭代对象集合来做到这一点有什么问题?

标签: powershell scope pipeline


【解决方案1】:

您可以使用Trace-Command 查看每个管道示例的底层参数绑定 (detailed documentation here)。

Trace-Command -Name ParameterBinding -PSHost -FilePath debug.txt -Expression {
  <# your pipeline here #>
}

您将能够看到评估每个表达式的时间和频率。

一般来说,每个表达式都会在其命令在管道中被调用时进行评估 - 它们不是预先加载的。您可以通过在管道末端注入除以零错误来测试这一点。如果前面的命令运行,那么在管道的该阶段的命令开始之前(或在调用该命令之前参数绑定开始),最后的表达式实际上不会被计算。

【讨论】:

    【解决方案2】:

    一般来说,管道命令的所有参数在管道启动之前绑定一次(甚至在第一个 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-ConsumerForEach-Object 的脚本块)。

    Add-Counter 在管道开始之前只被调用一次(对于Test-Producer 参数)。然后它会被调用两次,在每次调用 Test-Consumer 之前调用一次。实际上Add-Counter是在子管道$_ | Test-Consumer运行之前调用的,所以管道参数绑定的规则仍然适用于这个子管道。

    实验 3 - 脚本参数

    正如我在开头所写的,接受管道输入的参数可以是后期绑定的。为了测试这一点,我们通过指定ValueFromPipelineByPropertyName 属性来修改Test-Consumercounter 参数:

    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-Consumer2process{} 块为counter 参数获取不同的值,类似于使用ForEach-Object 的第二个实验。与第二个示例的不同之处在于Add-Counter 调用与process 块交错,因为Test-Consumer2 中的beginend 只需要调用一次,因此效率更高。在第二个实验中,必须为每个输入元素调用它们。

    请注意,这不适用于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 运算符只比较不同的引用。

    【讨论】:

      【解决方案3】:

      我想我明白你的意思了吗?在第一个示例中,添加成员仅运行一次。一些命令会阻塞管道,直到它们收集所有元素,如排序对象。看起来 add-member 会这样做,即使在 Powershell 7 中也是如此。 $myobjects 在两种情况下看起来都一样。 Write-host 可以是一个很好的调试工具,因为它在管道之外写入。有时您需要$() 来替换单个管道。

      # case 1 add-member only
      $myobjects = 'item
      a
      b
      c' | convertfrom-csv
      
      
      $myObjects | Add-Member MyHash @{x=$(write-host hi; get-random)}
      
      hi
      
      
      $myobjects
      
      item MyHash
      ---- ------
      a    {x}
      b    {x}
      c    {x}
      
      
      $myobjects.MyHash  # values all the same
      
      Name                           Value
      ----                           -----
      x                              442950873
      x                              442950873
      x                              442950873
      
      
      
      # case 2 with foreach-object added
      $myobjects = 'item
      a
      b
      c' | convertfrom-csv
      
      
      $myObjects | % { $_ | Add-Member MyHash @{x=$(write-host hi;get-random)} }
      
      hi
      hi
      hi
      
      
      $myobjects
      
      item MyHash
      ---- ------
      a    {x}
      b    {x}
      c    {x}
      
      
      $myobjects.myhash # values different
      
      Name                           Value
      ----                           -----
      x                              444785508
      x                              1083187689
      x                              137326227
      

      【讨论】:

        【解决方案4】:

        如果您想大致了解 Add-Member cmdlet 针对这种特定情况所做的工作,您可以这样查看:

        $objects = 0..10 | ForEach-Object {
            [pscustomobject]@{
                Val = $_
            }
        }
        
        function Add-Member2 {
        [cmdletbinding()]
        param(
            [parameter(Mandatory)]
            [string]$NotePropertyName,
            [parameter(Mandatory)]
            [object]$NotePropertyValue,
            [parameter(ValueFromPipeline, Mandatory)]
            [object]$InputObject
        )
            begin {
                $newProp = [psnoteproperty]::new(
                    $NotePropertyName,
                    $NotePropertyValue
                )
            }
        
            process {
                $InputObject.PSObject.Properties.Add($newProp)
            }
        }
        
        $objects | Add-Member2 -NotePropertyName 'MasterHash' -NotePropertyValue @{ x = 'y' }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2015-05-09
          • 2023-01-13
          • 1970-01-01
          • 1970-01-01
          • 2011-08-28
          • 2016-02-13
          • 1970-01-01
          相关资源
          最近更新 更多