在 PowerShell 中为数组的所有对象选择一个属性的值

假设我们有一个对象数组 $object,假设这些对象有一个“ Name”属性。

这就是我想做的

 $results = @()
$objects | %{ $results += $_.Name }

这个方法有效,但是能用更好的方法来实现吗?

如果我这样做:

 $results = objects | select Name

$results是一个具有 Name 属性的对象数组。

还有更好的办法吗?

335873 次浏览

我认为您可以使用 Select-ObjectExpandProperty参数。

例如,要获取工作目录列表并只显示 Name 属性,需要执行以下操作:

ls | select -Property Name

这仍然返回 DirectoryInfo 或 FileInfo 对象。您总是可以通过管道到 记住(别名 gm)来检查通过管道传递的类型。

ls | select -Property Name | gm

因此,对于 扩张这个对象是您正在查看的属性类型的对象,您可以执行以下操作:

ls | select -ExpandProperty Name

在您的示例中,只需执行以下操作,就可以将变量设置为字符串数组,其中字符串是 Name 属性:

$objects = ls | select -ExpandProperty Name

作为一个更简单的解决方案,你可以使用:

$results = $objects.Name

它应该用包含 $objects中元素的所有“ Name”属性值的数组填充 $results

为了补充以前存在的,有益的答案与指导的 何时使用何种方法性能比较

  • 在管道 [1]的外部,使用(需要 PSv3 +) :

    $objects.Name # returns .Name property values from all objects in $objects
    

    Rageandqq 的回答所示,它在语法上比 快多了简单。

    • 访问 收藏品级别的属性以获取其 元素值作为 数组(如果有2个或更多元素) ,这种方法称为 < em > < strong > 成员-访问枚举 ,是 PSv3 + 特性。

    • 或者,在 PSv2中使用 foreach 声明,它的输出也可以直接赋值给变量: < pre > $result = foreach ($Objects 中的 $obj){ $obj。姓名

    • 如果首先在内存中收集(管道)命令的所有输出是可行的,还可以将 < em > 将 管道与成员访问枚举结合起来; 例如:

       (Get-ChildItem -File | Where-Object Length -lt 1gb).Name
      
    • 权衡 :

      • 都是 输入收集和输出数组 必须整体符合记忆。
      • 如果输入集合本身是命令(管道)(例如,(Get-ChildItem).Name)的结果,则可以访问结果数组元素之前的 命令必须首先运行到完成
  • 管道 中,如果您必须将结果传递给另一个命令,特别是如果原始输入不能作为一个整体放入内存中,则使用 < strong > use: < pre > $Objects | 选择-对象-ExpandProperty Name

    • Scott Saad 的回答中解释了对 -ExpandProperty的需求(您只需要它来获得属性 价值)。
    • 您可以从管道的 在流动行为中获得通常的管道优势,即逐个对象处理,它通常立即生成输出并保持内存使用不变(除非您最终将结果收集到内存中)。
    • 权衡 :
      • 使用 管道比较慢。

对于 很小输入集合(数组) ,您可能不会注意到 的区别,而且,特别是在命令行上,有时能够轻松地键入命令更为重要。


这是一个 容易打字的替代品,但是它是 最慢的方法; 它使用 ForEach-Object通过它的内置别名 %简化句法(同样是 PSv3 +) : 例如,下面的 PSv3 + 解决方案很容易附加到现有的命令:

$objects | % Name      # short for: $objects | ForEach-Object -Process { $_.Name }

注意 : 使用管道并不是这种方法缓慢的主要原因,它是 ABC0(和 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 + < a href = “ https://Learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about _ array # foreach”rel = “ noReferrer”> .ForEach() < em > array method 另一个表现良好的选择,但是请注意,它与成员访问枚举一样是 需要首先收集内存中的所有输入:

# By property name (string):
$objects.ForEach('Name')


# By script block (more flexibility; like ForEach-Object)
$objects.ForEach({ $_.Name })
  • 这种方法是 类似于成员访问枚举,具有相同的权衡,只是应用的流水线逻辑是 没有; 它是 稍慢于 < em > 成员访问枚举 ,但仍然是 明显快于 < em > 管道

  • 对于通过 姓名(绳子参数)提取单个属性值,此解决方案与成员访问枚举相当(尽管后者在语法上更简单)。

  • 这是 脚本块变体({ ... })允许任意的变换,这是 基于管道的更快的全部内存替代方案 < a href = “ https://Learn.microsoft.com/powershell/module/microsoft.powershell.core/foreach-object”rel = “ noReferrer”> ABC1 < em > cmdlet (%)

注意: 与 .Where()兄弟姐妹(内存中相当于 Where-Object)一样,.ForEach()数组方法返回一个 收藏品([System.Collections.ObjectModel.Collection[psobject]]的一个实例) ,即使只生成 输出对象。
相比之下,成员访问枚举、 Select-ObjectForEach-ObjectWhere-Object按原样返回单个输出对象,而不将其封装在集合(数组)中。


比较各种方法的性能

下面是 样本时间的各种方法,基于 10,000物体的输入集合,在10次运行中取平均值; 绝对数字并不重要,因各种因素而异,但它应该给你一个 亲戚性能的感觉(计时来自一个单核的 Windows 10 VM:

很重要

  • 相对性能的变化取决于输入对象是 常规.NET 类型的实例(例如,作为 Get-ChildItem的输出)还是 [pscustomobject]实例的实例(例如,作为 Convert-FromCsv的输出)。
    原因是 [pscustomobject]属性是由 PowerShell 动态管理的,而且它可以比(静态定义的)常规属性更快地访问它们。NET 类型。下面将介绍这两种情况。

  • 测试使用内存中已满的集合作为输入,以便关注纯属性提取性能。使用流式 cmdlet/function 调用作为输入,性能差异通常不会那么明显,因为在该调用中花费的时间可能占用了大部分时间。

  • 为简便起见,别名 %用于 ForEach-Objectcmdlet。

一般结论 ,适用于普通.NET 类型和 [pscustomobject]输入:

  • 到目前为止,成员枚举($collection.Name)和 foreach ($obj in $collection)解决方案是最快的 ,比最快的基于流水线的解决方案快10倍或更多。

  • 令人惊讶的是,% Name的性能比 % { $_.Name }差得多-见 GitHub 的问题

  • PowerShell Core 的表现始终优于 Windows PowerShell。

时间与 常规.NET 类型 :

  • PowerShell Core v7.0.0-preview.3
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
  • Windows PowerShell v5.1.18362.145
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

结论:

  • 在 PowerShell 核心中,.ForEach('Name')的表现明显优于 .ForEach({ $_.Name }),而在 Windows PowerShell 上,奇怪的是,后者的表现更为出色,尽管只是略快一些。

时机与 [pscustomobject]实例 :

  • PowerShell Core v7.0.0-preview.3
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
  • Windows PowerShell v5.1.18362.145
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],引用属性的基于字符串的方法的性能优于基于脚本块的方法。


测试的源代码 :

注:

  • 这个要点下载函数 Time-Command来运行这些测试。

    • 假设您已经查看了链接的代码以确保它是安全的(我个人可以向您保证,但您应该经常检查) ,您可以直接安装它,如下所示:

      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

$objecarr 实际上不是一个[ Array ] ,而是一个[ ArrayList ]。因此,在使用 成员枚举成员枚举之前,您可能必须查看包含您的集合的黑盒内部。

(请版主注意: 这应该是对 rageandqq 的回答的评论,但我还没有足够的声誉。)

我每天都学到新东西!谢谢你。我也是这么想的。我是直接这么做的: $ListOfGGUIDs = $objects.{Object GUID} 这基本上又让我的变量变成了一个对象!后来我意识到我需要先将它定义为一个空数组, $ListOfGGUIDs = @()