且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

排序-对象-输入对象

更新时间:2023-08-19 18:01:58

Theo's helpful answer显示了使用.NET API对已在内存中并存储在变量中的数组进行快速就地排序的方法

如果希望排序并消除重复项(相当于Sort-Object -Unique),则可以使用System.Collections.Generic.SortedSet<T>实例:

PS> [System.Collections.Generic.SortedSet[string]]::new(
      [string[]] ('foo', 'bar', 'baz', 'foo'), 
      [System.StringComparer]::InvariantCultureIgnoreCase
    )

bar
baz
foo

注意:如果将[object]用作SortedSet的泛型类型参数,则不需要[string[]]强制转换,但通常***使用特定类型。

注意:结果不是数组,但可以像使用foreach语句一样在管道中对其进行枚举。将结果复制到数组中-例如,当您需要应用索引时(例如,[0])- 预分配相同类型的目标数组并使用.CopyTo()方法填充它($arr = [string[]]::new($sortedSet.Count); $sortedSet.CopyTo($arr))

另一种方法是先对有重复项的数组进行排序,然后再应用System.Linq.Enumerable.Distinct()

[string[]] $arr = 'foo', 'bar', 'baz', 'foo'
[Array]::Sort($arr, [System.StringComparer]::InvariantCultureIgnoreCase)
[Linq.Enumerable]::Distinct($arr)

注意:结果是一个惰性可枚举,而不是一个数组,但它可以像使用foreach语句一样在管道中进行枚举。调用.ToArray()方法以显式创建数组,例如当您需要应用索引时(例如,[0])。


关于您的问题

那么,-InputObject到底是用来做什么的?

对于多数cmdlet来说,不幸的是,-InputObject参数只是实现细节:它的目的是通过管道启用输入,并且它直接与数组(集合)一起使用是没有意义的,例如Sort-Object

  • GitHub issue #4242要求清楚地记录-InputObject,并包含确实有意义地支持直接使用-InputObject的cmdlet的列表,但不是作为管道输入的替代,而是使用不同的语义,当使用-InputObject时,将数组(集合)作为一个整体进行操作;例如,1, 'foo' | Get-MemberGet-Member -InputObject (1, 'foo')的工作方式(有意义)不同:前者报告数组的元素的类型,而后者报告数组本身的类型。

  • 数据处理cmdlet(相对于格式化cmdlet)中,实际上只有Write-OutputOut-String(部分也是格式化cmdlet)支持直接-InputObject与数组一起使用;例如:

    # Both commands produce the same output.
    1, 2 | Write-Output
    Write-Output -InputObject 1, 2
    
    • 即使在那里,嵌套的数组的行为也不同,因为这两种方法的枚举深度不同:

      # NOT the same, due to nesting.
      1, (2, (3, 4)) | Write-Output # -> 1, 2, (3, 4)
      Write-Output -InputObject 1, (2, (3, 4)) # -> 1, (2, (3, 4))
      

这种不支持直接、数组值的-InputObject参数是很遗憾的,因为它可以极大地加快速度,特别是在数据已满的情况下:

绕过管道中经常出现的逐个流(需要在每个对象的发送命令和接收命令之间进行某种握手),可以极大地提高性能。

-Valuecmdlet的-Value参数就是一个例子,接受直接数组值参数而不是管道输入,直接使用-Value大大加快了操作速度。

# Write 100,000 (1e5) numbers to a file:
# Via the pipeline.
1..1e5 | Set-Content temp.txt
# Via -Value - this is much, much faster.
# E.g. on my macOS machine with PowerShell 7.1, about 100(!) times faster.
Set-Content temp.txt -Value (1..1e5)

潜在改进

注意:

  • 理论上,cmdlet可以***地实现自己对-InputObject的数组支持,但这很麻烦,因为需要额外的逻辑,并且(B)需要将参数声明为数组类型(这效率很低,因为即使是通过管道接收的单个对象也包装在数组中),或者将其声明为object,这可能会丧失类型安全性。

理想情况下,PowerShell本身应提供此支持,具体如下:

    扩展[Parameter]属性 具有可以与现有ValueFromPipelineValueFromPipelineByPropertyName属性组合的新的布尔型EnumerateArgument属性,当设置为$true时,将指示PowerShell:
    • 隐式接受指定(标量)参数类型的数组,并直接使用该参数,例如,[int[]]用于[int]类型的参数

    • 枚举这些数组,就像在管道中一样,为每个枚举对象调用cmdlet的process块(在PowerShell中实现的cmdlet(高级函数))/.ProcessRecord()方法(二进制cmdlet)。

假设(人为)示例:

function ConvertTo-Long {

  [CmdletBinding()]
  param(
    # WISHFUL THINKING: implicit array support for direct -InputObject arguments
    [Parameter(ValueFromPipeline, EnumerateArgument)]
    [int] $InputObject
  )

  process {
    [long] $InputObject   
  }

}

# The following calls would then be equivalent:
1, 2, 3 | ConvertTo-Long
ConvertTo-Long -InputObject 1, 2, 3

注意:

  • 该改进将适用于任何管道绑定参数(不仅仅是-InputObject)。

  • 可以说,EnumerateArgument在默认情况下应该是$true,而那些将数组作为参数传递具有不同含义的罕见cmdlet,例如Get-Member,应该选择退出--然而,这将是一个向后兼容性问题

  • 由于建议的增强仍然涉及为每个枚举对象调用process块/.ProcessRecord()方法,因此速度不会像自定义实现在一次调用中执行枚举那样显著。然而,对我来说,仅统一管道和直接-InputObject使用之间的行为的前景就使得这种改进是值得的。