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

135

Допустим, у нас есть массив объектов $ objects. Допустим, у этих объектов есть свойство «Имя».

Это то, чем я хочу заниматься

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

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

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

 $results = objects | select Name

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

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

Сильвен Реверди
источник
4
Просто для полноты картины , можно также удалить «+ =» из исходного кода, так что Еогеасп выбирает только имя: $results = @($objects | %{ $_.Name }). Иногда это может быть удобнее вводить в командной строке, хотя я думаю, что ответ Скотта в целом лучше.
Emperor XLII
1
@EmperorXLII: Хорошая мысль, а в PSv3 + вы можете даже упростить:$objects | % Name
mklement0

Ответы:

212

Я думаю, вы могли бы использовать 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
Скотт Саад
источник
73

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

$results = $objects.Name

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

rageandqq
источник
Обратите внимание, что это не работает в Exchange Management Shell. При использовании Exchange нам необходимо использовать$objects | select -Property Propname, OtherPropname
Bassie
2
@Bassie: доступ к свойству на уровне коллекции для получения значений его членов в виде массива называется перечислением членов и является функцией PSv3 + ; предположительно, ваша оболочка Exchange Management Shell - PSv2.
mklement0
32

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

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

    $ объекты . название
    как показано в ответе rageandqq , который синтаксически проще и намного быстрее .

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

    $ объекты | Select-Object -ExpandProperty Name

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

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


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

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

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

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

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

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

  • Скрипт-блок - вариант , позволяет произвольные преобразования ; это более быстрая альтернатива ForEach-Object cmdlet ( %), работающая по принципу « все в памяти» .


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

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

Важный

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

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

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

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

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

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

  • PowerShell Core здесь неизменно превосходит Windows Powershell.

Тайминги с обычными типами .NET :

  • PowerShell Core v7.0.0-превью.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 версии 5.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 Ядра , .ForEach('Name')явно превосходит .ForEach({ $_.Name }). Любопытно, что в Windows PowerShell последняя работает быстрее, хотя и незначительно.

Тайминги с [pscustomobject]экземплярами :

  • PowerShell Core v7.0.0-превью.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 версии 5.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из этого Gist, чтобы запустить эти тесты.

  • Вместо этого установите $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*
mklement0
источник
1

Внимание! Перечисление членов работает, только если сама коллекция не имеет члена с таким же именем. Итак, если у вас есть массив объектов FileInfo, вы не можете получить массив длин файлов, используя

 $files.length # evaluates to array length

И прежде чем вы скажете «хорошо, очевидно», подумайте об этом. Если у вас был массив объектов со свойством емкости, тогда

 $objarr.capacity

будет работать нормально, ЕСЛИ $ objarr на самом деле не [Array], а, например, [ArrayList]. Поэтому перед использованием перечисления членов вам, возможно, придется заглянуть внутрь черного ящика, содержащего вашу коллекцию.

(Примечание для модераторов: это должен быть комментарий к ответу rageandqq, но у меня еще недостаточно репутации.)

Убер Клугер
источник
Это хороший момент; этот запрос функции GitHub требует отдельного синтаксиса для перечисления членов. Обходной путь при конфликте имен заключается в использовании .ForEach()метода массива следующим образом:$files.ForEach('Length')
mklement0