3

I'm trying to use a higher-order function to perform a certain task, but it seems that the scope isn't working as I expected. The code is as follows:

function DoSomething($scriptBlock, $message) {

    # if not only running a check, run the given code
    if ($shouldRunCheck) {
       return $scriptBlock.Invoke()
    }

    Write-Host $message -foreground cyan
    # write $message to file
}

This seems to work fine, but when I call it I don't seem to be able to save to variables outside of the script block.

$myArray = @()
$myArray += 'test 1'

DoSomething {
   write-host $myArray # test 1
   $myArray += 'test 2'

   write-host $myArray # test 2
}

write-host $myArray # test 1
$myArray += 'test 3'
write-host $myArray # test 1 test 3

Essentially I'm need to add to the array variable from within the callback function but it just seems to over-write the variable as if the variable is read only?

2 Answers 2

3

+1 for @Theo explanation, I would although try to avoid to use the $Global: and the $Script: scopes as they might get confusing.
It is also possible to refer to the current scope of the caller by using ([Ref]$myArray).Value:

function DoSomething($scriptBlock) {
    return $scriptBlock.Invoke()
}

$myArray = @('test 1')
$scriptBlock = {
    ([Ref]$myArray).Value += 'test 2'
}

DoSomething $scriptBlock

$myArray += 'test 3'
Write-Host $myArray
test 1 test 2 test 3

But the explanation from Theo also implies that the use += on a PowerShell array is a bad idea as you will recreate the array each time which is just slow.

In other words for a better performance, it is better to use an ArrayList and the Add method rather than assigning items:

function DoSomething($scriptBlock) {
    return $scriptBlock.Invoke()
}

$myArray = New-Object System.Collections.ArrayList
$myArray.Add('test 1')
$scriptBlock = {
    $myArray.Add('test 2')
}

DoSomething $scriptBlock

$myArray.Add('test 3')
Write-Host $myArray
test 1 test 2 test 3

You might also consider to use the mighty PowerShell pipeline for something like this:

$scriptBlock = {
    'test 2'
}

$myArray = @(
    'test 1'
    DoSomething $scriptBlock
    'test 3'
)
Write-Host $myArray
test 1 test 2 test 3
Sign up to request clarification or add additional context in comments.

2 Comments

The ([Ref]$myArray).Value += 'test 2' option is a good shout.
adding ([ref] ...) does not work for me, the variable is still set to 0 (I use integers instead of strings)
3

You are correct about the Scoping.

What happens is that inside the scriptblock, the variable $myArray is created new as type [String] when $myArray += 'test 2' is performed. This new variable does not exist outside the function, so the original $myArray is not altered at all.

To do that, you need to use scoping on the $myArray and declare it as $script:myArray = @('test 1') so it can be accessed throughout the entire script.
Then inside the scriptblock you add the new value to it using $script:myArray += 'test 2', like:

function DoSomething($scriptBlock) {
    return $scriptBlock.Invoke()
}

$script:myArray = @('test 1')                # declared in script-scope

# while inside the main script, you can simply access it as $myArray
Write-Host "MainScript:  $myArray"           # --> test 1   

$scriptBlock = {
   Write-Host "ScriptBlock: $script:myArray" # --> test 1
   $script:myArray += 'test 2'

   Write-Host "ScriptBlock: $script:myArray" # --> test 1 test 2
}

DoSomething $scriptBlock

Write-Host "MainScript:  $myArray"          # --> test 1 test 2
$myArray += 'test 3'
Write-Host "MainScript:  $myArray"          # --> test 1 test 2 test 3

Result:

MainScript:  test 1
ScriptBlock: test 1
ScriptBlock: test 1 test 2
MainScript:  test 1 test 2
MainScript:  test 1 test 2 test 3

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.