Tutto quello che c'è da sapere su $null

$null di PowerShell spesso sembra semplice ma presenta molte sfumature. Diamo un'occhiata a $null in modo da sapere cosa accade quando ci si imbatte in modo imprevisto in un valore $null.

Nota

La versione originale di questo articolo è apparsa sul blog scritto da @KevinMarquette. Il team di PowerShell ringrazia Kevin per averne condiviso il contenuto. È possibile visitare il suo blog all'indirizzo PowerShellExplained.com.

Che cos’è NULL?

È possibile considerare NULL come un valore sconosciuto o vuoto. Una variabile è NULL finché non le viene assegnato un valore o un oggetto. Questo può essere importante perché sono disponibili alcuni comandi che richiedono un valore e generano errori se il valore è NULL.

PowerShell $null

$null è una variabile automatica in PowerShell utilizzata per rappresentare NULL. È possibile assegnarla a variabili, utilizzarla nei confronti e utilizzarla come segnaposto per NULL in una raccolta.

PowerShell considera $null come un oggetto con valore NULL. Questo comportamento è diverso rispetto a quello che si può prevedere se si è abituati a usare un altro linguaggio.

Esempi di $null

Quando si tenta di usare una variabile che non è stata inizializzata, il valore è $null. Questo è uno dei modi più comuni in cui i valori $null si intrufolano nel codice.

PS> $null -eq $undefinedVariable
True

Se si digita il nome di una variabile in modo scorretto, PowerShell la considera come una variabile diversa con valore $null.

Un altro motivo per cui si incontrano valori $null è quando provengono da altri comandi che non forniscono alcun risultato.

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

Conseguenze di $null

I valori $null influiscono sul codice in modo diverso a seconda della posizione in cui compaiono.

Nelle stringhe

Se si usa $null in una stringa, è un valore vuoto (o stringa vuota).

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

Questo è uno dei motivi per cui può essere utile inserire parentesi quadre tra le variabili quando vengono usate nei messaggi di log. È ancora più importante identificare gli estremi dei valori delle variabili quando il valore si trova alla fine della stringa.

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

In questo modo è facile individuare stringhe vuote e valori $null.

Nell’equazione numerica

Quando un valore $null viene usato in un'equazione numerica, i risultati non sono validi se non restituiscono un errore. In alcuni casi $null restituisce 0 e altre volte rende l'intero risultato $null. Di seguito è riportato un esempio con una moltiplicazione che restituisce 0 o $null in base all'ordine dei valori.

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

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

Al posto di una raccolta

Una raccolta consente di usare un indice per accedere ai valori. Se si tenta di eseguire l'indicizzazione in una raccolta effettivamente null, viene visualizzato questo errore: 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 si dispone di una raccolta, ma si tenta di accedere a un elemento che non è presente nella raccolta, si ottiene un risultato $null.

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

Al posto di un oggetto

Se si tenta di accedere a una proprietà o a una proprietà secondaria di un oggetto che non dispone della proprietà specificata, si ottiene un valore $null come per una variabile non definita. Non è importante se la variabile è $null o un oggetto effettivo in questo caso.

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

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

Metodo su un'espressione con valori null

La chiamata di un metodo su un oggetto $null genera un 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

Ogni volta che viene visualizzata la frase You cannot call a method on a null-valued expression la prima cosa che si cerca sono i punti in cui si chiama un metodo su una variabile senza prima verificare $null.

Ricerca di $null

È possibile che si sia notato che $null viene sempre posizionato a sinistra quando si verificano $null negli esempi. Questo comportamento è intenzionale e accettato come procedura consigliata per PowerShell. Esistono alcuni scenari in cui il posizionamento a destra non fornisce il risultato previsto.

Guardiamo l'esempio seguente e proviamo a stimare i risultati:

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

Se non si definisce $value, il primo restituisce $true e il messaggio è The array is $null. Il problema qui è che è possibile creare un $value che consente a entrambi di essere $false

$value = @( $null )

