Compartilhar via


Tudo o que você queria saber sobre o $null

Normalmente, o $null do PowerShell parece ser simples, mas tem inúmeras nuances. Vamos examinar o $null para entender o que acontece quando nos deparamos inesperadamente com um valor $null.

Observação

A versão original deste artigo foi publicada no blog escrito por @KevinMarquette. A equipe do PowerShell agradece a Kevin por compartilhar o conteúdo conosco. Confira o blog dele em PowerShellExplained.com.

O que é NULL?

Você pode pensar no NULL como um valor desconhecido ou vazio. Uma variável será NULL até que você atribua um valor ou um objeto a ela. Isso pode ser importante porque há alguns comandos que exigem um valor e geram erros se o valor for NULL.

$null do PowerShell

O $null é uma variável automática do PowerShell usada para representar NULL. Você poderá atribui-la a variáveis, usá-la em comparações ou empregá-la como um espaço reservado para NULL em uma coleção.

O PowerShell trata $null como um objeto de valor NULL. Isso será diferente do que você pode esperar se estiver acostumado com outras linguagens.

Exemplos de $null

Sempre que você tentar usar uma variável que não foi inicializada, o valor será $null. Essa é uma das maneiras mais comuns pelas quais os valores $null se infiltram em seu código.

PS> $null -eq $undefinedVariable
True

Se você digitar o nome de uma variável incorretamente por engano, o PowerShell a verá como uma variável diferente e o valor atribuído será $null.

A outra maneira de encontrar valores $null é quando eles são provenientes de outros comandos que não fornecem nenhum resultado.

PS> function Get-Nothing {}
PS> $value = Get-Nothing
PS> $null -eq $value
True

Impacto do $null

Os valores $null impactam seu código de maneira diferente, dependendo do local em que aparecem.

Em cadeias de caracteres

Se você usar o $null em uma cadeia de caracteres, ela será um valor em branco (ou uma cadeia de caracteres vazia).

PS> $value = $null
PS> Write-Output "'The value is $value'"
'The value is '

Essa é uma das razões pelas quais eu gosto de colocar colchetes ao redor das variáveis ao usá-las nas mensagens de log. É ainda mais importante identificar os limites dos valores das variáveis quando o valor está ao final da cadeia de caracteres.

PS> $value = $null
PS> Write-Output "The value is [$value]"
The value is []

Isso torna as cadeias de caracteres vazias e os valores $null fáceis de identificar.

Em equações numéricas

Quando um valor $null é usado em uma equação numérica, os resultados são inválidos caso não gerem erro. Às vezes, o $null é avaliado como 0 e outras vezes torna todo o resultado $null. Aqui está um exemplo com multiplicação que fornece 0 ou $null, dependendo da ordem dos valores.

PS> $null * 5
PS> $null -eq ( $null * 5 )
True

PS> 5 * $null
0
PS> $null -eq ( 5 * $null )
False

No lugar de uma coleção

Uma coleção permite que você use um índice para acessar os valores. Se você tentar indexar uma coleção que corresponde a null, receberá o seguinte erro: Cannot index into a null array.

PS> $value = $null
PS> $value[10]
Cannot index into a null array.
At line:1 char:1
+ $value[10]
+ ~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : NullArray

Se você tiver uma coleção, mas tentar acessar um elemento que não pertence à coleção, receberá $null como resultado.

$array = @( 'one','two','three' )
$null -eq $array[100]
True

No lugar de um objeto

Se você tentar acessar uma propriedade ou uma subpropriedade de um objeto que não tem aquela propriedade especificada, obterá um valor $null, assim como ocorreria para uma variável indefinida. Não importa se a variável é $null ou um objeto real nesse caso.

PS> $null -eq $undefined.some.fake.property
True

PS> $date = Get-Date
PS> $null -eq $date.some.fake.property
True

Método em uma expressão com valor nulo

Chamar um método em um objeto $null gera um RuntimeException.

