【问题标题】:Avoid variable in module to be overriden by caller and vice-versa避免模​​块中的变量被调用者覆盖,反之亦然
【发布时间】:2020-02-07 16:08:21
【问题描述】:

我正在 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 "$PSScriptRoot\MyModule\MyModule.psm1" -Force

$result = 3 # some other computation

Get-Foo -x 42 -Y 0

输出是:

x/y = 3

如您所见,调用者声明的变量名与我模块中的变量名冲突。

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

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

【问题讨论】:

  • 尽管作用域确实在这里发挥了作用,但一个问题是产生了终止错误。因此,$result 永远不会按预期设置。您当前位置的if 声明从不考虑这种可能性。如果将 if 语句移到 try {} 块内,则不会出现主要症状。
  • @AdminOfThings,我假设如果 $result 未设置,它将包含默认值(0 表示整数,或 null 表示更真实的场景)
  • @SteveB,默认情况下在 PowerShell 中引用不存在的变量会计算为 $null(如果您有 Set-StrictMode -Version 1 或更高版本,则会触发语句终止错误)。由于$null -ne 0$true,因此您的if 语句块被输入。
  • 我很感激,史蒂夫。

标签: powershell


【解决方案1】:

Ivan Mirchev's helpful answerrobdy's helpful answerAdminOfThings 的问题cmets 提供了关键的指示;让我总结和补充它们:

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

  • 在没有 本地$result 变量的情况下,在 祖先作用域中定义的变量可能是可见的(父作用域、祖父母作用域, ...),感谢 PowerShell 的动态范围

    • 在你的例子中,$result 是在 global 范围中定义的,模块也可以看到,所以你的模块函数看到了 价值。

      • 但是,请注意,分配$result 会隐式地创建一个具有该名称的本地变量,而不是修改原始变量。 一旦在本地创建,变量遮蔽同名的祖先;也就是说,它会隐藏它,除非您在定义它的范围内明确引用它。
    • 1234563一个脚本
  • 有关 PowerShell 中范围的详细信息,请参阅 this answer


解决方案

  • 在函数的开头初始化局部变量$result以保证其存在 - 见下文。

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

    • 您可以使用范围说明符$local:,如果$y0,这将导致$local:result 引用一个不存在的 变量(除非您在失败的除法之前初始化它),PowerShell 默认为$null

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

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

    • 一种独立于严格模式但冗长且速度较慢的替代方法是使用 Get-Variable cmdlet 显式测试您的局部变量是否存在:

      • 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"
    }
}

【讨论】:

    【解决方案2】:

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

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

    当然,在你的例子中,如果看起来有点矫枉过正,但在现实生活中可能是合理的。

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

    $result => $private:result
    

    $script:result 喜欢Mike suggested

    【讨论】:

    • “声明”(初始化和隐式创建)是一种明智的方法。我假设您的意思不是$private:,而是$local: 指的是(可能不存在,因此$null)局部变量。引用 $script: 范围在技术上是可行的,但在概念上是令人困惑的。
    【解决方案3】:

    这样做的原因是您在除以零时没有为变量 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.Exception.message

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

    WARNING: Something get wrong
    x/y =
    

    这并没有真正的意义。

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

    如果您有更多问题,我很乐意为您解答。

    【讨论】:

    • 好指点,但我建议不要提及全局变量的修改(除非不推荐)。
    【解决方案4】:

    为此,您需要对 Powershell Scope Types 有一个很好的了解。 有四种不同类型的范围: 全球范围, 脚本范围, 私有范围, 本地范围

    我认为您需要使用脚本范围,因为这些范围是在您运行/执行 PS1 脚本/模块时创建的。这意味着您必须像这样定义变量:

    $script:x
    $script:y
    

    【讨论】:

    • 使用 $script: 前缀完美。但是,这会使模块代码的可读性降低,编写起来也更复杂。
    • 我同意。我也不喜欢它,但在这种情况下我们必须使用范围。也许有人有一个很好的选择来避免在每个变量之前使用作用域类型?太好了,我可以帮你解决这个问题:-)
    • script 范围纳入混合是不必要的并且在概念上令人困惑(除了问题与$result 相关,而不是$x$y) ;您可以使用$local: 显式引用局部变量($local:result),如果默认情况下不存在,则评估为$null),或者更好的是,显式初始化,从而在开始时创建感兴趣的局部变量你的功能:$result = 0.
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-29
    • 2014-03-21
    • 1970-01-01
    • 2021-12-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多