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



考虑以下PowerShell代码:

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

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

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

感谢您抽出时间。如果这太过分,请提前道歉;黏糊糊的";这个场地的一个问题。

附言:值得一提的是,我通常的环境是PowerShell v5.1。

为什么?

行为违反直觉:

运算符-lt-le-gt-ge,尽管它们也可以具有数字含义,但似乎将$null操作数视为空字符串(''),即它们默认为字符串比较,正如postanote有用答案中的示例命令所暗示的那样。

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

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

也就是说,0 -le $null的行为似乎也类似于'0' -le '',因此返回$false

虽然这种行为在以独占字符串为基础的运算符(如-match-like)中是意料之中的,但对于运算符来说,也支持数字是令人惊讶的,特别是考虑到其他此类运算符-以及那些以独占numeric的运算符-默认为$nullnumeric解释,如CCD_ 23

  • +-/do强制LHS$null0(默认为[int]);例如$null + 00
  • *;例如$null * 0再次是$null

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

存在额外的不一致性:-eqnever$null操作数执行类型强制:

  • $null -eq <RHS>只有在<RHS>也是$null(或"自动化零"-见下文)的情况下才是$true,并且是当前可靠地测试值是否为$null的唯一方法。(换句话说:$null -eq ''而不是'' -eq ''-相同,此处不发生类型强制。)

    • GitHub PR#10704不幸陷入停滞,它旨在实现$null测试的专用语法,如<LHS> -is $null
  • 类似地,<LHS> -eq $null也不对$null执行类型强制,并且仅以$null作为LHS返回$true

    • 但是,对于值为数组的<LHS>-eq充当过滤器(就像大多数运算符所做的那样),返回元素的子数组,这些元素是$null;例如1, $null, 2, $null, 3 -eq $null返回2元素阵列$null, $null
    • 这种滤波行为是只有$null -eq <RHS>——以$null作为标量LHS——作为(标量)$null的测试是可靠的原因

注意,行为同样适用于";自动化零";PowerShell用于表示命令(从技术上讲,是[System.Management.Automation.Internal.AutomationNull]::Value单例)的(非)输出的值,因为此值与表达式中的$null相同;例如,$(& {}) -lt 0也是$true——有关更多信息,请参阅此答案。

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


这个结果可以而且应该被依赖吗?

由于操作程序之间的行为不一致,我不会依赖它,尤其是因为很难记住何时应用哪些规则,而且至少有一种假设的机会可以修复不一致性;然而,考虑到这将相当于突破的变化,这可能不会发生。

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

当一个(基本标量)二进制运算符被给定一个$null操作数和一个非$null操作数时,无论哪个是LHS,哪个是RHS:

  • 对于仅对数字/布尔/字符串操作数(例如//-and/-match)执行操作的运算符:将$null操作数强制为运算符所暗示的类型。

  • 对于在多个"中操作的运算符;域"-文本和数字(例如-eq)-将$null操作数强制为其他操作数的类型。

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

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


在一个可能值为$null的变量中,什么是可靠测试整数值(或其他类型)的最佳方法(即最简洁、性能最佳等)?

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

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

PowerShell(Core)7+中,使用??,这是一个空合并运算符,可用于任何类型的操作数:

# 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返回Double,即使[long]的结果已经足够了,也会导致潜在的准确性损失-有关更多信息,请参阅此答案。

测试$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
#>

相关内容

最新更新