3

我只是好奇我是否遗漏了任何文档,或者是否有一种不同/更好的方法可以消除对文档的需求。也许我是唯一一个试图从一组数据Select-Object中选择唯一实例的人。-First X

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

我为这个糟糕的例子道歉,但考虑一个包含 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使用它所做的操作顺序并且没有任何文档围绕-Uniquecmdlet.

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
4

1 回答 1

4

实际上,/////参数首先应用于原始输入-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 开始太慢(请参阅底部部分),这是一个更快的解决方法,正如您自己发现的那样:使用 aux。System.Collections.Generic.HashSet`1实例结合ForEach-Object; 该示例还显示了对当前缺少的不区分大小写的支持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变量以(最终)释放其内存 - 请参阅下一个。

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

  • 注意:虽然原则上您可以在一个-End块中执行此操作,但这不适用于Select-Object -First因为过早停止管道不会上游 cmdlet 有机会运行它们的结束块 - 请参阅GitHub 问题 #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 的替代方案via [System.Linq.Enumerable]::Distinct(),但它有重要的 限制

  • 输出是无序的,即保证保留输入顺序

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

  • 此外,输入数组必须是强类型的,如果不是的话。PowerShell[int[]]使用[object[]]诸如_ _ ,对于大型输入集合,它本身可能需要一段时间。

[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调用确实增加了处理开销,但该命令总体上可能只处理所需数量的输入对象,即一旦找到所需数量的唯一对象就停止处理。

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

    • 在实践中,从 PowerShell 7.2 开始,对于较大的输入集合Select-Object -Unique来说速度过慢;当前有问题的实现在 GitHub 问题#11221#7707中进行了讨论。

    • 这种仅考虑到目前为止接收到的输入的概念能力与形成对比,后者也提供了一个开关,但必须在产生输出之前首先收集所有输入,因为必须考虑所有输入对象以进行正确排序。Sort-Object-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-Object输出感兴趣的对象,那么这是更好的选择。

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


测试 cmdlet 是产生流式输出还是首先收集所有输入

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

# 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

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

于 2021-10-14T21:15:04.993 回答