hashtable をKey-Valueのコレクションとして使いたい

のですが、普通にforeach, ForeEach-Object しただけでは期待した動作になりませんでした。

  • 期待する結果
PS C:\> $items = @{aaa=111;bbb=222;ccc=333}
PS C:\> $items | ForEach-Object {"{0}={1}" -f $_.Key, $_.Value}
aaa=111
bbb=222
ccc=333
  • 実際は
PS C:\> $items = @{aaa=111;bbb=222;ccc=333}
PS C:\> $items | ForEach-Object {"{0}={1}" -f $_.Key, $_.Value}
=
  • 要素が列挙されず、Hashtableがそのまま渡されてる!
PS C:\> $items | ForEach-Object {$_.GetType().FullName}
System.Collections.Hashtable
  • System.Collrctions.IEnumerableが実装されているはずですよね?
PS C:\> $items.GetEnumerator().GetType().FullName
System.Collections.Hashtable+HashtableEnumerator

IEnumerable.GetEnumerator はちゃんとあります。

  • Enumeratorでforeachしてみる?
PS C:\> $items.GetEnumerator() | ForEach-Object {$_.GetType().FullName}
System.Collections.DictionaryEntry
System.Collections.DictionaryEntry
System.Collections.DictionaryEntry

あ!できた?

PS C:\> $items.GetEnumerator() | ForEach-Object {"{0}={1}" -f $_.Key, $_.Value}
aaa=111
bbb=222
ccc=333

おおお!できました。

  • でもGetEnumerator()つけるのなんかダサい気がする

調べてみると、以下のような書き方が一般的のようですね。

Hashtableをforeachしても・・・ - PowerShell Scripting Weblog

PS C:\> $items.Keys | ForEach-Object {"{0}={1}" -f $_, $items[$_]}
bbb=222
ccc=333
aaa=111

コマンドレットにパイプで渡すと?

  • テスト用のfunctionで見てみましょう
function Test-Parameter
{
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeLine=$true, Mandatory=$true)]
        [PSObject]
        $InputObject
    )
    Begin
    {
        "*** Begin ***"
    }
    Process
    {
        "*** Process ***"
        " - InputObject: {0}" -f $InputObject.GetType()
        if($InputObject -is [System.Collections.IEnumerable])
        {
            " - foreach {"
            foreach($o in $InputObject){"    {0}={1}" -f $o.GetType(),$o}
            " }"
        }
        else
        {
            " - InputObject: {0}={1}" -f $InputObject.GetType(),$InputObject
        }
    }
    End
    {
        "*** End ***"
    }
}
PS C:\> $items = @{aaa=111;bbb=222;ccc=333}
PS C:\> $items | Test-Parameter

*** Begin ***
*** Process ***
 - InputObject: System.Collections.Hashtable
 - foreach {
    System.Collections.Hashtable=System.Collections.Hashtable
 }
*** End ***

想像通りですが、パイプで渡しても列挙されずに、ProcessにはHashtableが1回だけ入ってきました。

  • GetEnumerator()してから渡してみる
PS C:\> $items.GetEnumerator() | Test-Parameter
*** Begin ***
*** Process ***
 - InputObject: System.Collections.DictionaryEntry
 - InputObject: System.Collections.DictionaryEntry=System.Collections.DictionaryEntry
*** Process ***
 - InputObject: System.Collections.DictionaryEntry
 - InputObject: System.Collections.DictionaryEntry=System.Collections.DictionaryEntry
*** Process ***
 - InputObject: System.Collections.DictionaryEntry
 - InputObject: System.Collections.DictionaryEntry=System.Collections.DictionaryEntry
*** End ***

ちゃんと要素数だけ DictionaryEntry が渡されてきました。

コレクションを処理するコマンドレットに、Hashtableをコレクションとして渡したいケースがあれば、 Hashtable.GetEnumerator() で渡すのもアリかも。

他にも

string 型もIEnumerable<char> を実装していますが、そのままでは列挙しません。

PS C:\> "abcdefg" | ForEach-Object {"{0}:{1}" -f $_.GetType().FullName,$_}
System.String:abcdefg

PS C:\> "abcdefg".GetEnumerator() | ForEach-Object {"{0}:{1}" -f $_.GetType().FullName,$_}
System.Char:a
System.Char:b
System.Char:c
System.Char:d
System.Char:e
System.Char:f
System.Char:g

まあ、string をchar配列として使いたいケースは稀ですし、使いたい場合はToCharArray()で変換可能ですので、あまり気にする必要はないかも。

PS C:\> "abcdefg".ToCharArray() | ForEach-Object {"{0}:{1}" -f $_.GetType().FullName,$_}
System.Char:a
System.Char:b
System.Char:c
System.Char:d
System.Char:e
System.Char:f
System.Char:g