Selecione os valores de uma propriedade em todos os objetos de uma matriz no PowerShell

134

Digamos que temos uma matriz de objetos $ objects. Digamos que esses objetos tenham uma propriedade "Nome".

Isto é o que eu quero fazer

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

Isso funciona, mas pode ser feito de uma maneira melhor?

Se eu fizer algo como:

 $results = objects | select Name

$resultsé uma matriz de objetos com uma propriedade Name. Eu quero que $ results contenham uma matriz de nomes.

Existe uma maneira melhor?

Sylvain Reverdy
fonte
4
Apenas para completar, você também pode remover o "+ =" do seu código original, de modo que o foreach seleciona somente Nome: $results = @($objects | %{ $_.Name }). Às vezes, pode ser mais conveniente digitar na linha de comando, embora eu ache a resposta de Scott geralmente melhor.
Imperador XLII
1
@EmperorXLII: Bom argumento, e no PSv3 + você pode até simplificar:$objects | % Name
mklement0

Respostas:

212

Eu acho que você pode ser capaz de usar o ExpandPropertyparâmetro de Select-Object.

Por exemplo, para obter a lista do diretório atual e apenas exibir a propriedade Name, faça o seguinte:

ls | select -Property Name

Isso ainda está retornando objetos DirectoryInfo ou FileInfo. Você sempre pode inspecionar o tipo que está passando pelo pipeline canalizando para Get-Member (alias gm).

ls | select -Property Name | gm

Portanto, para expandir o objeto para ser o tipo de propriedade que você está vendo, você pode fazer o seguinte:

ls | select -ExpandProperty Name

No seu caso, você pode fazer o seguinte para que uma variável seja uma matriz de strings, onde as strings são a propriedade Name:

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

Como uma solução ainda mais fácil, você pode simplesmente usar:

$results = $objects.Name

O qual deve preencher $resultscom uma matriz de todos os valores da propriedade 'Nome' dos elementos $objects.

rageandqq
fonte
Observe que isso não funciona Exchange Management Shell. Quando usando o Exchange precisamos usar$objects | select -Property Propname, OtherPropname
Bassie
2
@ Bassie: Acessar uma propriedade no nível da coleção para obter os valores de seus membros como uma matriz é chamada enumeração de membro e é um recurso do PSv3 + ; presumivelmente, seu Shell de Gerenciamento do Exchange é PSv2.
mklement0
32

Para complementar as respostas preexistentes e úteis, com orientações sobre quando usar qual abordagem e uma comparação de desempenho .

  • Fora de um pipeline, use (PSv3 +):

    $ objetos . Nome
    como demonstrado na resposta de rageandqq , que é sintaticamente mais simples e muito mais rápida .

    • O acesso a uma propriedade no nível da coleção para obter os valores de seus membros como uma matriz é chamado enumeração de membro e é um recurso do PSv3 +.
    • Como alternativa, no PSv2 , use a foreach instrução cuja saída você também pode atribuir diretamente a uma variável:
      $ results = foreach ($ obj em $ objetos) {$ obj.Name}
    • Compensações :
      • A coleção de entrada e a matriz de saída devem caber na memória como um todo .
      • Se a coleção de entrada for o resultado de um comando (pipeline) (por exemplo, (Get-ChildItem).Name), esse comando deverá primeiro ser executado até a conclusão antes que os elementos da matriz resultante possam ser acessados.
  • Em um pipeline em que o resultado deve ser processado posteriormente ou os resultados não cabem na memória como um todo, use:

    objetos $ | Nome do objeto Select -ExpandProperty

    • A necessidade -ExpandPropertyé explicada na resposta de Scott Saad .
    • Você obtém os benefícios usuais do pipeline do processamento um por um, que normalmente produz saída imediatamente e mantém o uso da memória constante (a menos que você finalmente colete os resultados na memória).
    • Tradeoff :
      • O uso do pipeline é comparativamente lento .

Para pequenas coleções de entradas (matrizes), você provavelmente não notará a diferença e, especialmente na linha de comando, às vezes é possível digitar o comando com facilidade.


Aqui está uma alternativa fácil de digitar , que, no entanto, é a abordagem mais lenta ; usa sintaxe simplificada ForEach-Objectchamada instrução de operação (novamente, PSv3 +):; por exemplo, é fácil anexar a seguinte solução PSv3 + a um comando existente:

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

Por uma questão de completude: O pouco conhecido método de array PSv4 +.ForEach() , mais abrangente discutido neste artigo , é outra alternativa :

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

