166

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

这就是我想做的

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

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

如果我这样做:

 $results = objects | select Name

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

有没有更好的办法?

4

5 回答 5

255

我想你也许可以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
于 2011-03-03T05:15:38.413 回答
84

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

$results = $objects.Name

$results其中应填充$objects.

于 2014-07-24T15:58:18.540 回答
48

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

  • 在管道[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 名称

    • 需要-ExpandPropertyScott Saad 的回答中进行了解释(您需要它来仅获取属性value)。
    • 您可以从管道的流式传输行为中获得通常的管道优势,即一对一的对象处理,这通常会立即产生输出并保持内存使用不变(除非您最终将结果收集到内存中)。
    • 权衡
      • 管道的使用速度比较

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


这是一个易于键入的替代方法,但它是最慢的方法;它使用称为操作语句的简化ForEach-Object语法(同样,PSv3+):例如,以下 PSv3+ 解决方案很容易附加到现有命令:

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

注意:管道的使用不是这种方法缓慢的主要原因,它是 (and ) cmdlet的低效实现,ForEach-ObjectWhere-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-ObjectForEach-ObjectWhere-Object原样返回单个输出对象,而不将其包装在集合(数组)中。


比较各种方法的性能

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

重要的

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

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

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

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

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

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

  • PowerShell Core 在这里始终优于 Windows Powershell。

常规 .NET 类型的计时

  • PowerShell 核心 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 核心 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]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] 从技术上讲,即使是没有|,管道运算符的命令,也会在幕后使用管道,但为了讨论的目的,使用管道仅指使用|, 管道运算符的命令,因此根据定义涉及多个命令

于 2018-02-20T14:58:57.727 回答
2

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

 $files.length # evaluates to array length

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

 $objarr.capacity

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

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

于 2020-06-26T07:01:58.540 回答
0

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

于 2021-12-11T07:25:01.607 回答