软件包变量和词法变量

软件包变量(package variable)一般也被称为全局变量, 使用 our 可以声明软件包变量. 软件包变量会被存入符号表. 操作符号表和 typeglob 的时候操作的是软件包变量.

词法变量(lexical variable)使用关键字 my 声明, 作用域有限, 且不会被存入符号表.

符号表类似于一个哈希表, 只是类似, 并不完全相同. 我们可以使用 keys 关键字来查询其中保存的内容, 比如:

foreach (keys %main::) {
    print $_ . "\n";
}

如果有定义 package 的名字, 则可以这样查看:

package Foo;

foreach (keys %Foo::) {
    print $_ . "\n";
}

其中的 %main::%Foo:: 都是符号表.

基于此, 我们使用 keys 可以观察一下使用 ourmy 声明的变量的不同:

our:

package Foo;

our $bar;
# infact, it will also print "bar" if we declare $bar by using local
# local $bar;

# this will print "bar"
foreach (keys %Foo::) {
    print $_ . "\n";
}

my:

package Foo;

my $bar;

# this will print nothing
foreach (keys %Foo::) {
    print $_ . "\n";
}

可以很明显的发现, 使用 our 声明的变量会被加入符号表 %Foo::, 而使用 my 声明的变量则不会. 这是这两种声明方法比较大的区别: 一个声明的是软件包变量, 存在于符号表中, 另一个声明的是词法变量, 不会存在于符号表.

our / local / my

其实前面提到了 ourlocal 声明的变量属于一类, 都属于软件包变量, 而 my 声明的属于另一类, 叫做词法变量, 但是他们的关系还是有点复杂, 举些例子来分析和理解.

一. 重复使用 our 声明变量会覆盖前面的变量, 即使是在新的作用域中:

our $foo = 1;
{
    our $foo = 2;
}
print $foo; # now $foo is 2

二. 在新的作用域中使用 localmy 不会影响原作用域中的变量:

our $foo = 1;
{
    local $foo = 2;
}
print $foo; # $foo is still 1

########################

our $foo = 1;
{
    my $foo = 2;
}
print $foo; # $foo is still 1

在这里看起来 localmy 的表现形式又很类似了, 让人比较迷惑, 其实他们还是有很大区别的, 继续举例子说明.

三. local 会在新作用域中暂时改变符号表中的变量值, 直到作用域退出. 而 my 创建的变量由于不存在于符号表中, 所以不会影响符号表中的同名变量:

our $foo = 1;
sub print_val {
    print $foo;
}
{
    local $foo = 2;
    print_val(); # this will print 2
}

########################

our $foo = 1;
sub print_val {
    print $foo;
}
{
    my $foo = 2;
    print_val(); # this will print 1
}

分析下上面的例子, 在我们调用了一个函数来打印 $foo 的值的时候, 情况又发生了一些变化.

首先, 我们在函数 print_val 中打印的 $foo 肯定是 our $foo 声明出来的. 注意一点, ourlocal 声明的变量都存在于符号表中, 可以认为他们就是同一个变量. local 的效果就是在新的作用域中, 暂时把外部的变量的值保存起来, 然后把符号表中该变量的值设置为新的值, 等到作用域退出后, 将保存的值恢复到符号表中. 所以在我们使用 local $foo = 2$foo 重新赋值为 2 以后, print_val 函数打印出来的 $foo 也变成了 2. 因为他们都是软件包变量, 而且在新的作用域中, 它已经被暂时替换为了 1.

而第二种写法使用 my 声明变量的时候, 注意它声明出来的是词法变量, 并不存在于符号表中, 所以可以认为此时的 $foo 跟外部使用 our $foo 声明的变量 $foo 毫无关系, 根本就是两个变量. 所以我们当把这个词法变量 $oo 声明出来且赋值为 2 的时候, 对软件包变量$foo 没有任何影响, 自然使用 print_val 函数打印软件包变量 $oo 的时候依然会打印出最初的值 1 了.

研究的时候需要重点关注一下此处的区别.

typeglob

$fooscalar, @fooarray, %foohash 一样, *footypeglob. typeglob 也和符号表一样, 类似于一个哈希表但是却不是一个哈希表, 只是行为有些类似.

我们可以从中取值但是不能对其赋值:

$foo = *foo{SCALAR};
@foo = *foo{ARRAY};
%foo = *foo{HASH};

*foo{SCALAR}   = 5; # error
*foo{WHATEVER} = 5; # error

而且我们不能通过 keys 来查看 typeglob 中到底有哪些键名:

print keys *foo; # Experimental keys on scalar is now forbidden

实际上, typeglob 中的键名只有如下几种, 是固定不可变的:

