Sélectionnez les valeurs d'une propriété sur tous les objets d'un tableau dans PowerShell

135

Disons que nous avons un tableau d'objets $ objets. Disons que ces objets ont une propriété "Name".

C'est ce que je veux faire

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

Cela fonctionne, mais cela peut-il être fait d'une meilleure manière?

Si je fais quelque chose comme:

 $results = objects | select Name

$resultsest un tableau d'objets ayant une propriété Name. Je veux que $ results contienne un tableau de noms.

Y a-t-il un meilleur moyen?

Sylvain Reverdy
la source
4
Juste pour être complet, vous pouvez également supprimer le « + = » à partir de votre code d' origine, de sorte que le foreach sélectionne uniquement le nom: $results = @($objects | %{ $_.Name }). Cela peut parfois être plus pratique à taper sur la ligne de commande, même si je pense que la réponse de Scott est généralement meilleure.
Empereur XLII
1
@EmperorXLII: Bon point, et dans PSv3 + vous pouvez même simplifier à:$objects | % Name
mklement0

Réponses:

212

Je pense que vous pourrez peut-être utiliser le ExpandPropertyparamètre de Select-Object.

Par exemple, pour obtenir la liste du répertoire actuel et simplement afficher la propriété Name, on ferait ce qui suit:

ls | select -Property Name

Cela renvoie toujours des objets DirectoryInfo ou FileInfo. Vous pouvez toujours inspecter le type passant par le pipeline en redirigeant vers Get-Member (alias gm).

ls | select -Property Name | gm

Ainsi, pour développer l'objet afin qu'il soit celui du type de propriété que vous regardez, vous pouvez effectuer les opérations suivantes:

ls | select -ExpandProperty Name

Dans votre cas, vous pouvez simplement faire ce qui suit pour qu'une variable soit un tableau de chaînes, où les chaînes sont la propriété Name:

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

Comme solution encore plus simple, vous pouvez simplement utiliser:

$results = $objects.Name

Qui devrait remplir $resultsavec un tableau de toutes les valeurs de propriété 'Name' des éléments dans $objects.

rageandqq
la source
Notez que cela ne fonctionne pas dans Exchange Management Shell. Lors de l'utilisation d'Exchange, nous devons utiliser$objects | select -Property Propname, OtherPropname
Bassie
2
@Bassie: L'accès à une propriété au niveau de la collection pour obtenir les valeurs de ses membres sous forme de tableau est appelé énumération des membres et est une fonctionnalité PSv3 + ; vraisemblablement, votre Exchange Management Shell est PSv2.
mklement0
32

Pour compléter les réponses préexistantes et utiles avec des conseils sur le moment d'utiliser quelle approche et une comparaison des performances .

  • En dehors d'un pipeline, utilisez (PSv3 +):

    $ objets . Nom
    comme démontré dans la réponse de rageandqq , qui est à la fois syntaxiquement plus simple et beaucoup plus rapide .

    • L'accès à une propriété au niveau de la collection pour obtenir les valeurs de ses membres sous forme de tableau est appelé énumération des membres et est une fonctionnalité PSv3 +.
    • Sinon, dans PSv2 , utilisez l' foreach instruction , dont vous pouvez également affecter la sortie directement à une variable:
      $ results = foreach ($ obj dans $ objets) {$ obj.Name}
    • Compromis :
      • La collection d'entrée et le tableau de sortie doivent tous deux tenir dans la mémoire dans son ensemble .
      • Si la collection d'entrée est elle-même le résultat d'une commande (pipeline) (par exemple, (Get-ChildItem).Name), cette commande doit d'abord s'exécuter jusqu'à la fin avant que les éléments du tableau résultant ne soient accessibles.
  • Dans un pipeline où le résultat doit être traité plus avant ou où les résultats ne rentrent pas dans la mémoire dans son ensemble, utilisez:

    $ objets | Select-Object -ExpandProperty Name

    • Le besoin -ExpandPropertyest expliqué dans la réponse de Scott Saad .
    • Vous bénéficiez des avantages habituels du pipeline du traitement un par un, qui produit généralement une sortie immédiatement et maintient l'utilisation de la mémoire constante (à moins que vous ne collectiez les résultats en mémoire de toute façon).
    • Compromis :
      • L'utilisation du pipeline est relativement lente .

Pour les petites collections d'entrée (tableaux), vous ne remarquerez probablement pas la différence et, en particulier sur la ligne de commande, il est parfois plus important de pouvoir taper la commande facilement.


Voici une alternative facile à taper , qui est cependant l' approche la plus lente ; il utilise une ForEach-Objectsyntaxe simplifiée appelée instruction d'opération (encore une fois, PSv3 +):; Par exemple, la solution PSv3 + suivante est facile à ajouter à une commande existante:

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

Par souci d'exhaustivité: la méthode de tableau PSv4 +.ForEach() peu connue , plus complète discutée dans cet article , est encore une autre alternative :

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

