3

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.

2
  • 1
    IMO, This is should split into multiple questions. Commented Sep 13, 2023 at 16:50
  • 1
    You can also reset the position with $KeyPairs.Reset() and try to print $KeyPairs again to get the expected behaviour Commented Sep 13, 2023 at 16:58

2 Answers 2

6

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks @mklement0. This was very helpful and the forced enumeration with @(. . .) solved my issue.
5

$KeyPairs in your example $KeyPairs = $ht.GetEnumerator() is a misleading name, you do not have Key / Value pairs allocated in that variable, what you have is an object of the type HashtableEnumerator, a type that implements the IDictionaryEnumerator Interface, this interface, basically is a "contract" that specifies how a hash table can and should be enumerated. One of the specifics of the implementation is that the instance can be enumerated once and, if you want to enumerate it again, you must call its .Reset() method. This, as far as I know, is applicable for any type implementing the IEnumerator interace (base interface for IDictionaryEnumerator).

PS ..\pwsh> $ht = @{'Key1' = 'Value1'; 'Key2' = 'Value2' }
PS ..\pwsh> $enum = $ht.GetEnumerator()
PS ..\pwsh> $enum

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

PS ..\pwsh> $enum.Reset()
PS ..\pwsh> $enum

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

As for the second question:

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

That is correct, enumerators don't implement IList interface nor the implement an indexer.

If you want to use indexing you must convert that HashtableEnumerator into a collection of DictionaryEntry using @($ht.GetEnumerator()), in which case, using a hashtable in the first place wouldn't make sense, it would lose its purpose.

Or, a better approach would be to use an OrderedDictionary, this type supports accessing by index and by key and is as fast as a normal hashtable while performing look-ups:

$dict = [ordered]@{'Key1' = 'Value1'; 'Key2' = 'Value2' }
$dict[0]      # Value1
$dict.Keys[0] # Key1

# or, for a key / value pair:
[System.Collections.DictionaryEntry]::new($dict.Keys[0], $dict[0])

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

1 Comment

I don't know what happened to my comment here, but I'll post again. Thanks Santiago!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.