Все, что нужно знать о $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. Возможны ситуации, когда размещение $null справа не дает ожидаемого результата.
Взгляните на следующий пример и постарайтесь предсказать результаты:
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
. Матиас Йессен (Mathias Jessen, @IISResetMe) опубликовал хорошую статью на эту тему.
PSScriptAnalyzer и VSCode
Модуль 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.Count
Если вы попытаетесь получить доступ к свойству по значению $null
, это свойство также будет $null
. Свойство count
является исключением из этого правила.
PS> $value = $null
PS> $value.count
0
Если у вас есть значение $null
, то count
равняется 0
. Это специальное свойство добавляется PowerShell.
[PSCustomObject] Count
Почти у всех объектов в 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
. Но если поместить пустой массив внутри массива, он не считается элементом. а Count будет иметь значение 0
.
Если вы обрабатываете перечисленное значение NULL как коллекцию, то она пуста.
Если вы передаете значение NULL в параметр функции, который не является строго типизированным, PowerShell по умолчанию принудивает перечисленное значение NULL к $null
значению. Это означает, что внутри функции значение рассматривается как $null
не тип System.Management.Automation.Internal.AutomationNull .
Pipeline
Различия особенно заметны при работе с конвейером. Можно передать $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') ){...}
IsNotNullOrEmpty
Если значение является строкой, можно использовать статическую строковую функцию для проверки того, является ли значение $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
значение. Выполнение кода завершается ошибкой до назначения, поэтому переменной $result
даже не присваивается значение $null
. Переменная $result
сохраняет допустимое значение $result
из предыдущих итераций.
В этом примере Update-Something
выполняется несколько раз с одним и тем же объектом.
Устранить проблему мне удалось после того, как я задал $null
для $result
прямо в цикле 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, но статья получилась несколько отрывочной. Все потому, что значения $null
могут встречаться повсюду в PowerShell, и их трактовка будет обусловлена конкретным местом. Надеюсь, теперь вы лучше разобрались с $null
и непонятными ситуациями, где это состояние может повстречаться.
PowerShell