英文:
Why is piping null to set-variable causing different outcomes?
问题
在使用 set-variable
时,当将 null 值传递给它时,我看到了不同的行为。
在第一个设置中,我们创建了一个变量,并用 null 覆盖它,目前为止一切都好。
# 设置 1:传递 null 值
$test1 = "test1"
$null | set-variable test1
Write-Host "It is: $test1" # "It is: "
然而,在第二个设置中,据我所知,我们也是用 null 覆盖它,但变量没有改变。
# 设置 2:通过减少列表来传递 null 值
$testList = New-Object -TypeName 'System.Collections.ArrayList'
$testList.Add("test")
$test2 = "test2"
$testList | Select-Object -SkipLast 1 | set-variable test2
Write-Host "It is: $test2" # "It is: test2"
为什么第一个设置会覆盖变量,而第二个设置不会呢?
英文:
When using set-variable I am seeing different behaviors when piping a null value to it.
In the first setup, we create a variable, and overwriting it with null, so far so good.
# Setup 1: Passing a null value
$test1 = "test1"
$null | set-variable test1
Write-Host "It is: $test1" # "It is: "
# $null.gettype()
# You cannot call a method on a null-valued expression.
However in the second setup, to my believe, we are also overwriting it with null, however the variable is not changed.
# Setup 2: Passing a null value by deducting a list
$testList = New-Object -TypeName 'System.Collections.ArrayList'
$testList.Add("test")
$test2 = "test2"
$testList | Select-Object -SkipLast 1 | set-variable test2
Write-Host "It is: $test2" # "It is: test2"
# $($testList | Select-Object -SkipLast 1).gettype()
# You cannot call a method on a null-valued expression.
Why is the first setup overwriting the variable and the second setup is not?
答案1
得分: 3
这基本上是关于AutomationNull
和$null
之间的区别。请参阅关于$null
的一切,这是一篇深度文章。
$test2 = 'test2'
& { } | Set-Variable test2 # AutomationNull,不会执行Process块!
$test2 # 输出: test2
$null | Set-Variable test2 # $null不是AutomationNull,它会被传递
$test2 # 输出: $null
您可以使用此示例更好地理解发生了什么。正如您可能已经看到的,当通过AutomationNull
传递时,我们的函数的process
块从未被调用。
function Set-Var {
param(
[Parameter(ValueFromPipeline)]
[object] $Value,
[Parameter(Mandatory, Position = 0)]
[string] $Name
)
process {
$PSCmdlet.WriteVerbose("Setting value [$Value] to PSVariable [$Name]")
$psvar = $PSCmdlet.SessionState.PSVariable.Get($Name)
$psvar.Value = $Value
$PSCmdlet.SessionState.PSVariable.Set($psvar)
}
}
$test = 'hello'
$null | Set-Var test -Verbose
# VERBOSE: Setting value [] to PSVariable [test]
$test # 输出: $null
'foo' | Set-Var test -Verbose
# VERBOSE: Setting value [foo] to PSVariable [test]
$test # 输出: foo
& { } | Set-Var test -Verbose
# 没有详细输出,意味着从未调用Process块!
$test # 输出: foo
英文:
This is pretty much the difference between AutomationNull
and $null
. See Everything you wanted to know about $null
, great in depth article.
$test2 = 'test2'
& { } | Set-Variable test2 # AutomationNull, Process block does not run!
$test2 # Outputs: test2
$null | Set-Variable test2 # $null is not AutomationNull, it does get piped
$test2 # Outputs: $null
You can use this example to get a better understanding of what is going on. As you may see, the process
block of our function is never invoked when AutomationNull
is piped through.
function Set-Var {
param(
[Parameter(ValueFromPipeline)]
[object] $Value,
[Parameter(Mandatory, Position = 0)]
[string] $Name
)
process {
$PSCmdlet.WriteVerbose("Setting value [$Value] to PSVariable [$Name]")
$psvar = $PSCmdlet.SessionState.PSVariable.Get($Name)
$psvar.Value = $Value
$PSCmdlet.SessionState.PSVariable.Set($psvar)
}
}
$test = 'hello'
$null | Set-Var test -Verbose
# VERBOSE: Setting value [] to PSVariable [test]
$test # Outputs: $null
'foo' | Set-Var test -Verbose
# VERBOSE: Setting value [foo] to PSVariable [test]
$test # Outputs: foo
& { } | Set-Var test -Verbose
# No verbose output, meaning the Process Block was never invoked!
$test # Outputs: foo
答案2
得分: 2
让我为Santiago Squarzon的有用回答添加一些背景信息:
实际上,PowerShell有两种类型的null值:
-
标量null,可以说是类似于C#和其他语言中的
null
,例如,它是自动**$null
**变量的值。- 可以将其视为缺失对象的占位符。
$null
会原样传递到管道中。- 有趣的是,将
$null
用作foreach
语句的输入时,情况并非如此:foreach ($val in $null) { 'here!' }
不会产生任何输出,这意味着循环从未进入。 - 不幸的是,尝试访问不存在的变量的时候,会评估为
$null
,但在PowerShell中,除了在_测试_中使用(例如$null -eq $value
之类的)之外,很少会明确使用$null
。
-
可枚举null,可以说是PowerShell特有的概念:
-
可以将其视为枚举不到任何内容的可枚举。
-
不幸的是,截至本文编写时,这个特殊值没有官方名称,尽管通常称为**“Automation null”**,有时称为“empty null”。
-
对于它,没有自动变量,即没有类似于标量null的自动
$null
变量。 -
鉴于可枚举null在技术上是产生_没有输出_的命令的**“返回值”**,获取它的最简单方法是执行
$nullEnum = & {}
,即执行一个空脚本块。具体来说,可枚举null是[System.Management.Automation.Internal.AutomationNull]::Value
单例(文档链接没有提供有意义的信息)。 -
在**管道**中(例如,
$value | Write-Output
)...-
... 可枚举null表现得像一个没有元素的集合,这意味着,鉴于集合在管道中被_逐个元素地发送_(一个接一个地发送它们的元素),没有数据_被发送到管道中,这通常是_无操作:后续管道段中的命令不接收输入以进行操作。
-
请注意,自动枚举逻辑也适用于
switch
语句和比较运算符(它们作为具有可枚举LHS值的_过滤器_进行操作);将可枚举null作为switch
的输入有效地_跳过_语句(例如switch (& {}) { default { 'never get here' } }
);可枚举null作为比较操作的LHS值是否被视为可枚举取决于具体的运算符;-match
将其视为可枚举((& {}) -match ''
-> 空数组),-eq
则不会((& {}) -eq ''
->$false
)。
-
-
在**表达式**中(例如,
$null -eq $value
)...- ... 可枚举null表现得像
$null
- 此外,当将可枚举null 作为参数(参数值)传递给命令时,不可避免地会进行转换为
$null
- 请参阅GitHub问题#9150
- ... 可枚举null表现得像
-
上述内容解释了$null | Set-Variable test1
(通过管道接收到的$null
值设置了变量test1
)和& {} | Set-Variable test2
之间的区别(变量test2
从未创建或更新,因为Set-Variable
未接收到输入;您对1元素输入集合的Select-Object -SkipLast 1
调用产生了_没有输出_,因此发出了可枚举null)。
另请参阅:
-
这个答案还涉及到
$null
与[System.Management.Automation.Null]
处理的_历史_方面,涵盖了从v2到v3+的过渡中发生的行为更改。 -
GitHub问题#9150上的此评论总结了如何以一致的方式处理null二分法,如果不考虑向后兼容性。
鉴于基本行为差异,很重要:
-
正确地 记录 这两种null类型,以及为可枚举null 指定一个_官方名称_。
-
使其容易 以编程方式区分 这两种类型。
截至本文编写时(PowerShell 7.3.6),这两个要求都没有得到满足。
检测可枚举null目前令人费解且复杂:
$value = & {} # 获取可枚举null。
# 如果没有
<details>
<summary>英文:</summary>
<!-- language-all: sh -->
Let me add some **background information** to [Santiago Squarzon's helpful answer](https://stackoverflow.com/a/76631170/45375):
Indeed, PowerShell has **two types of null values**:
* The **_scalar_ null**, so to speak, analogous to `null` in C# and other languages, for instance, which is the value of the [automatic **`$null`** variable](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Automatic_Variables#null).
* You can think of it as **placeholder for a _missing object_**.
* **`$null` is sent through the pipeline as-is**.
* Curiously, the same is _not_ true for using `$null` as the input to a [`foreach`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Foreach) _statement_: `foreach ($val in $null) { 'here!' }` produces _no_ output, implying that the loop is never entered.
* `$null` is what trying to access *nonexistent variables* evaluates to, unfortunately,<sup>[1]</sup> but its explicit use - other than in _tests_ such as `$null -eq $value` - is rare in PowerShell.
* The **_enumerable_ null,** so to speak, which is a PowerShell-specific concept:
* You can think of it as an **enumerable that _enumerates nothing_**.
* Sadly, this special value has no official name as of this writing, thought it is often called **"Automation null"** and sometimes "empty null".
Also, there is ***no* automatic variable for it**, i.e. there is _no_ analog to the automatic `$null` variable for the _scalar_ null.
* Given that the enumerable null is **technically the "return value" of commands that produce _no output_**, the simplest way to obtain it is to execute `$nullEnum = & {}`, i.e. to execute an empty script block. Specifically, the enumerable null is the [`[System.Management.Automation.Internal.AutomationNull]::Value`](https://learn.microsoft.com/en-US/dotnet/api/System.Management.Automation.Internal.AutomationNull.Value) singleton (the documentation link provides no meaningful information).
* In a **_pipeline_** (e.g., `$value | Write-Output`)...
* ... the enumerable null **behaves like a _collection with no elements_**, which means that, given that collections are _enumerated_ in the pipeline (have their elements sent _one by one_), _no data_ is sent through the pipeline, which is (typically) a _no-op_: the **commands in subsequent pipeline segments receive *no input* to operate on**.
* Note that the automatic enumeration logic also applies to the [`switch`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Switch) statement and to the LHS of comparison operators (which act as _filters_ with enumerable LHS values); providing the enumerable null as input to `switch` effectively _skips_ the statement (e.g. `switch (& {}) { default { 'never get here' } }`); whether the enumerable null is treated as an enumerable as the LHS of a comparison operation depends on the specific operator; `-match` treats it as an enumerable (`(& {}) -match ''` -> empty array), `-eq` does not (`(& {}) -eq ''` -> `$false`)
* In an **_expression_** (e.g, `$null -eq $value`)...
* ... the enumerable null **behaves like `$null`**
* Additionally, when **passing the enumerable null _as an argument_** (parameter value) to a command, **_conversion to `$null`_ invariably happens** - see [GitHub issue #9150](https://github.com/PowerShell/PowerShell/issues/9150)
The above explains the difference between `$null | Set-Variable test1` (variable `test` is set to the `$null` value received via the pipeline) and `& {} | Set-Variable test2` (variable `test2` is never created or updated, because `Set-Variable` receives no input; your `Select-Object -SkipLast 1` call on the 1-element input collection produced _no output_, and therefore emitted the enumerable null).
See also:
* [This answer](https://stackoverflow.com/a/41568525/45375) also touches on _historic_ aspects of `$null` vs. `[System.Management.Automation.Null]` handling, covering the behavioral changes that happened in the transition from v2 to v3+.
* [This comment on GitHub issue #9150](https://github.com/PowerShell/PowerShell/issues/9150#issuecomment-473743805) summarizes how the null dichotomy _could_ be handled in a consistent manner, _if backward compatibility weren't a concern_.
---
**Given the fundamental behavioral differences**, it is important:
* to **properly _document_** these two null types, as well as **give the enumerable null an _official name_**.
* to **make it easy to *programmatically distinguish*** the two types.
As of this writing (PowerShell 7.3.6), neither requirement is met.
**Detecting the enumerable null** is currently cumbersome and obscure:
$value = & {} # Obtain the enumerable null.
Without the -and $value.psobject
part, you couldn't distinguish
$null from the enumerable null.
$isNullEnumerable =
$null -eq $value -and $value.psobject
While `$null -eq $value` returns `$true` for _both_ a true `$null` and the enumerable null, `$value.psobject` only returns a value for the enumerable null (which, when coerced to a Boolean, evaluates to `$true`). The reason is that, unlike `$null`, the enumerable null is technically _an object_ and therefore returns a value for the [intrinsic `psobject` property](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Intrinsic_Members#object-views).
As a result of the discussion in [GitHub issue #13465](https://github.com/PowerShell/PowerShell/issues/13465), **the following improvement has been *green-lit*, but is *yet to be implemented***:
NOT YET IMPLEMENTED as of PowerShell 7.3.6
$isNullEnumerable =
$value -is [System.Management.Automation.Null]
That is, you'll be able to use `-is`, the [type(-inheritance) / interface test operator](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Type_Operators) with the yet-to-be-introduced `[System.Management.Automation.Null]` type, which will supersede the "pubternal" `[System.Management.Automation.Internal.AutomationNull]` type.<sup>[2]</sup>
Unfortunately, also introducing a _type accelerator_ - which would simplify the test to `$value -is [AutomationNull]` - was decided _against_.
---
<sup>[1] This default value is unfortunate, because it means that `$noSuchVariable | ...` _sends `$null`_ through the pipeline. If the default were the "enumerable null" ("Automation null", `[System.Management.Automation.Null]::Value`) instead, no data would be sent, the way the `foreach` _statement_ already - but surprisingly - handles `$null` (too). With the enumerable null as the default, there would be no need for the asymmetry between pipeline and `foreach` behavior, and `$null` could _consistently_ be preserved as such.</sup>
<sup>[2] This change involves more than just a new _type name_: the new type's singleton (`[System.Management.Automation.Null]::Value`), i.e. the actual enumerable null, will then be *of that same type*, whereas the current singleton (`[System.Management.Automation.Internal.AutomationNull]::Value`) is of a _different_ type, namely just `[psobject]` - see [this GitHub comment](https://github.com/PowerShell/PowerShell/issues/9997#issuecomment-580743572) for details.</sup>
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论