【PowerShell】ループ完全ガイド|foreach・ForEach-Object・for・while の違いと使い分け

【PowerShell】ループ完全ガイド|foreach・ForEach-Object・for・while の違いと使い分け PowerShell

PowerShellには、繰り返し処理を書く方法が複数あります。foreach文、ForEach-Objectコマンドレット、for文、while文です。どれも「繰り返し」ですが、得意な場面が違います。

特に混乱しやすいのが、名前のよく似たforeach文とForEach-Objectです。見た目は似ていても、速度・メモリの使い方・パイプラインで使えるかが異なります。さらに、ForEach-Objectの中ではbreakcontinueが思ったとおりに動かないという落とし穴もあります。この記事では、実機で確認しながら、4つのループの使い分けを整理します。

先に結論

  • foreach ($item in $list) { }はコレクションを順に回す基本形で、最も高速です。
  • $list | ForEach-Object { $_ }はパイプラインの中で1件ずつ処理します。大量・ストリーム処理に向きます。
  • 実機の計測では、同じ10万回でforeach文が約46ミリ秒、ForEach-Objectが約245ミリ秒でした。速度ならforeach文です。
  • インデックスが必要ならfor、条件で繰り返すならwhileを使います。
  • ループの中断はbreak、スキップはcontinueです。
  • ForEach-Objectの中ではbreak/continueが外側のループに影響します。1件スキップはreturnを使います。

ループで扱うデータは、配列やCSVが中心です。配列とハッシュテーブルの使い方CSVを読み込んで一括処理する方法、ループ条件で使う比較演算子と条件分岐もあわせて参考になります。

スポンサーリンク

foreach文:コレクションを順に処理する(基本・最速)

配列やリストを1件ずつ処理する基本形がforeach文です。foreach ($要素 in $コレクション) { }と書きます。読みやすく、速度も最速です。

foreach-statement.ps1
$fruits = @("りんご", "みかん", "ぶどう")

foreach ($fruit in $fruits) {
    Write-Host $fruit
}

# 範囲や計算にも使える
foreach ($n in 1..5) {
    Write-Host ($n * $n)   # 1, 4, 9, 16, 25
}

1..5のような範囲も回せます。foreach文は、対象のコレクション全体をいったんメモリに読み込んでから回すため、件数が確定している配列の処理に向いています。

ForEach-Object:パイプラインで処理する

パイプライン(|)の中で繰り返すときはForEach-Objectを使います。現在の要素は自動変数$_$PSItemでも同じ)で参照します。エイリアスの%でも書けます。

foreach-object.ps1
# パイプラインで1件ずつ処理($_ が現在の要素)
@("りんご", "みかん") | ForEach-Object {
    Write-Host $_
}

# コマンドの出力をそのまま流して処理できる
Get-ChildItem "C:\work" | ForEach-Object {
    Write-Host $_.Name
}

# % はエイリアス(短く書ける)
1..3 | % { $_ * 10 }   # 10, 20, 30

ForEach-Objectは、Get-ChildItemなどの出力を受け取って1件ずつ処理できるのが強みです。全体をためずに流れてきたものから順に処理するため、巨大なデータや、件数が分からないストリーム処理に向きます。

【重要】foreach文とForEach-Objectの違い

名前は似ていますが、性質はかなり違います。実機で計測・確認した結果をまとめます。

項目 foreach 文 ForEach-Object
書き方 foreach ($x in $list) { } $list | ForEach-Object { }
速度 速い(約46ミリ秒) 遅い(約245ミリ秒)
パイプライン 使えない 使える(中核)
メモリ 全体を読み込む 流しながら処理(省メモリ)
現在の要素 自分で付けた変数名 $_ / $PSItem
向いている場面 件数が確定した配列の高速処理 コマンド出力・大量/ストリーム処理

