【PowerShell】配列とハッシュテーブルの使い方|宣言・追加・ループ・連想配列・並び替え

【PowerShell】配列とハッシュテーブルの使い方|宣言・追加・ループ・連想配列・並び替え PowerShell

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を読み書きする方法条件一致したデータだけを抽出する方法とあわせて読むと、実務での使いどころがつかめます。

スポンサーリンク

配列を作る(宣言と初期化)

配列の作り方は何通りかあります。カンマで区切るだけでも作れますが、意図を明確にするなら@()で囲む書き方がおすすめです。

array-declare.ps1
# カンマ区切りで作る
$fruits = "りんご", "みかん", "ぶどう"

# @() で囲む(意図が明確・要素1個や0個でも確実に配列になる)
$fruits = @("りんご", "みかん", "ぶどう")

# 空の配列
$empty = @()

# 連続した数値は範囲演算子で
$numbers = 1..5   # 1,2,3,4,5

# 型を固定した配列(数値以外を入れるとエラー)
[int[]]$ids = 1, 2, 3

1..5のような範囲演算子は、連番を作るときに便利です。[int[]]のように型を指定すると、その型以外の値が混入するのを防げます。

配列の要素にアクセスする

要素はインデックス(0始まり)で取り出します。マイナスのインデックスで末尾から数えることもでき、範囲で複数まとめて取り出すこともできます。

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

$fruits[0]    # りんご(最初)
$fruits[-1]   # ぶどう(最後)
$fruits[0..1] # りんご, みかん(範囲で取得)

$fruits.Count  # 3(要素数)
$fruits.Length # 3(Count と同じ)

末尾の要素は$fruits[-1]で取得できます。要素数は.Count(または.Length)で取れます。実機でも$fruits[-1]ぶどう$fruits[0..1]りんご, みかんになることを確認しています。

配列に要素を追加・削除する

追加には+=を使えますが、ここに大きな落とし穴があります。配列はサイズが固定のため、+=は要素を足しているのではなく、毎回すべての要素をコピーして新しい配列を作り直しています

array-add.ps1
$list = @(1, 2, 3)
$list += 4   # 見た目は追加だが、内部では新しい配列を作っている
# $list は 1,2,3,4

# 数十件程度なら問題ないが、数万回のループでは極端に遅くなる
大量ループでの+=は避ける

ループの中で+=を繰り返すと、要素が増えるたびに配列全体のコピーが発生し、件数に応じて処理時間が急増します。実機で確認しても、+=のたびに配列は別オブジェクトに置き換わっていました。件数が多い場合は、次のListを使ってください。

array-add-list.ps1
# 可変長リスト(追加が速い)
$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()も使えます。普通の配列と同じようにインデックスでアクセスできます。

配列をループ処理する

配列の全要素を順に処理する方法はいくつかあります。読みやすさと用途で使い分けます。

array-loop.ps1
$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文を使います。

配列を検索・絞り込み・並び替える

「含まれているか」の判定、条件での絞り込み、並び替えは、専用の演算子やコマンドで簡潔に書けます。

array-filter-sort.ps1
$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 -Unique1,2,3を返すことを確認しています。条件抽出の実例は条件一致したデータだけを抽出・整形する方法も参考になります。

配列の落とし穴:結果が0件・1件だと配列にならない

PowerShellで最もハマりやすいのがこれです。コマンドやWhere-Objectの結果が1件だと配列ではなく単一の値0件だと$nullになります。そのまま.Countforeachに渡すと、意図しない動きになります。

array-pitfall.ps1
# 結果が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).Count0、1件を@()で囲むとObject[]になりました。

ハッシュテーブル(連想配列)を作る・値を読む

ここからはハッシュテーブルです。キーと値のペアでデータを持ち、キーを指定して値を読み書きします。他の言語の連想配列・辞書(Dictionary・Map)に当たります。

hashtable-basic.ps1
# キー = 値 のペアで作る(; で区切る)
$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()を使うと既存キーで失敗するため、注意が必要です。

