以前、PowerShell 4.0 の コレクションフィルタリングについてまとめてみるでPowerShell 4.0で追加された .Where() を含めてコレクション処理について書きました。
今回は、ちょっと .Where() と .ForEach() の利用において注意が必要だと思われる状況なので記事を書いてみましょう。
.Where() メソッド利用時の注意
まずはコードを見てみましょう。
List<PSCustomObject>を生成します。
$list = New-Object -TypeName 'System.Collections.Generic.List[PSCustomObject]' 1,2 | Foreach { $object = [PSCustomObject]@{ ID = "$_" Array = @("first","second") } $list.Add($object) }
Where-Object での処理
で、Where-Objectを使ってID2のArrayに要素 "third"を追加します。
PS> $entry1 = $list | Where-Object {$_.ID -eq 1} PS> $entry1.Array += "third" PS> $entry1 ID Array -- ----- 1 {first, second, third}
[PSCustomObject]型です。
PS> $entry1.GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True False PSCustomObject System.Object
.Where() メソッドでの処理
では、これをPowerShell 4.0で追加された .Whereメソッドを使って行うと、$entry2.Array += "third"で失敗します。
PS> $entry2 = $list.Where({$_.ID -eq 2}) PS> $entry2.Array += "third" The property 'Array' cannot be found on this object. Verify that the property exists and can be set. At line:2 char:1 + $entry2.Array += "third" + ~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (:) [], RuntimeException + FullyQualifiedErrorId : PropertyAssignmentException
型を確認すると、[PSCustomObject]型ではなく、Collectionとして返ってきました。
PS> $entry2.GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True Collection`1 System.Object
これはPowerShellがどのように式結果を返すかを考えるとおのずとわかります。ヒントはMethodとパイプラインです。 PowerShellは、式結果が複数あった場合はCollectionとして返します。まずこれが原則です。
パイプライン を介する
パイプラインを通るとき、PowerShellは入力されたオブジェクトを展開、ストリームに渡して、右辺の式を実行、式結果を変数格納時にArray(1オブジェクトの場合はスカラ)としてまとめあげなおします。
つまりWhere-Objectの時は、パイプラインを通してオブジェクトの要素を処理、結果を元の型にまとめあげなおしています。なので、型[PSCustomObject]が維持されています。
$entry1 = $list | Where-Object {$_.ID -eq 1}
一方で、.Where()はどうでしょうか。これはメソッド処理されており、各要素が処理されてパイプラインを介さず、順に変数へ格納されています。
つまり、式結果要素を順番に新しいコレクションとして保持しています。 PSCustomObjectではないのでArrayというプロパティは存在せずアクセスできません。当然です。
$entry2 = $list.Where({$_.ID -eq 2})
つまり、これとやっていることは一緒ですね。
$array = 1..10
回避方法
簡単です。 パイプラインを通って変数に格納するか、パイプライン同様に式結果を変数格納前にまとめればいいのです。
パイプライン経由で変数に格納する
Set-Variableを使えばokです。 つまり、元のこれを
$entry2 = $list.Where({$_.ID -eq 2}) $entry2.Array += "third"
このようにパイプラインを介してSet-Variableで変数に格納します。
$list.Where({$_.ID -eq 2}) | Set-Variable entry2 $entry2.Array += "third"
期待した結果です。
PS> $entry2 ID Array -- ----- 2 {first, second, third}
式結果を変数格納前にまとめる
部分実行$()で括ってしまいます。
$entry2 = $($list.Where({$_.ID -eq 2})) $entry2.Array += "third"
期待した結果になっています。
PS> $entry2 ID Array -- ----- 2 {first, second, third}
.ForEach で要素にアクセスできない
問題は.ForEach()です。おそらくバグと見ておりConnectにも挙げています。
さきほどのコードを利用します。てきとーにList<PSCustomObject>を生成します。
$list = New-Object -TypeName 'System.Collections.Generic.List[PSCustomObject]' 1,2 | Foreach { $object = [PSCustomObject]@{ ID = "$_" Array = @("first","second") } $list.Add($object) }
Foreach-Objectやforeach($l in $list) では問題ありません。
PS> $list | ForEach-Object {$_.GetType()} IsPublic IsSerial Name BaseType -------- -------- ---- -------- True False PSCustomObject System.Object True False PSCustomObject System.Object
.ForEach()で各オブジェクトにアクセスを試みると失敗します。
PS> $list.ForEach({$_.GetType()}) You cannot call a method on a null-valued expression. At line:1 char:16 + $list.ForEach({$_.GetType()}) | ft -au | clip + ~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (:) [], RuntimeException + FullyQualifiedErrorId : InvokeMethodOnNull You cannot call a method on a null-valued expression. At line:1 char:16 + $list.ForEach({$_.GetType()}) | ft -au | clip + ~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (:) [], RuntimeException + FullyQualifiedErrorId : InvokeMethodOnNull
コレクション要素にアクセスできずnull判定なので.Whereの状況とは違います。
[PSCustomObject]には.ForEach()がないのはいいのですが、そもそも要素にアクセスできないようです。
# 単純な Array では問題ない ((1..10).ForEach{$_}).GetType() $fuga = (1..10).ForEach{$_} $fuga.ForEach{$_} # Collectionも問題ない (1..10).ForEach{$_} | Set-Variable hoge $hoge.ForEach{$_} $hoge.GetType() # PSCustomObjectは .ForEach() を含まないのでエラーとなる $list[0].ForEach{$_}
まとめ
.Where()いい。.ForEach()の挙動確認が必要。