Partilhar via


Tudo o que você queria saber sobre hashtables

Eu quero dar um passo atrás e falar sobre hashtables. Eu os uso o tempo todo agora. Eu estava ensinando alguém sobre eles depois de nossa reunião do grupo de usuários ontem à noite e percebi que tinha a mesma confusão sobre eles que ele. As hashtables são realmente importantes no PowerShell, por isso é bom ter uma compreensão sólida delas.

Observação

A versão original deste artigo apareceu no blog escrito por @KevinMarquette. A equipe do PowerShell agradece Kevin por compartilhar esse conteúdo conosco. Por favor, confira seu blog em PowerShellExplained.com.

Hashtable como uma coleção de elementos

Quero que você veja primeiro um Hashtable como uma coleção na definição tradicional de um hashtable. Esta definição dá-lhe uma compreensão fundamental de como eles funcionam quando são usados para coisas mais avançadas mais tarde. Ignorar este entendimento é muitas vezes uma fonte de confusão.

O que é uma matriz?

Antes de explicar o que é um Hashtable, preciso falar das matrizes primeiro. Para o propósito desta discussão, uma matriz é uma lista ou coleção de valores ou objetos.

$array = @(1,2,3,5,7,11)

Depois de ter os seus itens numa matriz, pode usar foreach para percorrer a lista ou usar um índice numérico para aceder a elementos individuais na matriz.

foreach($item in $array)
{
    Write-Output $item
}

Write-Output $array[3]

Você também pode atualizar valores usando um índice da mesma maneira.

$array[2] = 13

Eu apenas arranhei a superfície em matrizes, mas isso deve colocá-las no contexto certo à medida que passo para tabelas de dispersão.

O que é um hashtable?

Vou começar com uma descrição técnica básica do que são hashtables, no sentido geral, antes de mudar para as outras maneiras como o PowerShell as usa.

Uma hashtable é uma estrutura de dados, muito semelhante a uma matriz, exceto que você armazena cada valor (objeto) usando uma chave. É um armazenamento básico de chave/valor. Primeiro, criamos uma tabela hash vazia.

$ageList = @{}

Observe que chaves, em vez de parênteses, são usadas para definir uma hashtable. Em seguida, adicionamos um item usando uma chave como esta:

$key = 'Kevin'
$value = 36
$ageList.Add( $key, $value )

$ageList.Add( 'Alex', 9 )

O nome da pessoa é a chave e a sua idade é o valor que quero guardar.

Usando os colchetes para acesso

Depois de adicionar seus valores à hashtable, você pode retirá-los usando essa mesma chave (em vez de usar um índice numérico como faria para uma matriz).

$ageList['Kevin']
$ageList['Alex']

Quando eu quero a idade do Kevin, eu uso o nome dele para acessá-lo. Também podemos usar essa abordagem para adicionar ou atualizar valores na hashtable. Isto é como usar o método Add() acima.

$ageList = @{}

$key = 'Kevin'
$value = 36
$ageList[$key] = $value

$ageList['Alex'] = 9

Há outra sintaxe que você pode usar para acessar e atualizar valores que abordarei em uma seção posterior. Se você estiver chegando ao PowerShell de outro idioma, esses exemplos devem se encaixar em como você pode ter usado hashtables antes.

Criando hashtables com valores

Até agora, criei uma tabela hash vazia para estes exemplos. Você pode preencher previamente as chaves e os valores ao criá-los.

$ageList = @{
    Kevin = 36
    Alex  = 9
}

Como uma tabela de pesquisa

O valor real desse tipo de hashtable é que você pode usá-los como uma tabela de pesquisa. Aqui está um exemplo simples.

$environments = @{
    Prod = 'SrvProd05'
    QA   = 'SrvQA02'
    Dev  = 'SrvDev12'
}

$server = $environments[$env]

Neste exemplo, você especifica um ambiente para a variável $env e ele selecionará o servidor correto. Você pode usar um switch($env){...} para uma seleção como esta, mas uma hashtable é uma boa opção.

Isso fica ainda melhor quando você cria dinamicamente a tabela de pesquisa para usá-la mais tarde. Portanto, pense em usar essa abordagem quando precisar fazer referência cruzada a algo. Acho que veríamos isso ainda mais se o PowerShell não fosse tão bom em filtrar no tubo com Where-Object. Se você estiver em uma situação em que o desempenho é importante, essa abordagem precisa ser considerada.

