Выберите значения одного свойства для всех объектов массива в 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, что синтаксически проще и намного быстрее.
В конвейере, где результат должен обрабатываться дальше или результаты не помещаются в память в целом, используйте : $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*