PS> $value = $null
PS> $value.toString()
You cannot call a method on a null-valued expression.
At line:1 char:1
+ $value.tostring()
+ ~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

Sempre que vejo a frase You cannot call a method on a null-valued expression, a primeira coisa que procuro são locais onde algum método esteja sendo chamado em uma variável sem antes verificar se ele corresponde a $null.

Verificando se corresponde a $null

Talvez você tenha notado que eu sempre coloco o $null à esquerda ao verificar se corresponde a $null em meus exemplos. Isso é intencional e aceito como uma melhor prática do PowerShell. Há alguns cenários em que colocá-lo à direita não gera o resultado esperado.

Observe o próximo exemplo e tente prever os resultados:

if ( $value -eq $null )
{
    'The array is $null'
}
if ( $value -ne $null )
{
    'The array is not $null'
}

Se eu não definir $value, o primeiro será avaliado como $true e a mensagem será The array is $null. A armadilha aqui é que é possível criar um $value que permita que ambos sejam $false

$value = @( $null )

Nesse caso, o $value é uma matriz que contém um $null. O -eq verifica cada valor da matriz e retorna o $null com correspondência. Isso é avaliado como $false. O -ne retorna tudo que não corresponde a $null e, nesse caso, não há resultados (isso também é avaliado como $false). Nenhuma delas é $true, embora pareça que uma delas deveria ser.

Não só podemos criar um valor que faça com que ambos sejam avaliados como $false, também é possível criar um valor em que ambos sejam avaliados como $true. Mathias Jessen (@IISResetMe) tem uma boa postagem que explora esse cenário.

PSScriptAnalyzer e VSCode

O módulo do PSScriptAnalyzer tem uma regra que verifica a ocorrência desse problema chamado PSPossibleIncorrectComparisonWithNull.

PS> Invoke-ScriptAnalyzer ./myscript.ps1

RuleName                              Message
--------                              -------
PSPossibleIncorrectComparisonWithNull $null should be on the left side of equality comparisons.

Como o VS Code também usa as regras do PSScriptAnalyser, ele realça ou identifica isso como um problema em seu script.

Verificação simples

Uma forma comum pela qual as pessoas verificam um valor não $null é usando uma instrução if() simples sem a comparação.

if ( $value )
{
    Do-Something
}

Se o valor for $null, isso será avaliado como $false. Isso é fácil de ler, mas tenha cuidado para que ele esteja procurando exatamente o que você espera que ele procure. Eu li essa linha de código como:

Se $value tiver um valor.

Mas essa não é a realidade. Na verdade, a linha está dizendo:

Se $value não for $null ou 0 ou $false ou uma cadeia de caracteres vazia ou uma matriz vazia.

Aqui está um exemplo mais completo dessa instrução.

if ( $null -ne $value -and
        $value -ne 0 -and
        $value -ne '' -and
        ($value -isnot [array] -or $value.Length -ne 0) -and
        $value -ne $false )
{
    Do-Something
}

É perfeitamente possível usar uma verificação básica de if, contanto que você se lembre de que esses outros valores contam como $false e não apenas que uma variável tem um valor.

Eu tive esse problema ao refatorar alguns códigos há alguns dias. Ele tinha uma verificação de propriedade básica como esta.

if ( $object.property )
{
    $object.property = $value
}

Eu queria atribuir um valor à propriedade dos objetos somente se ele existisse. Na maioria dos casos, o objeto original tinha um valor que seria avaliado como $true na instrução if. Mas eu encontrei um problema em que o valor, ocasionalmente, não era definido. Eu depurei o código e descobri que o objeto tinha a propriedade, mas ela era um valor de cadeia de caracteres em branco. Isso impedia que ela fosse atualizada com a lógica anterior. Então, adicionei uma verificação de $null adequada e tudo funcionou.

if ( $null -ne $object.property )
{
    $object.property = $value
}

