Seleccione los valores de una propiedad en todos los objetos de una matriz en PowerShell

134

Digamos que tenemos una matriz de objetos $ objetos. Digamos que estos objetos tienen una propiedad "Nombre".

Esto es lo que quiero hacer

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

Esto funciona, pero ¿se puede hacer de una mejor manera?

Si hago algo como:

 $results = objects | select Name

$resultses una matriz de objetos que tiene una propiedad Name. Quiero que $ results contenga una variedad de nombres.

¿Hay una mejor manera?

Sylvain Reverdy
fuente
44
Simplemente para la corrección, también se puede quitar el "+ =" de su código original, por lo que el foreach sólo selecciona Nombre: $results = @($objects | %{ $_.Name }). Esto puede ser más conveniente para escribir en la línea de comando a veces, aunque creo que la respuesta de Scott es generalmente mejor.
Emperador XLII
1
@EmperorXLII: Buen punto, y en PSv3 + puedes incluso simplificarlo a:$objects | % Name
mklement0

Respuestas:

212

Creo que podrías usar el ExpandPropertyparámetro de Select-Object.

Por ejemplo, para obtener la lista del directorio actual y solo mostrar la propiedad Name, se haría lo siguiente:

ls | select -Property Name

Esto sigue devolviendo objetos DirectoryInfo o FileInfo. Siempre puede inspeccionar el tipo que llega a través de la canalización canalizando a Get-Member (alias gm).

ls | select -Property Name | gm

Por lo tanto, para expandir el objeto para que sea el tipo de propiedad que está viendo, puede hacer lo siguiente:

ls | select -ExpandProperty Name

En su caso, puede hacer lo siguiente para que una variable sea una matriz de cadenas, donde las cadenas son la propiedad Nombre:

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

Como una solución aún más fácil, puede usar:

$results = $objects.Name

Que debería llenarse $resultscon una matriz de todos los valores de propiedad 'Nombre' de los elementos en $objects.

rageandqq
fuente
Tenga en cuenta que esto no funciona en Exchange Management Shell. Cuando usemos Exchange necesitamos usarlo$objects | select -Property Propname, OtherPropname
Bassie
2
@Bassie: el acceso a una propiedad en el nivel de colección para obtener los valores de sus miembros como una matriz se denomina enumeración de miembros y es una característica de PSv3 + ; presumiblemente, su Shell de administración de Exchange es PSv2.
mklement0
32

Para complementar las respuestas útiles y preexistentes con orientación sobre cuándo usar qué enfoque y una comparación de rendimiento .

  • Fuera de una tubería, use (PSv3 +):

    $ objetos . Nombre
    como se demostró en la respuesta de rageandqq , que es sintácticamente más simple y mucho más rápido .

    • El acceso a una propiedad en el nivel de colección para obtener los valores de sus miembros como una matriz se denomina enumeración de miembros y es una característica de PSv3 +.
    • Alternativamente, en PSv2 , use la foreach declaración , cuya salida también puede asignar directamente a una variable:
      $ resultados = foreach ($ obj en $ objetos) {$ obj.Name}
    • Compensaciones :
      • Tanto la colección de entrada como la matriz de salida deben caber en la memoria como un todo .
      • Si la colección de entrada es en sí misma el resultado de un comando (canalización) (por ejemplo, (Get-ChildItem).Name), ese comando primero debe ejecutarse hasta su finalización antes de que se pueda acceder a los elementos de la matriz resultante.
  • En una tubería donde el resultado debe procesarse más o los resultados no caben en la memoria como un todo, use:

    $ objetos | Select-Object -ExpandProperty Name

    • La necesidad -ExpandPropertyse explica en la respuesta de Scott Saad .
    • Obtiene los beneficios de canalización habituales del procesamiento uno por uno, que generalmente produce resultados de inmediato y mantiene el uso constante de la memoria (a menos que finalmente recopile los resultados en la memoria de todos modos).
    • Compensación :
      • El uso de la tubería es relativamente lento .

Para pequeñas colecciones de entrada (matrices), probablemente no notará la diferencia y, especialmente en la línea de comandos, a veces es más importante poder escribir el comando fácilmente.


Aquí hay una alternativa fácil de escribir , que, sin embargo, es el enfoque más lento ; utiliza una ForEach-Objectsintaxis simplificada llamada declaración de operación (nuevamente, PSv3 +):; por ejemplo, la siguiente solución PSv3 + es fácil de agregar a un comando existente:

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

En aras de la exhaustividad: el método de matriz PSv4 +.ForEach() poco conocido , más completo que se analiza en este artículo , es otra alternativa :

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

