PowerShellでスクリプトを書くと、ほぼ必ず使うのが配列とハッシュテーブルです。配列は「順番に並んだ値の集まり」、ハッシュテーブルは「キーと値のペアの集まり(連想配列)」で、ファイル一覧の処理、設定値の管理、集計など、あらゆる場面で登場します。
基本は単純ですが、+=が毎回新しい配列を作る、結果が1件だと配列にならない、ハッシュテーブルは順序を保証しないといった、知らないとハマる挙動があります。この記事では、これらを実機のPowerShellで確認しながら、確実に使えるように整理します。
- 配列は
$a = 1, 2, 3または@(1, 2, 3)で作ります。空配列は@()です。 $a += 4は配列に追加しているように見えて、毎回まるごと新しい配列を作り直します。大量ループではListを使います。- コマンドの結果が0件だと
$null、1件だと配列ではなく単一の値になります。@()で囲むと常に配列にできます。 - ハッシュテーブルは
@{ Name = "山田"; Age = 30 }で作り、$ht.Nameや$ht["Name"]で読みます。 - キーの追加・更新は
$ht["City"] = "東京"が安全です。.Add()は既存キーで例外になります。 - 挿入順を保ちたいときは
[ordered]@{ ... }を使います。
配列やハッシュテーブルは、CSVの行データを扱うときにも頻出します。CSVを読み書きする方法や条件一致したデータだけを抽出する方法とあわせて読むと、実務での使いどころがつかめます。
配列を作る(宣言と初期化)
配列の作り方は何通りかあります。カンマで区切るだけでも作れますが、意図を明確にするなら@()で囲む書き方がおすすめです。
# カンマ区切りで作る
$fruits = "りんご", "みかん", "ぶどう"
# @() で囲む(意図が明確・要素1個や0個でも確実に配列になる)
$fruits = @("りんご", "みかん", "ぶどう")
# 空の配列
$empty = @()
# 連続した数値は範囲演算子で
$numbers = 1..5 # 1,2,3,4,5
# 型を固定した配列(数値以外を入れるとエラー)
[int[]]$ids = 1, 2, 3
1..5のような範囲演算子は、連番を作るときに便利です。[int[]]のように型を指定すると、その型以外の値が混入するのを防げます。
配列の要素にアクセスする
要素はインデックス(0始まり)で取り出します。マイナスのインデックスで末尾から数えることもでき、範囲で複数まとめて取り出すこともできます。
$fruits = @("りんご", "みかん", "ぶどう")
$fruits[0] # りんご(最初)
$fruits[-1] # ぶどう(最後)
$fruits[0..1] # りんご, みかん(範囲で取得)
$fruits.Count # 3(要素数)
$fruits.Length # 3(Count と同じ)
末尾の要素は$fruits[-1]で取得できます。要素数は.Count(または.Length)で取れます。実機でも$fruits[-1]がぶどう、$fruits[0..1]がりんご, みかんになることを確認しています。
配列に要素を追加・削除する
追加には+=を使えますが、ここに大きな落とし穴があります。配列はサイズが固定のため、+=は要素を足しているのではなく、毎回すべての要素をコピーして新しい配列を作り直しています。
$list = @(1, 2, 3) $list += 4 # 見た目は追加だが、内部では新しい配列を作っている # $list は 1,2,3,4 # 数十件程度なら問題ないが、数万回のループでは極端に遅くなる
ループの中で+=を繰り返すと、要素が増えるたびに配列全体のコピーが発生し、件数に応じて処理時間が急増します。実機で確認しても、+=のたびに配列は別オブジェクトに置き換わっていました。件数が多い場合は、次のListを使ってください。
# 可変長リスト(追加が速い)
$list = [System.Collections.Generic.List[object]]::new()
$list.Add("a")
$list.Add("b")
$list.Add("c")
$list.Count # 3
$list.Remove("b") # 値を削除(True を返す)
$list[0] # a(配列と同じようにアクセスできる)
Listは.Add()で末尾に追加でき、配列を作り直さないため高速です。途中の要素を削除する.Remove()や.RemoveAt()も使えます。普通の配列と同じようにインデックスでアクセスできます。
配列をループ処理する
配列の全要素を順に処理する方法はいくつかあります。読みやすさと用途で使い分けます。
$fruits = @("りんご", "みかん", "ぶどう")
# foreach 文(最も読みやすい)
foreach ($fruit in $fruits) {
Write-Host $fruit
}
# ForEach-Object(パイプラインの中で使う。$_ が現在の要素)
$fruits | ForEach-Object { Write-Host $_ }
# for 文(インデックスが必要なとき)
for ($i = 0; $i -lt $fruits.Count; $i++) {
Write-Host "$i : $($fruits[$i])"
}
単純に全要素を回すならforeach文が読みやすく速いです。パイプラインの途中で処理するならForEach-Objectを使い、現在の要素は$_で参照します。インデックス番号が必要なときだけfor文を使います。
配列を検索・絞り込み・並び替える
「含まれているか」の判定、条件での絞り込み、並び替えは、専用の演算子やコマンドで簡潔に書けます。
$numbers = 1..10
# 含まれているかの判定
$numbers -contains 5 # True
5 -in $numbers # True(-contains と向きが逆)
# 条件で絞り込む
$numbers | Where-Object { $_ -gt 7 } # 8, 9, 10
$numbers.Where({ $_ % 2 -eq 0 }) # 2,4,6,8,10(メソッド構文)
# 並び替え・重複除去
3, 1, 2, 1 | Sort-Object # 1,1,2,3
3, 1, 2, 1 | Sort-Object -Unique # 1,2,3(重複を除く)
$numbers | Sort-Object -Descending # 降順
-containsと-inは判定の向きが逆なだけで結果は同じです。絞り込みはWhere-Object、または配列の.Where()メソッドで書けます。並び替えはSort-Objectで、-Uniqueを付けると重複も同時に除けます。実機でもSort-Object -Uniqueが1,2,3を返すことを確認しています。条件抽出の実例は条件一致したデータだけを抽出・整形する方法も参考になります。
配列の落とし穴:結果が0件・1件だと配列にならない
PowerShellで最もハマりやすいのがこれです。コマンドやWhere-Objectの結果が1件だと配列ではなく単一の値、0件だと$nullになります。そのまま.Countやforeachに渡すと、意図しない動きになります。
# 結果が1件だと配列ではなく単一の値(スカラー)になる
$one = (1..5 | Where-Object { $_ -eq 3 })
$one.GetType().Name # Int32(配列ではない)
# 結果が0件だと $null になる
$none = (1..5 | Where-Object { $_ -gt 99 })
$null -eq $none # True
# @() で囲めば、常に配列として扱える
@($one).GetType().Name # Object[]
@($none).Count # 0(null でも安全に数えられる)
@($one).Count # 1
件数が0・1・複数のどれになるか分からない結果は、@(...)で囲んで配列に固定するのが定石です。こうすれば、.Countで件数を数えても、foreachで回しても、件数にかかわらず安全に動きます。実機でも、@($null).Countは0、1件を@()で囲むとObject[]になりました。
ハッシュテーブル(連想配列)を作る・値を読む
ここからはハッシュテーブルです。キーと値のペアでデータを持ち、キーを指定して値を読み書きします。他の言語の連想配列・辞書(Dictionary・Map)に当たります。
# キー = 値 のペアで作る(; で区切る)
$user = @{
Name = "山田"
Age = 30
City = "東京"
}
# 空のハッシュテーブル
$config = @{}
# 値の読み出し(2通り)
$user.Name # 山田(ドット記法)
$user["Age"] # 30(角かっこ記法)
$user.Count # 3(ペアの数)
$user.ContainsKey("Name") # True
値の読み出しは$user.Nameと$user["Age"]のどちらでも書けます。キーが変数に入っている場合は角かっこ記法($user[$key])が便利です。キーの存在確認はContainsKey()を使います。
ハッシュテーブルに追加・更新・削除する
値の追加と更新は同じ書き方でできます。ここで.Add()を使うと既存キーで失敗するため、注意が必要です。
$user = @{ Name = "山田"; Age = 30 }
# 追加も更新も [キー] = 値 でOK(安全)
$user["City"] = "東京" # 新規追加
$user["Age"] = 31 # 既存を更新
# 削除
$user.Remove("Name")
# .Add() は「新規追加専用」。既存キーに使うと例外になる
$user.Add("Country", "日本") # OK(新規)
# $user.Add("Country", "米国") # 既にあるキーなので例外
実機で確認したところ、すでに存在するキーに.Add()を使うと例外(エラー)になりました。追加と更新を区別せずに書きたいときは、$user["キー"] = 値の形を使ってください。こちらはキーがあれば更新、なければ追加と、どちらでも安全に動きます。
ハッシュテーブルをループ処理する
全ペアを順に処理するには、キーを回す方法と、GetEnumerator()でキーと値を同時に取り出す方法があります。
$user = @{ Name = "山田"; Age = 30; City = "東京" }
# キーを回して値を引く
foreach ($key in $user.Keys) {
Write-Host "$key : $($user[$key])"
}
# GetEnumerator でキーと値を同時に取り出す
$user.GetEnumerator() | ForEach-Object {
Write-Host "$($_.Key) = $($_.Value)"
}
$user.Keys # キーの一覧
$user.Values # 値の一覧
順序を保つには [ordered] を使う
通常のハッシュテーブルは、追加した順番を保証しません。ループで取り出す順番が、書いた順とは限らないのです。挿入順を保ちたいときは、宣言の前に[ordered]を付けます。
# 通常のハッシュテーブルは順序が不定
$normal = @{ z = 1; a = 2; m = 3 }
# [ordered] を付けると、書いた順(z, a, m)を保つ
$ordered = [ordered]@{
z = 1
a = 2
m = 3
}
$ordered.Keys # z, a, m の順で取り出せる
[ordered]@{}で作ると、型はOrderedDictionaryになり、キーの順番が宣言どおりに保たれます。実機でも、[ordered]付きはz, a, mの順を維持しました。CSVの列順を保ったまま出力したい場合などに役立ちます。
ハッシュテーブルで集計する(出現回数のカウント)
ハッシュテーブルが特に活きるのが集計です。「キーごとの出現回数」を数える処理は、ログ解析や集計でよく使います。
$words = "a", "b", "a", "c", "a", "b"
$counts = @{}
foreach ($w in $words) {
if ($counts.ContainsKey($w)) {
$counts[$w]++ # あれば1増やす
} else {
$counts[$w] = 1 # なければ初期化
}
}
# 結果: a=3, b=2, c=1
$counts.GetEnumerator() | Sort-Object Value -Descending |
ForEach-Object { "$($_.Key) : $($_.Value)" }
キーがあれば++で1増やし、なければ1で初期化する、という形が基本パターンです。最後にGetEnumerator()とSort-Object Valueを組み合わせると、出現回数の多い順に並べられます。実機でもa=3, b=2, c=1と正しく集計できました。
スプラッティング:パラメータをハッシュテーブルで渡す
ハッシュテーブルの応用として、コマンドのパラメータをまとめて渡す「スプラッティング」があります。引数が多いコマンドを読みやすく書けます。
# パラメータをハッシュテーブルにまとめる
$params = @{
Path = "C:\work"
Filter = "*.txt"
Recurse = $true
ErrorAction = "SilentlyContinue"
}
# @ を付けて渡す($params ではなく @params)
Get-ChildItem @params
# 同じことを1行で書くと長くなる
# Get-ChildItem -Path "C:\work" -Filter "*.txt" -Recurse -ErrorAction SilentlyContinue
変数を渡すとき、$paramsではなく@params(先頭が@)と書くのがスプラッティングです。共通のパラメータを使い回したり、条件によってパラメータを組み立てたりするときに重宝します。
よくある失敗
ループ内の+=で大量データを処理して遅くなる
+=は毎回配列を作り直すため、件数が増えると急激に遅くなります。数万件規模なら[System.Collections.Generic.List[object]]の.Add()を使ってください。
結果が1件・0件のときに配列として扱えない
Where-Objectなどの結果は、1件だと単一の値、0件だと$nullになります。件数が不定なら@(...)で囲んで配列に固定します。
.Add()で既存キーに追加しようとして例外
ハッシュテーブルの.Add()は新規追加専用です。更新もしたいなら$ht["キー"] = 値を使えば、追加と更新のどちらでも安全です。
ハッシュテーブルの順序を当てにする
通常のハッシュテーブルは挿入順を保証しません。順番が重要なら[ordered]@{}を使ってください。
キーの大文字小文字を区別すると思い込む
PowerShellのハッシュテーブルは既定で大文字小文字を区別しません。$ht["name"]と$ht["Name"]は同じキーを指します。区別が必要な場合は専用の辞書を使います。
よくある質問
+=でも体感差はありません。数千〜数万件を超えるループで配列を組み立てるなら、Listの.Add()に切り替えてください。[PSCustomObject]はプロパティを持つオブジェクトです。一覧表示やCSV出力にはPSCustomObject、内部での集計やキー引きにはハッシュテーブルが向いています。$arr = $arr | Where-Object { 条件 }で除外した新しい配列を作るか、Listの.RemoveAt()を使ってください。$ht.GetEnumerator() | Sort-Object Nameでキー順、Sort-Object Valueで値順に並べられます。元のハッシュテーブル自体の順序は変わらないため、表示や出力のたびに並べ替えます。まとめ
- 配列は
@(1, 2, 3)、空配列は@()、連番は1..5で作ります。 +=は毎回新しい配列を作るため、大量ループではListの.Add()を使います。- 結果が0件・1件だと配列にならないので、
@(...)で囲んで配列に固定します。 - ハッシュテーブルは
@{ Name = "山田" }で作り、$ht["キー"] = 値で安全に追加・更新します。 - 順序を保つなら
[ordered]@{}、集計には「あれば++・なければ初期化」のパターンを使います。 - パラメータをまとめて渡すスプラッティング(
@params)も覚えておくと便利です。
配列とハッシュテーブルは、PowerShellのほぼすべてのスクリプトで使う土台です。+=の挙動と、結果が配列にならない落とし穴さえ押さえておけば、データの加工や集計を安定して書けるようになります。

