代码改变世界

Windows PowerShell 2.0之脚本块变量作用域

2010-11-21 00:53 @天行健中国元素 阅读(...) 评论(...) 编辑 收藏

变量作用域用来定义程序中哪些部分可以使用某个变量。PowerShell中对象的作用域规则设计得很巧妙,类似变量,但是却更强大,在脚本块、函数和脚本边界传递。本文仅给出脚本块中作用域的清晰定义,其他有关函数和脚本文件的作用域会分其他系列文章中详细介绍。

变量作用域定义

作用域是计算机科学中用于定义变量可以在哪些代码区域中起作用,类似于命名空间。这种机制允许用于在不同的环境下使用相同命名的对象,这些对象不会因为命名相同而造成冲突。很少有语言不支持不同变量有不同作用域的概念,即每个变量在任何地方都有效。在代码量较大的情况下,为代码编写制造了隐患,开发人员必须在后面很小心地变量;一旦出现重名,后面的重名变量将会覆盖前面重名的变量值,如:

$myVariable = 4
#.......省略大量代码........
$myVariable = 7

第2个变量赋值成功后将会丢失第一个变量的值,即发生不易捕获的bug。

包括PowerShell在内的现代编程语言通过允许每个逻辑代码块拥有自己不同的变量空间来解决这个问题。通过这种方法,在一个代码块中初始化并赋值给某个变量不会影响其他单元。脚本块、函数和脚本文件也是这样,即在脚本块内部赋值给变量不会覆盖代码块外部相同名称的变量,如:

$myVariable  = 6
&{
   $myVariable = 7
}

上例中脚本块中的变量$myVariable保存的值7不会覆盖脚本块外的同名变量。

作用域本质上是分等级的,每个脚本块会创建新的本地作用域并声明自己的变量。它会继承在父作用域定义的变量,父作用域通常是全局的。脚本块可以访问在父定义域中定义的变量,而通常这样的访问是只读的。脚本块可以读取在其之前定义的变量,如果要修改变量,则要执行额外的操作。下例演示脚本块如何从父定义域读取变量并输出到控制台:

PS C:\> $name =  "Liu Tao"
PS C:\> $personAction = {Write-Host "Hello $name"}
PS C:\> &$personAction
Hello Liu Tao

改变$name变量值并再次调用脚本块将会得到不同的信息:

PS C:\> $name =  "Wang Lei"
PS C:\> $personAction = {Write-Host "Hello $name"}
PS C:\> &$personAction
Hello Wang Lei

如果在脚本块内部赋值给变量,PowerShell会首先检查变量之前在当前作用域中是否定义。如果没有定义,则会创建新的本地变量。下例在$personAction脚本块中为$name变量赋值:

PS C:\> $personAction = {
>> $name = "LiuTao"
>> Write-Host "`$name changed to $name"
>> }
>>
PS C:\> $name = "WangLei"
PS C:\> &$personAction
$name changed to LiuTao
PS C:\> $name
WangLei

在上例中需要强调脚本块中使用了修改后的变量,用LiLei作为人名,并且脚本块中的$name变量和脚本块外的$name不同。即二者是完全不同的变量,本地变量屏蔽了全局变量,这就是为什么全局的$name变量会在脚本块执行后仍然保持原有的值的原因。可以在脚本块中修改全局变量,只是操作稍微复杂一些。

一旦认识到变量前的美元符语法只是后台调用Get-Variable和Set-Variable的便捷速记后,即可改变任何作用域中的变量值。使用Get-Variable可以获取变量的PSVariable对象引用,下例使用其获取$name变量的信息:

PS C:\> Get-Variable name

Name                           Value
----                           -----
name                           WangLei

要强调的是既然$变量名的形式是Get-Variable的速记,那么在使用Get-Variable查询变量信息时变量名前不需要加$。大多数情况下变量名只是传递给Get-Variable的参数,关键是变量值,如果只获取变量值,则传递-valueOnly参数即可,如:

PS C:\> Get-Variable name -valueOnly
WangLei

类似地Set-Variable用变量名查找变量并修改其值,如:

PS C:\> Set-Variable name "XiaoGang"
PS C:\> $name
XiaoGang

下例使用Get-Variable和Set-Variable重写$PersonAction:

PS C:\> Set-Variable "name" "LiuTao"
PS C:\> Get-Variable "name" -valueOnly
LiuTao
PS C:\> $personAction = {
>> Set-Variable name "WangLei"
>> Write-Host "`$name changed to $name"
>> }
>>
PS C:\> &$personAction
$name changed to WangLei
PS C:\> Get-Variable "name" -ValueOnly
LiuTao

Get-Variable和Set-Variable语法比之前使用的赋值语法的功能更加的强大,二者均支持-scope参数,用来指定将会在哪个作用域范围内搜索和设置变量名。scope参数值为0代表当前作用域,1代表父作用域,2代表祖父作用域等。可以用这些参数来在当前脚本块中修改父作用域的变量值,下例重写$personAction:

PS C:\> $name = "LiuTao"
PS C:\> $personAction = {
>> Set-Variable name "WangLei" -scope 1
>> Write-Host "`$name changed to $name"
>> }
>>
PS C:\> &$personAction
$name changed to WangLei
PS C:\> $name
WangLei

在上例中可以看到最后一个语句中父作用域中的$name变量值在脚本块中被改变。

在大多数情况下,用户并不关心多层嵌套的作用域,而是对当前作用域和全局作用域之间的区别感兴趣,这样可以在变量名前加前缀以标识变量作用域。仍然以$personAction脚本块为例,可以把$name变量定义为全局变量,并在脚本块内部修改其值:

