Selezionare i valori di una proprietà su tutti gli oggetti di un array in PowerShell


135

Diciamo che abbiamo una matrice di oggetti $ objects. Supponiamo che questi oggetti abbiano una proprietà "Nome".

Questo è quello che voglio fare

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

Funziona, ma può essere fatto in un modo migliore?

Se faccio qualcosa del genere:

 $results = objects | select Name

$resultsè una matrice di oggetti con una proprietà Name. Voglio che $ results contenga una matrice di nomi.

C'è un modo migliore?


4
Solo per completezza, si potrebbe anche togliere il "+ =" dal codice originale, in modo che il foreach seleziona solo Nome: $results = @($objects | %{ $_.Name }). Questo può essere più conveniente da digitare alla riga di comando a volte, anche se penso che la risposta di Scott sia generalmente migliore.
Imperatore XLII

1
@EmperorXLII: buon punto, e in PSv3 + puoi anche semplificare:$objects | % Name
mklement0

Risposte:


212

Penso che potresti essere in grado di utilizzare il ExpandPropertyparametro di Select-Object.

Ad esempio, per ottenere l'elenco della directory corrente e visualizzare solo la proprietà Name, si dovrebbe fare quanto segue:

ls | select -Property Name

Restituisce ancora oggetti DirectoryInfo o FileInfo. È sempre possibile controllare il tipo proveniente dalla pipeline effettuando il piping su Get-Member (alias gm).

ls | select -Property Name | gm

Quindi, per espandere l'oggetto in modo che sia quello del tipo di proprietà che stai guardando, puoi fare quanto segue:

ls | select -ExpandProperty Name

Nel tuo caso, puoi semplicemente fare quanto segue per far sì che una variabile sia una matrice di stringhe, dove le stringhe sono la proprietà Name:

$objects = ls | select -ExpandProperty Name

73

Come soluzione ancora più semplice, potresti semplicemente usare:

$results = $objects.Name

Che dovrebbe riempire $resultscon una matrice di tutti i valori delle proprietà 'Nome' degli elementi in $objects.


Nota che non funziona Exchange Management Shell. Quando si utilizza Exchange abbiamo bisogno di usare$objects | select -Property Propname, OtherPropname
Bassie

2
@Bassie: l'accesso a una proprietà a livello di raccolta per ottenere i valori dei suoi membri come un array è chiamato enumerazione dei membri ed è una funzione PSv3 + ; presumibilmente, Exchange Management Shell è PSv2.
mklement0

32

Per integrare le risposte preesistenti e utili con una guida su quando utilizzare quale approccio e un confronto delle prestazioni .

  • Al di fuori di una pipeline, utilizzare (PSv3 +):

    $ oggetti . Nome
    come dimostrato nella risposta di rageandqq , che è sia sintatticamente più semplice che molto più veloce .

    • L'accesso a una proprietà a livello di raccolta per ottenere i valori dei suoi membri come array è chiamato enumerazione dei membri ed è una funzionalità di PSv3 +.
    • In alternativa, in PSv2 , usa l' foreach istruzione , il cui output puoi anche assegnare direttamente a una variabile:
      $ results = foreach ($ obj in $ oggetti) {$ obj.Name}
    • Svantaggi :
      • Sia la raccolta di input che l'array di output devono rientrare nella memoria nel suo insieme .
      • Se la raccolta di input è essa stessa il risultato di un comando (pipeline) (ad esempio, (Get-ChildItem).Name), tale comando deve prima essere eseguito fino al completamento prima di poter accedere agli elementi dell'array risultante.
  • In una pipeline in cui il risultato deve essere ulteriormente elaborato o i risultati non rientrano nella memoria nel suo insieme, utilizzare:

    $ oggetti | Select-Object -ExpandProperty Name

    • La necessità -ExpandPropertyè spiegata nella risposta di Scott Saad .
    • Ottieni i soliti vantaggi della pipeline dell'elaborazione uno a uno, che in genere produce immediatamente l'output e mantiene costante l'uso della memoria (a meno che alla fine non raccolga comunque i risultati in memoria).
    • Scambio :
      • L'uso della pipeline è relativamente lento .

Per raccolte di input di piccole dimensioni (array), probabilmente non noterai la differenza e, soprattutto sulla riga di comando, talvolta è più importante poter digitare facilmente il comando.


Ecco un'alternativa facile da scrivere , che tuttavia è l' approccio più lento ; utilizza una ForEach-Objectsintassi semplificata chiamata istruzione di operazione (di nuovo, PSv3 +):; ad esempio, la seguente soluzione PSv3 + è facile da aggiungere a un comando esistente:

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

Per completezza: il poco noto metodo array PSv4 +.ForEach() , più comprensibile discusso in questo articolo , è ancora un'altra alternativa :

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