Não vou dizer que é mais rápido, mas se encaixa na regra do Se o desempenho importa, teste-o.

Multisseleção

Geralmente, você pensa em uma hashtable como um par chave/valor, onde você fornece uma chave e obtém um valor. O PowerShell permite que você forneça uma matriz de chaves para obter vários valores.

$environments[@('QA','DEV')]
$environments[('QA','DEV')]
$environments['QA','DEV']

Neste exemplo, uso a mesma tabela hash de pesquisa mencionada acima e forneço três estilos diferentes de arrays para encontrar as correspondências. Esta é uma joia escondida no PowerShell que a maioria das pessoas não conhece.

Iterando hashtables

Como uma hashtable é uma coleção de pares chave/valor, você itera sobre ela de forma diferente do que faz para uma matriz ou uma lista normal de itens.

A primeira coisa a notar é que, se você canalizar seu hashtable, o pipe o trata como um objeto.

PS> $ageList | Measure-Object
count : 1

Mesmo que a propriedade Count indique quantos valores ela contém.

PS> $ageList.Count
2

Você contorna esse problema usando a propriedade Values se tudo o que você precisa é apenas os valores.

PS> $ageList.Values | Measure-Object -Average
Count   : 2
Average : 22.5

Muitas vezes, é mais útil enumerar as chaves e usá-las para acessar os valores.

PS> $ageList.Keys | ForEach-Object{
    $message = '{0} is {1} years old!' -f $_, $ageList[$_]
    Write-Output $message
}
Kevin is 36 years old
Alex is 9 years old

Aqui está o mesmo exemplo com um loop foreach(){...}.

foreach($key in $ageList.Keys)
{
    $message = '{0} is {1} years old' -f $key, $ageList[$key]
    Write-Output $message
}

Estamos a percorrer cada chave na tabela hash e, em seguida, a usá-la para aceder ao valor. Este é um padrão comum ao trabalhar com hashtables como uma coleção.

GetEnumerator()

Isso nos leva a GetEnumerator() para iterar sobre nosso hashtable.

$ageList.GetEnumerator() | ForEach-Object{
    $message = '{0} is {1} years old!' -f $_.Key, $_.Value
    Write-Output $message
}

O enumerador dá-lhe cada par chave/valor um após o outro. Ele foi projetado especificamente para este caso de uso. Obrigado ao Mark Kraus por me lembrar deste.

Enumeração com Erro

Um detalhe importante é que você não pode modificar uma hashtable enquanto ela está sendo enumerada. Se começarmos com o nosso exemplo $environments básico:

$environments = @{
    Prod = 'SrvProd05'
    QA   = 'SrvQA02'
    Dev  = 'SrvDev12'
}

E tentar definir todas as chaves para o mesmo valor de servidor não resulta.

$environments.Keys | ForEach-Object {
    $environments[$_] = 'SrvDev03'
}

An error occurred while enumerating through a collection: Collection was modified;
enumeration operation may not execute.
+ CategoryInfo          : InvalidOperation: tableEnumerator:HashtableEnumerator) [],
 RuntimeException
+ FullyQualifiedErrorId : BadEnumeration

Isso também falhará, embora pareça que deveria estar bem.

foreach($key in $environments.Keys) {
    $environments[$key] = 'SrvDev03'
}

Collection was modified; enumeration operation may not execute.
    + CategoryInfo          : OperationStopped: (:) [], InvalidOperationException
    + FullyQualifiedErrorId : System.InvalidOperationException

O truque para essa situação é clonar as chaves antes de fazer a enumeração.

$environments.Keys.Clone() | ForEach-Object {
    $environments[$_] = 'SrvDev03'
}

Observação

Não é possível clonar uma hashtable que contenha uma única chave. O PowerShell lança um erro. Em vez disso, você converte a propriedade Keys em uma matriz e, em seguida, itera sobre a matriz.

@($environments.Keys) | ForEach-Object {
    $environments[$_] = 'SrvDev03'
}

Hashtable como uma coleção de propriedades

