Skip to content

06 · Arrays & Hashtables

Creating arrays

$fruits = "apple", "banana", "cherry"     # comma creates an array
$numbers = @(1, 2, 3, 4, 5)                # explicit array syntax
$empty = @()                                # empty array

$fruits[0]      # apple
$fruits[-1]     # cherry   <- negative indices count from the end
# arrays are typed as System.Object[] by default — mixed types are allowed
$mixed = 1, "two", 3.0, $true
$mixed.GetType().Name   # Object[]

# strongly-typed arrays constrain every element
[int[]]$scores = 90, 85, 77

Common array operations

$numbers = 1, 2, 3, 4, 5

$numbers.Count            # 5
$numbers.Length            # 5   <- same as .Count for arrays

$numbers + 6                # returns a NEW array: 1 2 3 4 5 6 (arrays are fixed-size)
$numbers[1..3]              # slice: 2 3 4
$numbers[0,2,4]             # index list: 1 3 5
# arrays are fixed-size; += actually creates a new array each time (slow for big loops)
$list = @()
$list += "a"
$list += "b"
# fine for small scripts; use a generic List[T] or ArrayList for large/growing collections

Growable collections: ArrayList and List[T]

$list = [System.Collections.Generic.List[string]]::new()
$list.Add("a")
$list.Add("b")
$list.Remove("a")
$list          # b
# a strongly-typed generic list is the recommended growable collection
$scores = [System.Collections.Generic.List[int]]::new()
$scores.AddRange(@(10, 20, 30))
$scores.Add(40)
$scores.Count   # 4

Iterating arrays

$fruits = "apple", "banana", "cherry"

foreach ($fruit in $fruits) {
    Write-Output "I like $fruit"
}

$fruits | ForEach-Object { Write-Output "Pipeline: $_" }

Hashtables

Hashtables are PowerShell's key-value dictionary — similar to a Python dict or a Bash associative array, but a first-class type usable everywhere.

$capitals = @{
    France  = "Paris"
    Japan   = "Tokyo"
    Germany = "Berlin"
}

$capitals["Japan"]     # Tokyo
$capitals.Japan         # Tokyo   <- dot notation also works for simple keys
# adding, updating, removing
$capitals["Italy"] = "Rome"      # add
$capitals["Japan"] = "Kyoto"     # update (overwrite)
$capitals.Remove("Germany")       # remove

$capitals.ContainsKey("France")   # True
$capitals.Keys                    # France, Japan, Italy
$capitals.Values                  # Paris, Kyoto, Rome

Iterating a hashtable

$capitals = @{ France = "Paris"; Japan = "Tokyo"; Germany = "Berlin" }

foreach ($key in $capitals.Keys) {
    Write-Output "$key -> $($capitals[$key])"
}

# or, using GetEnumerator() to get key/value pairs directly
$capitals.GetEnumerator() | ForEach-Object {
    Write-Output "$($_.Key) -> $($_.Value)"
}

By default, @{} is an unordered Hashtable — key order isn't guaranteed. Use [ordered]@{} when insertion order matters:

$ordered = [ordered]@{ First = 1; Second = 2; Third = 3 }
$ordered.Keys    # First, Second, Third   <- guaranteed insertion order

A practical example: word frequency count

$text = "the quick brown fox jumps over the lazy dog the fox runs"
$counts = @{}

foreach ($word in $text -split " ") {
    if ($counts.ContainsKey($word)) {
        $counts[$word]++
    } else {
        $counts[$word] = 1
    }
}

$counts.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 3
Name  Value
----  -----
the       3
fox       2
quick     1

Cheat sheet

Syntax Meaning
$a = 1,2,3 create an array
$a[0] / $a[-1] index / last element
$a[1..3] slice
$a.Count number of elements
[System.Collections.Generic.List[T]]::new() growable, strongly-typed list
@{ k = v } create a hashtable
$h["key"] / $h.key access a value
$h.ContainsKey("k") check key existence
$h.GetEnumerator() iterate key/value pairs
[ordered]@{} hashtable that preserves insertion order

How It Actually Works

A PowerShell array literal (@(1,2,3) or the comma operator's implicit array-building) creates a fixed-size System.Object[] under the hood — this is why "appending" with $arr += $x is not appending at all: it allocates a brand-new array one element larger, copies every existing element into it, and rebinds $arr to the new array. For an N-iteration loop doing +=, that's O(N²) total copying, which is the actual mechanical reason the docs steer you toward [System.Collections. Generic.List[T]] or an output-collecting pipeline instead — List[T] uses amortized-doubling internal storage so .Add() is O(1) amortized.

Hashtables (@{}) are PowerShell-literal syntax for System.Collections.Hashtable, a classic open-addressing/bucket hash table keyed by GetHashCode()/Equals() — by default case-insensitive for string keys because PowerShell's hashtable literal uses an IEqualityComparer (StringComparer.OrdinalIgnoreCase)-backed comparer rather than .NET's normal case-sensitive default, matching PowerShell's general case-insensitivity elsewhere. [ordered]@{} swaps the backing type to System.Collections.Specialized.OrderedDictionary, which maintains an internal parallel array tracking insertion order alongside the hash buckets — enumeration order is a property of that extra structure, not an accident of bucket layout the way plain Hashtable enumeration order (implementation-defined, and not guaranteed stable across .NET versions) can appear to be.

Iterating a hashtable with foreach ($kv in $table) yields DictionaryEntry structs (a Key/Value pair struct), while $table.GetEnumerator() explicitly requests the same enumerator the foreach statement uses implicitly — this is why $table.Keys | ForEach- Object { $table[$_] } and foreach ($kv in $table) { $kv.Value } produce the same values through two different enumeration paths (one walks keys and re-indexes, the other walks entries directly).

🔀 See this in another language

Exercise

Write inventory.ps1 that builds a hashtable mapping item names to quantities (e.g. apples = 12, bananas = 7, oranges = 20), prints each item and its quantity sorted by quantity descending using GetEnumerator() | Sort-Object, and prints the total quantity across all items using Measure-Object -Sum on the hashtable's values.