# By script block (more flexibility; like ForEach-Object)
$objects.ForEach({ $_.Name })
  • Essa abordagem é semelhante à enumeração de membros , com as mesmas vantagens, exceto que a lógica do pipeline não é aplicada; é marginalmente mais lento , embora ainda visivelmente mais rápido que o oleoduto.

  • Para extrair um único valor de propriedade por nome ( argumento de seqüência de caracteres ), esta solução está em pé de igualdade com a enumeração de membros (embora a última seja sintaticamente mais simples).

  • A variante de bloco de script , permite transformações arbitrárias ; é uma alternativa mais rápida - tudo na memória de uma só vez - ao ForEach-Object cmdlet baseado em pipeline ( %) .


Comparando o desempenho das várias abordagens

Aqui estão exemplos de tempos para as várias abordagens, com base em uma coleção de 10,000objetos de entrada , com média de 10 execuções; os números absolutos não são importantes e variam com base em muitos fatores, mas devem fornecer uma sensação de desempenho relativo (os tempos vêm de uma VM do Windows 10 de núcleo único:

Importante

  • O desempenho relativo varia de acordo com se os objetos de entrada são instâncias de tipos .NET regulares (por exemplo, como saída por Get-ChildItem) ou [pscustomobject]instâncias (por exemplo, como saída por Convert-FromCsv).
    O motivo é que as [pscustomobject]propriedades são gerenciadas dinamicamente pelo PowerShell e podem acessá-las mais rapidamente do que as propriedades regulares de um tipo .NET regular (definido estaticamente). Ambos os cenários são abordados abaixo.

  • Os testes usam coleções já com memória na íntegra como entrada, para focar no desempenho de extração de propriedade pura. Com um cmdlet de streaming / chamada de função como entrada, as diferenças de desempenho geralmente serão muito menos pronunciadas, pois o tempo gasto dentro dessa chamada pode ser responsável pela maior parte do tempo gasto.

  • Por questões de brevidade, o alias %é usado para o ForEach-Objectcmdlet.

Conclusões gerais , aplicáveis ​​ao tipo e [pscustomobject]entrada regulares do .NET :

  • A enumeração de membros ( $collection.Name) e as foreach ($obj in $collection)soluções são de longe as mais rápidas , por um fator 10 ou mais rápido que a solução mais rápida baseada em pipeline.

  • Surpreendentemente, tem um % Namedesempenho muito pior do que % { $_.Name }- veja este problema do GitHub .

  • O PowerShell Core supera consistentemente o Windows Powershell aqui.

Horários com tipos .NET regulares :

  • 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

Conclusões:

  • No PowerShell Core , .ForEach('Name')supera claramente o desempenho .ForEach({ $_.Name }). Curiosamente, no Windows PowerShell, o último é mais rápido, embora apenas marginalmente.

Tempos com [pscustomobject]instâncias :

  • 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

Conclusões:

  • Observe como a [pscustomobject]entrada .ForEach('Name')supera de longe a variante baseada em bloco de script .ForEach({ $_.Name }),.

  • Da mesma forma, a [pscustomobject]entrada torna o pipeline Select-Object -ExpandProperty Namemais rápido, no Windows PowerShell praticamente igual .ForEach({ $_.Name }), mas no PowerShell Core ainda é 50% mais lento.

  • Resumindo: com a exceção estranha de % Name, com [pscustomobject]os métodos baseados em string de referência, as propriedades superam as baseadas em scriptblock.


Código fonte dos testes :

Nota:

  • Baixar função Time-Commandde esta Síntese para executar esses testes.

  • Configure $useCustomObjectInputpara $truemedir com [pscustomobject]instâncias.

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

Cuidado, a enumeração de membros funcionará apenas se a coleção em si não tiver nenhum membro com o mesmo nome. Portanto, se você tivesse uma matriz de objetos FileInfo, não poderia obter uma matriz de tamanhos de arquivo usando

 $files.length # evaluates to array length

E antes de dizer "bem, obviamente", considere isso. Se você tivesse uma matriz de objetos com uma propriedade de capacidade,

 $objarr.capacity

funcionaria bem, a menos que $ objarr não fosse realmente um [Array], mas, por exemplo, um [ArrayList]. Portanto, antes de usar a enumeração de membros, talvez seja necessário procurar dentro da caixa preta que contém sua coleção.

(Nota aos moderadores: este deve ser um comentário à resposta de rageandqq, mas ainda não tenho reputação suficiente.)

Uber Kluger
fonte
É um bom argumento; essa solicitação de recurso do GitHub solicita uma sintaxe separada para a enumeração de membros. A solução alternativa para colisões de nomes é usar o .ForEach()método de matriz da seguinte maneira:$files.ForEach('Length')
mklement0