PowerShell中的变量作用域

70

PowerShell中令人遗憾的一点是函数和脚本块采用动态作用域。

但另一件让我感到惊讶的事情是,在内部作用域中,变量表现为写时复制。

$array=@("g")
function foo()
{
    $array += "h"
    Write-Host $array
}

& {
    $array +="s"
    Write-Host $array
}
foo

Write-Host $array

输出结果为:

g s
g h
g

这使动态作用域变得稍微不那么痛苦。但是如何避免写时复制呢?

5个回答

93
The PowerShell scopes article (about_Scopes) is nice, but too verbose, so this is a quotation from my article:
In general, PowerShell scopes are similar to .NET scopes. They include:
- Global: public - Script: internal - Private: private - Local: current stack level - Numbered scopes: from 0..N where each step is up to the stack level (and 0 is Local)
Here is a simple example that describes the usage and effects of scopes:
$test = 'Global Scope'
Function Foo {
    $test = 'Function Scope'
    Write-Host $Global:test                                  # Global Scope
    Write-Host $Local:test                                   # Function Scope
    Write-Host $test                                         # Function Scope
    Write-Host (Get-Variable -Name test -ValueOnly -Scope 0) # Function Scope
    Write-Host (Get-Variable -Name test -ValueOnly -Scope 1) # Global Scope
}
Foo

正如您所见,您只能在指定了命名作用域的情况下使用类似$Global: test的语法,$0:test始终为$null。


77

你可以使用作用域修饰符*-Variable cmdlets。

作用域修饰符包括:

  • global,用于访问/修改最外层的作用域(例如交互式shell)
  • script,用于在运行脚本的作用域(.ps1文件). 如果没有运行脚本,则操作与global相同。

(有关*-Variable cmdlet的-Scope参数,请查看帮助。)

例如,在您的第二个示例中,要直接修改全局变量$array

& {
  $global:array +="s"
  Write-Host $array
}

详情请参见帮助主题about_scopes


谢谢提供的信息。我已经大致阅读了关于作用域的主题。但是这份文档中没有提到的一件事是变量具有动态作用域。 :( - mathk
这在我检查脚本错误时让我感到困惑;我有一个简单的计数器,每当脚本遇到错误时就会增加一次,以确定发送电子邮件中的信息。最终的错误计数总是零,无论我设置多少个错误条件。现在我明白了为什么会这样,并知道如何解决它。 - KeithS
我认为这种方法只能回答一两个非常特定的情况,而不能回答普遍情况。 - bielawski

17
不只是变量。当它说“项目”时,它指的是变量、函数、别名和psdrives。所有这些都有范围。
长说明 Windows PowerShell通过限制变量、别名、函数和Windows PowerShell驱动器(PSDrives)的读取和更改位置来保护对其的访问。通过执行一些简单的作用域规则,Windows PowerShell有助于确保您不会意外更改不应更改的项目。
以下是作用域的基本规则:
- 您在作用域中包含的项目在创建它的作用域以及任何子作用域中可见,除非您明确地使其为私有。您可以将变量、别名、函数或Windows PowerShell驱动器放置在一个或多个作用域中。
- 在作用域内创建的项目只能在创建它的作用域中更改,除非您明确指定不同的作用域。
你看到的写时复制问题是因为PowerShell处理数组的方式。向此数组添加内容实际上会摧毁原始数组并创建一个新数组。由于它是在该范围内创建的,因此在函数或脚本块退出并且范围被处理掉时被销毁。
您可以在更新变量时显式地指定作用域,或者您可以使用[ref]对象来进行更新,或者编写脚本以便在父级作用域中更新对象或哈希表的属性或对象或哈希表键。这不会在本地作用域中创建新对象,而是修改父作用域中的对象。

确实,函数和别名有点像变量。但是我并不关心这个。特别是在某些语言中,函数的作用域与变量不同(例如Common Lisp),但它们仍然是变量。 - mathk
如果有一天你编写了一个 Powershell 脚本,在子作用域中创建别名或函数,那么这可能会引起一些关注。 - mjolinor
但是你没有回答问题。@Richard 给出了正确的答案。 - mathk
我试图解释当你向数组添加内容时,无法避免写时复制。你必须控制它将副本写入的位置。如果您没有告诉它其他位置,它将写入到本地作用域。您还可以通过不向数组添加内容,而是添加或更新对象的属性或哈希表的键来避免写时复制。 - mjolinor
2
但这会使PowerShell成为一种更糟糕的语言。如果作用域语义根据变量类型而改变,那就是可怕的设计选择,在我看来。此外,它还将动态作用域带回到你的视野中。 :( - mathk
2
好吧,就是这样。我只是试图解释正在发生的事情。复制写是这个问题的本质。显式地作用域变量并没有避免复制写,它只是指定了复制将要被写入的位置。 - mjolinor

5

虽然其他帖子提供了许多有用的信息,但似乎只是让你不必去看文档。我认为最有用的答案却没有被提及!

([ref]$var).value = 'x'

这会修改 $var 的值,不管它处于哪个作用域。您不需要知道它的作用域;只需要知道它已经存在。使用OP的示例:

$array=@("g")
function foo()
{
    ([ref]$array).Value += "h"
    Write-Host $array
}
& {
    ([ref]$array).Value +="s"
    Write-Host $array
}
foo
Write-Host $array

生成:

g s
g s h
g s h

解释:
([ref]$var)会给你一个指向变量的指针。由于这是一个读操作,它会解析到实际上创建该名称的最近范围。如果变量不存在,它也会解释错误,因为[ref]无法创建任何东西,它只能返回对已经存在的某些东西的引用。

.value然后带您访问保存变量定义的属性;然后您可以进行设置。

您可能会尝试像这样做,因为它有时看起来像是有效的。

([ref]$var) = "New Value"

不要这样做!
看起来可以工作的情况只是一种错觉,因为PowerShell只在一些非常狭窄的情况下才会执行某些操作,例如在命令行上。你不能指望它能够如此操作。实际上,在OP示例中它并不起作用。


0
关于PowerShell的一个令人沮丧的事情是函数和脚本块是动态作用域。使用.GetNewClosure()创建一个闭包,以捕获来自即时本地作用域的变量:
function New-Closure
{
    $foo = 42
    {$foo}.GetNewClosure()
}

❯ $Closure = New-Closure
❯ & $Closure
# output: 42

该范围可通过Module属性访问:

❯ {"some new scriptblock"}.Module
# no output

❯ $Closure.Module

ModuleType Version    PreRelease Name                                ExportedCommands
---------- -------    ---------- ----                                ----------------
Script     0.0                   __DynamicModule_f89a39f6-f9f7-40ce…

函数是否也应该动态作用域是一种语言设计决策。我想不出任何一种脚本语言不允许函数从调用者的作用域中访问动态变量 - 也许你是在Haskell上成长的 ;-)

编辑:函数可以访问包含模块的作用域。PowerShell部署的单位是模块。如果您不想编写模块,可以创建一个动态模块:

New-Module {
    $foo = 23
    function Get-EnclosedFoo
    {
        $foo
    }
}
# output: dynamic module

Get-EnclosedFoo
# output: 23

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接