速度の数値は、同じ10万回のループをMeasure-Commandで計測した実機の結果です。約5倍の差がありました。手元の配列を速く回すならforeach文、コマンドの出力を流れの中で処理するならForEach-Object、と覚えると迷いません。

for文:インデックスが必要なとき

「何番目か」という番号を使いたいときはfor文です。カウンタ変数を自分で進めます。

for-statement.ps1
# 初期化; 継続条件; 更新 の3つを書く
for ($i = 0; $i -lt 5; $i++) {
    Write-Host "$i 回目"
}

# 配列をインデックス付きで回す
$items = @("A", "B", "C")
for ($i = 0; $i -lt $items.Count; $i++) {
    Write-Host "$i : $($items[$i])"
}

継続条件には-lt(より小さい)などの比較演算子を使います。インデックスが不要なら、読みやすいforeach文のほうが向いています。

while / do-while / do-until

回数ではなく「条件が成り立つ間」繰り返すならwhileです。doを使うと、条件判定が末尾になり、最低1回は実行されます。

while-do.ps1
# while:先に条件を判定(条件次第で0回もありうる)
$i = 0
while ($i -lt 3) {
    Write-Host $i
    $i++
}

# do-while:後ろで判定(最低1回は実行される)
$n = 0
do {
    Write-Host $n
    $n++
} while ($n -lt 3)

# do-until:条件が真になるまで繰り返す
$m = 0
do {
    Write-Host $m
    $m++
} until ($m -ge 3)

whileは先に条件を見るため、最初から条件を満たさなければ1回も実行されません。do-whiledo-untilは本体を実行してから判定するので、必ず1回は実行されます。untilは「条件が真になったら止める」という逆向きの書き方です。

break と continue でループを制御する

ループの途中で抜けるにはbreak、その回だけ飛ばして次へ進むにはcontinueを使います。これはforeach文・forwhileで共通の動きです。

break-continue.ps1
foreach ($n in 1..5) {
    if ($n -eq 2) { continue }   # 2 だけ飛ばす
    if ($n -eq 4) { break }      # 4 でループを抜ける
    Write-Host $n
}
# 出力: 1, 3

実機でも、continue2を飛ばし、break4に達した時点で抜け、13だけが出力されました。ここまでは多くの言語と同じ感覚です。問題は、次のForEach-Objectの中での挙動です。

【注意】ForEach-Objectではbreak/continueの挙動が違う

ForEach-Object{ }は、foreach文の本体とは違い「ループ」ではなく、各要素ごとに呼ばれるスクリプトブロックです。そのため、breakcontinueを書くと、ForEach-Object自体ではなく外側を囲むループに作用してしまいます。

foreach-object-return.ps1
# NG: ForEach-Object 内の break は外側のループまで抜けてしまう
foreach ($i in 1..3) {
    1..5 | ForEach-Object {
        if ($_ -eq 3) { break }   # 外側の foreach ごと終了してしまう
        Write-Host "$i-$_"
    }
}
# 期待は各 i で 1,2 を出すことだが、最初の i=1 の 1,2 を出した時点で全体が止まる

# OK: 1件だけスキップしたいなら return を使う
1..5 | ForEach-Object {
    if ($_ -eq 3) { return }   # この要素だけ飛ばして次へ
    Write-Host $_
}
# 出力: 1, 2, 4, 5
ForEach-Objectのスキップは return

実機で確認したところ、ForEach-Objectの中でbreakを使うと、外側のforeachループごと終了してしまいました。continueも同様に外側へ作用します。ForEach-Objectで「この要素だけ飛ばして次へ」としたいときは、continueではなくreturnを使ってください。returnはそのスクリプトブロックの呼び出しだけを終えるため、次の要素の処理へ進みます。実機でもreturn3だけを飛ばし、1, 2, 4, 5になりました。

ループの結果を変数に集める

ループの中で出力した値は、ループ全体を変数に代入することで、まとめて配列として受け取れます。$result = foreach (...) { }の形です。

