PowerShell V4 で待望のメソッド構文での .Where()と.ForEach()が追加されました。
これまでにも、パイプラインを介した Where-Object によるコレクションのフィルタリングはありましたが、メソッド構文が導入されたのは大きな一歩です。
しかしPowerShell におけるコレクションフィルタリングについてまとまった記事は少ないようです。そこで PowerShell V1,2,3,4 における Where構文を初めとする 各種コレクションフィルタリングに関してまとめてみましょう。
また、PowerShell V4 で追加された Where()メソッド構文での WhereOperatorSelectionMode についても紹介します。
コレクションフィルタリング一覧
PowerShell において、コレクションをフィルタするために以下の方法が使われます。
利用可能なバージョンとその特徴を見てみましょう。
ver | 概要 | 長所 | 短所 |
---|---|---|---|
V1- | Where-Object{} |
・ どのPSバージョンでも実行可能 ・ オブジェクトをパイプランを通してストリーム処理することでメモリが最小限で済む ・ スクリプトブロックではどのような記述も可能 |
・実行速度が遅い |
V3- | Where-Object省略記法 |
・ Where フィルタよりわずかに高速 ・ オブジェクトをパイプランを通してストリーム処理することでメモリが最小限で済む |
・ スクリプトブロックでは一つのプロパティ指定するのみに限定されており複雑な記法は負荷 実行速度がそこまで早くない ・ PowerShell 3.0以降でのみ動作する |
V4- | .Whereメソッド記法 |
・ パイプラインを通したWhere構文より2倍程度高速 ・ スクリプトブロックではどのような記述も可能 |
・ オブジェクトをパイプランを通してストリーム処理できないため、処理はメモリに収まる範囲に限定される ・ PowerShell 4.0以降でのみ動作する |
V1- | filter構文内でフィルタリング |
・ どのPSバージョンでも実行可能 ・ オブジェクトをパイプランを通してストリーム処理することでメモリが最小限で済む ・ スクリプトブロックではどのような記述も可能 ・ Where-ObjectやWhereメソッド構文の利用より高速 |
・ロジック定義箇所を参照しないよ処理内容がわからないため可読性が高くない |
V1- | foreach構文内でフィルタリング |
・ どのPSバージョンでも実行可能 ・ ロープ内でどのような記述も可能 ・ 最速 |
・オブジェクトをパイプランを通してストリーム処理できないため、処理はメモリに収まる範囲に限定される |
各種コレクションフィルタリングによる速度
ではサンプルコードを使って、各種コレクションフィルタリングでどのような速度差があるか見てみましょう。
# -- stopwatch function -- # function Measure-Stopwatch{ [CmdletBinding()] param( [parameter(Mandatory=$true)] [ScriptBlock]$Command, [switch]$Days, [switch]$Hours, [switch]$Minutes, [switch]$Seconds, [switch]$Milliseconds ) $sw = New-Object System.Diagnostics.StopWatch # Start Stopwatch $sw.Start() #TargetCommand to measure $command.Invoke() # Stop Stopwatch $sw.Stop() #Show Result switch ($true){ $Days {$sw.Elapsed.TotalDays} $Hours {$sw.Elapsed.TotalHours} $Minutes {$sw.Elapsed.TotalMinutes} $Seconds {$sw.Elapsed.TotalSeconds} $Milliseconds {$sw.Elapsed.TotalMilliseconds} default {$sw.Elapsed} } #Reset Result $sw.Reset() } # -- define repeat count -- # $repeat = 1000 # -- test ScriptBlock -- # $ps1Where = { foreach ($r in 1..$repeat) { Get-Process | where { $_.Name -eq "powershell_ise" } > $null } } $ps3Where = { foreach ($r in 1..$repeat) { Get-Process | where Verb -eq "powershell_ise" > $null } } $ps4Where = { foreach ($r in 1..$repeat) { (Get-Process).Where({$_.Name -eq "powershell_ise"}) > $null } } $Filter= { filter filterCommand {if ($_.Name -eq "powershell_ise"){$_}} foreach ($r in 1..$repeat) { Get-Process | filterCommand > $null } } $Foreach = { foreach ($r in 1..$repeat) { foreach ($process in Get-Process) { if ($process.Name -eq "powershell_ise") { $process > $null } } } }
実行してみましょう。
# -- start main -- # Write-Host "running ps1- Where-Object cluese" -ForegroundColor DarkGray $PS1Millisec = Measure-Stopwatch -Command $ps1Where -Milliseconds Write-Host "running ps3- Where-Object Simplfied syntax" -ForegroundColor DarkGray $PS3Millisec = Measure-Stopwatch -Command $ps3Where -Milliseconds Write-Host "running ps4 Where method collection" -ForegroundColor DarkGray $PS4Millisec = Measure-Stopwatch -Command $ps4Where -Milliseconds Write-Host "running Filter collection test" -ForegroundColor DarkGray $FilterMillisec = Measure-Stopwatch -Command $Filter -Milliseconds Write-Host "running Foreach conditional collection" -ForegroundColor DarkGray $ForeachMillisec = Measure-Stopwatch -Command $Foreach -Milliseconds # -- show result -- # New-Object PSObject -Property ([ordered]@{ "ps1[ms]" = $ps1Millisec "ps3[ms]" = $ps3Millisec "ps4[ms]" = $ps4Millisec "filter[ms]" = $FilterMillisec "foreach[ms]" = $ForeachMillisec }) | Format-List
実行速度の結果です。 PowerShell V4で導入された Whereメソッド構文が、パイプラインを介したWhere-Objectに比較してかなりの高速化が図られていることがわかります。
ps1[ms] : 10810.8184 ps3[ms] : 8796.8583 ps4[ms] : 6419.6145 filter[ms] : 5709.9363 foreach[ms] : 5071.7525
また、Get-ProcessではなくGet-Commandにした場合、foreachを利用したコレクションフィルタリングは1000回のループとなるとうまくメモリに収まらず効率的に動作しません。
が、パイプラインやWhereメソッド構文であれば問題なく動作したことも特筆しておきます。 (もちろん パイプラインを利用した Where-Object
よりも高速に動作します。)
PowerShell V4 で導入された WhereOperatorSelectionMode について
これについては、まだ海外でもまとまった記事がほとんどありませんのでいい機会です。
WhereOperatorSelectionMode の一覧は、以下のmsdnにあります。
Enum一覧を取得するには、以下のコードを実行します。
[Enum]::GetNames([System.Management.Automation.WhereOperatorSelectionMode])
結果です。
Default First Last SkipUntil Until Split
これらのオペレータは、.Where({ScriptBlock},"Enum",Value)
のように第二引数、第三引数にいれます。*1
では、それぞれのオペレータについてについてみていきましょう。
通常
偶数を抽出します。
(1..10).Where({$_ % 2 -eq 0})
結果です。
2 4 6 8 10
Default
何も指定していない状態と同一です。
(1..10).Where({$_ % 2 -eq 0},"Default")
結果です。
2 4 6 8 10
First
ScriptBlockの実行結果の最初から数えて、第三引数で指定した数を出力します。
(1..10).Where({$_ % 2 -eq 0},"first",2)
2 4
Last
ScriptBlockの実行結果の最後から数えて、第三引数で指定した数を出力します。
(1..10).Where({$_ % 2 -eq 0},"last",2)
8 10
SkipUntil
ScriptBlockの実行結果が満たされるまでスキップし、第三引数で指定した数だけ出力します。
以下の場合は、2~10までのコレクションで、-1して3になるまでスキップし1つだけ出力なので 4のみ出力します。
(2..10).Where({$_ - 1 -eq 3},"SkipUntil",1)
4
以下なら余が1の数値までスキップし、2つ出力です。
(2..10).Where({$_ % 2 -eq 1},"SkipUntil",2)
3 4
$null を指定するとコレクションに操作を加えず、そのまま実行します。
(2..10).Where($null,"SkipUntil",2)
4 5 6 7 8 9 10
Until
ScriptBlockの実行結果が満たされるまで第三引数で指定した数だけ出力し、残りはスキップします。
(1..10).Where($null,"Until",5)
1 2 3 4 5
Split
指定したスクリプトブロック結果でコレクションを分割し、それぞれを第三引数で指定した数だけ配列要素として出力し、配列の残りは別の配列要素に格納します。
奇数を出力後に、偶数を出力しています。
(1..10).where({$_%2},"Split")
1 3 5 7 9 2 4 6 8 10
スクリプトブロックで満たされた偶数は、出力されたコレクションの初めの要素に格納されています。
(1..10).where({$_%2},"Split")[0]
1 3 5 7 9
3を指定しているので、インデックス0の出力要素は3つです。
PS> (1..10).where({$_ % 2},"Split","3")[0]
1 3 5
残りの要素は、[1]に格納されています。
(1..10).where({$_ % 2},"Split","3")[1]
2 4 6 7 8 9 10
まとめ
Whereメソッド構文による速度面での恩恵は大きく、ワンライナーでパイプラインを介さずにかけるのは大いに役立ちます。
また、WhereOperatorSelectionModeによる処理は非常に強力で、さらにパイプラインを容易に減らしコード記述が楽になります。
ぜひぜひ使って行けばいいと思います。*2