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

190

假设我们有一个对象数组 $objects。假设这些对象有一个 "Name" 属性。

这是我想要做的事情

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

这段代码可以运行,但有更好的方法吗?

如果我像下面这样做:

 $results = objects | select Name

$results 是一个对象数组,其中每个对象都有一个 Name 属性。我想让 $results 变成一个名字数组。

有更好的方法吗?


5
为了完整起见,您还可以从原始代码中删除 "+=",这样 foreach 只会选择 Name: $results = @($objects | %{ $_.Name })。有时在命令行键入更方便,但我认为Scott的答案通常更好。 - Emperor XLII
2
@EmperorXLII:说得好,而且在PSv3+中,你甚至可以简化为:$objects | % Name - mklement0
5个回答

274

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

例如,要获取当前目录的列表并仅显示名称属性,可以执行以下操作:

ls | select -Property Name

这仍然返回DirectoryInfo或FileInfo对象。您可以通过将其管道传输到Get-Member(别名gm)来检查通过管道传输的类型。

ls | select -Property Name | gm

所以,要将对象扩展为你正在查看的属性类型,可以执行以下操作:

ls | select -ExpandProperty Name

在您的情况下,您可以按照以下方式使变量成为一个字符串数组,其中字符串是Name属性:

$objects = ls | select -ExpandProperty Name

6
我应该将这个页面加入书签。我已经使用了这个答案很多次。 - David Klempfner

93

作为更加简单的解决方案,你可以直接使用:

$results = $objects.Name

需要使用$objects中元素的所有'Name'属性值填充$results数组。


2
请注意,此方法无法在“Exchange管理Shell”中使用。在使用Exchange时,我们需要使用“$objects | select -Property Propname, OtherPropname”。 - Bassie
6
在集合级别访问属性以获取其成员值作为数组的过程称为“_member enumeration_”,是PSv3+功能。可以推测您的Exchange Management Shell版本为PSv2。 - mklement0
这是最好的答案。 - alvarez

63

为了补充现有的有用答案,提供关于何时使用哪种方法以及性能比较的指导。

  • 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:

      • Both the input collection and output array must fit into memory as a whole.
      • If the input collection is itself the result of a command (pipeline) (e.g., (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

    • The need for -ExpandProperty is explained in Scott Saad's answer (you need it to get only the property value).
    • You get the usual pipeline benefits of the pipeline's streaming behavior, i.e. one-by-one object processing, which typically produces output right away and keeps memory use constant (unless you ultimately collect the results in memory anyway).
    • Tradeoff:
      • Use of the pipeline is comparatively slow.

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


这里有一种易于输入的替代方法,但是它是最慢的方法;它使用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-ObjectForEach-ObjectWhere-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 类型的时间:

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


测试代码的源代码:

注意:

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


1
如此详尽的回答 - 这是 Stack Overflow 如此出色的一个例子。 - whytheq
很高兴听到这个消息,@whytheq;我非常感谢您的好评。 - mklement0

2

注意,仅当集合本身没有相同名称的成员时,成员枚举才起作用。因此,如果您有一个FileInfo对象数组,则无法使用以下方式获取文件长度数组:

 $files.length # evaluates to array length

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

 $objarr.capacity

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

(注:这应该是对rageandqq答案的评论,但我还没有足够的声望。)


这是一个很好的观点; 这个GitHub功能请求要求为成员枚举提供单独的语法。名称冲突的解决方法是使用.ForEach()数组方法,如下所示:$files.ForEach('Length') - mklement0

0

我每天都在学习新东西!谢谢你的帮助。我也一直在尝试实现同样的功能。我一开始是这样做的: $ListOfGGUIDs = $objects.{Object GUID} 这基本上又将我的变量变成了一个对象!后来我意识到需要先定义为空数组, $ListOfGGUIDs = @()


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接