Wählen Sie die Werte einer Eigenschaft für alle Objekte eines Arrays in PowerShell aus

133

Angenommen, wir haben ein Array von Objekten $ Objekte. Angenommen, diese Objekte haben die Eigenschaft "Name".

Das möchte ich tun

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

Das funktioniert, aber kann es besser gemacht werden?

Wenn ich so etwas mache wie:

 $results = objects | select Name

$resultsist ein Array von Objekten mit einer Name-Eigenschaft. Ich möchte, dass $ results ein Array von Namen enthält.

Gibt es einen besseren Weg?

Sylvain Reverdy
quelle
4
Der Vollständigkeit halber können Sie auch das "+ =" aus Ihrem ursprünglichen Code entfernen, sodass der foreach nur Name: auswählt $results = @($objects | %{ $_.Name }). Dies kann manchmal bequemer über die Befehlszeile eingegeben werden, obwohl ich denke, dass Scotts Antwort im Allgemeinen besser ist.
Kaiser XLII
1
@ EmperorXLII: Guter Punkt, und in PSv3 + können Sie sogar vereinfachen, um:$objects | % Name
mklement0

Antworten:

211

Ich denke, Sie können möglicherweise den ExpandPropertyParameter von verwenden Select-Object.

Um beispielsweise die Liste des aktuellen Verzeichnisses abzurufen und nur die Eigenschaft Name anzuzeigen, gehen Sie wie folgt vor:

ls | select -Property Name

Dies gibt weiterhin DirectoryInfo- oder FileInfo-Objekte zurück. Sie können den Typ, der durch die Pipeline kommt, jederzeit überprüfen, indem Sie ihn an Get-Member (Alias gm) weiterleiten .

ls | select -Property Name | gm

Um das Objekt so zu erweitern, dass es dem Objekttyp entspricht, den Sie betrachten, können Sie Folgendes tun:

ls | select -ExpandProperty Name

In Ihrem Fall können Sie einfach Folgendes tun, damit eine Variable ein Array von Zeichenfolgen ist, wobei die Zeichenfolgen die Eigenschaft Name sind:

$objects = ls | select -ExpandProperty Name
Scott Saad
quelle
73

Als noch einfachere Lösung können Sie einfach Folgendes verwenden:

$results = $objects.Name

Welches sollte $resultsmit einem Array aller 'Name' Eigenschaftswerte der Elemente in füllen $objects.

rageandqq
quelle
Beachten Sie, dass dies in nicht funktioniert Exchange Management Shell. Wenn wir Exchange verwenden, müssen wir$objects | select -Property Propname, OtherPropname
Bassie
2
@Bassie: Der Zugriff auf eine Eigenschaft auf Sammlungsebene, um die Werte ihrer Mitglieder als Array abzurufen, wird als Mitgliederaufzählung bezeichnet und ist eine PSv3 + -Funktion . Vermutlich ist Ihre Exchange-Verwaltungsshell PSv2.
mklement0
32

Ergänzend zu den bereits vorhandenen, hilfreichen Antworten mit Anleitungen, wann welcher Ansatz zu verwenden ist, und einem Leistungsvergleich .

  • DraußenVerwenden Sie einer Pipeline (PSv3 +):

    $ Objekte .Name
    wie in der Antwort von rageandqq gezeigt , die sowohl syntaktisch einfacher als auch syntaktisch einfacher ist viel schneller ist .

    • Der Zugriff auf eine Eigenschaft auf Sammlungsebene , um die Werte ihrer Mitglieder als Array abzurufen, wird als Mitgliederaufzählung bezeichnet abzurufen, und ist eine PSv3 + -Funktion.
    • Alternativ können Sie in PSv2 dieforeach Anweisung verwenden , deren Ausgabe Sie auch direkt einer Variablen zuweisen können:
      $ results = foreach ($ obj in $ Objekten) {$ obj.Name}
    • Kompromisse :
      • Sowohl die Eingabesammlung als auch das Ausgabearray müssen als Ganzes in den Speicher passen .
      • Wenn die Eingabesammlung selbst das Ergebnis eines Befehls (einer Pipeline) ist (z. B. (Get-ChildItem).Name), muss dieser Befehl zuerst vollständig ausgeführt werden, bevor auf die Elemente des resultierenden Arrays zugegriffen werden kann.
  • Verwenden Sie in einer Pipeline, in der das Ergebnis weiterverarbeitet werden muss oder die Ergebnisse nicht in den gesamten Speicher passen, Folgendes:

    $ Objekte | Select-Object -ExpandProperty Name

    • Die Notwendigkeit -ExpandPropertywird in der Antwort von Scott Saad erläutert .
    • Sie erhalten die üblichen Pipeline-Vorteile einer Einzelverarbeitung, die in der Regel sofort eine Ausgabe erzeugt und die Speichernutzung konstant hält (es sei denn, Sie erfassen die Ergebnisse letztendlich ohnehin im Speicher).
    • Kompromiss :
      • Die Nutzung der Pipeline ist vergleichsweise langsam .

Bei kleinen Eingabesammlungen (Arrays) werden Sie den Unterschied wahrscheinlich nicht bemerken , und insbesondere in der Befehlszeile ist es manchmal wichtiger, den Befehl einfach eingeben zu können.


Hier ist eine einfach zu tippende Alternative , die jedoch der langsamste Ansatz ist . Es verwendet eine vereinfachte ForEach-ObjectSyntax, die als Operationsanweisung bezeichnet wird (wieder PSv3 +) :; Die folgende PSv3 + -Lösung lässt sich einfach an einen vorhandenen Befehl anhängen:

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

Der Vollständigkeit halber: Die wenig bekannte PSv4 + -Array- .ForEach() Methode , die in diesem Artikel ausführlicher behandelt wird , ist eine weitere Alternative :

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

