PowerShell Select-对象:将 -Unique 与 First/Last/Skip/Index 一起使用

PowerShell Select-Object: Using -Unique with First/Last/Skip/Index

我很好奇我是否遗漏了任何文档,或者是否有 different/better 方法可以消除对文档的需求。也许我是唯一一个尝试使用 Select-Object 到 select 来自一组数据的 -First X 唯一实例。

根据下面的测试,它看起来像使用 Select-Object-Unique 开关和某种类型的限制器(FirstLastSkip, Index, 等等) 本质上会导致在删除重复项之前应用限制器。这在概念上对我来说没有意义,但似乎也没有记录在案。

对于这个糟糕的例子,我深表歉意,但考虑一个包含 20 个项目的数组,每个项目出现两次:

PS > $array = @() ; 1..10 | % { $array += $_ ; $array += $_ }
PS > $array -Join ','
1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10  ##Displaying the array on a single comma separated line

假设有人给你 $array,但你最多只能处理 5 个对象的输入。过滤掉你得到的东西,你可能会想使用 Select-Object。起初你最终得到 5 个对象,但有重复项,所以快速思考你只需添加 -Unique 开关,然后你意识到输出仍然不太正确。

PS > ($array | Select-Object -First 5) -Join ','
1,1,2,2,3  ##5 objects as expected, but with duplicates
PS > ($array | Select-Object -Unique -First 5) -Join ','
1,2,3  ##No duplicates, but less than the expected 5 objects...

为了获得我期望的结果,我需要 Select-Object 在返回最终对象集之前删除重复项。虽然知道这一点并没有错,但对我来说似乎很奇怪 Select-Object 使用它所做的操作顺序,而且没有任何文档说明 -Unique 开关是应用在 cmdlet.

的末尾
PS > ($array | Select-Object -Unique | Select-Object -First 5) -Join ','
1,2,3,4,5  ##This is my expected outcome, 5 objects returned without any duplicates

确实,-First / -Last / -Skip / -Index / -SkipIndex / -SkipLast 参数首先应用于原始输入,并且 -Unique 应用于 结果输出 .

简单的解决方法使用两个Select-Object调用:一个找到唯一对象,另一个从唯一对象中选择所需数量:

PS> 1, 1, 2, 3 | Select-Object -Unique | Select-Object -First 2
1
2

鉴于 Select-Object -Unique 过慢 自 PowerShell 7.2(见底部),这里是 更快的解决方法,正如您自己发现的那样:使用辅助。 System.Collections.Generic.HashSet`1 instance combined with ForEach-Object;该示例还显示了对 case-insensitivity 的支持,Select-Object -Unique 目前缺乏这种支持(见底部):

# Create an aux. hash set that keeps tracks of what objects have
# already been seen, using case-*insensitive* comparisons.
$auxHashSet = [Collections.Generic.HashSet[string]]::new(
                [StringComparer]::InvariantCultureIgnoreCase
              )

# Stream to ForEach-Object, where the aux. hash set is used
# to only pass out objects that haven't previously been seen.
'a', 'A', 'B', 'c' |
  ForEach-Object { if ($auxHashSet.Add($_)) { $_ } } |
    Select-Object -First 2

这会根据需要输出 'a', 'B'。请注意,您可能想要删除 $auxHashSet 变量以便(最终)释放其内存 - 请参阅下一个。

使用带有 ForEach-Object-Begin 块,可以使管道更加独立,但请注意所有脚本块 运行 直接在调用者的scope,这样 $auxHashSet 仍然在那里创建,并且会在命令后继续存在,因此您仍然需要手动删除它,从而(最终)释放它的内存。

  • 注意:虽然原则上你可以在-End块中做到这一点,但Select-Object -First,因为管道的过早停止 不会 给上游 cmdlet 机会 运行 它们的结束块 - 请参阅 GitHub issue #7930 讨论这个令人惊讶的行为。
'a', 'A', 'B', 'c' |
  ForEach-Object -Begin { 
    $auxHashSet = [Collections.Generic.HashSet[string]]::new([StringComparer]::InvariantCultureIgnoreCase) 
  } -Process {
    if ($auxHashSet.Add($_)) { $_ } 
  } |
    Select-Object -First 2
# Remove the aux. variable and (eventually) free its memory.
Remove-Variable auxHashSet 

请注意,还有一个 基于 LINQ 的替代方案,通过 [System.Linq.Enumerable]::Distinct(),但它具有重要的约束

  • 输出是无序输入顺序不是保证被保留.

  • 无法从 PowerShell 方法的输入集合命令(要将 PowerShell 命令的输出传递给方法,必须预先将其完整收集在数组中)- 但是,output 来自 LINQ 方法,例如 Distinct() 有效流式传输,因为返回了一个惰性可枚举[1]

  • 此外,输入数组必须是强类型,如果还没有的话。 PowerShell 通过诸如 [int[]] 之类的转换使这变得容易,但请注意,使用基于 [object[]] 的数组作为输入(这就是常规 PowerShell 数组,例如用于收集命令输出),但是请注意,这涉及创建数组的 copy,具有大量输入集合的数组本身可能需要一段时间。

[Linq.Enumerable]::Distinct(
  [string[]] ('a', 'A', 'B', 'c'), 
  [StringComparer]::InvariantCultureIgnoreCase
) | Select-Object -First 2

这也输出 'a', 'B'(尽管不能保证输出元素的顺序)。

如果约束不是问题并且您需要在 整个 输入集合(或其中的大部分)中找到唯一元素,则此解决方案要快得多比散列集辅助的 ForEach-Object 解决方案,特别是如果您的输入集合已经是强类型的。

如果在相同的约束下,您不关心惰性输出行为,只想获得所有不同对象的内存中集合 - 同样,无序 - 您可以直接使用 System.Collections.Generic.HashSet`1 实例:

