在 PowerShell 中,为什么 $null -lt 0 = $true?那可靠吗?

In PowerShell, why is $null -lt 0 = $true? Is that reliable?

考虑以下 PowerShell 代码:

> $null -gt 0
False
> $null -ge 0
False
> $null -eq 0
False
> $null -le 0
True
> $null -lt 0
True

当然,对于显式设置为 $null 或不存在的变量的 $variable 也是如此。

  1. 这是为什么?这对我来说没有多大意义。我觉得 $null 根据定义没有一个可以这样测试的值,或者至少在这样的测试中它会评估为零。但除此之外,我想我不知道我实际期望的行为是什么。谷歌搜索(或搜索 SO)例如“Why is null less than zero in Powershell”似乎没有产生任何结果,但我确实看到了其他几种语言的相关问题和答案。
  2. 可以并且应该这个结果可以依赖吗?
  3. 除了使用 GetType() 或“IsNumeric”、“IsNullOrEmpty”等的各种实现来测试变量之外,什么是可靠测试的最佳(即最简洁、性能最佳等)方法变量中可能具有 $null 值的整数值(或其他类型)?还是其中一种方法被认为是非常标准的?

感谢您的宝贵时间。如果这个问题对这个场地来说太“模糊”,请提前道歉。

P.S。值得一提的是,我常用的环境是 PowerShell v5.1。

测试$null比较结果

(0).GetType()
('').GetType()
(' ').GetType()
($null).GetType()
# Results
<#
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Int32                                    System.ValueType
True     True     String                                   System.Object
True     True     String                                   System.Object
You cannot call a method on a null-valued expression.
#>

Measure-Object -InputObject (0).GetType()
Measure-Object -InputObject ('').GetType()
Measure-Object -InputObject (' ').GetType()
Measure-Object -InputObject ($null).GetType()
# Results
<#
Count    : 1
Average  : 
Sum      : 
Maximum  : 
Minimum  : 
Property : 

Count    : 1
Average  : 
Sum      : 
Maximum  : 
Minimum  : 
Property : 

Count    : 1
Average  : 
Sum      : 
Maximum  : 
Minimum  : 
Property : 

You cannot call a method on a null-valued expression.
#>


$Null -eq '' 
[string]$Null -eq ''
$Null -eq [string]''
[string]$Null -eq [string]''
# Results
<#
False
True
False
True
#>

$Null -eq '' 
[bool]$Null -eq ''
$Null -eq [bool]''
[bool]$Null -eq [bool]''
# Results
<#
False
True
False
True
#>


$Null -eq '' 
[int]$Null -eq ''
$Null -eq [int]''
[int]$Null -eq [int]''
# Results
<#
False
True
False
True
#>


$Null -eq '' 
[double]$Null -eq ''
$Null -eq [double]''
[double]$Null -eq [double]''
# Results
<#
False
True
False
True
#>



