假设我们有一个对象数组 $objects。假设这些对象有一个 "Name" 属性。
这是我想要做的事情
$results = @()
$objects | %{ $results += $_.Name }
这段代码可以运行,但有更好的方法吗?
如果我像下面这样做:
$results = objects | select Name
$results
是一个对象数组,其中每个对象都有一个 Name 属性。我想让 $results
变成一个名字数组。
有更好的方法吗?
假设我们有一个对象数组 $objects。假设这些对象有一个 "Name" 属性。
这是我想要做的事情
$results = @()
$objects | %{ $results += $_.Name }
这段代码可以运行,但有更好的方法吗?
如果我像下面这样做:
$results = objects | select Name
$results
是一个对象数组,其中每个对象都有一个 Name 属性。我想让 $results
变成一个名字数组。
有更好的方法吗?
我认为你可以使用Select-Object
的ExpandProperty
参数。
例如,要获取当前目录的列表并仅显示名称属性,可以执行以下操作:
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
需要使用$objects
中元素的所有'Name'属性值填充$results
数组。
为了补充现有的有用答案,提供关于何时使用哪种方法以及性能比较的指导。
Outside of a pipeline[1], use (requires PSv3+):
$objects.Name # returns .Name property values from all objects in $objects
as demonstrated in rageandqq's answer, which is both syntactically simpler and much faster.
Accessing a property at the collection level to get its elements' values as an array (if there are 2 or more elements) is called member-access enumeration and is a PSv3+ feature.
Alternatively, in PSv2, use the foreach
statement, whose output you can also assign directly to a variable:
$results = foreach ($obj in $objects) { $obj.Name }
If collecting all output from a (pipeline) command in memory first is feasible, you can also combine pipelines with member-access enumeration; e.g.:
(Get-ChildItem -File | Where-Object Length -lt 1gb).Name
Tradeoffs:
(Get-ChildItem).Name
), that command must first run to completion before the resulting array's elements can be accessed.In a pipeline, in case you must pass the results to another command, notably if the original input doesn't fit into memory as a whole, use:
$objects | Select-Object -ExpandProperty Name
-ExpandProperty
is explained in Scott Saad's answer (you need it to get only the property value).对于小型输入集合(数组),您可能不会注意到差异,尤其是在命令行上,有时能够轻松输入命令更加重要。
这里有一种易于输入的替代方法,但是它是最慢的方法;它使用ForEach-Object
通过其内置别名%
,带有简化语法(再次,PSv3+):
例如,以下PSv3+解决方案很容易附加到现有命令:
$objects | % Name # short for: $objects | ForEach-Object -Process { $_.Name }
注意:使用管道不是这种方法缓慢的主要原因,而是由于ForEach-Object
(和Where-Object
)cmdlet的效率低下,至少到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 })
这种方法类似于成员访问枚举,具有相同的权衡,但不应用管道逻辑;它比成员访问枚举略慢,但仍然比管道明显快。
对于通过名称(字符串参数)提取单个属性值,此解决方案与成员访问枚举相当(尽管后者在语法上更简单)。
脚本块变体({ ... }
)允许任意转换;它是基于内存的 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
输出)。
原因是PowerShell动态管理[pscustomobject]
属性,并且可以比静态定义的常规.NET类型更快地访问它们的属性。两种情况都在下面涵盖。
测试使用已经完全存储在内存中的集合作为输入,以便专注于纯属性提取性能。如果将流式传递的cmdlet /函数调用作为输入,则性能差异通常不会那么明显,因为在其中花费的时间可能占大部分时间。
为简洁起见,别名%
用于ForEach-Object
cmdlet。
一般结论,适用于常规.NET类型和[pscustomobject]
输入:
成员枚举 ($collection.Name
) 和 foreach ($obj in $collection)
的解决方案是迄今为止最快的,比基于管道的最快解决方案快10倍或更多。
令人惊讶的是,% Name
的性能比 % { $_.Name }
差得多 - 参见 这个 GitHub 问题。
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]
输入时,.ForEach('Name')
比基于脚本块的变体.ForEach({ $_.Name })
表现要好得多。
同样,[pscustomobject]
输入使得基于管道的Select-Object -ExpandProperty Name
更快,在Windows PowerShell中几乎与.ForEach({ $_.Name })
相当,但在PowerShell Core中仍然慢约50%。
简而言之:除了% Name
这个奇怪的例外情况外,使用[pscustomobject]
时,基于字符串的属性引用方法优于基于脚本块的方法。
测试代码的源代码:
注意:
Download function Time-Command
from this Gist to run these tests.
Assuming you have looked at the linked code to ensure that it is safe (which I can personally assure you of, but you should always check), you can install it directly as follows:
irm https://gist.github.com/mklement0/9e1f13978620b09ab2d15da5535d1b27/raw/Time-Command.ps1 | iex
Set $useCustomObjectInput
to $true
to measure with [pscustomobject]
instances instead.
$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答案的评论,但我还没有足够的声望。)
.ForEach()
数组方法,如下所示:$files.ForEach('Length')
。 - mklement0我每天都在学习新东西!谢谢你的帮助。我也一直在尝试实现同样的功能。我一开始是这样做的:
$ListOfGGUIDs = $objects.{Object GUID}
这基本上又将我的变量变成了一个对象!后来我意识到需要先定义为空数组,
$ListOfGGUIDs = @()
$results = @($objects | %{ $_.Name })
。有时在命令行键入更方便,但我认为Scott的答案通常更好。 - Emperor XLII$objects | % Name
。 - mklement0