Até agora, o tipo de objetos que colocamos em nossa hashtable eram todos do mesmo tipo de objeto. Eu usei idades em todos esses exemplos e a chave era o nome da pessoa. Esta é uma ótima maneira de encarar a questão quando a sua coleção de objetos tem cada um um nome. Outra maneira comum de usar hashtables no PowerShell é manter uma coleção de propriedades em que a chave é o nome da propriedade. Vou entrar nessa ideia neste próximo exemplo.

Acesso baseado em propriedade

O uso do acesso baseado em propriedade altera a dinâmica das hashtables e como você pode usá-las no PowerShell. Aqui está o exemplo habitual que mencionámos acima, tratando as chaves como propriedades.

$ageList = @{}
$ageList.Kevin = 35
$ageList.Alex = 9

Assim como os exemplos acima, este exemplo adiciona essas chaves se elas ainda não existirem na hashtable. Dependendo de como você definiu suas chaves e quais são seus valores, isso é um pouco estranho ou um ajuste perfeito. O exemplo da lista etária tem funcionado muito bem até este ponto. Precisamos de um novo exemplo para que isso pareça correto daqui para a frente.

$person = @{
    name = 'Kevin'
    age  = 36
}

E podemos adicionar e aceder a atributos no $person desta forma.

$person.city = 'Austin'
$person.state = 'TX'

De repente, este hashtable começa a sentir e agir como um objeto. Ainda é uma coleção de coisas, então todos os exemplos acima ainda se aplicam. Nós apenas abordamos isso de um ponto de vista diferente.

Verificação de chaves e valores

Na maioria dos casos, você pode apenas testar o valor com algo assim:

if( $person.age ){...}

É simples, mas tem sido a fonte de muitos bugs para mim, porque eu estava ignorando um detalhe importante na minha lógica. Comecei a usá-lo para testar se uma chave estava presente. Quando o valor era $false ou zero, essa instrução retornava $false inesperadamente.

if( $person.age -ne $null ){...}

Isso contorna esse problema para valores zero, mas não resolve o contraste entre $null e chaves inexistentes. Na maioria das vezes, não é necessário fazer essa distinção, mas existem métodos para quando tal distinção é necessária.

if( $person.ContainsKey('age') ){...}

Também temos uma ContainsValue() para a situação em que você precisa testar um valor sem conhecer a chave ou iterar toda a coleção.

Remoção e limpeza de chaves

Você pode remover chaves com o método Remove().

$person.Remove('age')

Atribuir-lhes um valor $null apenas deixa você com uma chave que tem um valor $null.

Uma maneira comum de limpar uma tabela hash é simplesmente inicializá-la para uma tabela hash vazia.

$person = @{}

Embora isso funcione, tente usar o método Clear(), em vez disso.

$person.Clear()

Este é um daqueles casos em que o uso do método cria código de auto-documentação e torna as intenções do código muito limpas.

Todas as coisas divertidas

Hashtables ordenados

Por padrão, as hashtables não são ordenadas (ou classificadas). No contexto tradicional, a ordem não importa quando você sempre usa uma chave para acessar valores. Você pode achar que deseja que as propriedades permaneçam na ordem em que você as define. Felizmente, há uma maneira de fazer isso com a palavra-chave ordered.

$person = [ordered]@{
    name = 'Kevin'
    age  = 36
}

Agora, quando você enumera as chaves e os valores, eles permanecem nessa ordem.

Tabelas de dispersão integradas

Ao definir uma hashtable em uma linha, você pode separar os pares chave/valor com um ponto-e-vírgula.

$person = @{ name = 'kevin'; age = 36; }

Isso será útil se você estiver criando-os no tubo.

Expressões personalizadas em comandos de pipeline comuns

Existem alguns cmdlets que suportam o uso de hashtables para criar propriedades personalizadas ou calculadas. É comum ver isso com Select-Object e Format-Table. As hashtables têm uma sintaxe especial que se parece com esta quando totalmente expandida.

$property = @{
    Name = 'TotalSpaceGB'
    Expression = { ($_.Used + $_.Free) / 1GB }
}

O Name é o que o cmdlet rotularia essa coluna. O Expression é um bloco de script que é executado onde $_ é o valor do objeto no pipe. Aqui está o script em ação:

$drives = Get-PSDrive | where Used
$drives | Select-Object -Property Name, $property

Name     TotalSpaceGB
----     ------------
C    238.472652435303

