先日Twitter上でちょっと話題になってたのでメモを残しておきます。
PowerShellのHashtableはコレクション扱いされない
こちらは割と既知の話で、
pierre3.hatenablog.com
や
winscript.jp
にある通りPowerShellのHashtableはコレクション扱いされません。
例えばC#で以下の様に書けるコードが
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
だけでなくパイプラインも同様です。
$hash = @{
key1 = "Value1";
key2 = "Value2"
}
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()
メソッドを使ってやる必要があります。
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")
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()
メソッドを使う必要があります。
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)かを判定してると思われる部分を見つけたので以下に転記しておきます。
private static void InitializeGetEnumerableCache()
{
lock (s_getEnumerableCache)
{
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
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)
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)が列挙可能になっています。