Clear-Host 
0, $null | 
ForEach {
    ('#')*40
    "`nTest `$null as default"
    $null -gt $PSItem
    $null -ge $PSItem
    $null -eq $PSItem
    $null -le $PSItem
    $null -lt $PSItem


    "`n"
    ('#')*40
    "Using $PSItem"
    "`nTest `$null as string"
    "Left Side`tRight Side`tBoth Sides"
    Write-Host ([string]$null -gt $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -gt [string]$PSItem) -NoNewline
    Write-Host "`t|`t" ([string]$null -gt [string]$PSItem)

    Write-Host ([string]$null -ge $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -ge [string]$PSItem) -NoNewline
    Write-Host "`t|`t" ([string]$null -ge [string]$PSItem)

    Write-Host ([string]$null -eq $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -eq [string]$PSItem) -NoNewline
    Write-Host "`t|`t" ([string]$null -eq [string]$PSItem)

    Write-Host ([string]$null -le $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -le [string]$PSItem) -NoNewline
    Write-Host "`t|`t" ([string]$null -le [string]$PSItem)


    Write-Host ([string]$null -lt $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -lt [string]$PSItem) -NoNewline
    Write-Host "`t|`t" ([string]$null -lt [string]$PSItem)


    "`n"
    ('#')*40
    "Using $PSItem"
    "`nTest `$null as boolean"
    "Left Side`tRight Side`tBoth Sides"
    Write-Host ([boolean]$null -gt $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -gt [boolean]$PSItem) -NoNewline
    Write-Host "`t|`t" ([boolean]$null -gt [boolean]$PSItem)

    Write-Host ([boolean]$null -ge $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -ge [boolean]$PSItem) -NoNewline
    Write-Host "`t|`t" ([boolean]$null -ge [boolean]$PSItem)

    Write-Host ([boolean]$null -eq $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -eq [boolean]$PSItem) -NoNewline
    Write-Host "`t|`t" ([boolean]$null -eq [boolean]$PSItem)

    Write-Host ([boolean]$null -le $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -le [boolean]$PSItem) -NoNewline
    Write-Host "`t|`t" ([boolean]$null -le [boolean]$PSItem)

    Write-Host ([boolean]$null -lt $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -lt [boolean]$PSItem) -NoNewline
    Write-Host "`t|`t" ([boolean]$null -lt [boolean]$PSItem)


    "`n"
    ('#')*40
    "Using $PSItem"
    "`nTest `$null as int"
    "Left Side`tRight Side`tBoth Sides"
    Write-Host ([int]$null -gt $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -gt [int]$PSItem) -NoNewline
    Write-Host "`t|`t" ([int]$null -gt [int]$PSItem)

    Write-Host ([int]$null -ge $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -ge [int]$PSItem) -NoNewline
    Write-Host "`t|`t" ([int]$null -ge [int]$PSItem)

    Write-Host ([int]$null -eq $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -eq [int]$PSItem) -NoNewline
    Write-Host "`t|`t" ([int]$null -eq [int]$PSItem)

    Write-Host ([int]$null -le $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -le [int]$PSItem) -NoNewline
    Write-Host "`t|`t" ([int]$null -le [int]$PSItem)

    Write-Host ([int]$null -lt $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -lt [int]$PSItem) -NoNewline
    Write-Host "`t|`t" ([int]$null -lt [int]$PSItem)


    "`n"
    ('#')*40
    "Using $PSItem"
    "`nTest `$null as double"
    "Left Side`tRight Side`tBoth Sides"
    Write-Host ([double]$null -gt $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -gt [double]$PSItem) -NoNewline
    Write-Host "`t|`t" ([double]$null -gt [double]$PSItem)

    Write-Host ([double]$null -ge $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -ge [double]$PSItem) -NoNewline
    Write-Host "`t|`t" ([double]$null -ge [double]$PSItem)

    Write-Host ([double]$null -eq $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -eq [double]$PSItem) -NoNewline
    Write-Host "`t|`t" ([double]$null -eq [double]$PSItem)

    Write-Host ([double]$null -le $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -le [double]$PSItem) -NoNewline
    Write-Host "`t|`t" ([double]$null -le [double]$PSItem)

    Write-Host ([double]$null -lt $PSItem) -NoNewline
    Write-Host "`t|`t" ($null -lt [double]$PSItem) -NoNewline
    Write-Host "`t|`t" ([double]$null -lt [double]$PSItem)
}
# Results
<#
########################################

Test $null as default
False
False
False
True
True


########################################
Using 0

Test $null as string
Left Side   Right Side  Both Sides
False   |    False  |    False
False   |    False  |    False
False   |    False  |    False
True    |    True   |    True
True    |    True   |    True


########################################
Using 0

Test $null as boolean
Left Side   Right Side  Both Sides
False   |    False  |    False
True    |    False  |    True
True    |    False  |    True
True    |    True   |    True
False   |    True   |    False


########################################
Using 0

Test $null as int
Left Side   Right Side  Both Sides
False   |    False  |    False
True    |    False  |    True
True    |    False  |    True
True    |    True   |    True
False   |    True   |    False


########################################
Using 0

Test $null as double
Left Side   Right Side  Both Sides
False   |    False  |    False
True    |    False  |    True
True    |    False  |    True
True    |    True   |    True
False   |    True   |    False
########################################

Test $null as default
False
True
True
True
False


########################################
Using 

Test $null as string
Left Side   Right Side  Both Sides
True    |    False  |    False
True    |    False  |    True
False   |    False  |    True
False   |    True   |    True
False   |    True   |    False


########################################
Using 

Test $null as boolean
Left Side   Right Side  Both Sides
True    |    False  |    False
True    |    False  |    True
False   |    False  |    True
False   |    True   |    True
False   |    True   |    False


########################################
Using 

Test $null as int
Left Side   Right Side  Both Sides
True    |    False  |    False
True    |    False  |    True
False   |    False  |    True
False   |    True   |    True
False   |    True   |    False


########################################
Using 

Test $null as double
Left Side   Right Side  Both Sides
True    |    False  |    False
True    |    False  |    True
False   |    False  |    True
False   |    True   |    True
False   |    True   |    False
#>

Why is that?

行为违反直觉:

运算符-lt-le-gt-ge,尽管他们可以具有 numeric 的意思,似乎将 $null 操作数视为 空字符串 (''),即它们默认为 字符串比较 ,因为 中的示例命令暗示。

也就是说,$null -lt 0 实际上与 '' -lt '0' 的计算结果相同,这解释了 $true 结果,因为在 词法比较条件是否满足
虽然您也可以将 $null -eq 0 设想为 '' -eq '0',但 -eq 的情况比较特殊 - 见下文。

