避免调用程序覆盖模块中的变量,反之亦然



我正在Powershell中构建一个自定义模块来分解一些代码。

在模块中的函数中,我使用变量。但是,如果调用者使用相同的变量名,它可能会干扰我的模块。

例如,这里有一个小模块(MyModule.psm1):

function Get-Foo{
param(
[int]$x,
[int]$y
)
try{
$result = $x/$y
} catch{
Write-Warning "Something get wrong"
}
if($result -ne 0){
Write-Host "x/y = $result"
}
}
Export-ModuleMember -Function "Get-Foo"

以及使用该模块的示例脚本:

Import-Module "$PSScriptRootMyModuleMyModule.psm1" -Force
$result = 3 # some other computation
Get-Foo -x 42 -Y 0

输出为:

x/y = 3

正如您所看到的,调用者声明了一个与我的模块中的变量名冲突的变量名。

避免这种行为的最佳做法是什么?

作为一项要求,我必须假设模块的开发人员不会是主要的脚本开发人员。因此,模块上的内部不应该是已知的(有点黑盒)

Ivan Mirchev的有用答案、robdy的有用答案以及AdminOfThings对该问题的评论提供了关键的指导;让我总结并补充一下:

  • 在函数内部,如果参数变量$y包含0,则永远不会创建本地$result变量,因为除以零会导致语句终止错误(触发catch块)。

  • 在没有本地$result变量的情况下,祖先作用域中定义的变量可能是可见的(父作用域、祖父母作用域…),这要归功于PowerShell的动态作用域

    • 在您的案例中,$result是在全局范围中定义的,模块也可以看到,所以您的模块函数看到了这个值。

      • 但是,请注意,赋值给$result会隐式创建一个名为局部变量,而不是修改祖先变量在本地创建后,变量阴影化为同名的祖先;也就是说,它隐藏它,除非您在定义它的范围中显式引用它
    • 还要注意的是,模块在设计上不会从全局作用域之外的模块外部调用程序中看到变量,例如如果您的模块是从脚本调用的。

    • 有关PowerShell中作用域的详细信息,请参阅此答案。


解决方案

  • 在函数开始时初始化局部变量$result以保证其存在-请参阅下文。

  • 或者,明确地引用局部变量-我提到这些选项主要是为了完整性和说明基本概念,我认为它们不实用:

    • 您可以使用作用域说明符$local:,在$y0的情况下,这将导致$local:result引用一个不存在的变量(除非您在失败的除法之前对其进行了初始化),PowerShell默认为$null:

      • if ($null -ne $local:result) { Write-Host "x/y = $result" }

      • 注意事项:如果Set-StrictMode-Version 1或更高版本生效,对不存在的变量的引用会导致语句终止错误(这意味着默认情况下,函数/脚本作为一个整体将在下一条语句中继续执行)。

    • 一个严格的模式无关但详细且较慢的替代方法是使用Get-Variablecmdlet显式测试本地变量的存在

      • if (Get-Variable -ErrorAction Ignore -Scope Local result) { Write-Host "x/y = $result" }

带初始化的解决方案==预先创建局部变量:

function Get-Foo{
param(
[int]$x,
[int]$y
)
# Initialize and thereby implicitly create
# $result as a local variable.
$result = 0
try{
$result = $x/$y
} catch{
Write-Warning "Something get wrong"
}
# If the division failed, $result still has its initial value, 0
if($result -ne 0){
Write-Host "x/y = $result"
}
}

我不确定这是否真的是最佳实践,但我避免这种情况的方法是始终声明我使用的变量(除非我特别需要使用父作用域中的变量,这有时会发生)。这样你就可以确保你永远不会达到模块中父范围的值:

# Declare
$result = $null
# Do something
$result = $x/$y

当然,在你的例子中,如果看起来过于夸张,但在现实生活中可能是合理的。

我能想到的另一种方法是改变范围。

$result => $private:result

或者像迈克建议的那样去$script:result

这是因为当除以零时,您没有为变量结果赋值,因为它会产生错误。全局作用域(PowerShell控制台)中有一个变量$result,该变量将被继承到函数作用域中(子作用域和继承是父子关系,反之亦然)!例如,如果您有一个值,该值被分配给chatch块中的变量$result,它可以解决问题。类似于:

function Get-Foo{
param(
[int]$x,
[int]$y
)
try{
$result = $x/$y
} catch{
Write-Warning "Something get wrong"
$result = $_.exception.message 
}
if($result -ne 0){
Write-Host "x/y = $result"
}
}

注意:$_.exception.message=$error。例外情况。在这种情况下为消息

另一种方法是在函数$global:result = $null的开头对变量结果使用scope修饰符。通过这种方式,您将使全局变量为空(或提供其他值),但结果是:

WARNING: Something get wrong
x/y =

这并没有真正的意义。

更多详细信息:get-help about_Scopes -ShowWindow或:https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_scopes?view=powershell-6

如果你还有更多的问题,我很乐意回答。

为此,您需要充分了解Powershell作用域类型。有四种不同类型的作用域:全球范围,脚本范围,私有范围,本地作用域

我认为您需要利用脚本作用域,因为这些作用域是在运行/执行PS1脚本/模块时创建的。这意味着您必须定义如下变量:

$script:x
$script:y

最新更新