先日Twitter上でちょっと話題になってたのでメモを残しておきます。
PowerShellのHashtableはコレクション扱いされない
こちらは割と既知の話で、
や
にある通りPowerShellのHashtableはコレクション扱いされません。
例えばC#で以下の様に書けるコードが
// C# (LinqPad) var hash = new System.Collections.Hashtable(); hash.Add("Key1", "Value1"); hash.Add("Key2", "Value2"); foreach (DictionaryEntry item in hash) { Debug.WriteLine(string.Format("{0} 型の値 {1}={2} が渡されました。", item.GetType(), item.Key, item.Value)); }
PowerShellでは異なる挙動をします。
foreach
だけでなくパイプラインも同様です。
# 適当なHashTable $hash = @{ key1 = "Value1"; key2 = "Value2" } # C#とは違い、PowerShellではforeach文でHashTableの要素を列挙できない foreach ($item in $hash) { Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $item.GetType(), $item.Key, $item.Value) } # パイプラインでも同様に要素を列挙できない $hash | ForEach-Object { Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $_.GetType(), $_.Key, $_.Value) }
Hashtableの各要素を列挙可能にするにはGetEnumerator()
メソッドを使ってやる必要があります。
# 列挙するには GetEnumerator() メソッドを使う foreach ($item in $hash.GetEnumerator()) { Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $item.GetType(), $item.Key, $item.Value) } # パイプラインも同様 $hash.GetEnumerator() | ForEach-Object { Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $_.GetType(), $_.Key, $_.Value) }
Dictonaryもコレクション扱いされない
そして、この挙動はDictionaryを使った場合も同様になります。
$dict = New-Object "System.Collections.Generic.Dictionary[string, string]" $dict.Add("Key1", "Value1") $dict.Add("Key2", "Value2") # C#とは違い、PowerShellではforeach文でDictionaryの要素を列挙できない foreach ($item in $dict) { Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $item.GetType(), $item.Key, $item.Value) } # パイプラインでも同様に要素を列挙できない $dict | ForEach-Object { Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $_.GetType(), $_.Key, $_.Value) }
こちらも列挙可能にするにはGetEnumerator()
メソッドを使う必要があります。
# 列挙するには GetEnumerator() メソッドを使う foreach ($item in $dict.GetEnumerator()) { Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $item.GetType(), $item.Key, $item.Value) } # パイプラインでも同様 $dict.GetEnumerator() | ForEach-Object { Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $_.GetType(), $_.Key, $_.Value) }
では何が列挙可能(Enumerable)なのか?
PowerShellのコレクションにおいて何が列挙可能かについて仕様書等を調べてみましたが、具体的に明記されたものを見つけることができませんでした。*1
仕方ないので現時点の最新のソースコードを調べたところ、System.Management.Automation.LanguagePrimitives
クラス(その1、その2)でどの型が列挙可能(Enumerable)かを判定してると思われる部分を見つけたので以下に転記しておきます。
// LanguagePrimitives.cs より private static void InitializeGetEnumerableCache() { lock (s_getEnumerableCache) { // PowerShell doesn't treat strings as enumerables so just return null. // we also want to return null on common numeric types very quickly s_getEnumerableCache.Clear(); s_getEnumerableCache.Add(typeof(string), LanguagePrimitives.ReturnNullEnumerable); s_getEnumerableCache.Add(typeof(int), LanguagePrimitives.ReturnNullEnumerable); s_getEnumerableCache.Add(typeof(double), LanguagePrimitives.ReturnNullEnumerable); } } // ・・・中略・・・ private static GetEnumerableDelegate CalculateGetEnumerable(Type objectType) { #if !CORECLR if (typeof(DataTable).IsAssignableFrom(objectType)) { return LanguagePrimitives.DataTableEnumerable; } #endif // Don't treat IDictionary or XmlNode as enumerable... if (typeof(IEnumerable).IsAssignableFrom(objectType) && !typeof(IDictionary).IsAssignableFrom(objectType) && !typeof(XmlNode).IsAssignableFrom(objectType)) { return LanguagePrimitives.TypicalEnumerable; } return LanguagePrimitives.ReturnNullEnumerable; }
この記述によれば、
- System.Data.DataTableクラス*2
- System.String、System.Collections.IDictionaryインターフェイス、System.Xml.XmlNodeを除いたSystem.Collections.IEnumerableインターフェイス
が列挙可能の様でこれまでの挙動とも一致します。
// Don't treat IDictionary or XmlNode as enumerable...
のコメントの通り、Hashtableを含むIDictionary
は意図的に列挙できない様にされている事がわかります。
ちなみに、HashtableやDictonaryでGetEnumerator()
メソッドを使うと列挙可能になるのは、このメソッドの戻り値がHashtableEnumerator
やEnumerator
型になりIDictionary
インターフェイスが無くなるためです。
おまけ
DataTable
も列挙可能*3ということなので最後に動作確認をしてみます。
$table = New-Object 'System.Data.DataTable' -ArgumentList ('Table1') $c1 = $table.Columns.Add('Code',[string]) $c2 = $table.Columns.Add('Name',[string]) $c3 = $table.Columns.Add('Age',[int]) $table.PrimaryKey = $c1 [void]$table.Rows.Add("001", "山田", 20) [void]$table.Rows.Add("002", "田中", 25) # C#とは異なりDataTableがそのままforeachで使える foreach ($row in $table) { Write-Host ("{0} 型の値 {1},{2},{3} が渡されました。" -f $row.GetType(), $row.Code, $row.Name, $row.Age) } # パイプラインも列挙可能 $table | ForEach-Object { Write-Host ("{0} 型の値 {1},{2},{3} が渡されました。" -f $_.GetType(), $_.Code, $_.Name, $_.Age) }
確かに各行(DataRow)が列挙可能になっています。