I'm reading a tutorial and learned that PowerShell supports ordered hashes. When would I use that feature?
Sample code of what I'm talking about:
$hash = [ordered]@{ ID = 1; Shape = "Square"; Color = "Blue"}
Let me complement Maximilian Burszley's helpful answer with a broader perspective:
tl;dr
Most of the time you want [ordered] @{ ... }
([System.Collections.Specialized.OrderedDictionary]
) (PSv3+):
.Keys
and .Values
collection properties).Typically, you can use [ordered] @{ ... }
interchangeably with @{ ... }
, the regular hashtable, [hashtable]
a.k.a [System.Collections.Hashtable]
, because both types implement the [IDictionary]
interface, which is how parameters that accept hash tables are typically typed.
The performance penalty you pay for using [ordered]
is negligible.
Some background:
For technical reasons, the most efficient implementation of a hashtable (hash table) is to let the ordering of entries be the outcome of implementation details, without guaranteeing any particular order to the caller.
This is fine for use cases where all you do is to perform isolated lookups by key, where the ordering among keys (entries) is irrelevant.
However, often you do care about the ordering of entries:
in the simplest case, for display purposes; there is something disconcerting about seeing the definition order jumbled; e.g.:
@{ one = 1; two = 2; three = 3 }
Name Value
---- -----
one 1
three 3 # !!
two 2
more importantly, the enumeration of entries may need to be predictable for further programmatic processing; e.g. (note: strictly speaking, property order doesn't matter in JSON, but it is again important for the human observer):
# No guaranteed property order.
PS> @{ one = 1; two = 2; three = 3 } | ConvertTo-Json
{
"one": 1,
"three": 3, # !!
"two": 2
}
# Guaranteed property order.
PS> [ordered] @{ one = 1; two = 2; three = 3 } | ConvertTo-Json
{
"one": 1,
"two": 2,
"three": 3
}
It's unfortunate that PowerShell's hashtable-literal syntax, @{ ... }
, doesn't default to [ordered]
[1], but it is too late to change that.
There is one context in which [ordered]
is implied, however: if you cast a hashtable literal to [pscustomobject]
in order to create a custom object:
[pscustomobject] @{ ... }
is syntactic sugar for [pscustomobject] [ordered] @{ ... }
; that is, the resulting custom object's properties are ordered based on the entry order in the hashtable literal; e.g.:
PS> [pscustomobject] @{ one = 1; two = 2; three = 3 }
one two three # order preserved!
--- --- -----
1 2 3
Note, however, that this only works exactly as shown above: if the cast applied directly to a hashtable literal; if you use a variable to store the hashtable in first or if you even just enclose the literal in (...)
the ordering is lost:
PS> $ht = @{ one = 1; two = 2; three = 3 }; [pscustomobject] $ht
one three two # !! Order not preserved.
--- ----- ---
1 3 2
PS> [pscustomobject] (@{ one = 1; two = 2; three = 3 }) # Note the (...)
one three two # !! Order not preserved.
--- ----- ---
1 3 2
Therefore, if you construct a hashtable iteratively first and then cast it to [pscustomobject]
, you must start with an [ordered]
hashtable to get predictable ordering of properties; this technique is useful, because it's easier to create hashtable entries than it is to add properties to a custom object; e.g.:
$oht = [ordered] @{} # Start with an empty *ordered* hashtable
# Add entries iteratively.
$i = 0
foreach ($name in 'one', 'two', 'three') {
$oht[$name] = ++$i
}
[pscustomobject] $oht # Convert the ordered hashtable to a custom object
Finally, note that [ordered]
can only be applied to hashtable literal; you cannot use it to convert a preexisting regular hashtable to an ordered one (which wouldn't make any sense anyway, because you have no defined order to begin with):
PS> $ht = @{ one = 1; two = 2; three = 3 }; [ordered] $ht # !! Error
...
The ordered attribute can be specified only on a hash literal node.
...
On a side note: Neither ordered nor regular hashtables enumerate their entries when sent through the pipeline; they are sent as a whole.
To enumerate the entries, use the .GetEnumerator()
method; e.g.:
@{ one = 1; two = 2; three = 3 }.GetEnumerator() | ForEach-Object { $_.Value }
1
3 # !!
2
As for the performance impact of using [ordered]
:
As noted, it is negligible; here are some sample timings, averaged across 10,000 runs, using Time-Command
:
Time-Command -Count 10,000 { $ht=@{one=1;two=2;three=3;four=4;five=5;six=6;seven=7;eight=8;nine=9}; foreach($k in $ht.Keys){$ht.$k} },
{ $ht=[ordered] @{one=1;two=2;three=3;four=4;five=5;six=6;seven=7;eight=8;nine=9}; foreach($k in $ht.Keys){$ht.$k} }
Sample timings (Windows PowerShell 5.1 on Windows 10, single-core VM):
Command TimeSpan Factor
------- -------- ------
$ht=@{one=1;two=2;th... 00:00:00.0000501 1.00
$ht=[ordered] @{one=... 00:00:00.0000527 1.05
That is, [ordered]
amounted to a mere 5% slowdown.
[1] Maximilian Burszley points out one tricky aspect specific to [ordered]
hashtables:
With numeric keys, distinguishing between a key and an index can become tricky; to force interpretation of a number as a key, cast it to [object]
or use dot notation (.
, property-access syntax) instead of index syntax ([...]
):
# Ordered hashtable with numeric keys.
PS> $oht = [ordered] @{ 1 = 'one'; 2 = 'two' }
PS> $oht[1] # interpreted as *index* -> 2nd entry
two
PS> $oht[[object] 1] # interpreted as *key* -> 1st entry.
one
PS> $oht.1 # dot notation - interpreted as *key* -> 1st entry.
one
That said, numeric keys aren't common, and to me the benefit of defaulting to predictable enumeration outweighs this minor problem.
The .NET type underlying [ordered]
, System.Collections.Specialized.OrderedDictionary
, has been available since v1, so PowerShell could have chosen it as the default implementation for @{ ... }
from the get-go, even in PowerShell v1.
Given PowerShell's commitment to backward compatibility, changing the default is no longer an option, however, as that could break existing code, namely in the following ways:
There may be existing code that checks untyped arguments for whether they're a hashtable with -is [hashtable]
, which would no longer work with an ordered hashtable (however, checking with -is [System.Collections.IDictionary]
would work).
There may be existing code that relies on hashtables with numeric keys, in which case the index-syntax lookup behavior would change (see example above).
The reason for an ordered
dictionary is for display / typecast purposes. For example, if you want to cast your hashtable
to a PSCustomObject
and you want your keys to be in the order you enter them, you use ordered
.
The use case here is when you use Export-Csv
, the headers are in the right order. This is just one example I could think of off the top of my head. By design, the hashtable
type doesn't care about the order you enter keys/values and will be different each time you display it to the success stream.
An additional use-case for the ordered
dictionary: you can treat your hashtable as an array and use numerical accessors to find items, such as $myOrderedHash[-1]
will grab the last item added to the dictionary.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With