[Collections.Generic.HashSet[string]]::new(
  [string[]] ('a', 'A', 'B', 'c'), 
  [System.StringComparer]::InvariantCultureIgnoreCase
)

这输出 'a', 'B', 'c',但值得注意的是作为 哈希集对象 ,而不是数组,但是,由于可枚举,它的行为类似于数组在 PowerShell 的枚举上下文中,特别是在管道中。


Select-Object -Unique陷阱,对比Sort-Object

  • 虽然额外的 Select-Object 调用确实增加了处理开销,但 该命令总体上具有 潜力 仅处理根据需要输入许多对象,即一旦找到所需数量的唯一对象就停止处理。

  • 但是,从 PowerShell 7.2 开始,似乎 Select-Object -Unique 的实现 效率低下 并且出乎意料地 在产生输出之前首先收集所有输入,即使没有概念这样做的理由:应该 能够产生 输出,即有条件地输出输入对象当它们被接收时, 因为它只需要考虑到目前为止.

    收到了哪些输入对象
    • 实际上,从 PowerShell 7.2 开始,Select-Object -Unique 速度过慢输入集合; GitHub issues #11221 and #7707.

      中讨论了当前有问题的实现
    • 这种概念性仅考虑收到的输入的能力到目前为止[=65形成对比=],它也提供了一个 -Unique 开关,但是 必要 必须 首先收集所有输入 在产生输出 之前,因为必须考虑 所有 个输入对象以进行正确排序。

      • 从 PowerShell 7.2 开始,Sort-Object -Unique 实际上比 Select-Object -Unique 快得多。
    • 至于如何 Select-Object -Unique 以更有效的流式方式实现:到目前为止 看到的对象可以存储在 System.Collections.Generic.HashSet`1 实例以促进有效测试输入对象是否被认为等于已经输出的对象;有关 PowerShell 示例,请参阅

  • 如果Select-Object -Unique固定权衡如下:

    • 感兴趣的输出对象相对于所有输入对象的比例越小,您使用 Select-Object -Unique 就越好(即使您必须 sort 结果对象 afterwards).

    • 如果你需要输出/考虑所有输入对象,并假设输出感兴趣的对象排序 是理想的/可接受的,Sort-Object 是更好的选择。

  • 从 PowerShell 7.2 开始,Select-Object -Unique 对于字符串输入意外地区分大小写 ,即使尽管 PowerShell 默认情况下通常不区分大小写 - 请参阅 GitHub issue #12059.


测试一个 cmdlet 是否产生输出或首先收集所有输入:

没有检查 cmdlet 的 source code,这是一种测试方法 - 中间 管道段是要测试的命令:

# Test Sort-Object -Unique
# Because the command cannot stream, for conceptual reasons, 
# it takes a while for the one and only output object to appear.
1..1e5 | Sort-Object -Unique | Select-Object -First 1
# Test Select-Object -Unique
# The command *could* stream, conceptually speaking, in which case
# the output object would appear right away.
# However, as of PowerShell 7.2, the command isn't implemented
# in a streaming fashion, so it takes a - surprisingly long - while
# for the output object to appear.
# it takes a while for the one and only output object to appear.
1..1e5 | Select-Object -Unique | Select-Object -First 1

如果上面的给定管道产生它的唯一输出对象near instantly,感兴趣的命令是streaming;如果在输出对象出现之前需要一段时间,它会首先收集所有输入。