Eu coloquei isso numa variável, mas poderia ser facilmente definido diretamente e pode encurtar Name para n e Expression para e já agora.

$drives | Select-Object -Property Name, @{n='TotalSpaceGB';e={($_.Used + $_.Free) / 1GB}}

Eu pessoalmente não gosto de como isso torna longos os comandos e muitas vezes incentiva comportamentos inadequados nos quais não me vou aprofundar. É mais provável que eu crie uma nova hashtable ou pscustomobject com todos os campos e propriedades que eu quero em vez de usar essa abordagem em scripts. Mas há muito código por aí que faz isso, então eu queria que você estivesse ciente disso. Falo sobre a criação de um pscustomobject mais tarde.

Expressão de classificação personalizada

É fácil classificar uma coleção se os objetos tiverem os dados nos quais você deseja classificar. Você pode adicionar os dados ao objeto antes de classificá-lo ou criar uma expressão personalizada para Sort-Object.

Get-ADUser | Sort-Object -Property @{ e={ Get-TotalSales $_.Name } }

Neste exemplo, estou pegando uma lista de usuários e usando algum cmdlet personalizado para obter informações adicionais apenas para a classificação.

Ordenar uma lista de Hashtables

Se tiver uma lista de tabelas de hash que pretenda ordenar, descobrirá que o Sort-Object não trata as suas chaves como propriedades. Podemos contornar isso usando uma expressão de ordenação personalizada.

$data = @(
    @{name='a'}
    @{name='c'}
    @{name='e'}
    @{name='f'}
    @{name='d'}
    @{name='b'}
)

$data | Sort-Object -Property @{e={$_.name}}

"Splatting de hashtables em cmdlets"

Esta é uma das minhas coisas favoritas sobre hashtables que muitas pessoas não descobrem cedo. A ideia é que, em vez de fornecer todas as propriedades a um cmdlet numa única linha, pode agrupá-las numa hashtable primeiro. Então, pode passar a tabela hash para a função de forma especial. Aqui está um exemplo de criação de um escopo DHCP da maneira normal.

Add-DhcpServerV4Scope -Name 'TestNetwork' -StartRange '10.0.0.2' -EndRange '10.0.0.254' -SubnetMask '255.255.255.0' -Description 'Network for testlab A' -LeaseDuration (New-TimeSpan -Days 8) -Type "Both"

Sem usar splatting, todos esses elementos precisam ser definidos numa só linha. Ele rola para fora da tela ou vai envolver onde quer que pareça. Agora compare isso com um comando que usa splatting.

$DHCPScope = @{
    Name          = 'TestNetwork'
    StartRange    = '10.0.0.2'
    EndRange      = '10.0.0.254'
    SubnetMask    = '255.255.255.0'
    Description   = 'Network for testlab A'
    LeaseDuration = (New-TimeSpan -Days 8)
    Type          = "Both"
}
Add-DhcpServerV4Scope @DHCPScope

A utilização do sinal @ em vez de $ é o que invoca a operação splat.

Basta reservar um momento para apreciar como esse exemplo é fácil de ler. Eles são exatamente o mesmo comando com todos os mesmos valores. A segunda é mais fácil de entender e manter daqui para frente.

Eu utilizo o método de splatting sempre que o comando fica demasiado longo. Eu defino muito longo como fazendo com que minha janela role para a direita. Se eu identificar três propriedades para uma função, é provável que a reescreva usando uma "hashtable" splatted.

Splatting para parâmetros opcionais

Uma das maneiras mais comuns de usar o splatting é lidar com parâmetros opcionais que vêm de algum outro lugar no meu script. Digamos que eu tenha uma função que envolve uma chamada Get-CimInstance que tem um argumento $Credential opcional.

$CIMParams = @{
    ClassName = 'Win32_BIOS'
    ComputerName = $ComputerName
}

if($Credential)
{
    $CIMParams.Credential = $Credential
}

Get-CimInstance @CIMParams

Começo por criar a minha tabela hash com parâmetros comuns. Então eu adiciono o $Credential se ele existe. Como estou usando o splatting aqui, só preciso ter a chamada para Get-CimInstance no meu código uma vez. Este padrão de design é muito limpo e pode lidar com muitos parâmetros opcionais facilmente.

Para ser justo/a, poderia-se escrever os seus comandos de modo a permitir valores $null para os parâmetros. ** Você nem sempre tem controle sobre os outros comandos que invoca.

