【问题标题】:Select the values of one property on all objects of an array in PowerShell在 PowerShell 中选择数组的所有对象上的一个属性的值
【发布时间】:2011-03-03 04:55:08
【问题描述】:

假设我们有一个对象数组 $objects。假设这些对象具有“名称”属性。

这就是我想做的事情

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

这可行,但可以以更好的方式完成吗?

如果我这样做:

 $results = objects | select Name

$results 是一个具有 Name 属性的对象数组。我希望 $results 包含一个名称数组。

有没有更好的办法?

【问题讨论】:

  • 为了完整起见,您还可以从原始代码中删除“+=”,以便 foreach 仅选择名称:$results = @($objects | %{ $_.Name })。虽然我认为Scott 的答案通常更好。
  • @EmperorXLII:很好,在 PSv3+ 中你甚至可以简化为:$objects | % Name

标签: arrays powershell member-enumeration


【解决方案1】:

我想你也许可以使用Select-ObjectExpandProperty 参数。

例如,要获取当前目录的列表并只显示 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

【讨论】:

  • 我应该只为这个页面添加书签。这个答案我用过很多次了。
【解决方案2】:

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

$results = $objects.Name

应该用$objects 中元素的所有“名称”属性值的数组填充$results

【讨论】:

  • 请注意,这在Exchange Management Shell 中不起作用。使用 Exchange 时,我们需要使用 $objects | select -Property Propname, OtherPropname
  • @Bassie:在集合级别访问属性以将其成员的值作为数组获取称为成员枚举,是PSv3+ feature;据推测,您的 Exchange 命令行管理程序是 PSv2。
【解决方案3】:

通过何时使用哪种方法性能比较的指导来补充现有的有用答案。

  • 在管道之外[1],使用 (PSv3+):

    $objects.Name
    正如rageandqq's answer 中所展示的那样,它在语法上更简单并且更快

    • collection 级别访问属性以将其 elements' 值作为 array(如果有 2 个或更多元素)是名为member-access enumeration,是PSv3+的功能。

    • 或者,在 PSv2 中,使用foreach 语句,您也可以将其输出直接分配给变量:

      $results = foreach ($obj in $objects) { $obj.Name }
    • 如果首先在内存中收集(管道)命令的所有输出是可行的,您还可以管道与成员访问枚举结合;例如:

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

      • 输入集合和输出数组 必须作为一个整体装入内存。
      • 如果输入集合本身是命令(管道)的结果(例如,(Get-ChildItem).Name),则该命令必须首先运行完成,然后才能访问结果数组的元素。李>
  • 管道中,如果您必须将结果传递给另一个命令,特别是如果原始输入不适合整个内存,使用:

    $objects | Select-Object -ExpandProperty 名称

    • -ExpandProperty 中解释了对 -ExpandProperty 的需求(您需要它来仅获取属性 value)。
    • 您会从管道的流式传输 行为中获得通常的管道优势,即一对一的对象处理,这通常会立即产生输出并保持内存使用不变(除非您最终将结果收集到反正内存)。
    • 权衡
      • 管道的使用相对缓慢

对于 small 输入集合(数组),您可能不会注意到差异,而且,尤其是在命令行上,有时能够轻松键入命令更重要。


这是一个易于输入的替代方法,但它是最慢的方法;它使用simplified ForEach-Object syntax called an operation statement(同样,PSv3+): ;例如,以下 PSv3+ 解决方案很容易附加到现有命令:

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

注意:使用管道并不是这种方法速度慢的主要原因,它是ForEach-Object(和Where-Object)cmdlet 的低效实现 ,至少 PowerShell 7.2。 This excellent blog post 解释问题;它导致了功能请求GitHub issue #10982;以下解决方法大大加快了操作(仅比foreach 语句慢一点,但仍比.ForEach() 快):

# Speed-optimized version of the above.
# (Use `&` instead of `.` to run in a child scope)
$objects | . { process { $_.Name } }

PSv4+ .ForEach() array method(在 this article 中进行了更全面的讨论)是另一种性能良好的替代方案,但请注意它需要收集所有首先在内存中输入,就像成员访问枚举:

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

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

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

  • script-block 变体 ({ ... }) 允许任意转换;它是一种更快的 - all-in-memory-at-once - 替代基于管道的ForEach-Object cmdlet (%)

注意:.ForEach() 数组方法,就像它的 .Where() sibling(在内存中等效于 Where-Object),总是返回一个 集合(一个实例[System.Collections.ObjectModel.Collection[psobject]]),即使只生成一个输出对象。
相比之下,成员访问枚举 Select-ObjectForEach-ObjectWhere-Object 按原样返回单个输出对象,而不将其包装在集合(数组)中。


比较各种方法的性能

这里是各种方法的采样时间,基于 10,000 对象的输入集合,在 10 次运行中取平均值;绝对数字并不重要,并且会因许多因素而异,但它应该让您了解相对性能(时间来自单核 Windows 10 VM:

重要

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

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

  • 为简洁起见,别名 % 用于 ForEach-Object cmdlet。

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

  • 成员枚举 ($collection.Name) 和 foreach ($obj in $collection) 解决方案是迄今为止最快的,比基于管道的最快解决方案快 10 倍或更多。

  • 令人惊讶的是,% Name 的性能比 % { $_.Name } 差得多 - 请参阅 this GitHub issue

  • 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 Core 中,.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] 引用属性的基于字符串的方法优于基于脚本块的方法。


测试源代码

注意:

  • this Gist 下载函数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] 从技术上讲,即使是没有| 的命令pipeline operator,也会在后台使用管道,但出于本次讨论的目的,使用管道仅指对于使用管道运算符| 的命令,因此根据定义涉及多个命令

【讨论】:

  • 迄今为止我在 stackoverflow 上看到的最好的响应之一...干得好!
【解决方案4】:

注意,成员枚举仅在集合本身没有同名成员时才有效。因此,如果您有一个 FileInfo 对象数组,则无法通过使用获得文件长度数组

 $files.length # evaluates to array length

在你说“很明显”之前,请考虑一下。如果你有一个具有容量属性的对象数组,那么

 $objarr.capacity

可以正常工作除非 $objarr 实际上不是 [Array],而是例如 [ArrayList]。因此,在使用成员枚举之前,您可能需要查看包含您的集合的黑盒子。

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

【讨论】:

  • 这是一个好点; this GitHub feature request 要求为成员枚举提供单独的语法。名称冲突的解决方法是使用.ForEach() 数组方法,如下所示:$files.ForEach('Length')
【解决方案5】:

我每天都学到新东西!这次真是万分感谢。我试图达到同样的目的。我是直接这样做的: $ListOfGGUIDs = $objects.{Object GUID} 这基本上使我的变量再次成为对象!后来我意识到我需要先将它定义为一个空数组, $ListOfGGUIDs = @()

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-11-02
    • 1970-01-01
    • 2019-12-03
    • 1970-01-01
    • 2013-07-31
    • 2017-02-10
    • 1970-01-01
    相关资源
    最近更新 更多