此外,0 放在 LHS 上仍然像字符串比较一样(除了 -eq 见下文)-即使通常是 LHS 的类型导致 RHS 被强制为相同类型.

也就是说,0 -le $null 看起来也像 '0' -le '',因此 returns $false.

虽然这种行为在完全基于字符串的运算符(例如-match-like中是预料之中的,对于 也支持数字 的运算符来说,这是令人惊讶的,特别是考虑到 其他此类运算符 - 以及 独有的 numeric - 默认为 numeric 解释 $null,如 0.

  • +-/ do 强制 LHS $null0 ([int] 默认);例如$null + 00
  • * 不是;例如,$null * 0 又是 $null.

其中,-/ 专有的 数字,而 +* 也适用于 字符串数组上下文。

有一个额外的不一致:-eq 从不$null操作数执行类型强制:

  • $null -eq <RHS> 只有 $true 如果 <RHS> 也是 $null(或“自动化null" - 见下文),目前是 可靠地测试值是否为 $null 的唯一方法。 (换句话说:$null -eq '''' -eq '' 而不是 - 这里没有类型强制转换 。)

      不幸的是,
    • GitHub PR #10704 旨在为 $null 测试实现专用语法,例如 <LHS> -is $null.
  • 类似地,<LHS> -eq $null 也不对 $null 和 returns $true 执行类型强制$null 作为左轴;

    • 然而,具有 array-valued <LHS>-eq 充当 filter(与大多数运算符一样),返回 元素的子数组 $null;例如,1, $null, 2, $null, 3 -eq $null 返回 2 元素数组 $null, $null.
    • 这种过滤行为是只有 $null -eq <RHS> - $null 作为标量 LHS - 作为(标量)$null.
    • 的可靠测试的原因

请注意,行为同样适用于 PowerShell 用来表示来自 命令的(非)输出的“自动化 null”值(技术上,[System.Management.Automation.Internal.AutomationNull]::Value singleton), because this value is treated the same as $null in expressions; e.g. $(& {}) -lt 0 is also $true - see 了解更多信息。

类似地,这些行为也适用于恰好包含 $null 可空值类型 的实例(例如,[System.Nullable[int]] $x = $null; $x -lt 0 也是 $true)谢谢,Dávid Laczkó,但请注意它们在 PowerShell 中的使用很少见。


Can and should this result be relied on?

由于操作符之间的行为不一致,我不会依赖它,尤其是因为很难记住什么时候适用哪些规则 - 至少有一个假设不一致将被修复的机会;考虑到这将构成 破坏性 更改,但是,这可能不会发生。

如果向后兼容性不是问题,以下行为将消除不一致并制定易于概念化和记忆的规则:

当给定一个(基本标量的)二元运算符一个 $null 操作数和一个非 $null 操作数时 - 无论哪个是左轴,哪个是右轴:

  • 对于在数字/布尔/字符串操作数上独占的运算符(例如//-and/-match): 将 $null 操作数强制转换为运算符隐含的类型。

  • 对于在多个“域”中运行的运算符——文本和数字(例如-eq)——强制$null other 操作数类型的操作数。

请注意,这将另外需要使用不同语法的专用 $null 测试,例如上述 PR 中的 -is $null

注意:以上 不适用于 collection 运算符,-in-contains(以及它们的否定变体 -notin-notcontains),因为它们的逐元素相等比较的行为类似于 -eq,因此从不将类型强制应用于 $null 值。


what is the best (i.e. most concise, best performing, etc.) way to reliably test for integer values (or other types for that matter) in a variable that might have a value of $null?

以下解决方案将 $null 操作数强制为 0

  • 注意:(...) 下面 -lt 操作的 LHS 用于概念清晰,但并非绝对必要 - 请参阅 about_Operator_Precedence

PowerShell (Core) 7+ 中,使用 ??null-coalescing operator any 类型的操作数:

# PowerShell 7+ only
($null ?? 0) -lt 0 # -> $false

Windows PowerShell 中,不支持此运算符,使用虚拟计算:

# Windows PowerShell
(0 + $null) -lt 0  # -> $false

虽然 [int] $null -lt 0 之类的东西也有效,但它需要您提交 特定的 数字类型,因此如果操作数恰好高于 [int]::MaxValue, 表达式会失败; [double] $null -lt 0 会将这种风险降至最低,但至少在假设上会导致准确性下降。

虚拟添加 (0 +) 绕过了这个问题,让 PowerShell 应用它通常的 按需类型扩展.

顺便说一句:这种 自动类型扩展也会表现出意想不到的行为,因为其结果需要比任一操作数的类型更宽的类型的全整数计算总是扩展到 [double],即使更大的 整数 类型就足够了 ;例如([int]::MaxValue + 1).GetType().Name returns Double,即使 [long] 结果已经足够,但可能会导致准确性损失 - 请参阅 了解更多信息。