首页 > 解决方案 > 为什么要在 PowerShell 中使用有序哈希?

问题描述

我正在阅读教程并了解到 PowerShell 支持有序哈希。我什么时候会使用该功能?

我正在谈论的示例代码:

$hash = [ordered]@{ ID = 1; Shape = "Square"; Color = "Blue"}

标签: powershell

解决方案


让我用更广阔的视角补充Maximilian Burszley 的有用答案:

tl;博士

您为使用而付出的性能损失可以[ordered]忽略不计。


一些背景:

由于技术原因,哈希表(哈希表)最有效的实现是让条目的顺序成为实现细节的结果,而不保证调用者的任何特定顺序。

这适用于您所做的只是通过键执行隔离查找的用例,其中键(条目)之间的顺序无关紧要。

但是,您通常确实关心条目的顺序:

  • 在最简单的情况下,用于显示目的;看到定义顺序混乱有些令人不安;例如:

      @{ one = 1; two = 2; three = 3 } 
    
      Name                           Value
      ----                           -----
      one                            1
      three                          3   # !! 
      two                            2
    
  • 更重要的是,条目的枚举可能需要可预测以进行进一步的程序化处理;例如(注意:严格来说,属性顺序在 JSON 中并不重要,但对于人类观察者来说又很重要):

      # No guaranteed property order.
      PS> @{ one = 1; two = 2; three = 3 } | ConvertTo-Json
      {
       "one": 1,
       "three": 3, # !!
       "two": 2
      }
    
      # Guaranteed property order.
      PS> [ordered] @{ one = 1; two = 2; three = 3 } | ConvertTo-Json
      {
        "one": 1,
        "two": 2,
        "three": 3
      }
    

不幸的是,PowerShell 的 hashtable-literal 语法@{ ... }默认为 [ordered][1],但改变它为时已晚。

但是,有一个上下文[ordered] 隐含的:如果您将哈希表文字转换[pscustomobject]为以创建自定义对象:

[pscustomobject] @{ ... }[pscustomobject] [ordered] @{ ... };语法糖 也就是说,生成的自定义对象的属性根据哈希表文字中的条目顺序进行排序;例如:

PS> [pscustomobject] @{ one = 1; two = 2; three = 3 }

one two three  # order preserved!
--- --- -----
  1   2     3

但是请注意,这只适用于如上所示:如果强制转换直接应用于哈希表文字;如果您首先使用变量存储哈希表,或者您甚至只是将文字包含在(...)排序中,则会丢失:

PS> $ht = @{ one = 1; two = 2; three = 3 }; [pscustomobject] $ht

one three two  # !! Order not preserved.
--- ----- ---
  1     3   2


PS> [pscustomobject] (@{ one = 1; two = 2; three = 3 }) # Note the (...)

one three two  # !! Order not preserved.
--- ----- ---
  1     3   2

因此,如果您先迭代地构造一个哈希表,然后将其强制转换为[pscustomobject],则必须从[ordered]哈希表开始以获得可预测的属性排序;这种技术很有用,因为创建哈希表条目比向自定义对象添加属性更容易;例如:

$oht = [ordered] @{} # Start with an empty *ordered* hashtable 
# Add entries iteratively.
$i = 0
foreach ($name in 'one', 'two', 'three') {
  $oht[$name] = ++$i
}
[pscustomobject] $oht # Convert the ordered hashtable to a custom object

最后,注意[ordered]只能应用于 hashtable literal;您不能使用它将预先存在的常规哈希表转换为有序哈希表(无论如何这都没有任何意义,因为您一开始没有定义的顺序):

PS> $ht = @{ one = 1; two = 2; three = 3 }; [ordered] $ht # !! Error
...
The ordered attribute can be specified only on a hash literal node.
...

附带说明:通过管道发送时,有序哈希表和常规哈希表都不会枚举它们的条目;它们是作为一个整体发送的。
要枚举条目,请使用.GetEnumerator()方法;例如:

@{ one = 1; two = 2; three = 3 }.GetEnumerator() | ForEach-Object { $_.Value }
1
3  # !!
2

至于使用的性能影响[ordered]

如前所述,它可以忽略不计;以下是一些样本计时,平均 10,000 次运行,使用Time-Command

Time-Command -Count 10,000 { $ht=@{one=1;two=2;three=3;four=4;five=5;six=6;seven=7;eight=8;nine=9}; foreach($k in $ht.Keys){$ht.$k} }, 
                           { $ht=[ordered] @{one=1;two=2;three=3;four=4;five=5;six=6;seven=7;eight=8;nine=9}; foreach($k in $ht.Keys){$ht.$k} }

示例计时(Windows 10 上的 Windows PowerShell 5.1,单核 VM):

Command                 TimeSpan         Factor
-------                 --------         ------
$ht=@{one=1;two=2;th... 00:00:00.0000501 1.00  
$ht=[ordered] @{one=... 00:00:00.0000527 1.05  

也就是说,[ordered]仅放缓了 5%。


[1] Maximilian Burszley 指出了哈希表特有的一个棘手方面[ordered]

使用数字,区分索引会变得很棘手;要强制将数字解释为,请将其强制转换为[object]或使用点表示法.属性访问语法)而不是索引语法[...]

# Ordered hashtable with numeric keys.
PS> $oht = [ordered] @{ 1 = 'one'; 2 = 'two' }

PS> $oht[1]  # interpreted as *index* -> 2nd entry
two

PS> $oht[[object] 1] # interpreted as *key* -> 1st entry.
one

PS> $oht.1 # dot notation - interpreted as *key* -> 1st entry.
one

也就是说,数字键并不常见,对我来说,默认为可预测枚举的好处超过了这个小问题。

.NET 类型底层[ordered], System.Collections.Specialized.OrderedDictionary, 从 v1 开始就可用,因此 PowerShell 可以@{ ... }从一开始就选择它作为默认实现,即使在 PowerShell v1 中也是如此。

鉴于 PowerShell 对向后兼容性的承诺,更改默认值不再是一种选择,因为这可能会破坏现有代码,即通过以下方式:

  • 可能存在检查无类型参数是否为带有 的哈希表的现有代码-is [hashtable],这将不再适用于有序哈希表(但是,检查带有-is [System.Collections.IDictionary] 起作用)。

  • 可能存在依赖于带有数字键的哈希表的现有代码,在这种情况下,索引语法查找行为会发生变化(参见上面的示例)。


推荐阅读