为什么我需要在PowerShell中使用有序哈希表?

10

我正在阅读一份教程,了解到PowerShell支持有序哈希表。我应该在什么情况下使用这个特性?

以下是我所说的示例代码:

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

14

让我从更广的角度来补充Maximilian Burszley的有用答案

简而言之

大多数情况下,您需要使用[ordered] @{ ... }[System.Collections.Specialized.OrderedDictionary])(PSv3+):
  • 它按照定义的顺序枚举条目(也反映在.Keys.Values集合属性中)。
  • 它还允许像数组一样通过索引访问条目。
通常,您可以将[ordered] @{ ... }与普通哈希表@{ ... }、别名为[hashtable][System.Collections.Hashtable]互换使用,因为两种类型都实现了[IDictionary]接口,这是接受哈希表参数的参数的通常类型。

使用[ordered]所付出的性能惩罚是微不足道的。


一些背景:

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

对于仅通过键执行隔离查找的用例,其中键(条目)之间的排序无关紧要,这很好。

然而,通常您确实关心条目的排序:

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

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

      # 没有保证的属性顺序。
      PS> @{ one = 1; two = 2; three = 3 } | ConvertTo-Json
      {
       "one": 1,
       "three": 3, # !!
       "two": 2
      }
    
      # 保证的属性顺序。
      PS> [ordered] @{ one = 1; two = 2; three = 3 } | ConvertTo-Json
      {
        "one": 1,
        "two": 2,
        "three": 3
      }
    

很遗憾的是,PowerShell的哈希表字面语法@{ ... }不会默认为[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],则必须从有序的哈希表开始,以获得可预测的属性排序;这种技术很有用,因为创建哈希表条目比向自定义对象添加属性更容易。例如:
$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] 只能应用于哈希表 字面量;您不能使用它将现有的常规哈希表转换为有序哈希表(这也没有任何意义,因为您一开始就没有定义顺序):
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 10000 { $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 PowerShell 5.1 在 Windows 10 上,单核 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

话虽如此,数字键并不常见,对我来说,默认使用可预测的枚举的好处胜过这个小问题。

[ordered] 下面的 .NET 类型 System.Collections.Specialized.OrderedDictionary 自 v1 以来就可用了,因此 PowerShell 可以选择它作为 @{ ... } 的默认实现,即使在 PowerShell v1 中也是如此。

考虑到 PowerShell 对向后兼容性的承诺,更改默认设置已不再是一个选项,因为这可能会破坏现有代码,即以下方式:

  • 可能存在检查未经类型化的参数是否为哈希表的现有代码,使用 -is [hashtable],这将无法与有序哈希表一起使用(但使用 -is [System.Collections.IDictionary] 将可以)。

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


1
“不会默认为有序,但现在改变已经太晚了。”为什么?也就是说,如果以后的某个重大版本中改变了这种方式,会导致什么问题? - hyde
2
@hyde:虽然在转移到PowerShell Core时确实有一些问题,但目前总体承诺仍然是“向后兼容永远”。可能会出现哪些问题?使用反射查看传递的类型的代码;使用具有数字键的哈希表,突然执行索引访问的代码,这只是其中两个例子。 - mklement0
你已经在评论中涵盖了一些内容,但“很不幸,PowerShell的哈希表文字语法@{...}没有默认为[ordered],但现在改变已经太晚了...鉴于PowerShell对向后兼容性的承诺,更改默认选项不再是一个选择,因为这可能会破坏现有的代码。”不确定从“没有保证的顺序”到“没有保证的顺序,但它总是排序”如何防止更改。您不必在反思时返回不同的结果--实施更加困难,但不会破坏。我不确定第二个(索引访问); 你能解释一下吗? - ruffin
@ruffin,请查看我的更新。简而言之:反向兼容性的问题不在于排序,而在于数字键查找的行为变化以及期望哈希表字面量是[hashtable]类型(我不确定您是否建议这样做,但调整反射行为的结果似乎是不明智的)。 - mklement0
1
@mklement0 "_我不确定您是否建议篡改反射行为结果_。" 嗯...是的,那正是它,而且更疯狂的事情已经发生过,但这也可能是我喜欢找借口进行monkeypatch的部分,所以我们不要听他的。我理解您的观点。;^) 感谢您的更改。 - ruffin

10
一个有序字典的原因是为了显示/类型转换目的。例如,如果您想将您的哈希表转换为PSCustomObject并且您希望键按您输入的顺序排列,您可以使用ordered
这里的用例是当您使用Export-Csv时,标题是正确排序的。这只是我能想到的一个例子。按设计,hashtable类型不关心您输入键/值的顺序,并且每次将其显示到成功流时都会有所不同。 ordered字典的另一个用例:您可以将您的哈希表视为数组,并使用数字访问器查找项目,例如$myOrderedHash[-1]将抓取添加到字典中的最后一个项。

实际上,由于内置的语法糖,在将哈希表转换为[pscustomobject]时,您不需要使用[ordered] - mklement0
2
有时我会在函数中使用哈希表来构建一个对象($hashtable[$dynamic] = $value),并在最后进行强制转换 return [pscustomobject]$hashtable - Maximilian Burszley
1
是的,但值得指出的是,只有在您首先创建(有序)哈希表,然后从变量(或表达式)转换时才需要 [ordered]。有太多不必要的 [pscustomobject] [ordered] @{ ... } 的例子存在。 - mklement0
1
@mklement0 我现在听到你了。不,我不执行那个操作。 - Maximilian Burszley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接