Выберите значения одного свойства для всех объектов массива в PowerShell

Скажем, у нас есть массив объектов $objects. Пусть говорят, что эти объекты имеют свойство "Имя".

Это то, что я хочу сделать

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

Это работает, но лучше ли это сделать?

Если я сделаю что-то вроде:

 $results = objects | select Name

$results - это массив объектов, имеющих свойство Name. Я хочу, чтобы $results содержал массив имен.

Есть ли лучший способ?

Ответ 1

Я думаю, вы могли бы использовать параметр 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

Ответ 2

В качестве еще более простого решения вы можете просто использовать:

$results = $objects.Name

Который должен заполнить $results массивом всех значений свойств "Имя" элементов в $objects.

Ответ 3

В дополнение к уже существующим, полезные ответы с указанием , когда использовать, какой подход и сравнения производительности.

  • Вне конвейера используйте:

    $objects.Name
    (PSv3+), как показано в rageandqq answer, что синтаксически проще и намного быстрее.

    • Доступ к свойству на уровне коллекции для получения значений его элементов в виде массива называется перечислением членов и функцией PSv3+.
    • В качестве альтернативы, в PSv2 используйте оператор foreach, выходные данные которого также можно назначить непосредственно переменной:
      $results = foreach ($obj in $objects) { $obj.Name }
    • Компромисс:
      • И входной набор и выходной массив должнывмещаться в память в целом.
      • Если набор входных данных сам является результатом команды (конвейера) (например, (Get-ChildItem).Name), эта команда должна сначала выполнить до завершения, прежде чем будут доступны элементы массива.
  • В конвейере, где результат должен обрабатываться дальше или результаты не помещаются в память в целом, используйте :

    $objects | Select-Object -ExpandProperty Name

    • Необходимость в -ExpandProperty объясняется в ответе Скотта Саада.
    • Вы получаете обычные преимущества конвейерной обработки поочередно, которая обычно производит вывод сразу и поддерживает постоянное использование памяти (если вы все равно не соберете результаты в памяти).
    • Компромисс:
      • Использование конвейера сравнительно медленное.

Для небольших входных коллекций (массивов) вы, вероятно, не заметите разницу, и, особенно в командной строке, иногда важнее легко набрать команду.


Вот простой в вводе альтернативный вариант, который, однако, является самым медленным подходом; он использует упрощенный синтаксис ForEach-Object, называемый оператором операции (опять же, PSv3+): ; например, следующее решение PSv3+ легко добавить к существующей команде:

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

Для полноты картины: малоизвестный PSv4+ .ForEach() метод массива, более подробно рассмотренный в этой статье, является еще одной альтернативой:

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

# By script block (more flexibility; like ForEach-Object)
$objects.ForEach({ $_.Name })
  • Этот подход похож на перечисление членов, с теми же компромиссами, за исключением того, что конвейерная логика не применяется; он немного медленнее, но все же заметно быстрее, чем конвейер.

  • Для извлечения одного значения свойства по имени (строковый аргумент) это решение наравне с перечислением членов (хотя последнее синтаксически проще).

  • вариант блока скрипта, допускает произвольные преобразования; это более быстрая альтернатива "все в памяти за один раз" альтернативе ForEach-Object на основе конвейера (%).


Сравнение производительности различных подходов

Вот примеры времени для различных подходов, основанные на наборе входных данных 10,000 объектов, усредненных по 10 прогонам; абсолютные значения не важны и варьируются в зависимости от многих факторов, но они должны дать вам представление об относительной производительности (время определяется одноядерной виртуальной машиной Windows 10:

Важно

  • Относительная производительность варьируется в зависимости от того, являются ли входные объекты экземплярами обычных типов .NET (например, как выходные данные Get-ChildItem) или [pscustomobject] экземплярами (например, как выходные данные Convert-FromCsv).
    Причина в том, что свойства [pscustomobject] динамически управляются PowerShell, и он может получить к ним доступ быстрее, чем обычные свойства (статически определенного) обычного типа .NET. Оба сценария описаны ниже.

  • В тестах в качестве входных данных используются коллекции, уже находящиеся в памяти, чтобы сосредоточиться на чистой производительности извлечения свойств. При использовании потокового командлета/вызова функции в качестве входных данных различия в производительности, как правило, будут гораздо менее выраженными, поскольку время, потраченное на этот вызов, может составлять большую часть затраченного времени.

  • Для краткости псевдоним % используется для командлета ForEach-Object.

Общие выводы, применимые как к обычному типу .NET, так и к входу [pscustomobject]:

  • Решения для перечисления членов ($collection.Name) и foreach ($obj in $collection) являются самыми быстрыми, в 10 или более раз быстрее, чем самое быстрое решение на основе конвейера.

  • Удивительно, но % Name работает намного хуже, чем % { $_.Name } - см. эту проблему с GitHub.

  • Ядро PowerShell стабильно превосходит 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
Comparing property-value extraction methods with 10000 input objects, averaged over 10 runs...

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] строковые методы ссылки на свойства превосходят методы на основе блоков сценариев.


Исходный код для тестов:

Примечание:

  • Загрузите функцию Time-Command из этого списка, чтобы запустить эти тесты.

  • Установите для $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 home dir. tree
  #       may be less than $count
  $objects = Get-ChildItem -Recurse $HOME | 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*