Múltiplos splats

Pode desdobrar várias tabelas hash para o mesmo cmdlet. Se voltarmos ao nosso exemplo original de espalhamento:

$Common = @{
    SubnetMask  = '255.255.255.0'
    LeaseDuration = (New-TimeSpan -Days 8)
    Type = "Both"
}

$DHCPScope = @{
    Name        = 'TestNetwork'
    StartRange  = '10.0.0.2'
    EndRange    = '10.0.0.254'
    Description = 'Network for testlab A'
}

Add-DhcpServerv4Scope @DHCPScope @Common

Usarei esse método quando tiver um conjunto comum de parâmetros que estou passando para muitos comandos.

Splatting para código limpo

Não há nada de errado em espalhar um único parâmetro se tornar o teu código mais limpo.

$log = @{Path = '.\logfile.log'}
Add-Content "logging this command" @log

Splatting executáveis

Splatting também funciona em alguns executáveis que usam uma sintaxe /param:value. Robocopy.exe, por exemplo, tem alguns parâmetros como este.

$robo = @{R=1;W=1;MT=8}
robocopy source destination @robo

Eu não sei se isso é tão útil, mas eu achei interessante.

Adicionando hashtables

Tabelas de dispersão suportam operações de adição para combinar duas tabelas de dispersão.

$person += @{Zip = '78701'}

Isso só funciona se as duas tabelas hash não partilharem uma chave.

Hashtables aninhadas

Podemos usar hashtables como valores dentro de uma hashtable.

$person = @{
    name = 'Kevin'
    age  = 36
}
$person.location = @{}
$person.location.city = 'Austin'
$person.location.state = 'TX'

Comecei com uma tabela hash básica contendo duas chaves. Eu adicionei uma chave chamada location com uma tabela hash vazia. Em seguida, adicionei os dois últimos itens a essa location hashtable. Podemos fazer tudo isso em linha também.

$person = @{
    name = 'Kevin'
    age  = 36
    location = @{
        city  = 'Austin'
        state = 'TX'
    }
}

Isso cria a mesma hashtable que vimos acima e pode acessar as propriedades da mesma maneira.

$person.location.city
Austin

Há muitas maneiras de abordar a estrutura de seus objetos. Aqui está uma segunda maneira de examinar uma tabela hash aninhada.

$people = @{
    Kevin = @{
        age  = 36
        city = 'Austin'
    }
    Alex = @{
        age  = 9
        city = 'Austin'
    }
}

Isso mistura o conceito de usar hashtables como uma coleção de objetos e uma coleção de propriedades. Os valores ainda são fáceis de acessar mesmo quando estão aninhados usando qualquer abordagem que você preferir.

PS> $people.kevin.age
36
PS> $people.kevin['city']
Austin
PS> $people['Alex'].age
9
PS> $people['Alex']['City']
Austin

Eu tendo a usar a propriedade 'dot' quando estou a tratá-la como uma propriedade. Essas são geralmente coisas que defini estaticamente no meu código e as conheço do alto da minha cabeça. Se eu precisar percorrer a lista ou acessar programaticamente as chaves, uso os colchetes para fornecer o nome da chave.

foreach($name in $people.Keys)
{
    $person = $people[$name]
    '{0}, age {1}, is in {2}' -f $name, $person.age, $person.city
}

Ter a capacidade de aninhar hashtables dá-lhe muita flexibilidade e opções.

Examinando hashtables aninhadas

Assim que começares a aninhar tabelas hash, precisarás de uma maneira fácil de visualizá-las a partir do console. Se eu pegar esse último hashtable, eu recebo uma saída que se parece com isso e só vai tão fundo:

PS> $people
Name                           Value
----                           -----
Kevin                          {age, city}
Alex                           {age, city}

A minha escolha habitual de comando para olhar para essas coisas é ConvertTo-Json porque é muito claro e uso JSON frequentemente noutras coisas.

PS> $people | ConvertTo-Json
{
    "Kevin":  {
                "age":  36,
                "city":  "Austin"
            },
    "Alex":  {
                "age":  9,
                "city":  "Austin"
            }
}

Mesmo que você não conheça JSON, você deve ser capaz de ver o que você está procurando. Há um comando Format-Custom para dados estruturados como este, mas ainda gosto mais da visualização JSON.