In questo caso, $value è una matrice che contiene un $null. -eq controlla ogni valore nella matrice e restituisce il $null corrispondente. Restituisce $false. -ne restituisce tutti gli elementi che non corrispondono a $null e in questo caso non sono presenti risultati (questo restituisce anche $false). Nessuno dei due è $true anche se sembra che uno di essi debba esserlo.

Non solo è possibile creare un valore che fa in modo che entrambi restituiscano $false, ma è anche possibile creare un valore in cui entrambi restituiscono $true. Mathias Jessen (@IISResetMe) ha un buon post che si immerge in questo scenario.

PSScriptAnalyzer e VSCode

Il modulo PSScriptAnalyzer dispone di una regola che verifica la presenza di questo problema denominato PSPossibleIncorrectComparisonWithNull.

PS> Invoke-ScriptAnalyzer ./myscript.ps1

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

Poiché VS Code usa anche le regole di PSScriptAnalyser, evidenzia o identifica questo come un problema nello script.

Semplice controllo If

Un modo comune per verificare la presenza di un valore non $null consiste nell'utilizzare una semplice istruzione if() senza il confronto.

if ( $value )
{
    Do-Something
}

Se il valore è $null, viene restituito $false. Questa operazione è facile da leggere, ma è opportuno prestare attenzione a cercare esattamente ciò che ci si aspetta. Questa riga di codice è stata letta come:

Se $value dispone di un valore.

Ma non è tutto qui. Questa riga dice effettivamente:

Se $value non $null è o 0 o $false o una stringa vuota o una matrice vuota.

Di seguito è riportato un esempio più completo di tale istruzione.

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
}

È perfettamente accettabile usare un controllo di if di base purché si ricordi che gli altri valori vengono conteggiati come $false e non solo che una variabile ha un valore.

L’autore ha riscontrato questo problema durante il refactoring del codice alcuni giorni fa. Presentava un controllo delle proprietà di base simile al seguente.

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

Si desiderava assegnare un valore alla proprietà dell'oggetto solo se esisteva. Nella maggior parte dei casi, l'oggetto originale aveva un valore che restituiva $true nell'istruzione if. Tuttavia, si è verificato un problema per cui il valore talvolta non era impostato. È stato eseguito il debug del codice e si è scoperto che l'oggetto aveva la proprietà, ma era un valore stringa vuoto. In questo modo non era possibile aggiornarlo con la logica precedente. Quindi, è stato aggiunto un controllo $null appropriato e ha funzionato.

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

Si tratta di piccoli bug, come questi, difficili da individuare e che inducono controllare in modo aggressivo i valori per $null.

$null.Count

Se si tenta di accedere a una proprietà su un valore $null, anche la proprietà è $null. La proprietà count costituisce l'unica eccezione a questa regola.

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

Quando si dispone di un valore $null, allora count è 0. Questa proprietà speciale viene aggiunta da PowerShell.

[PSCustomObject] Count

Quasi tutti gli oggetti in PowerShell hanno la proprietà Count. Un'eccezione importante è [PSCustomObject] in Windows PowerShell 5.1 (questo problema è stato risolto in PowerShell 6.0). Non dispone di una proprietà Count, pertanto si ottiene un valore $null se si tenta di usarlo. Lo sottolineiamo in modo che l’utente non provi a usare .Count anziché un controllo di $null.

L'esecuzione di questo esempio in Windows PowerShell 5.1 e PowerShell 6.0 fornisce risultati diversi.

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

Null vuoto

Esiste un tipo speciale di $null che agisce in modo diverso rispetto agli altri. Lo chiameremo $null vuoto, ma si tratta effettivamente di un System.Management.Automation.Internal.AutomationNull. Questo $null vuoto è quello che si ottiene come risultato di una funzione o di un blocco di script che non restituisce alcun valore (risultato void).

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

Se viene confrontato con $null, si ottiene un valore di $null. Quando viene usato in una valutazione in cui è richiesto un valore, il valore è sempre $null. Tuttavia, se si inserisce l'oggetto all'interno di una matrice, questa viene considerato come una matrice vuota.

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

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

