Поделиться через


Все, что вы хотели знать о $null

PowerShell $null часто кажется простым, но существует множество нюансов. Давайте рассмотрим $null, чтобы узнать, что происходит при неожиданном возникновении значения $null.

Примечание.

Оригинальная версия этой статьи появилась в блоге, написанном @KevinMarquette. Команда PowerShell благодарит Кевина за предоставление этого содержимого нам. Пожалуйста, ознакомьтесь с его блогом на PowerShellExplained.com.

Что такое NULL?

Значение NULL можно рассматривать как неизвестное или пустое значение. Переменная имеет значение NULL, пока не назначьте ему значение или объект. Это может быть важно, так как существуют некоторые команды, требующие значения и создающие ошибки, если значение равно NULL.

$null PowerShell

$null — это автоматическая переменная в PowerShell, используемая для представления NULL. Вы можете назначить его переменным, использовать его в сравнениях и использовать его в качестве держателя места для NULL в коллекции.

PowerShell обрабатывает $null как объект со значением NULL. Это отличается от того, что вы можете ожидать, если вы исходите из другого языка.

Примеры $null

В любой момент, когда вы пытаетесь использовать переменную, которую вы не инициализировали, значение равно $null. Это один из наиболее распространенных способов, как значения $null проникают в ваш код.

PS> $null -eq $undefinedVariable
True

Если вы случайно наберете имя переменной с ошибкой, то PowerShell воспринимает ее как другую переменную, и присваивает ей значение $null.

Другой способ получения значений $null заключается в том, что они поступают из других команд, которые не дают никаких результатов.

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

Влияние $null

$null значения влияют на код по-разному в зависимости от того, где они отображаются.

В строках

Если вы используете $null в строке, это пустое значение (или пустая строка).

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

Это одна из причин, по которым мне нравится размещать скобки вокруг переменных при их использовании в сообщениях журнала. Еще более важно определить края значений переменной, когда значение находится в конце строки.

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

Это упрощает обнаружение пустых строк и значений $null.

В числовом уравнении

Если значение $null используется в числовом уравнении, и при этом не возникает ошибка, то результаты недействительны. Иногда $null принимает значение 0, а в других случаях делает весь результат $null. Ниже приведен пример умножения, который дает 0 или $null в зависимости от порядка значений.

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

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

Вместо коллекции

Коллекция позволяет использовать индекс для доступа к значениям. Если вы пытаетесь осуществить индексирование в коллекцию, которая фактически null, то вы получите эту ошибку: 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

Если у вас есть коллекция, но вы пытаетесь получить доступ к элементу, который не находится в коллекции, вы получите $null результат.

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

Вместо объекта

Если вы пытаетесь получить доступ к свойству или дочернему свойству объекта, который не имеет указанного свойства, вы получите значение $null, например, для неопределенной переменной. Не имеет значения, является ли переменная $null или фактическим объектом в данном случае.

PS> $null -eq $undefined.Some.Fake.Property
True

PS> $date = Get-Date
PS> $null -eq $date.Some.Fake.Property
True

Метод для выражения с значением NULL

Вызов метода для объекта $null вызывает 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

Всякий раз, когда я вижу фразу You cannot call a method on a null-valued expression, то первое, что я ищу, — это места, где я вызываю метод для переменной, не проверяя её на $null.

Проверка значения $null

Возможно, вы заметили, что я всегда размещаю $null слева при проверке $null в моих примерах. Это намеренно и принято в качестве рекомендации PowerShell. Есть некоторые сценарии, где размещение его справа не дает ожидаемый результат.

Просмотрите следующий пример и попробуйте спрогнозировать результаты:

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

Если я не определю $value, первое значение вычисляется как $true, и наше сообщение становится The array is $null. Ловушка здесь заключается в том, что можно создать $value, которое позволяет обоим из них быть $false.

$value = @( $null )

В этом случае $value представляет собой массив, содержащий $null. -eq проверяет каждое значение в массиве и возвращает соответствующий $null. Это выражается как $false. -ne возвращает все, что не соответствует $null, и в этом случае нет результатов (это также оценивается как $false). Ни один из них не $true, хотя кажется, что один из них должен быть.

Мы можем не только создать значение, которое заставляет их обоих принять $false, но и создать значение, где оба они принимают $true. Матиас Джессен (@IISResetMe) опубликовал хороший пост, который подробно рассматривает этот сценарий.

PSScriptAnalyzer и VS Code

Модуль PSScriptAnalyzer содержит правило, которое проверяет наличие этой проблемы с именем PSPossibleIncorrectComparisonWithNull.

PS> Invoke-ScriptAnalyzer ./myscript.ps1

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

Так как VS Code также использует правила PSScriptAnalyser, он также выделяет или идентифицирует это как проблему в скрипте.

Простая проверка с условием "if"

Распространенный способ проверки значения, отличного от $null, заключается в использовании простой инструкции if() без сравнения.

if ( $value )
{
    Do-Something
}

