Wybierz wartości jednej właściwości we wszystkich obiektach tablicy w programie PowerShell

139

Powiedzmy, że mamy tablicę obiektów $ objects. Powiedzmy, że te obiekty mają właściwość „Nazwa”.

To jest to, co chcę zrobić

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

To działa, ale czy można to zrobić w lepszy sposób?

Jeśli zrobię coś takiego:

 $results = objects | select Name

$resultsjest tablicą obiektów posiadających właściwość Name. Chcę, aby $ results zawierała tablicę nazw.

Czy jest lepszy sposób?

Sylvain Reverdy
źródło
4
Tylko dla kompletności, można również usunąć „+ =” z oryginalnego kodu, tak, że tylko foreach wybiera Nazwa: $results = @($objects | %{ $_.Name }). Czasami wygodniej jest pisać w wierszu poleceń, chociaż myślę, że odpowiedź Scotta jest generalnie lepsza.
Emperor XLII
1
@EmperorXLII: Słuszna uwaga, aw PSv3 + możesz nawet uprościć do:$objects | % Name
mklement0

Odpowiedzi:

217

Myślę, że możesz użyć ExpandPropertyparametru Select-Object.

Na przykład, aby uzyskać listę bieżącego katalogu i po prostu wyświetlić właściwość Name, należy wykonać następujące czynności:

ls | select -Property Name

Nadal zwraca obiekty DirectoryInfo lub FileInfo. Zawsze możesz sprawdzić typ przechodzący przez potok, przesyłając potok do Get-Member (alias gm).

ls | select -Property Name | gm

Aby więc rozwinąć obiekt tak, aby odpowiadał typowi właściwości, na którą patrzysz, możesz wykonać następujące czynności:

ls | select -ExpandProperty Name

W twoim przypadku możesz po prostu wykonać następujące czynności, aby zmienna była tablicą ciągów, gdzie ciągi są właściwością Nazwa:

$objects = ls | select -ExpandProperty Name
Scott Saad
źródło
75

Jeszcze łatwiejszym rozwiązaniem jest użycie:

$results = $objects.Name

Który powinien wypełnić $resultstablicę wszystkich wartości właściwości „Nazwa” elementów w $objects.

rageandqq
źródło
1
Zauważ, że to nie działa w Exchange Management Shell. Korzystając z Exchange, musimy użyć$objects | select -Property Propname, OtherPropname
Bassie
2
@Bassie: Dostęp do właściwości na poziomie kolekcji w celu pobrania wartości jej elementów w postaci tablicy nazywa się wyliczaniem elementów członkowskich i jest funkcją PSv3 + ; przypuszczalnie twoja Exchange Management Shell to PSv2.
mklement0
36

Uzupełnienie istniejących, pomocnych odpowiedzi wraz ze wskazówkami, kiedy zastosować które podejście, oraz porównaniem wyników .

  • Poza rurociągiem użyj (PSv3 +):

    $ obiektów . Nazwa
    jak pokazano w odpowiedzi rageandqq , która jest zarówno prostsza składniowo, jak i znacznie szybsza .

    • Uzyskiwanie dostępu do właściwości na poziomie kolekcji w celu uzyskania wartości jej elementów członkowskich jako tablicy nazywa się wyliczaniem elementów członkowskich i jest funkcją PSv3 +.
    • Alternatywnie w PSv2 użyj foreach instrukcji , której wyjście możesz również przypisać bezpośrednio do zmiennej:
      $ results = foreach ($ obj in $ objects) {$ obj.Name}
    • Kompromisy :
      • Zarówno kolekcja wejściowa, jak i tablica wyjściowa muszą pasować do pamięci jako całości .
      • Jeśli kolekcja danych wejściowych jest sama w sobie wynikiem polecenia (potoku) (np. (Get-ChildItem).Name), To polecenie musi najpierw zostać wykonane do końca, zanim będzie można uzyskać dostęp do elementów wynikowej tablicy.
  • W potoku, w którym wynik musi być dalej przetwarzany lub wyniki nie mieszczą się w całości w pamięci, użyj:

    $ obiektów | Select-Object -ExpandProperty Name

    • Potrzeba -ExpandPropertyjest wyjaśniona w odpowiedzi Scotta Saada .
    • Otrzymujesz zwykłe zalety potoku przetwarzania jeden po drugim, które zwykle generuje dane wyjściowe od razu i utrzymuje stałe użycie pamięci (chyba że ostatecznie i tak zbierzesz wyniki w pamięci).
    • Kompromis :
      • Wykorzystanie rurociągu jest stosunkowo powolne .

W przypadku małych kolekcji danych wejściowych (tablic) prawdopodobnie nie zauważysz różnicy , a zwłaszcza w wierszu poleceń, czasami łatwiejsze wpisanie polecenia jest ważniejsze.


Oto łatwa do wpisania alternatywa , która jest jednak najwolniejszym podejściem ; używa uproszczonej ForEach-Objectskładni zwanej instrukcją operacji (znowu PSv3 +):; np. następujące rozwiązanie PSv3 + można łatwo dodać do istniejącego polecenia:

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

Gwoli ścisłości.ForEach() : Inną alternatywą jest mało znana metoda tablicowa PSv4 + , bardziej wszechstronna omówiona w tym artykule :

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