È possibile disporre di una matrice che contiene un valore $null e il count è 1. Tuttavia, se si inserisce un risultato vuoto all'interno di una matrice, questo non viene conteggiato come elemento. Il conteggio è 0.

Se si considera il $null vuoto come una raccolta, allora è vuota.

Se si passa un valore vuoto a un parametro di funzione che non è fortemente tipizzato, PowerShell imposta il valore nothing in un $null valore per impostazione predefinita. Ciò significa che all'interno della funzione, il valore verrà considerato come $null anziché il tipo System.Management.Automation.Internal.AutomationNull .

Pipeline

Il punto principale in cui si verifica la differenza è quando si usa la pipeline. È possibile inviare tramite pipe un valore $null ma non un valore $null vuoto.

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

A seconda del codice, è necessario tenere conto di $null nella logica.

Verificare prima di tutto $null

  • Filtrare i valori null sulla pipeline (... | Where {$null -ne $_} | ...)
  • Gestire nella funzione della pipeline

foreach

Una delle caratteristiche che preferisco di foreach è il fatto che non esegue l'enumerazione di una raccolta di $null.

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

In questo modo si evita di dover controllare $null nella raccolta prima di enumerarla. Se si dispone di una raccolta di valori di $null, è ancora possibile che $node sia $null.

Foreach ha iniziato a funzionare in questo modo con PowerShell 3.0. Se si utilizza una versione precedente, questo non accade. Si tratta di una delle modifiche importanti da tenere presente quando si esegue il back-porting del codice per la compatibilità 2.0.

Tipi valore

Tecnicamente, solo i tipi di riferimento possono essere $null. Ma PowerShell è molto generoso e consente alle variabili di essere di qualsiasi tipo. Se si decide di tipizzare fortemente un tipo di valore, questo non può essere $null. PowerShell converte $null in un valore predefinito per molti tipi.

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

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

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

Esistono alcuni tipi che non dispongono di una conversione valida da $null. Questi tipi generano un errore 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

Parametri di funzione

L'uso di un valore fortemente tipizzato nei parametri della funzione è molto comune. In genere impariamo a definire i tipi di parametri anche se tendiamo a non definire i tipi di altre variabili negli script. È possibile che le funzioni presentino già alcune variabili fortemente tipizzate e che non ce ne si renda neanche conto.

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

Non appena si imposta il tipo di parametro come string, il valore non può mai essere $null. È comune verificare se un valore è $null per verificare se l'utente ha fornito un valore o meno.

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

$Value è una stringa vuota '' se non viene specificato alcun valore. In alternativa, usare la variabile automatica $PSBoundParameters.Value.

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

$PSBoundParameters contiene solo i parametri specificati quando è stata chiamata la funzione. È anche possibile usare il metodo ContainsKey per verificare la proprietà.

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

IsNotNullOrEmpty

Se il valore è una stringa, è possibile usare una funzione di stringa statica per verificare se il valore è $null o una stringa vuota nello stesso momento.

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

Mi trovo spesso a usare questo approccio quando so che il tipo di valore deve essere una stringa.

Quando si esegue il controllo $null

All’autore dell’articolo piace andare sul sicuro. Ogni volta che si chiama una funzione e la si assegna a una variabile, si esegue la ricerca di $null.

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

Si preferisce usare if o foreach rispetto a try/catch. try/catch viene comunque usato spesso. Tuttavia, se è possibile verificare la presenza di una condizione di errore o di un set di risultati vuoto, è possibile consentire la gestione delle eccezioni per le eccezioni true.

Inoltre si tende a controllare $null prima di indicizzare un valore o chiamare metodi su un oggetto. Queste due azioni hanno esito negativo per un oggetto $null, quindi è importante convalidarle per prime. Questi scenari sono già stati affrontati in precedenza in questo post.

Scenario senza risultati

È importante tenere presente che funzioni e comandi diversi gestiscono lo scenario senza risultati in modo diverso. Molti comandi di PowerShell restituiscono il $null vuoto e un errore nel flusso di errori. Altri, invece, generano eccezioni o forniscono un oggetto di stato. È comunque l’utente a dover sapere in che modo i comandi usati gestiscono gli scenari senza risultati e errori.