# By script block (more flexibility; like ForEach-Object)
$objects.ForEach({ $_.Name })
  • Cette approche est similaire à l'énumération des membres , avec les mêmes compromis, sauf que la logique de pipeline n'est pas appliquée; il est légèrement plus lent , mais toujours sensiblement plus rapide que le pipeline.

  • Pour extraire une seule valeur de propriété par nom ( argument de chaîne ), cette solution est comparable à l'énumération des membres (bien que cette dernière soit syntaxiquement plus simple).

  • La variante de bloc de script permet des transformations arbitraires ; c'est une alternative plus rapide - tout en mémoire à la fois - à la ForEach-Object cmdlet basée sur un pipeline ( %) .


Comparer les performances des différentes approches

Voici des exemples de minutages pour les différentes approches, basés sur une collection d' 10,000objets d' entrée , moyennés sur 10 exécutions; les nombres absolus ne sont pas importants et varient en fonction de nombreux facteurs, mais cela devrait vous donner une idée des performances relatives (les horaires proviennent d'une machine virtuelle Windows 10 à un seul cœur:

Important

  • Les performances relatives varient selon que les objets d'entrée sont des instances de types .NET normaux (par exemple, en sortie par Get-ChildItem) ou des [pscustomobject]instances (par exemple, en sortie par Convert-FromCsv).
    La raison est que les [pscustomobject]propriétés sont gérées dynamiquement par PowerShell et qu'il peut y accéder plus rapidement que les propriétés normales d'un type .NET régulier (défini de manière statique). Les deux scénarios sont traités ci-dessous.

  • Les tests utilisent des collections déjà en mémoire dans leur intégralité comme entrée, afin de se concentrer sur les performances d'extraction de propriété pure. Avec un appel d'applet de commande / de fonction en continu comme entrée, les différences de performances seront généralement beaucoup moins prononcées, car le temps passé à l'intérieur de cet appel peut représenter la majorité du temps passé.

  • Par souci de concision, l'alias %est utilisé pour l' ForEach-Objectapplet de commande.

Conclusions générales , applicables à la fois au type et à l' [pscustomobject]entrée .NET réguliers :

  • L'énumération des membres ( $collection.Name) et les foreach ($obj in $collection)solutions sont de loin les plus rapides , d'un facteur 10 ou plus que la solution basée sur le pipeline la plus rapide.

  • Étonnamment, les % Nameperformances sont bien pires que % { $_.Name }- voyez ce problème GitHub .

  • PowerShell Core surpasse constamment Windows Powershell ici.

Timings avec les types .NET normaux :

  • 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

Conclusions:

  • Dans PowerShell Core , .ForEach('Name')surpasse clairement .ForEach({ $_.Name }). Dans Windows PowerShell, curieusement, ce dernier est plus rapide, bien que marginalement.

Timings avec [pscustomobject]instances :

  • 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

Conclusions:

  • Notez comment avec [pscustomobject]entrée .ForEach('Name')par la variante surpasse largement basée sur un script bloc, .ForEach({ $_.Name }).

  • De même, l' [pscustomobject]entrée rend le pipeline Select-Object -ExpandProperty Nameplus rapide, dans Windows PowerShell pratiquement au même niveau que .ForEach({ $_.Name }), mais dans PowerShell Core encore environ 50% plus lent.

  • En bref: à l'exception étrange de % Name, avec [pscustomobject]les méthodes de référencement basées sur des chaînes, les propriétés surpassent celles basées sur des blocs de script.


Code source pour les tests :

Remarque:

  • Téléchargez la fonction à Time-Commandpartir de ce Gist pour exécuter ces tests.

  • Définissez plutôt $useCustomObjectInputsur $truepour mesurer avec des [pscustomobject]instances.

$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
la source
1

Attention, l' énumération des membres ne fonctionne que si la collection elle-même n'a aucun membre du même nom. Donc, si vous aviez un tableau d'objets FileInfo, vous ne pouviez pas obtenir un tableau de longueurs de fichier en utilisant

 $files.length # evaluates to array length

Et avant de dire «bien évidemment», considérez ceci. Si vous aviez un tableau d'objets avec une propriété de capacité, alors

 $objarr.capacity

fonctionnerait bien A MOINS QUE $ objarr ne soit en fait pas un [Array] mais, par exemple, un [ArrayList]. Donc, avant d'utiliser l' énumération des membres, vous devrez peut-être regarder à l'intérieur de la boîte noire contenant votre collection.

(Note aux modérateurs: cela devrait être un commentaire sur la réponse de rageandqq mais je n'ai pas encore assez de réputation.)

Uber Kluger
la source
C'est un bon point; cette demande de fonctionnalité GitHub demande une syntaxe distincte pour l'énumération des membres. La solution de contournement pour les collisions de noms consiste à utiliser la .ForEach()méthode de tableau comme suit:$files.ForEach('Length')
mklement0