hashtable-modify.ps1
$user = @{ Name = "山田"; Age = 30 }

# 追加も更新も [キー] = 値 でOK(安全)
$user["City"] = "東京"  # 新規追加
$user["Age"]  = 31      # 既存を更新

# 削除
$user.Remove("Name")

# .Add() は「新規追加専用」。既存キーに使うと例外になる
$user.Add("Country", "日本")  # OK(新規)
# $user.Add("Country", "米国")  # 既にあるキーなので例外
.Add()は既存キーで例外になる

実機で確認したところ、すでに存在するキーに.Add()を使うと例外(エラー)になりました。追加と更新を区別せずに書きたいときは、$user["キー"] = 値の形を使ってください。こちらはキーがあれば更新、なければ追加と、どちらでも安全に動きます。

ハッシュテーブルをループ処理する

全ペアを順に処理するには、キーを回す方法と、GetEnumerator()でキーと値を同時に取り出す方法があります。

hashtable-loop.ps1
$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]を付けます。

hashtable-ordered.ps1
# 通常のハッシュテーブルは順序が不定
$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の列順を保ったまま出力したい場合などに役立ちます。

ハッシュテーブルで集計する(出現回数のカウント)

ハッシュテーブルが特に活きるのが集計です。「キーごとの出現回数」を数える処理は、ログ解析や集計でよく使います。

hashtable-count.ps1
$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と正しく集計できました。

スプラッティング:パラメータをハッシュテーブルで渡す

ハッシュテーブルの応用として、コマンドのパラメータをまとめて渡す「スプラッティング」があります。引数が多いコマンドを読みやすく書けます。

splatting.ps1
# パラメータをハッシュテーブルにまとめる
$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"]は同じキーを指します。区別が必要な場合は専用の辞書を使います。

よくある質問

Q配列とハッシュテーブルはどう使い分けますか?
A順番に並んだ値を扱うなら配列、キーで値を引きたいならハッシュテーブルです。「3番目の要素」のように位置でアクセスするなら配列、「Nameの値」のように名前でアクセスするならハッシュテーブルが向いています。
Q+=はいつまで使ってよいですか?
A数十件から数百件程度で、ループ回数が少なければ+=でも体感差はありません。数千〜数万件を超えるループで配列を組み立てるなら、List.Add()に切り替えてください。
Qハッシュテーブルとオブジェクト(PSCustomObject)の違いは?
Aハッシュテーブルはキーで値を出し入れするデータ構造、[PSCustomObject]はプロパティを持つオブジェクトです。一覧表示やCSV出力にはPSCustomObject、内部での集計やキー引きにはハッシュテーブルが向いています。
Q配列の要素を途中で削除できますか?
A通常の配列は固定サイズのため、要素の途中削除には向きません。$arr = $arr | Where-Object { 条件 }で除外した新しい配列を作るか、List.RemoveAt()を使ってください。
Qハッシュテーブルをキーの順で並べるには?
A$ht.GetEnumerator() | Sort-Object Nameでキー順、Sort-Object Valueで値順に並べられます。元のハッシュテーブル自体の順序は変わらないため、表示や出力のたびに並べ替えます。

まとめ

  • 配列は@(1, 2, 3)、空配列は@()、連番は1..5で作ります。
  • +=は毎回新しい配列を作るため、大量ループではList.Add()を使います。
  • 結果が0件・1件だと配列にならないので、@(...)で囲んで配列に固定します。
  • ハッシュテーブルは@{ Name = "山田" }で作り、$ht["キー"] = 値で安全に追加・更新します。
  • 順序を保つなら[ordered]@{}、集計には「あれば++・なければ初期化」のパターンを使います。
  • パラメータをまとめて渡すスプラッティング(@params)も覚えておくと便利です。

配列とハッシュテーブルは、PowerShellのほぼすべてのスクリプトで使う土台です。+=の挙動と、結果が配列にならない落とし穴さえ押さえておけば、データの加工や集計を安定して書けるようになります。