Search code examples
powershellhashtableenumerationenumerator

HashTable enumeration with unexpected results


This may be a dumb question so please be kind LOL. I'm trying to wrap my head around something I just ran into.

First

I have a HashTable that I can enumerate using the following:

$ht = [hashtable]@{"Key1"="Value1";"Key2"="Value2"}
$ht.GetEnumerator()

Name                           Value
----                           -----
Key1                           Value1
Key2                           Value2

If I store this to a variable, it exists only for one call of the variable.

$KeyPairs = $ht.GetEnumerator()
$KeyPairs

Name                           Value
----                           -----
Key1                           Value1
Key2                           Value2

$KeyPairs
# Nothing is returned as if $KeyPairs lost its value

Can someone help me understand why that is?

Second

Typically with a collection, I sometimes want to target a single item for testing (such as viewing properties of one instance), and I can use Select-Object or via index:

$array = @("Value1","Value2")
$array | select -first 1
Value1

$array[0]
Value1

The hashtable enumerator seems to only support the Select-Option, not the index.

$ht = [hashtable]@{"Key1"="Value1";"Key2"="Value2"}
$ht.GetEnumerator() | Select -first 1
$ht.GetEnumerator() | Select -first 1

Name                           Value
----                           -----
Key1                           Value1


($ht.GetEnumerator() | measure).count
2

($ht.GetEnumerator())[0]

Name                           Value
----                           -----
Key1                           Value1
Key2                           Value2

Can someone explain that as well? Why can't I use the index option here? $ht.GetEnumerator() returns a Dictionary collection, and when using Select -first 1, it returns a single DictionaryEntry, but when referencing the index, it returns both dictionary entries.


Solution

  • Note: This answer applies equally to situations where an API directly returns an enumerator object.

    Using .GetEnumerator() returns an enumerator[1] for the key-value pairs, which is not the same as the results of the enumeration.

    • An enumerator must be iterated over via its .MoveNext() method in order to perform actual enumeration. However, you can let PowerShell do this for you, as shown below.

    To get the desired behavior, force enumeration via @(...), the array sub-expression operator, and use the results:

    # Note the use of @(...), which collects the enumerated objects
    # in an [object[]] array.
    
    # Get an array of key-value pairs.
    $KeyPairs = @($ht.GetEnumerator())
    
    # Get the first key-value pair.
    @($ht.GetEnumerator())[0]
    

    Note:

    • In the pipeline it is PowerShell itself that performs the enumeration of an enumerator object, which is why something like
      $ht.GetEnumerator() | ForEach-Object { <# work with each key-value pair #> } does work.

    • In the pipeline, hashtables / dictionaries, which are technically also collections, are not enumerated by default, unlike list-like collections such as arrays.[2] That is, hashtables / dictionaries are sent as a whole through the pipeline by default, which is why a call to .GetEnumerator() is needed to return an enumerator for their entries (key-value pairs), which the pipeline then enumerates.


    As for what you tried:

    $KeyPairs
    # Nothing is returned as if $KeyPairs lost its value

    Because $KeyPairs contains an enumerator, it is done enumerating after the first enumeration that output to the pipeline (the display) implicitly performed, and therefore there's nothing left to enumerate on re-invocation - unless you call $KeyPairs.Reset() first.
    However, note that not every enumerator is guaranteed to support .Reset() for repeating an enumeration - some enumerators invariably perform one-time-only enumerations.

    ($ht.GetEnumerator())[0] # !! DOESN'T WORK
    • An enumerator cannot be indexed into.

    • PowerShell treats it like a single object (which it is) and falls back to its own indexing, where it allows even single objects (scalars) to be indexed for the sake of unified handling of collections and scalars; in that case, [0] is an effective no-op, simply returning the single object itself (similar to how (42)[0] and (42)[-1] are the same as 42)


    [1] Specifically, .GetEnumerator() returns an object that implements the System.Collections.IDictionaryEnumerator interface.

    [2] See the bottom section of this answer for which types PowerShell does and doesn't automatically enumerate in the pipeline.