São pequenos bugs como esses que são difíceis de identificar e fazem com que eu verifique de maneira agressiva se os valores correspondem a $null.

$null.Count

Se você tentar acessar uma propriedade em um valor $null, a propriedade também será $null. A propriedade count é a exceção a essa regra.

PS> $value = $null
PS> $value.count
0

Quando você tem um valor $null, então o count é 0. Essa propriedade especial é adicionada pelo PowerShell.

[PSCustomObject] Count

Quase todos os objetos do PowerShell têm essa propriedade Count. Uma exceção importante é o [PSCustomObject], no Windows PowerShell 5.1 (isso é corrigido no PowerShell 6.0). Ele não tem a propriedade Count, então você receberá o valor $null se tentar usá-la. Eu chamo a atenção para isso aqui a fim de que você não tente usar .Count em vez de uma verificação $null.

A execução deste exemplo no Windows PowerShell 5.1 e no PowerShell 6.0 vai gerar resultados diferentes.

$value = [PSCustomObject]@{Name='MyObject'}
if ( $value.count -eq 1 )
{
    "We have a value"
}

Nulo enumerável

Há um tipo especial de $null que funciona de maneira diferente dos demais. Vou chamá-lo de nulo enumerável, mas, na verdade, trata-se de um System.Management.Automation.Internal.AutomationNull. Esse nulo enumerável é o que você recebe como resultado de uma função ou bloco de script que não retorna nada (um resultado nulo).

PS> function Get-Nothing {}
PS> $nothing = Get-Nothing
PS> $null -eq $nothing
True

Se você o comparar com $null, receberá o valor $null. Quando usado em uma avaliação em que um valor seja obrigatório, o valor será sempre $null. Mas se você o colocar dentro de uma matriz, ele será tratado da mesma forma que uma matriz vazia.

PS> $containempty = @( @() )
PS> $containnothing = @($nothing)
PS> $containnull = @($null)

PS> $containempty.count
0
PS> $containnothing.count
0
PS> $containnull.count
1

Você pode ter uma matriz que contém um valor $null e sua count será 1. Mas se você inserir uma matriz vazia dentro de uma matriz, ela não será contada como um item. A contagem será 0.

Se você tratar o nulo enumerável como uma coleção, ela estará vazia.

Se você passa um nulo enumerável para um parâmetro de função que não é fortemente tipado, o PowerShell impõe um nulo enumerável ao valor $null por padrão. Isso significa que dentro da função, o valor é tratado como $null em vez do tipo System.Management.Automation.Internal.AutomationNull.

Pipeline

O principal local em que você vê a diferença é ao usar o pipeline. É possível armazenar em pipe um valor $null, mas não um nulo enumerável.

PS> $null | ForEach-Object{ Write-Output 'NULL Value' }
'NULL Value'
PS> $nothing | ForEach-Object{ Write-Output 'No Value' }

Dependendo do seu código, você deve considerar o $null em sua lógica.

Verifique se corresponde a $null, primeiro

  • Filtrar nulo no pipeline (... | Where {$null -ne $_} | ...)
  • Gerenciá-lo na função do pipeline

foreach

Um dos meus recursos favoritos do foreach é que ele não enumera em uma coleção $null.

foreach ( $node in $null )
{
    #skipped
}

Isso evita que eu precise verificar se a coleção corresponde a $null antes de enumerá-la. Se você tiver uma coleção de valores de $null, o $node ainda poderá ser $null.

O foreach começou a funcionar dessa forma no PowerShell 3.0. Se você estiver em uma versão mais antiga, então não será assim. Essa é uma das alterações importantes a serem observadas ao realizar a portabilidade do código para a compatibilidade com a versão 2.0.

Tipos de valor

Tecnicamente, apenas tipos de referência podem ser $null. Mas o PowerShell é muito generosa e permite que variáveis sejam de qualquer tipo. Se você decidir forçar a conversão de um tipo de valor, ele não poderá ser $null. O PowerShell converte $null em um valor padrão para muitos tipos.

PS> [int]$number = $null
PS> $number
0

PS> [bool]$boolean = $null
PS> $boolean
False

PS> [string]$string = $null
PS> $string -eq ''
True

Mas há alguns tipos que não têm uma conversão válida do $null. Esses tipos geram um erro de Cannot convert null to type.

PS> [datetime]$date = $null
Cannot convert null to type "System.DateTime".
At line:1 char:1
+ [datetime]$date = $null
+ ~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : MetadataError: (:) [], ArgumentTransformationMetadataException
    + FullyQualifiedErrorId : RuntimeException

Parâmetros de função

Forçar a conversão de valores em parâmetros de função é algo muito comum. Em geral, aprenderemos a definir os tipos dos parâmetros, mesmo se estivermos acostumados a não definir os tipos das outras variáveis do script. Talvez você já tenha algumas variáveis com a conversão de tipo forçada em suas funções e sequer tenha percebido.

function Do-Something
{
    param(
        [String] $Value
    )
}

Assim que você definir o tipo do parâmetro como string, o valor nunca mais poderá ser $null. É comum verificar se um valor é $null para ver se o usuário forneceu algum valor ou não.

if ( $null -ne $Value ){...}

$Value é uma cadeia de caracteres '' vazia quando nenhum valor é fornecido. Em vez disso, use a variável automática $PSBoundParameters.Value.

if ( $null -ne $PSBoundParameters.Value ){...}

$PSBoundParameters contém apenas os parâmetros que foram especificados quando a função foi chamada. Você também pode usar o método ContainsKey para verificar a propriedade.

if ( $PSBoundParameters.ContainsKey('Value') ){...}

IsNotNullOrEmpty

Se o valor for uma cadeia de caracteres, você poderá usar uma função de cadeia de caracteres estática para verificar se o valor é $null ou uma cadeia de caracteres vazia ao mesmo tempo.

if ( -not [string]::IsNullOrEmpty( $value ) ){...}

Eu me pego usando isso com frequência quando sei que o tipo de valor deve ser uma cadeia de caracteres.

Quando eu verifico se corresponde a $null

Eu sou um criador de scripts conservador. Sempre que eu chamo uma função e atribuo seu resultado a uma variável, eu a verifico se corresponde a $null.

$userList = Get-ADUser kevmar
if ($null -ne $userList){...}

Eu prefiro usar if ou foreach do que try/catch. Não me entenda mal, eu também uso try/catch com frequência. Mas se eu posso testar uma condição de erro ou um conjunto de resultados vazio, posso permitir que meu tratamento de exceção seja para exceções verdadeiras.

Também costumo verificar se corresponde a $null antes de indexar um valor ou chamar métodos em um objeto. Essas duas ações falham em caso de objeto $null, portanto, acho importante validá-las primeiro. Já abordei esses cenários anteriormente nesta postagem.

Cenário sem resultados

É importante saber que funções e comandos diferentes gerenciam o cenário sem resultados de maneira diferente. Muitos comandos do PowerShell retornam o nulo enumerável e um erro no fluxo de erros. Mas outros geram exceções ou fornecem um objeto de status. É sua responsabilidade saber como os comandos que você usa gerenciam os cenários sem resultados e os cenários de erro.

Inicializando com $null

Um hábito que eu adquiri foi o de inicializar todas as minhas variáveis antes de usá-las. Isso é algo obrigatório em outras linguagens de programação. Na parte superior da função ou sempre no início do loop de um foreach, eu defino todos os valores que estou usando.

Aqui está um cenário que desejo examinar mais de perto contigo. É um exemplo de bug que já precisei enfrentar anteriormente.

function Do-Something
{
    foreach ( $node in 1..6 )
    {
        try
        {
            $result = Get-Something -ID $node
        }
        catch
        {
            Write-Verbose "[$result] not valid"
        }

        if ( $null -ne $result )
        {
            Update-Something $result
        }
    }
}