Criação de objetos

Às vezes, basta ter um objeto e usar uma tabela hash para manter propriedades simplesmente não resolve o problema. Mais comumente você deseja ver as chaves como nomes de coluna. Um pscustomobject torna isso fácil.

$person = [pscustomobject]@{
    name = 'Kevin'
    age  = 36
}

$person

name  age
----  ---
Kevin  36

Mesmo que não o cries como um pscustomobject inicialmente, podes sempre convertê-lo mais tarde, quando necessário.

$person = @{
    name = 'Kevin'
    age  = 36
}

[pscustomobject]$person

name  age
----  ---
Kevin  36

Eu já tenho um relatório detalhado para pscustomobject que deverás ler depois deste. Baseia-se em muitas das coisas aprendidas aqui.

Lendo e gravando hashtables no ficheiro

Guardar em CSV

Ter dificuldades para conseguir guardar uma hashtable num CSV é uma das dificuldades a que me referi acima. Converta o seu hashtable para um pscustomobject e ele será guardado corretamente no CSV. É útil começar com um 'pscustomobject' de modo a que a ordem das colunas seja preservada. Mas pode convertê-lo para um pscustomobject inline, se necessário.

$person | ForEach-Object{ [pscustomobject]$_ } | Export-Csv -Path $path

Novamente, confira meu write-up sobre como usar um pscustomobject.

Guardar uma tabela de hash aninhada num ficheiro

Se eu precisar salvar uma hashtable aninhada em um arquivo e, em seguida, lê-la novamente, uso os cmdlets JSON para fazê-lo.

$people | ConvertTo-Json | Set-Content -Path $path
$people = Get-Content -Path $path -Raw | ConvertFrom-Json

Há dois pontos importantes sobre este método. Primeiro é que o JSON é escrito multilinha, então eu preciso usar a opção -Raw para lê-lo de volta em uma única cadeia de caracteres. A segunda é que o objeto importado não é mais um [hashtable]. Agora é um [pscustomobject] e isso pode causar problemas se não estiveres preparado.

Preste atenção às hashtables profundamente aninhadas. Ao convertê-lo em JSON, você pode não obter os resultados esperados.

@{ a = @{ b = @{ c = @{ d = "e" }}}} | ConvertTo-Json

{
  "a": {
    "b": {
      "c": "System.Collections.Hashtable"
    }
  }
}

Use parâmetro Depth para garantir que você expandiu todas as hashtables aninhadas.

@{ a = @{ b = @{ c = @{ d = "e" }}}} | ConvertTo-Json -Depth 3

{
  "a": {
    "b": {
      "c": {
        "d": "e"
      }
    }
  }
}

Se você precisar que ele seja um [hashtable] na importação, então você precisa usar os comandos Export-CliXml e Import-CliXml.

Convertendo JSON para Hashtable

Se você precisar converter JSON em um [hashtable], há uma maneira que conheço para fazê-lo com o JavaScriptSerializer no .NET.

[Reflection.Assembly]::LoadWithPartialName("System.Web.Script.Serialization")
$JSSerializer = [System.Web.Script.Serialization.JavaScriptSerializer]::new()
$JSSerializer.Deserialize($json,'Hashtable')

A partir do PowerShell v6, o suporte a JSON utiliza o NewtonSoft JSON.NET e adiciona suporte a tabelas hash.

'{ "a": "b" }' | ConvertFrom-Json -AsHashtable

Name      Value
----      -----
a         b

O PowerShell 6.2 adicionou o parâmetro Depth ao ConvertFrom-Json. O padrão Depth é 1024.

Ler diretamente de um ficheiro

Se você tiver um arquivo que contenha uma hashtable usando a sintaxe do PowerShell, há uma maneira de importá-lo diretamente.

$content = Get-Content -Path $Path -Raw -ErrorAction Stop
$scriptBlock = [scriptblock]::Create( $content )
$scriptBlock.CheckRestrictedLanguage( $allowedCommands, $allowedVariables, $true )
$hashtable = ( & $scriptBlock )

Ele importa o conteúdo do arquivo para um scriptblocke, em seguida, verifica se não tem outros comandos do PowerShell antes de executá-lo.

A propósito, sabias que um manifesto de módulo (o ficheiro .psd1) é apenas uma tabela hash?