Если значение равно $null, это преобразуется в $false. Это легко читать, но будьте осторожны: убедитесь, что он ищет именно то, чего вы ожидаете. Я прочитал эту строку кода следующим образом:

Если $value имеет значение.

Но это не вся история. Эта строка на самом деле говорит:

Если $value не является $null и 0 и $false и пустой строкой, и пустым массивом.

Ниже приведен более полный пример этой инструкции.

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
}

Совершенно нормально использовать базовую проверку if при условии, что вы помните, что эти другие значения считаются как $false, а не просто то, что переменная имеет значение.

Я столкнулась с этой проблемой при рефакторинге некоторого кода несколько дней назад. У него была базовая проверка свойства, например.

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

Я хотел назначить значение свойству объекта только в том случае, если оно существует. В большинстве случаев исходный объект имел значение, которое будет оцениваться как $true в инструкции if. Но я столкнулся с проблемой, когда значение иногда не устанавливалось. Я отладил код и обнаружил, что объект имел свойство, но это было пустое строковое значение. Это не позволило ему обновиться по предыдущей логике. Я добавил правильную проверку $null, и все заработало.

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

Это такие мелкие ошибки, которые трудно обнаружить и из-за которых я начинаю внимательно проверять значения $null.

$null.Количество

Если вы пытаетесь получить доступ к свойству значения $null, это свойство также является $null. Свойство Count является исключением из этого правила.

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

Когда у вас есть значение $null, тогда Count равно 0. Это специальное свойство добавляется PowerShellом.

[PSCustomObject] Количество

Почти все объекты в PowerShell имеют это свойство Count. Одним из важных исключений является [pscustomobject] в Windows PowerShell 5.1 (это исправлено в PowerShell 6.0). У него нет свойства Count, поэтому вы получите $null значение, если вы пытаетесь использовать его. Я указываю на это здесь, чтобы вы не пытались использовать Count вместо проверки $null.

Выполнение этого примера в Windows PowerShell 5.1 и PowerShell 6.0 дает различные результаты.

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

Перечисление 'null'

Существует один особый тип $null, который действует не так, как другие. Я собираюсь назвать объект перечисляемым null, но это действительно System.Management.Automation.Internal.AutomationNull. Это перечисленное значение NULL является результатом функции или блока скрипта, возвращающего ничего (пустой результат).

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

Если сравнить его с $null, вы получите значение $null. При использовании в оценке, когда требуется значение, оно всегда равно $null. Но если поместить его в массив, он рассматривается так же, как пустой массив.

PS> $containEmpty = @( @() )
PS> $containNothing = @($nothing)
PS> $containNull = @($null)

PS> $containEmpty.Count
0
PS> $containNothing.Count
0
PS> $containNull.Count
1

Массив может содержать одно значение $null, и его Count является 1. Но если поместить пустой массив внутри массива, он не считается элементом. Счёт 0.

Если вы обрабатываете перечисляемое значение null как коллекцию, то она будет пустой.

Если вы передаете перечисляемое null в параметр функции, который не является строго типизированным, PowerShell по умолчанию преобразует перечисляемое null в значение $null. Это означает, что внутри функции значение обрабатывается как $null вместо типа System.Management.Automation.Internal.AutomationNull.

Трубопровод

Основное место, где вы видите разницу, заключается в использовании конвейера. Можно передать значение $null, но не перечисляемое значение null.

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

В зависимости от вашего кода, следует учесть $null в своей логике.

Либо сначала проверьте наличие $null

  • Удаление null на конвейере (... | where {$null -ne $_} | ...)
  • Обработайте его в функции конвейера

foreach

Одна из моих любимых функций foreach заключается в том, что она не перебирает коллекцию $null.

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

Это спасает меня от необходимости $null проверить коллекцию перед перечислением. Если у вас есть коллекция значений $null, $node по-прежнему может быть $null.

foreach начал работать таким образом с PowerShell 3.0. Если вы используете более старую версию, то это не актуально. Это одно из важных изменений, которые следует учитывать при обратном переносе кода для совместимости 2.0.

Типы значений

Теоретически только ссылочные типы могут быть $null. Но PowerShell очень щедрый и позволяет переменным быть любым типом. Если вы решите жестко определить тип значения, его нельзя будет $null. PowerShell преобразует $null в значение по умолчанию для многих типов.

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

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

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

Существуют некоторые типы, которые не имеют корректного преобразования из $null. Эти типы создают ошибку 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

Параметры функции

Использование строго типизированных значений в параметрах функции очень распространено. Как правило, мы научимся определять типы наших параметров, даже если мы, как правило, не определяем типы других переменных в наших скриптах. Возможно, у вас уже есть некоторые строго типизированные переменные в ваших функциях, и вы даже этого не осознаете.

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

После установки типа параметра в качестве stringзначение никогда не может быть $null. Принято проверять, равно ли значение $null, чтобы понять, предоставил ли пользователь данные.

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

$Value является пустой строкой '', если значение не указано. Вместо этого используйте автоматическую переменную $PSBoundParameters.Value.

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