# By script block (more flexibility; like ForEach-Object)
$objects.ForEach({ $_.Name })
  • Dieser Ansatz ähnelt der Mitgliederaufzählung mit denselben Kompromissen, außer dass die Pipeline-Logik nicht angewendet wird. es ist geringfügig langsamer , aber immer noch merklich schneller als die Pipeline.

  • Zum Extrahieren eines einzelnen Eigenschaftswerts nach Namen ( Zeichenfolgenargument ) entspricht diese Lösung der Elementaufzählung (obwohl letztere syntaktisch einfacher ist).

  • Die Skript-Block - Variante ermöglicht beliebige Transformationen ; Es ist eine schnellere Alternative zum Pipeline-basierten ForEach-Object Cmdlet ( %) .


Vergleich der Leistung der verschiedenen Ansätze

Hier finden Sie Beispiel-Timings für die verschiedenen Ansätze, basierend auf einer Eingabesammlung von 10,000Objekten , gemittelt über 10 Läufe. Die absoluten Zahlen sind nicht wichtig und variieren aufgrund vieler Faktoren. Sie sollten jedoch einen Eindruck von der relativen Leistung vermitteln (die Zeitangaben stammen von einer Single-Core-Windows 10-VM:

Wichtig

  • Die relative Leistung hängt davon ab, ob es sich bei den Eingabeobjekten um Instanzen regulärer .NET-Typen (z. B. als Ausgabe von Get-ChildItem) oder um [pscustomobject]Instanzen (z. B. als Ausgabe von Convert-FromCsv) handelt.
    Der Grund dafür ist, dass [pscustomobject]Eigenschaften von PowerShell dynamisch verwaltet werden und schneller auf sie zugreifen können als die regulären Eigenschaften eines (statisch definierten) regulären .NET-Typs. Beide Szenarien werden im Folgenden behandelt.

  • Bei den Tests werden bereits im Speicher befindliche Sammlungen als Eingabe verwendet, um sich auf die reine Eigenschaftsextraktionsleistung zu konzentrieren. Mit einem Streaming-Cmdlet / Funktionsaufruf als Eingabe sind Leistungsunterschiede im Allgemeinen viel weniger ausgeprägt, da die in diesem Aufruf verbrachte Zeit den größten Teil der aufgewendeten Zeit ausmachen kann.

  • Der Kürze halber %wird für das ForEach-ObjectCmdlet ein Alias verwendet .

Allgemeine Schlussfolgerungen , die sowohl für den regulären .NET-Typ als auch für die [pscustomobject]Eingabe gelten:

  • Die Mitgliederaufzählung ( $collection.Name) und die foreach ($obj in $collection)Lösungen sind bei weitem die schnellsten , um den Faktor 10 oder mehr schneller als die schnellste Pipeline-basierte Lösung.

  • Überraschenderweise ist % Namedie Leistung viel schlechter als % { $_.Name }- siehe dieses GitHub-Problem .

  • PowerShell Core übertrifft hier Windows Powershell durchweg.

Timings mit regulären .NET-Typen :

  • PowerShell Core v7.0.0-Vorschau.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

Schlussfolgerungen:

  • In Powershell - Core , .ForEach('Name')deutlich überlegen .ForEach({ $_.Name }). In Windows PowerShell ist letzteres seltsamerweise schneller, wenn auch nur am Rande.

Timings mit [pscustomobject]Instanzen :

  • PowerShell Core v7.0.0-Vorschau.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

Schlussfolgerungen:

  • Beachten Sie, wie die [pscustomobject]Eingabe die .ForEach('Name')auf Skriptblöcken basierende Variante bei weitem übertrifft .ForEach({ $_.Name }).

  • In ähnlicher Weise beschleunigt die [pscustomobject]Eingabe die Pipeline-Basis Select-Object -ExpandProperty Namein Windows PowerShell praktisch auf dem Niveau von .ForEach({ $_.Name }), aber in PowerShell Core immer noch etwa 50% langsamer.

  • Kurz gesagt: Mit der ungeraden Ausnahme % Name, mit [pscustomobject]den String-basierten Methoden zur Referenzierung der Eigenschaften übertreffen die Skript-basiert ist.


Quellcode für die Tests :

Hinweis:

  • Download-Funktion Time-Commandvon diesem Gist , um diese Tests auszuführen.

  • Stellen Sie $useCustomObjectInputzu $truemit messen , [pscustomobject]anstatt Instanzen.

$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
quelle
1

Achtung, die Mitgliederaufzählung funktioniert nur, wenn die Sammlung selbst kein gleichnamiges Mitglied hat. Wenn Sie also ein Array von FileInfo-Objekten hatten, konnten Sie mit nicht ein Array von Dateilängen abrufen

 $files.length # evaluates to array length

Und bevor Sie "gut offensichtlich" sagen, denken Sie darüber nach. Wenn Sie ein Array von Objekten mit einer Kapazitätseigenschaft hatten, dann

 $objarr.capacity

würde gut funktionieren, WENN $ objarr eigentlich kein [Array], sondern zum Beispiel eine [ArrayList] wäre. Also vor der Verwendung der Mitgliederaufzählung Sie möglicherweise in die Blackbox schauen, die Ihre Sammlung enthält.

(Hinweis für Moderatoren: Dies sollte ein Kommentar zur Antwort von rageandqq sein, aber ich habe noch nicht genug Ruf.)

Uber Kluger
quelle
Es ist ein guter Punkt; Diese GitHub-Funktionsanforderung fordert eine separate Syntax für die Mitgliederaufzählung an. Die Problemumgehung für Namenskollisionen besteht darin, die .ForEach()Array-Methode wie folgt zu verwenden:$files.ForEach('Length')
mklement0