Inizializzazione a $null

Un'abitudine che è possibile sviluppare è quella di eseguire l'inizializzazione di tutte le variabili prima di usarle. Questa operazione è necessaria in altri linguaggi. Nella parte superiore della funzione o quando si immette un ciclo foreach, si definiscono tutti i valori che si stanno usando.

Ecco uno scenario che è utile esaminare attentamente. Si tratta di un esempio di un bug che l’autore ha dovuto risolvere in passato.

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
        }
    }
}

La previsione è che Get-Something restituirà un risultato o un $null vuoto. Se si verifica un errore, lo si registra. Verifichiamo quindi di avere ottenuto un risultato valido prima di elaborarlo.

Il bug nascosto in questo codice si verifica quando Get-Something genera un'eccezione e non assegna un valore a $result. Si verifica un errore prima dell'assegnazione, quindi non viene nemmeno assegnata $null alla variabile $result. $result contiene ancora i $result validi precedenti di altre iterazioni. Update-Something per eseguire più volte sullo stesso oggetto in questo esempio.

Ho impostato $result su $null all'interno del ciclo foreach prima di usarlo per attenuare il problema.

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

Problemi di ambito

Questo consente anche di attenuare i problemi di ambito. In questo esempio, i valori vengono assegnati a $result più volte in un ciclo. Tuttavia, poiché PowerShell consente ai valori variabili dall'esterno della funzione di inserirsi nell'ambito della funzione corrente, l'inizializzazione all'interno della funzione attenua i bug che possono essere introdotti in questo modo.

Una variabile non inizializzata nella funzione non è $null se è impostata su un valore in un ambito padre. L'ambito padre può essere un'altra funzione che chiama la funzione e usa gli stessi nomi di variabile.

Se prendessimo lo stesso esempio Do-something ed eliminassimo il ciclo, otterremmo un risultato simile all'esempio seguente:

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 la chiamata a Get-Something genera un'eccezione, il controllo $null trova $result da Invoke-Something. L'inizializzazione del valore all'interno della funzione attenua questo problema.

L’assegnazione di un nome alle variabili è difficile ed è frequente che un autore usi gli stessi nomi di variabile in più funzioni. L’autore dell’articolo, per esempio, usa sempre $node,$result,$data. È quindi molto facile che i valori di ambiti diversi appaiano in posizioni in cui non dovrebbero essere.

Reindirizzamento dell'output a $null

Abbiamo parlato dei valori $null in tutto articolo, ma l'argomento non è completo se non parliamo del reindirizzamento dell'output a $null. A volte abbiamo comandi che restituiscono informazioni o oggetti che desideriamo eliminare. Il reindirizzamento dell'output a $null esegue questa operazione.

Out-Null

Il comando out-null è il modo predefinito per reindirizzare i dati della pipeline a $null.

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

Assegnazione a $null

È possibile assegnare i risultati di un comando a $null per ottenere lo stesso effetto dell'uso di Out-Null.

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

Poiché $null è un valore costante, non è mai possibile sovrascriverlo. Non è particolarmente bello il modo in cui viene visualizzato nel codice, ma spesso viene eseguito più velocemente di Out-Null.

Reindirizzamento a $null

È anche possibile usare l'operatore di reindirizzamento per inviare l'output a $null.

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

Se si usano file eseguibili da riga di comando che restituiscono flussi diversi, è possibile reindirizzare tutti i flussi di output a $null come segue:

git status *> $null

Riepilogo

Abbiamo parlato di molti argomenti in questo articolo e si tratta di un articolo più frammentato rispetto alla maggior parte delle solite analisi approfondite. Ciò è dovuto al fatto che i valori $null possono apparire in molte posizioni diverse in PowerShell e le sfumature sono specifiche per la posizione in cui si trovano. Ci auguriamo che abbiate ottenuto una migliore comprensione di $null e una consapevolezza degli scenari più oscuri che potreste incontrare.