Search code examples
arrayspowershellparameter-passingscriptblock

PowerShell unrolling arrays differently in function vs. script block


I have an array of hashmaps as follows, and I would like to compute the sum of the x property for each item in the list:

$Items = @(
    @{x = 110; y = 108 },
    @{x = 102; y = 100 },
    @{x = 116; y = 114 }
)

The following function does so correctly:

function Get-SumXFunction {
    Param($Items)
    $Measured = $Items | Measure-Object -Sum x
    return $Measured.Sum
}

But I ultimately want to use this function as a parameter in another function, and I read on this site that the correct way to do this in PowerShell is using a script block. So, I rewrote the function using a script block as follows:

$GetSumXScriptBlock = {
    Param($Items)
    $Measured = $Items | Measure-Object -Sum x
    Write-Output $Measured.Sum
}

(Note that I have changed return to Write-Output, as my understanding is that this is necessary to be able to assign the output of the script block to a variable later on.)

But the script block, when called using .Invoke($Items), doesn't do the sum at all! It just returns the x-value of the first item:

Write-Host "Get-SumXFunction:" (Get-SumXFunction $Items)
Write-Host "Get-SumXScriptBlock:" ($GetSumXScriptBlock.Invoke($Items))
Get-SumXFunction: 328
Get-SumXScriptBlock: 110

Why does the script block give different results than the function? How can I write my function as a script block that produces correct results?


Solution

  • The immediate fix is to use an aux. single-item array to wrap your array of hashtables:

    $GetSumXScriptBlock.invoke((, $Items))
    

    Note:

    • It is the signature of the .Invoke() method that makes this necessary: its only parameter is of type: params object[], which means that passing an array causes its elements to be considered individual arguments to pass to the script block, so that only the first hashtable in your array is passed to the - first and only - parameter of your script block, $Items.

    • Using the unary form of , the array constructor operator, wraps your array of hashtables in an outer, single-element array, therefore causing its only element - the array of hashtables - to be passed as a whole as the first positional script-block argument.


    The preferable fix is to invoke your script block with &, the call operator rather than via the .Invoke() method, which allows you to use the usual argument-mode syntax:

    & $GetSumXScriptBlock $Items
    

    Note:

    • Using the .Invoke() method to execute a script block in PowerShell code (as opposed to when using the PowerShell SDK, typically from C#) is best avoided in general, not just to for syntax reasons, but also because it changes the semantics of the call in several respects. See this answer for more information.

    As for return vs. Write-Output in your code:

    I have changed return to Write-Output, as my understanding is that this is necessary to be able to assign the output of the script block to a variable later on.

    Write-Output is not necessary in your script block, and neither is return - both are equivalent in your case, and can be simplified:

    In essence, a function is also a script block, only a named one (as is a script file).

    In PowerShell code in general, you can use implicit output, and you need return only for explicit flow control.
    Thus, just $Measured.Sum by itself as the last statement in both script blocks is sufficient.

    For more information about: