Compartilhar via


Tudo o que você queria saber sobre $null

O PowerShell $null geralmente parece ser simples, mas tem muitas 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 kevin por compartilhar este conteúdo conosco. Confira o blog dele no PowerShellExplained.com.

O que é NULL?

Você pode pensar em NULL como um valor desconhecido ou vazio. Uma variável é 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.

PowerShell: $null

$null é uma variável automática no PowerShell usada para representar NULL. Você pode atribuí-lo a variáveis, usá-lo em comparações e usá-lo como um espaço reservado para NULL em uma coleção.

O PowerShell trata $null como um objeto com um valor NULL. Isso é diferente do que você pode esperar se você vir de outro idioma.

Exemplos de $null

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

PS> $null -eq $undefinedVariable
True

Se você digitar incorretamente um nome de variável, o PowerShell o verá como uma variável diferente e o valor será $null.

Outra maneira de localizar valores $null é quando eles vêm de outros comandos que não fornecem resultados.

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

Impacto do $null

$null os valores afetam seu código de forma diferente, dependendo de onde eles aparecem.

Em cadeias de caracteres

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

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

Esse é um dos motivos pelos quais gosto de colocar colchetes em torno de variáveis ao usá-las em mensagens de log. É ainda mais importante identificar as bordas dos valores da variável quando o valor estiver no final da cadeia de caracteres.

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

Isso facilita a localização de cadeias de caracteres vazias e valores $null vazios.

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 valores. Se você tentar indexar em uma coleção que, na verdade null, receberá este 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 esteja na coleção, obterá um $null resultado.

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

No lugar de um objeto

Se você tentar acessar uma propriedade ou sub-propriedade de um objeto que não tem a propriedade especificada, obterá um $null valor como faria 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 no objeto $null dispara 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 é lugares em que estou chamando um método em uma variável sem primeiro verificá-la para $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 prática recomendada do PowerShell. Há alguns cenários em que colocá-lo à direita não lhe dá o resultado esperado.

Examine este 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 é um array que contém um $null. O -eq verifica cada valor no array e retorna o $null correspondente. Isso é avaliado como $false. O -ne retorna tudo o que não corresponde $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, como também é possível criar um valor em que ambos sejam avaliados como $true. Mathias Jessen (@IISResetMe) tem um bom post que se aprofunda nesse cenário.

PSScriptAnalyzer e VS Code

O módulo PSScriptAnalyzer tem uma regra que verifica esse 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 PSScriptAnalyser, ele também realça ou identifica isso como um problema em seu script.

Verificação simples de 'if'

Uma maneira comum de as pessoas verificarem um valor não $null é usar uma instrução simples if() 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. Li essa linha de código como:

Se $value tiver um valor.

Mas essa não é a história toda. Essa linha está realmente dizendo:

Se $value não for $null, 0, $false, 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
}

Não há problema em usar uma verificação básica if , desde que você se lembre que esses outros valores contam como $false e não apenas que uma variável tenha um valor.

Encontrei esse problema ao refatorar algum código 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 do objeto somente se ela existisse. Na maioria das vezes, o objeto original tinha um valor que seria avaliado para $true na sentença if. Mas encontrei um problema em que o valor ocasionalmente não estava sendo definido. Depurei o código e descobri que o objeto tinha a propriedade, mas era um valor de cadeia de caracteres em branco. Isso impediu que ele fosse atualizado com a lógica anterior. Adicionei uma verificação adequada $null e tudo funcionou.

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

São pequenos bugs como esses que são difíceis de detectar e me fazem verificar agressivamente os valores para $null.

$null. Contar

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

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

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

[PSCustomObject] Contar

Quase todos os objetos no PowerShell têm essa Count propriedade. Uma exceção importante é o [pscustomobject] no Windows PowerShell 5.1 (Isso é corrigido no PowerShell 6.0). Ele não tem uma Count propriedade, portanto, você obterá um $null valor 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.

Executar este exemplo no Windows PowerShell 5.1 e no PowerShell 6.0 oferece 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 age de maneira diferente dos outros. Vou chamá-lo de nulo enumerável, mas é realmente um System.Management.Automation.Internal.AutomationNull. Esse nulo enumerável é aquele que você obtém 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ê compará-lo com $null, obterá um valor de $null. Quando usado em uma avaliação em que um valor é necessário, o valor é sempre $null. Mas se você colocá-lo dentro de uma matriz, ele é 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 que seu Count é 1. Mas se você colocar 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, ele estará vazio.

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 .

