スクリプトブロックは本当に親スコープの変数を書き換えられないのか?

PowerShellでは基本的に、親スコープの変数を参照することはできても、それを書き換えることはできない。この規則はスクリプトプロックにも適用されている。

$x = 1
2..5 | & {process{$x = $_}}
$x
# => 1

このコードを実行すると「1」が表示される。スクリプト化して実行しても同じだ。

実はドットソース演算子を使ってスクリプトブロックを実行することで親スコープの変数を変更できる――

$x = 1
2..5 | . {process{$x = $_}}
$x
# => 5

――ように見えるのだけど、これはどちらかと言えば親スコープに変更を及ぼしているというよりも、子スコープを作らずに元のスコープで処理を実行している、という理解の方が正しい。

この辺りは「Get-Help about_Scopes」内に記述がある。

スクリプトおよび関数は、スコープのすべての規則に従います。スクリプトおよび関数は特定のスコープ内に作成され、コマンドレット パラメーターまたはスコープ修飾子を使用してスコープを変更しない限り、このスコープのみに作用します。

ただし、ドット ソース表記を使用することで、スクリプトまたは関数を現在のスコープに追加できます。この場合、スクリプトが現在のスコープ内で実行されると、スクリプトによって作成されるすべての関数、エイリアス、および変数が現在のスコープで利用可能になります。

http://technet.microsoft.com/ja-jp/library/dd315289.aspx

もし親スコープの変数を書き換えたいのなら、スコープ修飾子やSet-Variableのオプション-Scopeを使用して、どのスコープの変数なのか明確にする必要がある。この方法は、同じ名前の子スコープの変数によって隠れてしまった親スコープの変数を参照する際にも有効だ。ただしプライベート変数を除く。

$x = 1
2..5 | & {process{Set-Variable x -Scope 1 -Value $_}}
$x
# => 5

1つ上の親スコープを指定しているので、変更が反映されている。

ところが、よく観察してみると少なくとも以下のような例外がある。

  1. Foreach-Objectの引数のスクリプトブロック
  2. System.Windows.Controls名前空間のクラスのオブジェクトに設定したイベントハンドラ

少なくともこの2つのシチュエーションでは、スクリプトブロック内でスコープを明確にせずに1つ上の親スコープの変数を変更できるように見える。

$x = 1
2..5 | %{$x = $_}
$x
# => 5

このコードは、PowerShellのプロンプト上で手動で実行しても、スクリプト化して実行しても、結果として「5」が表示される。この処理を関数化して実行した場合も同じ。

イベントハンドラの件は、Add_Click()などのメソッドの引数に直書きしたスクリプトブロックの中で1つ上の親スコープの変数を書き換えると、普通に変更が反映される。

# You must execute this script in STA mode
# $ powershell -Sta -File <this script>

Set-StrictMode -version Latest

Add-Type -assemblyName PresentationFramework

$xaml = @'
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="$APP_TITLE"
    Width="20" Height="100"
>
  <StackPanel>
    <Button Name="button" Content="OK" />
  </StackPanel>
</Window>
'@

$window = [System.Windows.Markup.XamlReader]::Parse($xaml)

$clicked = $False
$button = $window.FindName('button')
$button.Add_Click({
    $clicked = $True
    $window.Close()
})

$window.ShowDialog() | Out-Null

$clicked

変数$clickedの書き換えが反映されるので、OKボタンを押下した場合のみTrueと表示される。それ以外の場合はFalseと表示される。

うーん、何かしら例外規則的なものがあるのだろうか?