# By script block (more flexibility; like ForEach-Object)
$objects.ForEach({ $_.Name })
  • To podejście jest podobne do wyliczania elementów członkowskich , z tymi samymi kompromisami, z wyjątkiem tego, że logika potoków nie jest stosowana; jest nieznacznie wolniejszy , choć nadal zauważalnie szybszy niż rurociąg.

  • W przypadku wyodrębniania pojedynczej wartości właściwości według nazwy ( argument ciągu ) to rozwiązanie jest równe wyliczaniu elementów członkowskich (chociaż to drugie jest prostsze składniowo).

  • Skrypt-block wariant umożliwia arbitralne transformacji ; jest to szybsza alternatywa dla polecenia ForEach-Object cmdlet ( %) opartego na potokach .


Porównanie wydajności różnych podejść

Oto przykładowe czasy dla różnych podejść, oparte na wejściowej kolekcji 10,000obiektów , uśrednione z 10 przebiegów; liczby bezwzględne nie są ważne i różnią się w zależności od wielu czynników, ale powinny dać poczucie względnej wydajności (czasy pochodzą z jednordzeniowej maszyny wirtualnej z systemem Windows 10:

Ważny

  • Względna wydajność różni się w zależności od tego, czy obiekty wejściowe są instancjami zwykłych typów .NET (np. Jako dane wyjściowe przez Get-ChildItem), czy [pscustomobject]instancjami (np. Jako dane wyjściowe przez Convert-FromCsv).
    Powodem jest to, że [pscustomobject]właściwości są dynamicznie zarządzane przez PowerShell i może uzyskać do nich dostęp szybciej niż zwykłe właściwości (zdefiniowanego statycznie) zwykłego typu .NET. Oba scenariusze omówiono poniżej.

  • Testy używają kolekcji już znajdujących się w pamięci jako danych wejściowych, aby skupić się na wydajności wyodrębniania czystej właściwości. W przypadku przesyłania strumieniowego polecenia cmdlet / wywołania funkcji jako danych wejściowych różnice w wydajności będą na ogół znacznie mniej wyraźne, ponieważ czas spędzony wewnątrz tego wywołania może stanowić większość spędzonego czasu.

  • Dla zwięzłości alias %jest używany w poleceniu ForEach-Objectcmdlet.

Ogólne wnioski , dotyczące zarówno zwykłego typu .NET, jak i [pscustomobject]danych wejściowych:

  • Wyliczanie elementów członkowskich ( $collection.Name) i foreach ($obj in $collection)rozwiązania są zdecydowanie najszybsze , o współczynnik 10 lub więcej szybciej niż najszybsze rozwiązanie oparte na potokach.

  • O dziwo, % Namedziała znacznie gorzej niż % { $_.Name }- zobacz ten numer GitHub .

  • PowerShell Core konsekwentnie przewyższa tutaj Windows Powershell.

Czasy ze zwykłymi typami .NET :

  • PowerShell Core v7.0.0 - wersja zapoznawcza 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 w wersji 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

Wnioski:

  • W PowerShell Rdzenia , .ForEach('Name')wyraźnie przewyższa .ForEach({ $_.Name }). Co ciekawe, w programie Windows PowerShell ten drugi jest szybszy, choć tylko nieznacznie.

Czasy z [pscustomobject]instancjami :

  • PowerShell Core v7.0.0 - wersja zapoznawcza 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 w wersji 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

Wnioski:

  • Uwaga jak z [pscustomobject]wejściem .ForEach('Name')zdecydowanie przewyższa skrypt blok oparty wariantu .ForEach({ $_.Name }).

  • Podobnie [pscustomobject]dane wejściowe sprawiają , że proces oparty na potoku jest Select-Object -ExpandProperty Nameszybszy, w programie Windows PowerShell praktycznie na równi z .ForEach({ $_.Name }), ale w programie PowerShell Core nadal o około 50% wolniejszy.

  • W skrócie: z dziwnym wyjątkiem % Name, [pscustomobject]metody odwoływania się do właściwości oparte na ciągach znaków przewyższają te oparte na skryptach.


Kod źródłowy do testów :

Uwaga:

  • Pobierz funkcję Time-Commandz tego streszczenia, aby uruchomić te testy.

  • Zamiast tego ustaw $useCustomObjectInputna $truepomiar z [pscustomobject]instancjami.

$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
źródło
Zdecydowanie jedna z najlepszych reakcji, jakie widziałem w przypadku przepełnienia stosu ... Dobra robota!
Jean-Philippe Martin
1
Doceniam miłe opinie, @ Jean-PhilippeMartin.
mklement0
1

Uwaga, wyliczanie elementów członkowskich działa tylko wtedy, gdy sama kolekcja nie ma elementu członkowskiego o tej samej nazwie. Więc jeśli masz tablicę obiektów FileInfo, nie możesz uzyskać tablicy długości plików przy użyciu

 $files.length # evaluates to array length

Zanim powiesz „oczywiście”, zastanów się nad tym. Gdybyś miał wtedy tablicę obiektów z właściwością capacity

 $objarr.capacity

działałoby dobrze, chyba że $ objarr faktycznie nie było [Array], ale na przykład [ArrayList]. Dlatego przed użyciem wyliczania członków może być konieczne zajrzenie do czarnego pola zawierającego Twoją kolekcję.

(Uwaga dla moderatorów: to powinien być komentarz do odpowiedzi rageandqq, ale nie mam jeszcze wystarczającej reputacji).

Uber Kluger
źródło
To dobra uwaga; to żądanie funkcji GitHub wymaga oddzielnej składni do wyliczania elementów członkowskich. Obejściem kolizji nazw jest użycie .ForEach()metody tablicowej w następujący sposób:$files.ForEach('Length')
mklement0