假设我们有一个对象数组 $objects。假设这些对象具有“名称”属性。
这就是我想做的
$results = @()
$objects | %{ $results += $_.Name }
这行得通,但是可以以更好的方式完成吗?
如果我这样做:
$results = objects | select Name
$results
是具有 Name 属性的对象数组。我希望 $results 包含一个名称数组。
有没有更好的办法?
假设我们有一个对象数组 $objects。假设这些对象具有“名称”属性。
这就是我想做的
$results = @()
$objects | %{ $results += $_.Name }
这行得通,但是可以以更好的方式完成吗?
如果我这样做:
$results = objects | select Name
$results
是具有 Name 属性的对象数组。我希望 $results 包含一个名称数组。
有没有更好的办法?
我想你也许可以ExpandProperty
使用Select-Object
.
例如,要获取当前目录的列表并只显示 Name 属性,可以执行以下操作:
ls | select -Property Name
这仍然返回 DirectoryInfo 或 FileInfo 对象。您始终可以通过管道到Get-Member(别名gm
)来检查通过管道的类型。
ls | select -Property Name | gm
因此,要将对象扩展为您正在查看的属性类型,您可以执行以下操作:
ls | select -ExpandProperty Name
在您的情况下,您可以执行以下操作以使变量成为字符串数组,其中字符串是 Name 属性:
$objects = ls | select -ExpandProperty Name
作为一个更简单的解决方案,您可以使用:
$results = $objects.Name
$results
其中应填充$objects
.
通过何时使用哪种方法和性能比较的指导来补充先前存在的有用答案。
在管道[1]之外,使用 (PSv3+):
$对象。姓名正如rageandqq 的回答所证明的那样,它在语法上更简单,而且速度更快。
在集合级别访问属性以将其元素的值作为数组获取(如果有 2 个或更多元素)称为成员枚举,并且是 PSv3+ 功能。
或者,在PSv2中,使用foreach
语句,您也可以将其输出直接分配给变量:
$results = foreach ($obj in $objects) { $obj.Name }
如果首先在内存中收集(管道)命令的所有输出是可行的,您还可以将管道与成员枚举结合起来;例如:
(Get-ChildItem -File | Where-Object Length -lt 1gb).Name
权衡:
(Get-ChildItem).Name
),则该命令必须首先运行完成,然后才能访问结果数组的元素。在管道中,如果您必须将结果传递给另一个命令,特别是如果原始输入不适合整个内存,请使用:
$对象 | Select-Object -ExpandProperty 名称
-ExpandProperty
在Scott Saad 的回答中进行了解释(您需要它来仅获取属性value)。对于小型输入集合(数组),您可能不会注意到差异,而且,尤其是在命令行上,有时能够轻松键入命令更为重要。
这是一个易于键入的替代方法,但它是最慢的方法;它使用称为操作语句的简化ForEach-Object
语法(同样,PSv3+):例如,以下 PSv3+ 解决方案很容易附加到现有命令:
$objects | % Name # short for: $objects | ForEach-Object -Process { $_.Name }
注意:管道的使用不是这种方法缓慢的主要原因,它是 (and ) cmdlet的低效实现,ForEach-Object
Where-Object
至少 PowerShell 7.2。这篇优秀的博客文章解释了这个问题;它导致了功能请求GitHub 问题 #10982;以下解决方法大大加快了操作速度(仅比foreach
语句慢一点,但仍然比 快.ForEach()
):
# Speed-optimized version of the above.
# (Use `&` instead of `.` to run in a child scope)
$objects | . { process { $_.Name } }
本文更全面讨论的PSv4+.ForEach()
数组方法是另一种性能良好的替代方法,但请注意,它需要先收集内存中的所有输入,就像成员枚举一样:
# By property name (string):
$objects.ForEach('Name')
# By script block (more flexibility; like ForEach-Object)
$objects.ForEach({ $_.Name })
这种方法类似于成员枚举,具有相同的权衡,只是不应用管道逻辑;它比成员枚举稍慢,但仍明显快于管道。
对于按名称(字符串参数)提取单个属性值,此解决方案与成员枚举相当(尽管后者在语法上更简单)。
脚本块{ ... }
变体( ) 允许任意转换;它是基于管道的cmdlet ( )的一种更快的 - all-in-memory-at-once - 替代方案ForEach-Object
%
。
注意:.ForEach()
数组方法,就像它的.Where()
兄弟(内存中的等价物Where-Object
)一样,总是返回一个集合(一个实例[System.Collections.ObjectModel.Collection[psobject]]
),即使只产生一个输出对象。
相比之下,成员枚举 ,Select-Object
和ForEach-Object
按Where-Object
原样返回单个输出对象,而不将其包装在集合(数组)中。
以下是各种方法的示例时序,基于10,000
对象的输入集合,在 10 次运行中取平均值;绝对数字并不重要,并且会因许多因素而异,但它应该让您了解相对性能(时间来自单核 Windows 10 虚拟机:
重要的
相对性能会根据输入对象是常规 .NET 类型的实例(例如,作为 的输出Get-ChildItem
)还是[pscustomobject]
实例(例如,作为 的输出Convert-FromCsv
)而有所不同。
原因是[pscustomobject]
属性由 PowerShell 动态管理,它可以比(静态定义的)常规 .NET 类型的常规属性更快地访问它们。下面介绍了这两种情况。
测试使用已经在内存中的完整集合作为输入,以便专注于纯属性提取性能。使用流式 cmdlet/函数调用作为输入,性能差异通常不会那么明显,因为在该调用中花费的时间可能占花费的大部分时间。
为简洁起见,别名%
用于ForEach-Object
cmdlet。
一般结论,适用于常规 .NET 类型和[pscustomobject]
输入:
成员枚举 ( $collection.Name
) 和foreach ($obj in $collection)
解决方案是迄今为止最快的,比最快的基于管道的解决方案快 10 倍或更多。
令人惊讶的是,% Name
性能比这个 GitHub 问题差得多。% { $_.Name }
PowerShell Core 在这里始终优于 Windows Powershell。
常规 .NET 类型的计时:
Factor Command Secs (10-run avg.)
------ ------- ------------------
1.00 $objects.Name 0.005
1.06 foreach($o in $objects) { $o.Name } 0.005
6.25 $objects.ForEach('Name') 0.028
10.22 $objects.ForEach({ $_.Name }) 0.046
17.52 $objects | % { $_.Name } 0.079
30.97 $objects | Select-Object -ExpandProperty Name 0.140
32.76 $objects | % Name 0.148
Factor Command Secs (10-run avg.)
------ ------- ------------------
1.00 $objects.Name 0.012
1.32 foreach($o in $objects) { $o.Name } 0.015
9.07 $objects.ForEach({ $_.Name }) 0.105
10.30 $objects.ForEach('Name') 0.119
12.70 $objects | % { $_.Name } 0.147
27.04 $objects | % Name 0.312
29.70 $objects | Select-Object -ExpandProperty Name 0.343
结论:
.ForEach('Name')
明显优于.ForEach({ $_.Name })
。奇怪的是,在 Windows PowerShell 中,后者更快,尽管只是稍微快一点。[pscustomobject]
实例计时:
Factor Command Secs (10-run avg.)
------ ------- ------------------
1.00 $objects.Name 0.006
1.11 foreach($o in $objects) { $o.Name } 0.007
1.52 $objects.ForEach('Name') 0.009
6.11 $objects.ForEach({ $_.Name }) 0.038
9.47 $objects | Select-Object -ExpandProperty Name 0.058
10.29 $objects | % { $_.Name } 0.063
29.77 $objects | % Name 0.184
Factor Command Secs (10-run avg.)
------ ------- ------------------
1.00 $objects.Name 0.008
1.14 foreach($o in $objects) { $o.Name } 0.009
1.76 $objects.ForEach('Name') 0.015
10.36 $objects | Select-Object -ExpandProperty Name 0.085
11.18 $objects.ForEach({ $_.Name }) 0.092
16.79 $objects | % { $_.Name } 0.138
61.14 $objects | % Name 0.503
结论:
请注意,[pscustomobject]
input.ForEach('Name')
的性能如何远远优于基于脚本块的变体.ForEach({ $_.Name })
.
同样,[pscustomobject]
输入使基于管道的Select-Object -ExpandProperty Name
速度更快,在 Windows PowerShell 中几乎与 PowerShell 相当.ForEach({ $_.Name })
,但在 PowerShell Core 中仍然慢约 50%。
简而言之:除了奇怪的例外% Name
,[pscustomobject]
基于字符串的引用属性的方法优于基于脚本块的方法。
测试的源代码:
笔记:
Time-Command
从此Gist下载函数以运行这些测试。
假设您已经查看了链接代码以确保它是安全的(我可以亲自向您保证,但您应该始终检查),您可以直接安装它,如下所示:
irm https://gist.github.com/mklement0/9e1f13978620b09ab2d15da5535d1b27/raw/Time-Command.ps1 | iex
改为使用实例$useCustomObjectInput
进行$true
测量。[pscustomobject]
$count = 1e4 # max. input object count == 10,000
$runs = 10 # number of runs to average
# Note: Using [pscustomobject] instances rather than instances of
# regular .NET types changes the performance characteristics.
# Set this to $true to test with [pscustomobject] instances below.
$useCustomObjectInput = $false
# Create sample input objects.
if ($useCustomObjectInput) {
# Use [pscustomobject] instances.
$objects = 1..$count | % { [pscustomobject] @{ Name = "$foobar_$_"; Other1 = 1; Other2 = 2; Other3 = 3; Other4 = 4 } }
} else {
# Use instances of a regular .NET type.
# Note: The actual count of files and folders in your file-system
# may be less than $count
$objects = Get-ChildItem / -Recurse -ErrorAction Ignore | Select-Object -First $count
}
Write-Host "Comparing property-value extraction methods with $($objects.Count) input objects, averaged over $runs runs..."
# An array of script blocks with the various approaches.
$approaches = { $objects | Select-Object -ExpandProperty Name },
{ $objects | % Name },
{ $objects | % { $_.Name } },
{ $objects.ForEach('Name') },
{ $objects.ForEach({ $_.Name }) },
{ $objects.Name },
{ foreach($o in $objects) { $o.Name } }
# Time the approaches and sort them by execution time (fastest first):
Time-Command $approaches -Count $runs | Select Factor, Command, Secs*
[1] 从技术上讲,即使是没有|
,管道运算符的命令,也会在幕后使用管道,但为了讨论的目的,使用管道仅指使用|
, 管道运算符的命令,因此根据定义涉及多个命令。
注意,成员枚举仅在集合本身没有同名成员时才有效。因此,如果您有一个 FileInfo 对象数组,则无法通过使用获取文件长度数组
$files.length # evaluates to array length
在你说“很明显”之前,请考虑一下。如果您有一个具有容量属性的对象数组,那么
$objarr.capacity
可以正常工作,除非$objarr 实际上不是 [Array],而是例如 [ArrayList]。因此,在使用成员枚举之前,您可能必须查看包含您的集合的黑匣子。
(版主注意:这应该是对rageandqq的回答的评论,但我还没有足够的声誉。)
我每天都学到新东西!这次真是万分感谢。我试图达到同样的效果。我直接这样做了:
$ListOfGGUIDs = $objects.{Object GUID}
这基本上使我的变量再次成为对象!后来我意识到我需要先将它定义为一个空数组,
$ListOfGGUIDs = @()