Виберіть значення однієї властивості для всіх об'єктів масиву в PowerShell


134

Скажімо, у нас є масив об'єктів $ об’єктів. Скажімо, ці об’єкти мають властивість "Ім'я".

Це я хочу зробити

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

Це працює, але чи можна це зробити кращим чином?

Якщо я роблю щось на кшталт:

 $results = objects | select Name

$resultsце масив об'єктів, що мають властивість Name. Я хочу, щоб результати $ містили масив Names.

Чи є кращий спосіб?


4
Просто для повноти картини , можна також видалити «+ =» з вихідного коду, так що Еогеасп вибирає тільки ім'я: $results = @($objects | %{ $_.Name }). Це може бути зручніше вводити в командному рядку часом, хоча я думаю , що відповідь Скотта , як правило, краща.
Імператор 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.


Зауважте, що це не працює Exchange Management Shell. При використанні Exchange нам потрібно використовувати$objects | select -Property Propname, OtherPropname
Bassie

2
@Bassie: доступ до властивості на рівні колекції для отримання значень членів як масиву називається перерахуванням членів і є функцією PSv3 + ; імовірно, ваша оболонка управління біржею - PSv2.
mklement0

32

Доповнити попередньо існуючі корисні відповіді керівництвом, коли використовувати який підхід та порівняння ефективності .

  • Поза трубопроводу використовуйте (PSv3 +):

    $ об’єктів . Ім'я
    як показано у відповіді rageandqq , яка є і синтаксично простішою, і значно швидшою .

    • Доступ до властивості на рівні колекції для отримання значень його членів як масиву називається перерахуванням членів і є функцією PSv3 +.
    • Крім того, в PSv2 використовуйте foreach оператор , вихід якого ви також можете призначити безпосередньо змінній:
      $ results = foreach ($ obj у $ об’єктах) {$ obj.Name}
    • Компроміси :
      • І вхідний, і вихідний масив повинні вміщуватися в пам'яті в цілому .
      • Якщо колекція вводу сама по собі є результатом команди (конвеєра) (наприклад, (Get-ChildItem).Name), ця команда спочатку повинна запуститися до завершення, перш ніж отримати доступ до елементів масиву.
  • У конвеєрі, де результат потрібно додатково обробляти або результати не вміщуються в пам'ять в цілому, використовуйте:

    $ об'єктів | Select-Object -ExpandProperty Name

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

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


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

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

Для повноти: Маловідомий .ForEach() метод масиву PSv4 + , більш всебічний, який розглядається в цій статті , є ще однією альтернативою :

# 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 VM:

Важливо

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

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

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

Загальні висновки , застосовні як до звичайного типу .NET, так і до [pscustomobject]вхідного:

  • Перерахування членів ( $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 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 Ядра , .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 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*

1

Обережно, перерахування членів працює лише в тому випадку, якщо в самій колекції немає однойменного учасника. Отже, якщо у вас був масив об’єктів FileInfo, ви не можете отримати масив довжин файлів за допомогою

 $files.length # evaluates to array length

І перш ніж сказати "ну очевидно", подумайте про це. Якщо у вас був масив об'єктів із властивістю ємності, то

 $objarr.capacity

спрацювало б нормально, якщо $ objarr насправді не був [масивом], а, наприклад, [ArrayList]. Тому перед використанням перерахування членів вам, можливо, доведеться заглянути всередину чорного поля, що містить вашу колекцію.

(Зауважте модераторам: це має бути коментар до відповіді rageandqq, але у мене ще недостатньо репутації.)


Це хороший момент; цей запит на функцію GitHub запитує окремий синтаксис для перерахування членів. Вирішення проблеми зіткнень з іменами полягає у використанні .ForEach()методу масиву таким чином:$files.ForEach('Length')
mklement0
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.