键值 变量写法 说明
SCALAR $foo 标量
ARRAY @foo 数组
HASH %foo 哈希
CODE &foo 函数
IO - 文件句柄
GLOB *foo typeglob
FORMAT - 报表的格式
NAME - 变量的名字
PACKAGES - 包的名字

别名 (alias)

我们可以通过把变量的 typeglob 赋值给另一个变量的 typeglob 来创建变量的别名.

创建别名的时候也有两种方式, 一种是创建了完全的别名, 即上一节提到的全部键值对应的部分都成为了别名. 另一种是创建部分别名, 将某个变量的引用赋给另一个变量的 typeglob, 那么只有这个类型的变量创建了别名:

*foo =  *bar; # full alias
*foo = \$bar; # partial alias

而且除了"完全别名"和"部分别名"这个不同以外, 他们还有一些微妙的区别, 此处需要结合最初讲到的 local 等内容来举例说明:

our $bar = 1;
*foo = *bar;
{
    local $bar = 2;
    print $foo; # this will print 2
}

########################

our $bar = 1;
*foo = \$bar;
{
    local $bar = 2;
    print $foo; # this will print 1
}

发现了吗, 在新的作用域中, "完全别名"的写法会受到 local 的影响, 而"部分别名"的写法不会受到 local 的影响.

我们可以通过打印各个变量的内存地址的方式来看看 local 的时候发生了什么, 而使用"完全别名"和"部分别名"两种写法的时候又各自发生了什么.

our $foo;
sub print_val {
    print \$foo . "\n";
}
print \$foo . "\n";     # SCALAR(0x3278c0)
{
    local $foo;
    print \$foo . "\n"; # SCALAR(0x72d430)
    print_val();        # SCALAR(0x72d430)
}

结合我们之前提到的知识, 可以发现, 使用 local 的时候, 会在新的作用域中, 创建另一个软件包变量, 而且能看见使用函数 print_val 打印出来的变量地址就是我们 local 产生的那个变量的地址, 证明了我们将外部作用域中同名软件包变量保存了起来, 然后使用新作用域中 local 产生的变量替换了这个软件包变量, 直到作用域退出后恢复以前的那个变量. 这里注意下实现方式是, 在新的作用域中, $foo 指向了 local 新产生的变量的地址, 原作用域中的变量依然存在, 在退出新作用域回到老作用域的时候, 将 $foo 重新指向老的变量地址, 实现了恢复的效果.

继续看"完全别名"下的效果:

our $bar;
print \$bar . "\n"; # SCALAR(0x48dfb8)

*foo = *bar;
print \$foo . "\n"; # SCALAR(0x48dfb8)

sub print_val {
    print \$foo . "\n"; # SCALAR(0x4fb590)
    print \$bar . "\n"; # SCALAR(0x4fb590)
}

{
    local $bar;
    print_val();
    print \$bar . "\n"; # SCALAR(0x4fb590)
    print \$foo . "\n"; # SCALAR(0x4fb590)
}

在这种"完全别名"的写法下, 外部作用域中两个变量 $foo$bar 地址一致, 新作用域中两个变量 $foo$bar 的地址也一致, 使用函数打印的时候, $foo$bar 也一致, 而且和 local 产生的变量一致, 说明 foo 真的就完全是 bar 的别名, 你就是我, 我就是你, 双宿双飞.

当进入新作用域, 使用 local 创建了新的变量 $bar 的时候, 外部的 $bar 也会指向这个新的变量, 而 $foo 又是永远指向 $bar 的, 所以 $foo 也会指向这个新的变量地址.

然而在"部分别名"中, 现象就不一样了:

our $bar;
print \$bar . "\n"; # SCALAR(0x327440)

*foo = \$bar;
print \$foo . "\n"; # SCALAR(0x327440)

sub print_val {
    print \$foo . "\n"; # SCALAR(0x327440)
    print \$bar . "\n"; # SCALAR(0x8ad430)
}

{
    local $bar;
    print_val();
    print \$bar . "\n"; # SCALAR(0x8ad430)
    print \$foo . "\n"; # SCALAR(0x327440)
}

使用"部分别名"的写法的时候, foo 其实只是外部变量 bar 的别名. 因为使用部分别名这种写法, 是让 $oo 指向外部这个 $bar 的变量地址, 并不是无条件指向 $bar 的最新地址.

所以当我们在新作用域中使用 local 创建了新的变量 $bar 的时候, $foo$bar 就分道扬镳了, 各自走上了不同的道路.

在新的作用域中, 创建了新的 $bar 的变量地址, 这会让所有名字为 $bar 的变量都指向这个地址. 然而原来的地址并没有消失, 而我们的 $foo 又是被定义为指向老的那个地址的, 它并没有必要跟着指向新的地址. 所以自然在这种状况下分别打印 $foo$bar 就看到的是不同的地址了.

Comments
Write a Comment