A expectativa aqui é que Get-Something retorne um resultado ou um nulo enumerável. Se houver um erro, nós o registramos em um log. Em seguida, verificamos se recebemos um resultado válido antes de processá-lo.

O bug que se oculta nesse código é quando Get-Something gera uma exceção e não atribui um valor a $result. Ela falha antes da atribuição, portanto, nem chegamos a atribuir $null à variável $result. $result ainda contém a $result anterior válida, obtida em outras iterações. Update-Something vai executar várias vezes no mesmo objeto neste exemplo.

Passei a definir $result como $null dentro do loop do foreach antes de usá-la para contornar esse problema.

foreach ( $node in 1..6 )
{
    $result = $null
    try
    {
        ...

Problemas de escopo

Isso também ajuda a atenuar problemas de escopo. Nesse exemplo, atribuímos valores a $result repetidamente em um loop. Mas como o PowerShell permite que valores variáveis de fora da função sejam acessados no escopo da função atual, inicializá-los dentro da função atenua os bugs que podem ser introduzidos dessa maneira.

Uma variável não inicializada em sua função não será $null se ela estiver definida como um valor em um escopo superior. O escopo superior pode ser outra função que chama sua função e usa os mesmos nomes de variáveis, por exemplo.

Se eu pegar o mesmo exemplo de Do-something e remover o loop, acabarei com algo parecido com este exemplo:

function Invoke-Something
{
    $result = 'ParentScope'
    Do-Something
}

function Do-Something
{
    try
    {
        $result = Get-Something -ID $node
    }
    catch
    {
        Write-Verbose "[$result] not valid"
    }

    if ( $null -ne $result )
    {
        Update-Something $result
    }
}

Se a chamada à Get-Something gerar uma exceção, então minha verificação se corresponde a $null encontrará a $result de Invoke-Something. A inicialização do valor dentro da sua função atenua esse problema.

Dar nomes às variáveis é algo difícil, de modo que é comum um autor usar os mesmos nomes de variáveis em diferentes funções. Eu, por exemplo, uso $node, $result e $data o tempo todo. Portanto, é muito comum que valores de escopos diferentes acabem aparecendo em locais onde não deveriam.

Redirecionar a saída para $null

Tenho falado sobre valores $null ao longo de todo este artigo, mas o tópico não estaria completo se eu não mencionasse o redirecionamento da saída para $null. Há ocasiões em que determinados comandos geram informações ou objetos que você deseja suprimir. O redirecionamento da saída para $null faz exatamente isso.

Out-Null

O comando Out-Null é a maneira interna de redirecionar os dados do pipeline para $null.

New-Item -Type Directory -Path $path | Out-Null

Atribuir $null

Você pode atribuir $null aos resultados de um comando para alcançar o mesmo efeito do uso de Out-Null.

$null = New-Item -Type Directory -Path $path

Como $null é um valor constante, você jamais poderá substituí-lo. Não gosto da aparência dele em meus códigos, mas geralmente é executado mais rápido do que Out-Null.

Redirecionar para $null

Você também pode usar o operador de redirecionamento para enviar a saída para $null.

New-Item -Type Directory -Path $path > $null

Se você estiver lidando com executáveis de linha de comando que geram saída em diferentes fluxos. Você poderá redirecionar todos os fluxos de saída para $null da seguinte maneira:

git status *> $null

Resumo

Eu abordei muitos aspectos sobre este assunto e estou ciente de que este artigo ficou mais fragmentado do que a maior parte dos meus estudos de aprofundamento. Isso ocorre porque valores $null podem aparecer em vários locais diferentes no PowerShell e todas as nuances são específicas ao local em que você os encontra. Espero que você saia com uma compreensão melhor do $null e com uma boa noção dos cenários mais obscuros que pode encontrar pela frente.