collect-output.ps1
# foreach 文の出力を変数に集める
$squares = foreach ($n in 1..5) {
    $n * $n
}
$squares   # 1, 4, 9, 16, 25

# ForEach-Object でも同じように集められる
$doubled = 1..5 | ForEach-Object { $_ * 2 }
$doubled   # 2, 4, 6, 8, 10

# 条件に合うものだけ集める
$evens = foreach ($n in 1..10) {
    if ($n % 2 -eq 0) { $n }
}
$evens   # 2, 4, 6, 8, 10

ループの中で値を出力(Write-Hostではなく、値をそのまま書く)すると、それが集まって配列になります。+=で1件ずつ配列に足すよりも、この書き方のほうが速く、読みやすくなります。配列の+=が遅い理由は配列とハッシュテーブルの使い方で解説しています。

よくある失敗

foreach文をパイプラインで使おうとする

foreach文はパイプラインの中では使えません。Get-ChildItem | foreach文のような書き方はできず、その用途ではForEach-Objectを使います。

ForEach-Object内のbreakで外側まで抜ける

ForEach-Objectの中のbreakは外側のループに作用します。1件スキップはreturnを使い、breakは使わないようにします。

大量データをforeach文で全部メモリに読み込む

巨大なファイルやコマンド出力をforeach文で扱うと、全体をメモリに読み込みます。流しながら処理したいならForEach-Objectを使います。

ループ内の+=で配列を組み立てて遅くなる

+=は毎回配列を作り直すため遅くなります。$result = foreach (...) { }でまとめて集めるほうが高速です。

ForEach-Objectの$_を別のスクリプトブロックで使う

$_は現在のパイプラインの要素を指します。入れ子のパイプラインでは指すものが変わるため、必要なら別の変数に退避してから使います。

よくある質問

Qforeach文とForEach-Objectはどちらを使うべきですか?
A手元の配列を速く回すならforeach文、コマンドの出力をパイプラインで処理するならForEach-Objectです。実機計測ではforeach文が約5倍高速でしたが、ForEach-Objectはパイプラインと省メモリが強みです。
QForEach-Objectで1件だけスキップするには?
Areturnを使います。continuebreakは外側のループに作用してしまうため、ForEach-Objectの中では使いません。returnはそのスクリプトブロックの呼び出しだけを終え、次の要素へ進みます。
Qfor文とforeach文はどう使い分けますか?
A「何番目か」という番号(インデックス)が必要ならfor、ただ全要素を順に処理するだけならforeach文が読みやすく高速です。インデックスを使わないならforeach文を選びます。
Qwhile と do-while の違いは?
Awhileは先に条件を判定するため、最初から条件を満たさなければ0回です。do-whileは本体を実行してから判定するので、必ず1回は実行されます。最低1回動かしたいときはdo-whileです。
Qループの結果をまとめて受け取るには?
A$result = foreach (...) { 値 }のように、ループ全体を変数に代入します。ループ内で出力した値が配列として集まります。+=で足していくより高速です。

まとめ

  • foreach文は配列を順に回す基本形で、最も高速です(実機で約5倍速)。
  • ForEach-Objectはパイプラインで1件ずつ処理し、$_で要素を参照します。省メモリです。
  • インデックスが必要ならfor、条件で繰り返すならwhiledo-whiledo-untilです。
  • 中断はbreak、スキップはcontinueforeach文・forwhile)です。
  • ForEach-Objectの中ではbreak/continueが外側に作用します。1件スキップはreturnを使います。
  • 結果は$result = foreach (...) { }でまとめて集めると高速です。

PowerShellのループは数が多いですが、「速さと配列ならforeach文」「パイプラインならForEach-Object」「番号が要るならfor」「条件で回すならwhile」と整理すれば迷いません。ForEach-Objectのスキップはreturn、という1点だけ特に覚えておいてください。