为什么变量的范围会根据它是 .ps1 还是 .psm1 文件而变化,如何缓解这种情况?
Why does the scope of variables change depending on if it's a .ps1 or .psm1 file, and how can this be mitigated?
我有一个执行脚本块的函数。为方便起见,脚本块不需要显式定义参数,而是可以使用 $_
和 $A
来引用输入。
在代码中,这样做是这样的:
$_ = $Value
$A = $Value2
& $ScriptBlock
这一切都包裹在一个函数中。最小示例:
function F {
param(
[ScriptBlock]$ScriptBlock,
[Object]$Value
[Object]$Value2
)
$_ = $Value
$A = $Value2
& $ScriptBlock
}
如果此函数写在 PowerShell 脚本文件 (.ps1
) 中,但使用 Import-Module
导入,F
的行为符合预期:
PS> F -Value 7 -Value2 1 -ScriptBlock {$_ * 2 + $A}
15
PS>
但是,当函数写入 PowerShell 模块文件 (.psm1
) 并使用 Import-Module
导入时,行为是意外的:
PS> F -Value 7 -Value2 1 -ScriptBlock {$_ * 2 + $A}
PS>
使用 {$_ + 1}
代替 1
。 $_
的值似乎改为 $null
。据推测,某些安全措施限制了 $_
变量的范围或以其他方式保护它。或者,$_
变量可能是由某个自动过程分配的。无论如何,如果只有 $_
变量受到影响,第一个不成功的示例将 return 1
.
理想情况下,解决方案将涉及明确指定脚本块所在环境的能力 运行。类似于:
Invoke-ScriptBlock -Variables @{"_" = $Value; "A" = $Value2} -InputObject $ScriptBlock
总之,问题是:
- 为什么模块文件中的脚本块不能访问调用它们的函数中定义的变量?
- 有没有一种方法可以在调用脚本块时显式指定脚本块可访问的变量?
- 是否有其他不涉及在脚本块中包含显式参数声明的解决方法?
乱序:
- Is there some other way of solving this that does not involve including an explicit parameter declaration in the script block?
是的,如果您只想填充 $_
,请使用 ForEach-Object
!
ForEach-Object
在调用者的本地范围内执行,这有助于您解决问题 - 除了 您不会 ,因为它还会自动将输入绑定到$_
/$PSItem
:
# this will work both in module-exported commands and standalone functions
function F {
param(
[ScriptBlock]$ScriptBlock,
[Object]$Value
)
ForEach-Object -InputObject $Value -Process $ScriptBlock
}
现在 F
将按预期工作:
PS C:\> F -Value 7 -ScriptBlock {$_ * 2}
Ideally, the solution would involve the ability to explicitly specify the environment in which a script block is run. Something like:
Invoke-ScriptBlock -Variables @{"_" = $Value; "A" = $Value2} -InputObject $ScriptBlock
使用ScriptBlock.InvokeWithContext()
执行脚本块:
$functionsToDefine = @{
'Do-Stuff' = {
param($a,$b)
Write-Host "$a - $b"
}
}
$variablesToDefine = @(
[PSVariable]::new("var1", "one")
[PSVariable]::new("var2", "two")
)
$argumentList = @()
{Do-Stuff -a $var1 -b two}.InvokeWithContext($functionsToDefine, $variablesToDefine, $argumentList)
或者,像您的原始示例一样包装在一个函数中:
function F
{
param(
[scriptblock]$ScriptBlock
[object]$Value
)
$ScriptBlock.InvokeWithContext(@{},@([PSVariable]::new('_',$Value)),@())
}
现在你知道如何解决你的问题了,让我们回到关于模块作用域的问题。
首先,值得注意的是,您实际上可以使用模块实现上述目标,但有点相反。
(在下文中,我使用了用New-Module
定义的内存模块,但模块范围解析行为描述与从磁盘导入脚本模块时相同)
虽然模块作用域 "bypasses" 正常范围解析规则(解释见下文),但 PowerShell 实际上支持反向 - 在特定模块的范围内显式执行。
只需将模块引用作为第一个参数传递给 &
调用运算符,PowerShell 会将后续参数视为要在所述模块中调用的命令:
# Our non-module test function
$twoPlusTwo = { return $two + $two }
$two = 2
& $twoPlusTwo # yields 4
# let's try it with explicit module-scoped execution
$myEnv = New-Module {
$two = 2.5
}
& $myEnv $twoPlusTwo # Hell froze over, 2+2=5 (returns 5)
- Why can't script blocks in module files access variables defined in functions from which they were called?
- If they can, why can't the
$_
automatic variable?
因为加载的模块保持 状态,而 PowerShell 的实现者希望从调用者的环境中隔离模块状态。
为什么这可能有用,为什么一个会排除另一个,你问?
考虑以下示例,一个用于测试奇数的非模块函数:
$two = 2
function Test-IsOdd
{
param([int]$n)
return $n % $two -ne 0
}
如果我们 运行 在脚本或交互式提示中执行上述语句,随后调用 Test-IsOdd
应该会产生预期的结果:
PS C:\> Test-IsOdd 123
True
到目前为止,非常好,但是依赖非本地 $two
变量在这种情况下存在缺陷 - 如果在我们的脚本或 shell 中的某处我们不小心重新分配了本地变量变量 $two
,我们可能会完全破坏 Test-IsOdd
:
PS C:\> $two = 1 # oops!
PS C:\> Test-IsOdd 123
False
这是意料之中的,因为默认情况下,变量作用域解析只会在调用堆栈中徘徊,直到到达全局作用域。
但有时您可能需要在一个或多个函数的执行过程中保持状态,如我们上面的示例。
模块通过遵循稍微不同的作用域解析规则来解决这个问题——模块导出的函数遵循我们称之为 模块作用域 的东西(在到达全局作用域之前)。
为了说明这如何解决我们之前的问题,考虑这个相同功能的模块导出版本:
$oddModule = New-Module {
function Test-IsOdd
{
param([int]$n)
return $n % $two -ne 0
}
$two = 2
}
现在,如果我们调用我们新的模块导出 Test-IsOdd
,我们可以预见地得到预期的结果,regardless of "contamination" in the callers scope:
PS C:\> Test-IsOdd 123
True
PS C:\> $two = 1
PS C:\> Test-IsOdd 123 # still works
True
这种行为虽然可能令人惊讶,但基本上 用于巩固模块作者与用户之间的隐式契约 - 模块作者不需要太担心什么是"out there"(调用者会话状态),用户可以期望发生的任何事情"in there"(加载模块的状态)都能正常工作,而不必担心他们分配给本地范围内变量的内容。
模块作用域行为不佳 documented in the help files,但在 Bruce Payette 的 "PowerShell In Action"(ISBN:9781633430297)
的第 8 章中有一定深度的解释
我有一个执行脚本块的函数。为方便起见,脚本块不需要显式定义参数,而是可以使用 $_
和 $A
来引用输入。
在代码中,这样做是这样的:
$_ = $Value
$A = $Value2
& $ScriptBlock
这一切都包裹在一个函数中。最小示例:
function F {
param(
[ScriptBlock]$ScriptBlock,
[Object]$Value
[Object]$Value2
)
$_ = $Value
$A = $Value2
& $ScriptBlock
}
如果此函数写在 PowerShell 脚本文件 (.ps1
) 中,但使用 Import-Module
导入,F
的行为符合预期:
PS> F -Value 7 -Value2 1 -ScriptBlock {$_ * 2 + $A}
15
PS>
但是,当函数写入 PowerShell 模块文件 (.psm1
) 并使用 Import-Module
导入时,行为是意外的:
PS> F -Value 7 -Value2 1 -ScriptBlock {$_ * 2 + $A}
PS>
使用 {$_ + 1}
代替 1
。 $_
的值似乎改为 $null
。据推测,某些安全措施限制了 $_
变量的范围或以其他方式保护它。或者,$_
变量可能是由某个自动过程分配的。无论如何,如果只有 $_
变量受到影响,第一个不成功的示例将 return 1
.
理想情况下,解决方案将涉及明确指定脚本块所在环境的能力 运行。类似于:
Invoke-ScriptBlock -Variables @{"_" = $Value; "A" = $Value2} -InputObject $ScriptBlock
总之,问题是:
- 为什么模块文件中的脚本块不能访问调用它们的函数中定义的变量?
- 有没有一种方法可以在调用脚本块时显式指定脚本块可访问的变量?
- 是否有其他不涉及在脚本块中包含显式参数声明的解决方法?
乱序:
- Is there some other way of solving this that does not involve including an explicit parameter declaration in the script block?
是的,如果您只想填充 $_
,请使用 ForEach-Object
!
ForEach-Object
在调用者的本地范围内执行,这有助于您解决问题 - 除了 您不会 ,因为它还会自动将输入绑定到$_
/$PSItem
:
# this will work both in module-exported commands and standalone functions
function F {
param(
[ScriptBlock]$ScriptBlock,
[Object]$Value
)
ForEach-Object -InputObject $Value -Process $ScriptBlock
}
现在 F
将按预期工作:
PS C:\> F -Value 7 -ScriptBlock {$_ * 2}
Ideally, the solution would involve the ability to explicitly specify the environment in which a script block is run. Something like:
Invoke-ScriptBlock -Variables @{"_" = $Value; "A" = $Value2} -InputObject $ScriptBlock
使用ScriptBlock.InvokeWithContext()
执行脚本块:
$functionsToDefine = @{
'Do-Stuff' = {
param($a,$b)
Write-Host "$a - $b"
}
}
$variablesToDefine = @(
[PSVariable]::new("var1", "one")
[PSVariable]::new("var2", "two")
)
$argumentList = @()
{Do-Stuff -a $var1 -b two}.InvokeWithContext($functionsToDefine, $variablesToDefine, $argumentList)
或者,像您的原始示例一样包装在一个函数中:
function F
{
param(
[scriptblock]$ScriptBlock
[object]$Value
)
$ScriptBlock.InvokeWithContext(@{},@([PSVariable]::new('_',$Value)),@())
}
现在你知道如何解决你的问题了,让我们回到关于模块作用域的问题。
首先,值得注意的是,您实际上可以使用模块实现上述目标,但有点相反。
(在下文中,我使用了用New-Module
定义的内存模块,但模块范围解析行为描述与从磁盘导入脚本模块时相同)
虽然模块作用域 "bypasses" 正常范围解析规则(解释见下文),但 PowerShell 实际上支持反向 - 在特定模块的范围内显式执行。
只需将模块引用作为第一个参数传递给 &
调用运算符,PowerShell 会将后续参数视为要在所述模块中调用的命令:
# Our non-module test function
$twoPlusTwo = { return $two + $two }
$two = 2
& $twoPlusTwo # yields 4
# let's try it with explicit module-scoped execution
$myEnv = New-Module {
$two = 2.5
}
& $myEnv $twoPlusTwo # Hell froze over, 2+2=5 (returns 5)
- Why can't script blocks in module files access variables defined in functions from which they were called?
- If they can, why can't the
$_
automatic variable?
因为加载的模块保持 状态,而 PowerShell 的实现者希望从调用者的环境中隔离模块状态。
为什么这可能有用,为什么一个会排除另一个,你问?
考虑以下示例,一个用于测试奇数的非模块函数:
$two = 2
function Test-IsOdd
{
param([int]$n)
return $n % $two -ne 0
}
如果我们 运行 在脚本或交互式提示中执行上述语句,随后调用 Test-IsOdd
应该会产生预期的结果:
PS C:\> Test-IsOdd 123
True
到目前为止,非常好,但是依赖非本地 $two
变量在这种情况下存在缺陷 - 如果在我们的脚本或 shell 中的某处我们不小心重新分配了本地变量变量 $two
,我们可能会完全破坏 Test-IsOdd
:
PS C:\> $two = 1 # oops!
PS C:\> Test-IsOdd 123
False
这是意料之中的,因为默认情况下,变量作用域解析只会在调用堆栈中徘徊,直到到达全局作用域。
但有时您可能需要在一个或多个函数的执行过程中保持状态,如我们上面的示例。
模块通过遵循稍微不同的作用域解析规则来解决这个问题——模块导出的函数遵循我们称之为 模块作用域 的东西(在到达全局作用域之前)。
为了说明这如何解决我们之前的问题,考虑这个相同功能的模块导出版本:
$oddModule = New-Module {
function Test-IsOdd
{
param([int]$n)
return $n % $two -ne 0
}
$two = 2
}
现在,如果我们调用我们新的模块导出 Test-IsOdd
,我们可以预见地得到预期的结果,regardless of "contamination" in the callers scope:
PS C:\> Test-IsOdd 123
True
PS C:\> $two = 1
PS C:\> Test-IsOdd 123 # still works
True
这种行为虽然可能令人惊讶,但基本上 用于巩固模块作者与用户之间的隐式契约 - 模块作者不需要太担心什么是"out there"(调用者会话状态),用户可以期望发生的任何事情"in there"(加载模块的状态)都能正常工作,而不必担心他们分配给本地范围内变量的内容。
模块作用域行为不佳 documented in the help files,但在 Bruce Payette 的 "PowerShell In Action"(ISBN:9781633430297)
的第 8 章中有一定深度的解释