Canalização

O local principal em que você vê a diferença é ao usar o pipeline. Você pode canalizar um $null valor, mas não um valor 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 na 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 me poupa de ter $null que verificar a coleção antes de enumerá-la. Se você tiver uma coleção de $null valores, $node ainda poderá ser $null.

O foreach começou a funcionar dessa forma com o PowerShell 3.0. Se você estiver em uma versão mais antiga, esse não será o caso. 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, somente tipos de referência podem ser $null. Mas o PowerShell é muito generoso e permite que as variáveis sejam qualquer tipo. Se você decidir digitar fortemente 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

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

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. Geralmente, aprendemos a definir os tipos de nossos parâmetros, mesmo que tendemos a não definir os tipos de outras variáveis em nossos scripts. 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 um string, o valor nunca poderá ser $null. É comum verificar se um valor é $null para ver se o usuário forneceu um 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 $PSBoundParameters.Value automática.

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

$PSBoundParameters contém apenas os parâmetros 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 encontro usando isso muitas vezes quando sei que o tipo de valor deve ser uma cadeia de caracteres.

Quando eu verifico se corresponde a $null

Eu sou um scripter defensivo. 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){...}

Prefiro usar if ou foreach não usar try/catch. Não me entenda mal, ainda uso muito try/catch. Mas se eu puder testar uma condição de erro ou um conjunto vazio de resultados, posso permitir que meu tratamento de exceção seja para exceções verdadeiras.

Eu também costumo verificar $null antes de acessar um valor ou chamar métodos em um objeto. Essas duas ações falham em um $null objeto, portanto, acho importante validá-las primeiro. Já abordei esses cenários anteriormente neste post.

Nenhum cenário de resultados

É importante saber que diferentes funções e comandos lidam com o cenário sem resultados de forma 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. Ainda cabe a você saber como os comandos usados lidam com os cenários sem resultados e erros.

Inicializando com $null

Um hábito que eu peguei é inicializar todas as minhas variáveis antes de usá-las. Você precisa fazer isso em outros idiomas. Na parte superior da minha função ou quando inseri um foreach loop, defino todos os valores que estou usando.

Aqui está um cenário que eu quero que você dê uma olhada de perto. É um exemplo de bug que tive que resolver no passado.

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 retorna um resultado ou um nulo enumerável. Se houver um erro, faremos um registro. Em seguida, verificamos se obtemos um resultado válido antes de processá-lo.

O bug oculto nesse código é quando Get-Something lança uma exceção e não atribui um valor a $result. Falha antes da atribuição, então nem chegamos a atribuir $null à variável $result. $result ainda contém os valores válidos anteriores $result de outras iterações. Update-Something para executar várias vezes no mesmo objeto neste exemplo.

Eu defina $result para a $null direita dentro do foreach loop antes de usá-lo para atenuar esse problema.

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

Problemas de escopo

Isso também ajuda a atenuar problemas de escopo. Nesse exemplo, atribuimos valores a $result uma e outra vez em um loop. No entanto, como o PowerShell permite que valores variáveis de fora da função se infiltrem no escopo da função atual, inicializá-los dentro da sua função mitiga os bugs que podem ser introduzidos dessa forma.

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 esse mesmo Do-something exemplo e remover o loop, eu acabaria com algo semelhante a 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.

A nomenclatura de variáveis é difícil e é comum que um autor use os mesmos nomes de variáveis em várias funções. Eu sei que uso $node,$result,$data o tempo todo. Portanto, seria muito fácil que valores de escopos diferentes aparecessem em locais onde não deveriam estar.

Redirecionar a saída para $null

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

Out-Null

O comando Out-Null é o método integrado de redirecionar dados de pipeline para $null.

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

Atribuir $null

Você pode atribuir os resultados de um comando a $null e ter o mesmo efeito que usar Out-Null.

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

Como $null é um valor constante, você nunca pode substituí-lo. Não gosto da aparência dele no meu código, mas geralmente tem um desempenho 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ê pode redirecionar todos os fluxos de saída para $null a seguinte maneira:

git status *> $null

Resumo

Eu cobri muito terreno neste tema e sei que este artigo está mais fragmentado do que a maioria das minhas análises aprofundadas. Isso ocorre porque valores $null podem aparecer em muitos lugares diferentes no PowerShell e todas as nuances são específicas para onde 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.