# By script block (more flexibility; like ForEach-Object)
$objects.ForEach({ $_.Name })
  • Questo approccio è simile all'enumerazione dei membri , con gli stessi compromessi, tranne per il fatto che la logica della pipeline non viene applicata; è leggermente più lento , sebbene notevolmente più veloce della conduttura.

  • Per estrarre un singolo valore di proprietà per nome ( argomento stringa ), questa soluzione è alla pari con l'enumerazione dei membri (sebbene quest'ultimo sia sintatticamente più semplice).

  • La variante a blocchi di script consente trasformazioni arbitrarie ; è un'alternativa più veloce, all-in-memory-at-once, al ForEach-Object cmdlet basato sulla pipeline ( %) .


Confronto delle prestazioni dei vari approcci

Ecco i tempi di campionamento per i vari approcci, basati su una raccolta di input di 10,000oggetti , calcolati in media su 10 esecuzioni; i numeri assoluti non sono importanti e variano in base a molti fattori, ma dovrebbero darti un senso di prestazioni relative (i tempi provengono da una VM Windows 10 single-core:

Importante

  • Le prestazioni relative variano in base al fatto che gli oggetti di input siano istanze di normali tipi .NET (ad es. Come output di Get-ChildItem) o [pscustomobject]istanze (ad es. Come output di Convert-FromCsv).
    Il motivo è che le [pscustomobject]proprietà sono gestite in modo dinamico da PowerShell e possono accedervi più rapidamente rispetto alle normali proprietà di un tipo .NET (definito staticamente) normale. Entrambi gli scenari sono trattati di seguito.

  • I test utilizzano come input raccolte già nella memoria piena, in modo da concentrarsi sulle prestazioni di estrazione delle proprietà pure. Con un cmdlet di streaming / chiamata di funzione come input, le differenze di prestazioni saranno generalmente molto meno pronunciate, poiché il tempo trascorso all'interno di quella chiamata può rappresentare la maggior parte del tempo trascorso.

  • Per brevità, alias %viene utilizzato per il ForEach-Objectcmdlet.

Conclusioni generali , applicabili sia al tipo .NET normale che [pscustomobject]all'input:

  • L'enumerazione dei membri ( $collection.Name) e le foreach ($obj in $collection)soluzioni sono di gran lunga le più veloci , di un fattore 10 o più veloci della soluzione basata sulla pipeline più veloce.

  • Sorprendentemente, si % Namecomporta molto peggio di % { $_.Name }- vedi questo problema GitHub .

  • PowerShell Core supera costantemente Windows Powershell qui.

Tempi con normali tipi .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

conclusioni:

  • In PowerShell Core , .ForEach('Name')chiaramente supera .ForEach({ $_.Name }). In Windows PowerShell, curiosamente, quest'ultimo è più veloce, anche se solo marginalmente.

Tempi con [pscustomobject]istanze :

  • 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

conclusioni:

  • Si noti come, con [pscustomobject]ingresso .ForEach('Name')dal Sorpassa gran lunga lo script blocco variante basata, .ForEach({ $_.Name }).

  • Allo stesso modo, l' [pscustomobject]input rende la pipeline Select-Object -ExpandProperty Namepiù veloce, in Windows PowerShell praticamente alla pari .ForEach({ $_.Name }), ma in PowerShell Core è ancora più lento di circa il 50%.

  • In breve: con la strana eccezione di % Name, con [pscustomobject]i metodi basati su stringhe di riferimento delle proprietà superano quelli basati su scriptblock.


Codice sorgente per i test :

Nota:

  • Scarica la funzione Time-Commandda questo Gist per eseguire questi test.

  • Impostare invece $useCustomObjectInputper $truemisurare con [pscustomobject]istanze.

$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

Attenzione, l' enumerazione dei membri funziona solo se la raccolta stessa non ha membri con lo stesso nome. Quindi se avessi una matrice di oggetti FileInfo, non potresti ottenere una matrice di lunghezze di file usando

 $files.length # evaluates to array length

E prima di dire "bene ovviamente", considera questo. Se allora avessi una matrice di oggetti con una proprietà di capacità

 $objarr.capacity

avrebbe funzionato bene MENO $ objarr erano in realtà non è un [Array], ma, per esempio, un [ArrayList]. Quindi prima di usare l' enumerazione dei membri potresti dover guardare all'interno della scatola nera che contiene la tua raccolta.

(Nota per i moderatori: questo dovrebbe essere un commento sulla risposta di rageandqq ma non ho ancora abbastanza reputazione.)


È un buon punto; questa richiesta di funzionalità GitHub richiede una sintassi separata per l'enumerazione dei membri. La soluzione alternativa per le collisioni di nomi consiste nell'utilizzare il .ForEach()metodo array come segue:$files.ForEach('Length')
mklement0
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.