在 Powershell 中,如何对 Collections.Generic.List 的 DirectoryInfo 进行排序?

In Powershell, how do I sort a Collections.Generic.List of DirectoryInfo?

我想要一个包含与 subjectPattern 匹配的文件的唯一目录的列表。 我可以获得列表,但要获得唯一的目录,我需要对其进行排序。但是因为列表是 Collections.Generic.List[DirectoryInfo] 类型,我找不到有效的 API.

function Get-Containers([Parameter(Mandatory)][string]$subjectPattern) {
    #NOTE: The class for directories is System.IO.DirectoryInfo, the class for files is System.IO.FileInfo
    $fatList = New-Object Collections.Generic.List[System.IO.DirectoryInfo]    
    $result = New-Object Collections.Generic.List[System.IO.DirectoryInfo]
    foreach ($leafName in (get-childitem -recurse -force -path . -include $subjectPattern)) {
        $fatList += (Get-Item $leafName).Directory
    }
    #Get-Unique only works on sorted collections, Sort-Object won't work without a Property,
    # but "FullName" is not a property of Collections.Generic.List
    # Furthermore, Sort() is not a method of [System.IO.DirectoryInfo]
    $result = ($fatList.Sort() | Get-Unique )
    return $result
}

如何排序,然后在 Collections.Generic.List[System.IO.DirectoryInfo] 中获取唯一项?

正在尝试更改:

$fatList = New-Object Collections.Generic.List[System.IO.DirectoryInfo]

到 HashSet,它只允许唯一值。

$fatList = New-Object Collections.Generic.Hashset[System.IO.DirectoryInfo] 

并注释掉:

#$result = ($fatList.Sort() | Get-Unique )

@AdminOfThings 正确,使用.Add() 方法。 https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.add?view=net-5.0。另外,您确定要使用 Directory 而不是 DirectoryName 吗?

编辑:我刚刚意识到我没有完全回答这个问题。使用无序项设置 HashSet 变量后(默认行为),将其通过管道传递给 Sort-Object cmdlet。

来自您的内联评论:

[...] Sort-Object won't work without a Property, but "FullName" is not a property of Collections.Generic.List

没关系,我们不是对多个 列表 进行排序,而是对恰好包含在单个列表中的多个 DirectoryInfo 对象进行排序。

最大的问题是:您需要就地排序吗?

“就地”排序意味着重新排列列表的对象,以便列表本身保留新的排序顺序 它的身份。这通常不会占用大量资源,但在 PowerShell 中稍微复杂一些。

另一种方法是枚举列表中的项目,对它们进行 外部 排序,然后(可选)将重新排序的项目包装在 new 列表 - 更容易实现,但会产生资源成本(根据集合的大小和比较的复杂性,您可能会注意到也可能不会注意到)。

就地排序

为了对多个 DirectoryInfo 对象进行排序,我们需要一种方法来指示 List[DirectoryInfo].Sort() 方法如何比较对象并确定哪个在另一个之前或之后排序顺序。

查看 Sort() 方法重载给了我们一个线索:

PS ~> $list = [System.Collections.Generic.List[System.IO.DirectoryInfo]]::new()
PS ~> $list.Sort

OverloadDefinitions
-------------------
void Sort()
void Sort(System.Collections.Generic.IComparer[System.IO.DirectoryInfo] comparer)
void Sort(int index, int count, System.Collections.Generic.IComparer[System.IO.DirectoryInfo] comparer)
void Sort(System.Comparison[System.IO.DirectoryInfo] comparison)

所以我们需要实现通用接口的东西 IComparer[T]

利用 PowerShell 在运行时使用 class 关键字定义新类型的能力,我们可以:

using namespace System.Collections.Generic
using namespace System.IO

class DirectoryInfoComparer : IComparer[DirectoryInfo]
{
    [string]$PropertyName
    [bool]$Descending = $false

    DirectoryInfoComparer([string]$property)
    {
        $this.PropertyName = $property
    }

    DirectoryInfoComparer([string]$property, [bool]$descending)
    {
        $this.PropertyName = $property
        $this.Descending = $descending
    }

    [int]Compare([DirectoryInfo]$a, [DirectoryInfo]$b)
    {
        $res = if($a.$($this.PropertyName) -eq $b.$($this.PropertyName))
        {
            0
        }
        elseif($a.$($this.PropertyName) -lt $b.$($this.PropertyName))
        {
            -1
        }
        else
        {
            1
        }

        if($this.Descending){
            $res *= -1
        }

        return $res 
    }
}

... 现在我们可以根据 属性 名称对列表进行就地排序,就像 Sort-Object:

# Create a list
$list = [List[DirectoryInfo]]::new()

# Add directories in non-sorted order
mkdir c,a,b -Force |ForEach-Object { $list.Add($_) }

# Instantiate a comparer based on the `FullName` property
$fullNameComparer = [DirectoryInfoComparer]::new("FullName")

# Now sort the list
$list.Sort($fullNameComparer)

# Observe that items are now sorted based on FullName value
$list.FullName

外部排序

既然我们知道了就地对通用集合进行排序所必须经历的试验,让我们回顾一下对集合进行外部排序的过程:

$sorted = $list |Sort-Object FullName

如果我们需要生成的(现已排序的)集合也属于 [List[Directory]] 类型,我们可以清除并重新填充原始列表:

$list.Clear()
$sorted |ForEach-Object {$list.Add($_)}

... 或者我们可以创建一个新的 [List[DirectoryInfo]] 实例:

$list = [List[DirectoryInfo]]::new([DirectoryInfo[]]$sorted)

SortedSet[DirectoryInfo]怎么样?

already suggested 一样,为了仅存储 唯一 项,“集合”可能是更好的集合类型。

HashSet[T] 类型是一个 无序 集,但是 .NET 也带有一个 SortedSet[T] type - 而你不会相信实现排序顺序需要什么 - 没错,一个 IComparer[T]! :-)

在这种情况下,我们希望在创建集合时将比较器注入构造函数:

# Once again, we need an IComparer[DirectoryInfo] instance
$comparer = [DirectoryInfoComparer]::new("FullName")

# Then we create the set, injecting our custom comparer
$set = [System.Collections.Generic.SortedSet[System.IO.DirectoryInfo]]::new($comparer)

# Now let's add a bunch of directories in completely jumbled order
Get-ChildItem -Recurse -Directory |Select -First 10 |Sort {Get-Random} |ForEach-Object {
    # The Add() method emits a boolean indicating whether the item 
    # is unique or already exists in the set, hence the [void] cast
    [void]$set.Add($_)
}

# Once again, observe that enumerating the set emits the items sorted
$set.FullName

如您所见,有多个选项可用,具有不同程度的复杂性和性能特征。从你的问题 为什么 你使用通用列表或者你为什么坚持使用 List.Sort() 排序它并不完全清楚,所以我的建议是对它们进行全面测试并且看看什么最适合你