As chaves podem ser qualquer objeto

Na maioria das vezes, as teclas são apenas cordas. Assim, podemos colocar citações em torno de qualquer coisa e torná-la uma chave.

$person = @{
    'full name' = 'Kevin Marquette'
    '#' = 3978
}
$person['full name']

Você pode fazer algumas coisas estranhas que você pode não ter percebido que poderia fazer.

$person.'full name'

$key = 'full name'
$person.$key

Só porque você pode fazer algo, isso não significa que você deve. Esse último parece apenas um bug esperando para acontecer e seria facilmente incompreendido por qualquer pessoa que leia seu código.

Tecnicamente, a sua chave não precisa ser uma string, mas é mais fácil concebê-las se apenas usar strings. No entanto, a indexação não funciona bem com as chaves complexas.

$ht = @{ @(1,2,3) = "a" }
$ht

Name                           Value
----                           -----
{1, 2, 3}                      a

Acessar um valor na tabela de hash pela sua chave nem sempre funciona. Por exemplo:

$key = $ht.Keys[0]
$ht.$($key)
a
$ht[$key]
a

Quando a chave é uma matriz, você deve encapsular a variável $key em uma subexpressão para que ela possa ser usada com notação de acesso de membro (.). Ou, você pode usar a notação de índice de matriz ([]).

Utilização em variáveis automáticas

$PSBoundParameters

$PSBoundParameters é uma variável automática que só existe dentro do contexto de uma função. Ele contém todos os parâmetros com os quais a função foi chamada. Este não é exatamente um hashtable, mas perto o suficiente para que você possa tratá-lo como um.

Isso inclui remover teclas e escaloná-las para outras funções. Se você estiver escrevendo funções de proxy, dê uma olhada mais de perto neste.

Consulte about_Automatic_Variables para obter mais detalhes.

Pegadinha dos PSBoundParameters

Uma coisa importante a lembrar é que isso inclui apenas os valores que são passados como parâmetros. Se você também tiver parâmetros com valores padrão, mas não forem passados pelo chamador, $PSBoundParameters não conterá esses valores. Isso é comumente negligenciado.

$PSDefaultParameterValues

Essa variável automática permite atribuir valores padrão a qualquer cmdlet sem alterá-lo. Dê uma olhada neste exemplo.

$PSDefaultParameterValues["Out-File:Encoding"] = "UTF8"

Isso adiciona uma entrada à hashtable $PSDefaultParameterValues que define UTF8 como o valor padrão para o parâmetro Out-File -Encoding. Isso é específico da sessão, então você deve colocá-lo em sua $PROFILE.

Eu uso isso com frequência para pré-atribuir valores que digito com bastante frequência.

$PSDefaultParameterValues[ "Connect-VIServer:Server" ] = 'VCENTER01.contoso.local'

Isso também aceita curingas para que você possa definir valores em massa. Aqui estão algumas maneiras de usar isso:

$PSDefaultParameterValues[ "Get-*:Verbose" ] = $true
$PSDefaultParameterValues[ "*:Credential" ] = Get-Credential

Para uma análise mais detalhada, consulte este excelente artigo sobre Padrões Automáticos por Michael Sorens.

Regex $Matches

Quando se usa o operador -match, é criada uma variável automática chamada $Matches com os resultados da correspondência. Se você tiver quaisquer subexpressões em seu regex, essas subcorrespondências também serão listadas.

$message = 'My SSN is 123-45-6789.'

$message -match 'My SSN is (.+)\.'
$Matches[0]
$Matches[1]

Correspondências nomeadas

Este é um dos meus recursos favoritos que a maioria das pessoas não conhece. Se você usar uma correspondência de regex nomeada, poderá acessar essa correspondência pelo nome nas correspondências.

$message = 'My Name is Kevin and my SSN is 123-45-6789.'

if($message -match 'My Name is (?<Name>.+) and my SSN is (?<SSN>.+)\.')
{
    $Matches.Name
    $Matches.SSN
}

No exemplo acima, o (?<Name>.*) é uma subexpressão nomeada. Esse valor é então colocado na propriedade $Matches.Name.

Group-Object -AsHashtable

Uma característica pouco conhecida do Group-Object é que ele pode transformar alguns conjuntos de dados em uma hashtable para você.

