支持词法范围的 ScriptBlock 参数(例如 Where-Object)
Supporting lexical-scope ScriptBlock parameters (e.g. Where-Object)
考虑以下任意函数和测试用例:
Function Foo-MyBar {
Param(
[Parameter(Mandatory=$false)]
[ScriptBlock] $Filter
)
if (!$Filter) {
$Filter = { $true }
}
#$Filter = $Filter.GetNewClosure()
Get-ChildItem "$env:SYSTEMROOT" | Where-Object $Filter
}
##################################
$private:pattern = 'T*'
Get-Help Foo-MyBar -Detailed
Write-Host "`n`nUnfiltered..."
Foo-MyBar
Write-Host "`n`nTest 1:. Piped through Where-Object..."
Foo-MyBar | Where-Object { $_.Name -ilike $private:pattern }
Write-Host "`n`nTest 2:. Supplied a naiive -Filter parameter"
Foo-MyBar -Filter { $_.Name -ilike $private:pattern }
在测试 1 中,我们将 Foo-MyBar
的结果通过 Where-Object
过滤器进行管道传输,该过滤器将返回的对象与私有范围变量 $private:pattern
中包含的模式进行比较。在这种情况下,这正确 returns C:\ 中以字母 T
.
开头的所有 files/folders
在测试 2 中,我们将相同的过滤脚本作为参数直接传递给 Foo-MyBar
。但是,当 Foo-MyBar
到达 运行 过滤器时,$private:pattern
不在范围内,因此 returns 没有项目。
我明白为什么会这样——因为传递给Foo-MyBar
的ScriptBlock不是闭包,所以不关闭 $private:pattern
变量并且该变量丢失。
我从评论中注意到,我之前有一个有缺陷的第三次测试,它试图通过 {...}.GetNewClosure(),但这不会关闭私有范围的变量——谢谢@PetSerAl 帮助我澄清了这一点。
问题是,Where-Object
如何在测试 1 中捕获 $private:pattern
的值,以及我们如何在我们自己的 functions/cmdlets 中实现相同的行为?
(最好不需要调用者必须了解闭包,或者知道将其过滤器脚本作为闭包传递。)
我注意到,如果我取消注释 Foo-MyBar
中的 $Filter = $Filter.GetNewClosure()
行,那么它永远不会 returns 任何结果,因为 $private:pattern
丢失了。
(正如我在顶部所说,函数和参数在这里是任意的,作为我实际问题的最短形式再现!)
how does Where-Object
capture the value of $private:pattern
in Test 1
正如在 the source code for Where-Object
in PowerShell Core 中所见,PowerShell 在内部调用过滤器脚本 而没有将其限制在自己的本地范围内 (_script
是私有支持FilterScript
参数的字段,注意传递给 DoInvokeReturnAsIs()
的 useLocalScope: false
参数):
protected override void ProcessRecord()
{
if (_inputObject == AutomationNull.Value)
return;
if (_script != null)
{
object result = _script.DoInvokeReturnAsIs(
useLocalScope: false, // <-- notice this named argument right here
errorHandlingBehavior: ScriptBlock.ErrorHandlingBehavior.WriteToCurrentErrorPipe,
dollarUnder: InputObject,
input: new object[] { _inputObject },
scriptThis: AutomationNull.Value,
args: Utils.EmptyArray<object>());
if (_toBoolSite.Target.Invoke(_toBoolSite, result))
{
WriteObject(InputObject);
}
}
// ...
}
how do we achieve the same behaviour in our own functions/cmdlets?
我们没有 - DoInvokeReturnAsIs()
(和类似的脚本块调用工具)被标记为 internal
,因此只能由包含在System.Management.Automation
大会
给出的示例不起作用,因为默认情况下调用函数将进入一个新的范围。 Where-Object
仍然会在不输入的情况下调用过滤器脚本,但是函数的作用域没有 private
变量。
有三种解决方法。
将函数放在与调用者不同的模块中
每个模块都有一个 SessionState
,它有自己的 SessionStateScope
堆栈。每个 ScriptBlock
都绑定到 SessionState
被解析。
如果您调用模块中定义的函数,则会在该模块的 SessionState
中创建一个新作用域,但不会在顶层 SessionState
中创建。因此,当 Where-Object
在不进入新范围的情况下调用过滤器脚本时,它会在 ScriptBlock
绑定到的 SessionState
的当前范围内执行此操作。
这有点脆弱,因为如果你想从你的模块中调用那个函数,你不能。它也会有同样的问题。
使用点源运算符调用函数
您很可能已经知道用于在不创建新范围的情况下调用脚本文件的点源运算符 (.
)。这也适用于命令名称和 ScriptBlock
对象。
. { 'same scope' }
. Foo-MyBar
但是请注意,这将在SessionState
的当前范围内调用该函数,该函数来自,因此您不能依赖.
始终在 调用者的 当前范围内执行。因此,如果您使用点源运算符调用与不同 SessionState
关联的函数 - 例如在(不同)模块中定义的函数 - 它可能会产生意想不到的效果。创建的变量将持续存在于未来的函数调用中,并且在函数本身内定义的任何辅助函数也将持续存在。
编写 Cmdlet
编译命令 (cmdlet) 在调用时不会创建新范围。您还可以使用与 Where-Object
相似的 API(尽管不完全相同)
下面是如何使用 public API 的
实现 Where-Object
的粗略实现
using System.Management.Automation;
namespace MyModule
{
[Cmdlet(VerbsLifecycle.Invoke, "FooMyBar")]
public class InvokeFooMyBarCommand : PSCmdlet
{
[Parameter(ValueFromPipeline = true)]
public PSObject InputObject { get; set; }
[Parameter(Position = 0)]
public ScriptBlock FilterScript { get; set; }
protected override void ProcessRecord()
{
var filterResult = InvokeCommand.InvokeScript(
useLocalScope: false,
scriptBlock: FilterScript,
input: null,
args: new[] { InputObject });
if (LanguagePrimitives.IsTrue(filterResult))
{
WriteObject(filterResult, enumerateCollection: true);
}
}
}
}
考虑以下任意函数和测试用例:
Function Foo-MyBar {
Param(
[Parameter(Mandatory=$false)]
[ScriptBlock] $Filter
)
if (!$Filter) {
$Filter = { $true }
}
#$Filter = $Filter.GetNewClosure()
Get-ChildItem "$env:SYSTEMROOT" | Where-Object $Filter
}
##################################
$private:pattern = 'T*'
Get-Help Foo-MyBar -Detailed
Write-Host "`n`nUnfiltered..."
Foo-MyBar
Write-Host "`n`nTest 1:. Piped through Where-Object..."
Foo-MyBar | Where-Object { $_.Name -ilike $private:pattern }
Write-Host "`n`nTest 2:. Supplied a naiive -Filter parameter"
Foo-MyBar -Filter { $_.Name -ilike $private:pattern }
在测试 1 中,我们将 Foo-MyBar
的结果通过 Where-Object
过滤器进行管道传输,该过滤器将返回的对象与私有范围变量 $private:pattern
中包含的模式进行比较。在这种情况下,这正确 returns C:\ 中以字母 T
.
在测试 2 中,我们将相同的过滤脚本作为参数直接传递给 Foo-MyBar
。但是,当 Foo-MyBar
到达 运行 过滤器时,$private:pattern
不在范围内,因此 returns 没有项目。
我明白为什么会这样——因为传递给Foo-MyBar
的ScriptBlock不是闭包,所以不关闭 $private:pattern
变量并且该变量丢失。
我从评论中注意到,我之前有一个有缺陷的第三次测试,它试图通过 {...}.GetNewClosure(),但这不会关闭私有范围的变量——谢谢@PetSerAl 帮助我澄清了这一点。
问题是,Where-Object
如何在测试 1 中捕获 $private:pattern
的值,以及我们如何在我们自己的 functions/cmdlets 中实现相同的行为?
(最好不需要调用者必须了解闭包,或者知道将其过滤器脚本作为闭包传递。)
我注意到,如果我取消注释 Foo-MyBar
中的 $Filter = $Filter.GetNewClosure()
行,那么它永远不会 returns 任何结果,因为 $private:pattern
丢失了。
(正如我在顶部所说,函数和参数在这里是任意的,作为我实际问题的最短形式再现!)
how does
Where-Object
capture the value of$private:pattern
in Test 1
正如在 the source code for Where-Object
in PowerShell Core 中所见,PowerShell 在内部调用过滤器脚本 而没有将其限制在自己的本地范围内 (_script
是私有支持FilterScript
参数的字段,注意传递给 DoInvokeReturnAsIs()
的 useLocalScope: false
参数):
protected override void ProcessRecord()
{
if (_inputObject == AutomationNull.Value)
return;
if (_script != null)
{
object result = _script.DoInvokeReturnAsIs(
useLocalScope: false, // <-- notice this named argument right here
errorHandlingBehavior: ScriptBlock.ErrorHandlingBehavior.WriteToCurrentErrorPipe,
dollarUnder: InputObject,
input: new object[] { _inputObject },
scriptThis: AutomationNull.Value,
args: Utils.EmptyArray<object>());
if (_toBoolSite.Target.Invoke(_toBoolSite, result))
{
WriteObject(InputObject);
}
}
// ...
}
how do we achieve the same behaviour in our own functions/cmdlets?
我们没有 - DoInvokeReturnAsIs()
(和类似的脚本块调用工具)被标记为 internal
,因此只能由包含在System.Management.Automation
大会
给出的示例不起作用,因为默认情况下调用函数将进入一个新的范围。 Where-Object
仍然会在不输入的情况下调用过滤器脚本,但是函数的作用域没有 private
变量。
有三种解决方法。
将函数放在与调用者不同的模块中
每个模块都有一个 SessionState
,它有自己的 SessionStateScope
堆栈。每个 ScriptBlock
都绑定到 SessionState
被解析。
如果您调用模块中定义的函数,则会在该模块的 SessionState
中创建一个新作用域,但不会在顶层 SessionState
中创建。因此,当 Where-Object
在不进入新范围的情况下调用过滤器脚本时,它会在 ScriptBlock
绑定到的 SessionState
的当前范围内执行此操作。
这有点脆弱,因为如果你想从你的模块中调用那个函数,你不能。它也会有同样的问题。
使用点源运算符调用函数
您很可能已经知道用于在不创建新范围的情况下调用脚本文件的点源运算符 (.
)。这也适用于命令名称和 ScriptBlock
对象。
. { 'same scope' }
. Foo-MyBar
但是请注意,这将在SessionState
的当前范围内调用该函数,该函数来自,因此您不能依赖.
始终在 调用者的 当前范围内执行。因此,如果您使用点源运算符调用与不同 SessionState
关联的函数 - 例如在(不同)模块中定义的函数 - 它可能会产生意想不到的效果。创建的变量将持续存在于未来的函数调用中,并且在函数本身内定义的任何辅助函数也将持续存在。
编写 Cmdlet
编译命令 (cmdlet) 在调用时不会创建新范围。您还可以使用与 Where-Object
相似的 API(尽管不完全相同)
下面是如何使用 public API 的
实现Where-Object
的粗略实现
using System.Management.Automation;
namespace MyModule
{
[Cmdlet(VerbsLifecycle.Invoke, "FooMyBar")]
public class InvokeFooMyBarCommand : PSCmdlet
{
[Parameter(ValueFromPipeline = true)]
public PSObject InputObject { get; set; }
[Parameter(Position = 0)]
public ScriptBlock FilterScript { get; set; }
protected override void ProcessRecord()
{
var filterResult = InvokeCommand.InvokeScript(
useLocalScope: false,
scriptBlock: FilterScript,
input: null,
args: new[] { InputObject });
if (LanguagePrimitives.IsTrue(filterResult))
{
WriteObject(filterResult, enumerateCollection: true);
}
}
}
}