# By script block (more flexibility; like ForEach-Object)
$objects.ForEach({ $_.Name })
  • Este enfoque es similar a la enumeración de miembros , con las mismas compensaciones, excepto que no se aplica la lógica de canalización ; es marginalmente más lento , aunque todavía notablemente más rápido que la tubería.

  • Para extraer un solo valor de propiedad por nombre ( argumento de cadena ), esta solución está a la par con la enumeración de miembros (aunque esta última es sintácticamente más simple).

  • La variante de bloque de script permite transformaciones arbitrarias ; es una alternativa más rápida, todo en memoria a la vez, al ForEach-Object cmdlet basado en canalización ( %) .


Comparar el rendimiento de los distintos enfoques.

Aquí hay ejemplos de tiempos para los diversos enfoques, basados ​​en una colección de entrada de 10,000objetos , promediados en 10 ejecuciones; los números absolutos no son importantes y varían en función de muchos factores, pero deberían darle una sensación de rendimiento relativo (los tiempos provienen de una VM de Windows 10 de un solo núcleo:

Importante

  • El rendimiento relativo varía en función de si los objetos de entrada son instancias de tipos .NET regulares (por ejemplo, como resultado Get-ChildItem) o [pscustomobject]instancias (por ejemplo, como resultado Convert-FromCsv).
    La razón es que [pscustomobject]PowerShell administra dinámicamente las propiedades y puede acceder a ellas más rápidamente que las propiedades regulares de un tipo .NET regular (definido estáticamente). Ambos escenarios se cubren a continuación.

  • Las pruebas utilizan colecciones ya en memoria llena como entrada, para enfocarse en el rendimiento de extracción de propiedad pura. Con una llamada de cmdlet / función de transmisión como entrada, las diferencias de rendimiento generalmente serán mucho menos pronunciadas, ya que el tiempo pasado dentro de esa llamada puede representar la mayor parte del tiempo dedicado.

  • Para abreviar, %se usa alias para el ForEach-Objectcmdlet.

Conclusiones generales , aplicables tanto al tipo regular de .NET como a la [pscustomobject]entrada:

  • La enumeración de miembros ( $collection.Name) y las foreach ($obj in $collection)soluciones son, con mucho, las más rápidas , por un factor de 10 o más más rápido que la solución basada en canalizaciones más rápida.

  • Sorprendentemente, % Namefunciona mucho peor que % { $_.Name }: vea este problema de GitHub .

  • PowerShell Core supera constantemente a Windows Powershell aquí.

Tiempos con tipos regulares de .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

Conclusiones:

  • En PowerShell Core , .ForEach('Name')claramente supera el rendimiento .ForEach({ $_.Name }). En Windows PowerShell, curiosamente, este último es más rápido, aunque solo marginalmente.

Tiempos con [pscustomobject]instancias :

  • 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

Conclusiones:

  • Nota cómo con [pscustomobject]entrada .ForEach('Name')por Supera con mucho, el script del bloque variante basa, .ForEach({ $_.Name }).

  • Del mismo modo, la [pscustomobject]entrada hace que la canalización sea Select-Object -ExpandProperty Namemás rápida, en Windows PowerShell prácticamente a la par .ForEach({ $_.Name }), pero en PowerShell Core todavía un 50% más lento.

  • En resumen: con la extraña excepción de % Name, con [pscustomobject]los métodos basados ​​en cadenas para hacer referencia a las propiedades superan a las basadas en scriptblock.


Código fuente para las pruebas :

Nota:

  • Descargue la función Time-Commandde este Gist para ejecutar estas pruebas.

  • Establezca $useCustomObjectInputa $truemedida con [pscustomobject]instancias en su lugar.

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

Precaución, la enumeración de miembros solo funciona si la colección en sí no tiene ningún miembro con el mismo nombre. Entonces, si tuviera una matriz de objetos FileInfo, no podría obtener una matriz de longitudes de archivo utilizando

 $files.length # evaluates to array length

Y antes de decir "bueno obviamente", considere esto. Si tenía una matriz de objetos con una propiedad de capacidad, entonces

 $objarr.capacity

funcionaría bien A MENOS QUE $ objarr en realidad no fuera una [Array] sino, por ejemplo, una [ArrayList]. Entonces, antes de usar la enumeración de miembros, es posible que deba mirar dentro del cuadro negro que contiene su colección.

(Nota para los moderadores: este debería ser un comentario sobre la respuesta de rageandqq pero todavía no tengo suficiente reputación).

Uber Kluger
fuente
Es un buen punto; Esta solicitud de función de GitHub solicita una sintaxis separada para la enumeración de miembros. La solución alternativa para las colisiones de nombres es utilizar el .ForEach()método de matriz de la siguiente manera:$files.ForEach('Length')
mklement0