Import-Csv $Path | Group-Object -AsHashtable -Property Email

Isso adicionará cada linha em uma hashtable e usará a propriedade especificada como a chave para acessá-la.

Copiando hashtables

Uma coisa importante a saber é que tabelas de hash são objetos. E cada variável é apenas uma referência a um objeto. Isso significa que é preciso mais trabalho para fazer uma cópia válida de uma tabela de dispersão.

Atribuição de tipos de referência

Quando se tem uma tabela hash e a atribui a uma segunda variável, ambas as variáveis apontam para a mesma tabela hash.

PS> $orig = @{name='orig'}
PS> $copy = $orig
PS> $copy.name = 'copy'
PS> 'Copy: [{0}]' -f $copy.name
PS> 'Orig: [{0}]' -f $orig.name

Copy: [copy]
Orig: [copy]

Isso destaca que eles são os mesmos porque alterar os valores em um também alterará os valores no outro. Isso também se aplica ao passar tabelas hash para outras funções. Se essas funções fizerem alterações nessa hashtable, seu original também será alterado.

Cópias rasas, nível único

Se tivermos uma hashtable simples como o nosso exemplo acima, podemos usar Clone() para fazer uma cópia superficial.

PS> $orig = @{name='orig'}
PS> $copy = $orig.Clone()
PS> $copy.name = 'copy'
PS> 'Copy: [{0}]' -f $copy.name
PS> 'Orig: [{0}]' -f $orig.name

Copy: [copy]
Orig: [orig]

Isso nos permitirá fazer algumas mudanças básicas em um que não impactam o outro.

Cópias rasas, aninhadas

A razão pela qual é chamada de cópia superficial é porque ela copia apenas as propriedades de nível básico. Se uma dessas propriedades for um tipo de referência (como outra tabela hash), esses objetos aninhados ainda apontarão uns para os outros.

PS> $orig = @{
        person=@{
            name='orig'
        }
    }
PS> $copy = $orig.Clone()
PS> $copy.person.name = 'copy'
PS> 'Copy: [{0}]' -f $copy.person.name
PS> 'Orig: [{0}]' -f $orig.person.name

Copy: [copy]
Orig: [copy]

Então você pode ver que, embora eu tenha clonado o hashtable, a referência a person não foi clonada. Precisamos fazer uma cópia profunda para realmente ter uma segunda tabela hash que não esteja vinculada ao primeiro.

Cópias profundas

Há algumas maneiras de fazer uma cópia integral de uma tabela hash (e mantê-la como uma tabela hash). Aqui está uma função usando o PowerShell para criar recursivamente uma cópia profunda:

function Get-DeepClone
{
    [CmdletBinding()]
    param(
        $InputObject
    )
    process
    {
        if($InputObject -is [hashtable]) {
            $clone = @{}
            foreach($key in $InputObject.Keys)
            {
                $clone[$key] = Get-DeepClone $InputObject[$key]
            }
            return $clone
        } else {
            return $InputObject
        }
    }
}

Ele não lida com outros tipos de referência ou matrizes, mas é um bom ponto de partida.

Outra maneira é usar o .NET para desserializá-lo usando CliXml como nesta função:

function Get-DeepClone
{
    param(
        $InputObject
    )
    $TempCliXmlString = [System.Management.Automation.PSSerializer]::Serialize($obj, [int32]::MaxValue)
    return [System.Management.Automation.PSSerializer]::Deserialize($TempCliXmlString)
}

Para hashtables extremamente grandes, a função de desserialização é mais rápida à medida que se expande. No entanto, há algumas coisas a considerar ao usar esse método. Como usa CliXml, consome muita memória e, se estiveres a clonar tabelas hash enormes, isso pode ser um problema. Outra limitação do CliXml é que há uma limitação de profundidade de 48. Ou seja, se você tiver uma hashtable com 48 camadas de hashtables aninhadas, a clonagem falhará e nenhuma hashtable será produzida.

Mais alguma coisa?

Percorri bastante terreno rapidamente. A minha esperança é que saias aprendendo algo novo ou entendendo-o melhor a cada vez que leias isto. Como eu cobri todo o espectro desse recurso, há aspetos que podem não se aplicar a você agora. Isso é perfeitamente aceitável e é meio que esperado, dependendo de quanto você trabalha com o PowerShell.