$PSBoundParameters содержит только параметры, указанные при вызове функции. Для проверки свойства можно также использовать метод ContainsKey.

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

НеЯвляетсяПустымИлиНулевым

Если значение является строкой, можно использовать статическую строковую функцию для проверки того, является ли значение $null или пустой строкой одновременно.

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

Я часто использую это, когда знаю, что тип значения должен быть строкой.

Когда я $null проверяю

Я пишу скрипты для защиты. Каждый раз, когда я вызываю функцию и назначаю ее переменной, я проверяю ее на $null.

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

Я предпочитаю использовать if или foreach по сравнению с try/catch. Не поймите меня неправильно, я все еще много использую try/catch. Но если я могу проверить состояние ошибки или пустой набор результатов, можно разрешить обработку исключений для истинных исключений.

Я также, как правило, проверяю наличие $null перед индексированием значения или вызовом методов объекта. Эти два действия завершаются ошибкой для объекта $null, поэтому я считаю, что важно сначала проверить их. Я уже рассмотрел эти сценарии ранее в этом посте.

Нет сценария результатов

Важно знать, что различные функции и команды обрабатывают сценарий отсутствия результатов по-разному. Многие команды PowerShell возвращают перечисленное значение NULL и ошибку в потоке ошибок. Но другие выбрасывают исключения или предоставляют соответствующий объект состояния. Вам по-прежнему необходимо понимать, как команды, которые вы используете, справляются в случае отсутствия результатов и возникновения ошибок.

Инициализация в $null

Одна из привычек, которую я приобрёл, — это инициализировать все мои переменные перед их использованием. Это необходимо сделать на других языках. В начале функции или по мере входа в цикл foreach, я определяю все используемые значения.

Вот сценарий, на который я хочу, чтобы вы внимательно посмотрели. Это пример ошибки, которую мне пришлось отслеживать до этого.

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

Ожидается, что Get-Something возвращает либо результат, либо перечисляемый NULL. Если возникла ошибка, мы регистрируем ее. Затем мы проверяем, чтобы убедиться, что мы получили допустимый результат перед обработкой.

Ошибка, скрытая в этом коде, заключается в том, что Get-Something создает исключение и не назначает значение $result. Он завершается ошибкой до назначения, поэтому мы даже не назначаем $null переменной $result. $result всё ещё содержит предыдущие валидные $result из других итераций. Update-Something выполнить несколько раз на одном и том же объекте в этом примере.

Я устанавливаю $result$null прямо внутри цикла foreach, прежде чем использовать его для устранения этой проблемы.

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

Проблемы с масштабом

Это также помогает устранить проблемы с областью охвата. В этом примере мы присваиваем значения $result снова и снова в цикле. Но так как PowerShell позволяет переменным значениям извне функции входить в область текущей функции, инициализация их внутри функции устраняет ошибки, которые можно ввести таким образом.

Неинициализированная переменная в вашей функции не будет равна $null, если в родительской области ей присвоено значение. Родительская область видимости может быть другой функцией, которая вызывает вашу функцию и использует те же имена переменных.

Если я возьму тот же пример Do-something и удалю цикл, я получу в итоге что-то похожее на этот пример.

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

Если вызов Get-Something вызывает исключение, то проверка $null находит $result из Invoke-Something. Инициализация переменной внутри функции устраняет эту проблему.

Именование переменных является сложной задачей, и авторы часто используют одни и те же имена переменных в разных функциях. Я знаю, что я использую $node,$result,$data все время. Таким образом, значения из разных областей могут легко появляться там, где это не предусмотрено.

Перенаправление выходных данных в $null

Я говорил о значениях $null на протяжении всей этой статьи, но тема не завершена, если не упомяну перенаправление выходных данных в $null. Иногда возникают команды, которые выводят информацию или объекты, которые нужно отключить. Перенаправление выходных данных в $null осуществляет это действие.

Out-Null

Команда Out-Null — это встроенный способ перенаправления данных конвейера в $null.

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

Назначить $null

Результаты команды можно присвоить переменной $null для того же эффекта, что и с помощью Out-Null.

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

Так как $null является константным значением, его нельзя перезаписать. Мне не нравится, как он выглядит в моем коде, но он часто выполняется быстрее, чем Out-Null.

Перенаправление на $null

Можно также использовать оператор перенаправления для отправки выходных данных в $null.

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

Если вы работаете с исполняемыми файлами командной строки, которые выводят данные в разные потоки. Вы можете перенаправить все выходные потоки в $null следующим образом:

git status *> $null

Сводка

Я охватил множество тем на этот раз и знаю, что эта статья более фрагментирована, чем большинство моих детальных исследований. Это связано с тем, что $null-значения могут появляться во многих разных местах в PowerShell, и все нюансы зависят от того, где они появляются. Я надеюсь, что вы уйдете от этого с лучшим пониманием $null и осведомленностью о более неясных сценариях, с которыми вы можете столкнуться.