在 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()
排序它并不完全清楚,所以我的建议是对它们进行全面测试并且看看什么最适合你
我想要一个包含与 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()
排序它并不完全清楚,所以我的建议是对它们进行全面测试并且看看什么最适合你