关于 $null 的各项须知内容

PowerShell $null 似乎很简单,但它有很多细微差别。 让我们仔细了解 $null 一下,以便了解意外遇到 $null 值时会发生什么情况。

注释

本文的 原始版本 出现在 @KevinMarquette撰写的博客上。 PowerShell 团队感谢 Kevin 与我们共享此内容。 请在 PowerShellExplained.com查看他的博客。

什么是 NULL?

可以将 NULL 视为未知值或空值。 变量为 NULL,直到为其赋值或分配一个对象。 这一点可能很重要,因为有些命令需要一个值,如果值为 NULL,则生成错误。

PowerShell $null

$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。 这里的陷阱是,可以创建一个允许二者都为 $false$value

$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 不是 $null0$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
}

只要你记住其他值被视为 $false 而不只是变量具有值,就完全可以使用基本的 if 检查。

几天前重构一些代码时,我遇到了此问题。 它的基本属性检查如下所示。

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

仅当对象属性存在时,我才希望为对象属性赋值。 在大多数情况下,原始对象的值在 if 语句中的计算结果为 $true。 但我遇到了一个问题,即此值偶尔未设置。 我调试了代码,发现对象具有该属性,但它是一个空白字符串值。 这阻止了它使用以前的逻辑进行更新。 因此,我添加了一个适当的 $null 检查,一切才正常工作。

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

这类小 bug 很难发现,也促使我主动检查 $null 值。

$null.Count

如果尝试访问 $null 值的属性,则该属性也为 $null。 该 Count 属性是此规则的例外。

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

如果具有值 $null ,则为 Count0. 此特殊属性由 PowerShell 添加。

[PSCustomObject] 计数

PowerShell 中几乎所有对象都具有该 Count 属性。 Windows PowerShell 5.1 中的一个重要例外 [pscustomobject] (这是在 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

从 PowerShell 3.0 开始,foreach 就是这样工作的。 如果碰巧使用的是较旧版本,则情况并非如此。 这是回移植代码实现 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
    )
}

只要将参数的类型设置为 a 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){...}

我更喜欢使用ifforeach,而不是使用try/catch。 别误会,我仍然会大量使用 try/catch。 但是,如果我可以测试错误条件或空结果集,我可以允许我的异常处理适用于真正的异常。

我倾向于在对某个对象的值进行索引或调用方法之前,先检查 $null。 这两个动作在对象 $null 上失败,因此我觉得重要的是首先验证它们。 我已在本文前面介绍了这些方案。

无结果方案

请务必知道,不同的函数和命令以不同的方式处理无结果方案。 许多 PowerShell 命令返回可枚举 null 和错误流中的一个错误。 其他命令会引发异常或提供一个状态对象。 你仍需了解你使用的命令如何应对无结果和错误情况。

初始化至 $null

我选取的一个习惯是在使用变量之前初始化所有变量。 你需要用其他语言完成这件事。 在函数顶部或在进入 foreach 循环时,我会定义正在使用的所有值。

下面是一个我想仔细研究的方案。 这是我之前追踪的一个 bug 示例。

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 返回一个结果或一个可枚举的空值。 如果出现错误,我们会记录该错误。 然后,在处理结果之前,我们检查确保结果有效。

此代码中隐藏的 bug 是在 Get-Something 引发异常时,不会为 $result它赋值。 在赋值之前它就失败了,因此我们甚至不会将$null分配给$result变量。 $result 仍包含其他迭代的上一个有效 $result 值。 Update-Something 在本例中对同一对象执行多次。

我在 foreach 循环的内部设定 $result$null,然后再用它来缓解此问题。

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 检查将从 Invoke-Something 找到 $result。 初始化函数中的值可缓解此问题。

命名变量很难,并且作者在多个函数中使用相同的变量名称很常见。 我知道我一直使用$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,并意识到你可能会遇到更令人费解的情形。