PS C:\> $global:name = "LiuTao"
PS C:\> $personAction = {
>> $global:name = "WangLei"
>> Write-Host "`$global:name changed to $global:name"
>> }
>>
PS C:\> &$personAction
$global:name changed to WangLei
PS C:\> $global:name
WangLei

在前面的例子中为name变量冠以$global:前缀使其成为一个全局变量。需要强调的是这里只是在global前添加了美元符,并没有在实际变量名前再次添加美元符。

PowerShell支持如下4种作用域前缀。

(1)global:适用范围最广的作用域,所有代码中的global变量都是有效的。

(2)script:变量仅在当前脚本文件中可见,在脚本文件以外无效。

(3)local:默认作用域,变量在当前和嵌套的作用域中可见,赋值操作语法能在当前的local作用域中修改变量值。

(4)private:最严格的作用域,变量仅在当前作用域有效。有了这个关键字,就可以在子脚本块中隐藏变量。

下例演示如何在脚本块中隐藏变量:

PS C:\> $private:numberValue = 1
PS C:\> $checkNumberValue = {
>> if ($numberValue -eq $null)
>>  {
>>    Write-Host "Cannot access to `$numberValue"
>>  }
>> }
>>
PS C:\> &$checkNumberValue
Cannot access to $numberValue
PS C:\> $NumberValue
1

在上例中只有$numberValue为$null时才会在控制台输出文字,这里输出了文字,可见在$checkNumberValue脚本块中$numberValue无效。这样就在子脚本块中屏蔽了父作用域的变量,而变量在父作用域仍然是可见的。即使将变量声明为private也无法从Get-Variable和Set-Variable中隐藏变量。下例当中仍以$checkNumberValue脚本块为例:

PS C:\> $checkNumberValue = {
>> if ((Get-Variable "numberValue" -valueOnly -scope 1) -ne $null)
>>  {
>>    Write-Host "Can access `$numberValue from parent scope"
>>  }
>> }
>>
PS C:\> &$checkNumberValue
Can access $numberValue from parent scope

其中并没有显式地初始化变量$numberValue的值。以前一个实例中已经初始化了$numberValue值。在解析一个没有作用域前缀的变量时,PowerShell会在private和local两个作用域中查找,接着在script和global中查找,因此在前面在子脚本块中读取父作用域的变量$numberValue会返回真值。在没有指定前缀的情况下,为同名变量赋新值总会创建local作用域的变量,如:

PS C:\> $global:name = "LiuTao"
PS C:\> $personAction = {
>> Write-Host "`$name = $name"
>> $name = "WangLei"
>> Write-Host "`$name changed to $name"
>> }
>>
PS C:\> &$personAction
$name = LiuTao
$name changed to WangLei
PS C:\> $name
LiuTao

其中脚本块读取$name变量并获取全局变量,在后面为变量赋值创建了local作用域下的变量,在最后能看到全局变量的值没有变化。

在当前作用域中,PowerShell支持通过在命令前加点的形式执行命令的特殊语法,这种语法称为“dot-sourcing”(援引)。援引脚本块使脚本块中的变量在当前作用域中可用。下例中会在脚本块中声明变量:

PS C:\> $numberBlock = {
>> $numberInBlock = 1
>> Write-Host $numberInBlock
>> }
>>
PS C:\> &$numberBlock
1
PS C:\> $numberInBlock -eq $null
True

其中变量$numberInBlock仅在脚本块中有效。可以通过验证脚本块中变量的有效性验证援引脚本块的功能。下例中首先援引脚本块:

PS C:\> .$numberBlock
1
PS C:\> $numberInBlock
1

可以看到的在对脚本块执行dot-sourcing操作之后,脚本块中的变量在当前作用域中有效。dot-sourcing的作用可以看做是将脚本块中的环境释放到当前作用域中,并执行脚本块中的代码。在前面的例子中$numberInBlock变量可用,因为$numberBlock脚本块的内容已经释放并执行。需要强调的是通过援引操作执行脚本块不再需要在脚本块名前添加&符号调用。通过比较可以看出调用脚本块(使用&)和援引脚本块的区别只是执行脚本块,并不将脚本块的变量环境释放到当前作用域。

在调用管道以后,在同一个管道中的脚本块共享相同的作用域。下例包含两个脚本块的管道传递给应用另一个管道的cmdlet,第1个脚本块中声明的变量会在第2个脚本块中有效:

PS C:\> dir *.txt | foreach {$file = $_.Name; return 3}|
>> foreach {"got $file"}
>>
got digit.txt
got largetext.txt
got smalltext.txt
got test.txt
got test2.txt

需要强调的是第1个foreach块设置了$file变量并返回了哑元数,$file变量在第2个foreach块中仍然可用。

总结

变量作用域能够有效地控制变量的生存周期,合理的设计程序中变量作用域的范围可以很好的简化程序的复杂度,尽可能避免程序中变量名污染的问题,使PowerShell强大的变量处理能力得到很好的应用。程序中的变量作用域需要掌握的原则是在能够传递数据的前提下,尽量减小变量的作用域范围,这样既能保护数据不被外界恶意调用,也不会造成同名变量在不同作用域下产生冲突。

赛迪网地址:http://news.ccidnet.com/art/32855/20100612/2086063_1.html

作者: 付海军
出处:http://fuhj02.cnblogs.com
版权:本文版权归作者和博客园共有
转载:欢迎转载,为了保存作者的创作热情,请按要求【转载】,谢谢
要求:未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
个人网站: http://txj.shell.tor.hu/