Perl 中的 Typeglob 及 Exporter 工作原理

每个包都有一个符号表

Perl 中每个包,都有一个符号表,存放了当前包中定义的变量和子例程。符号表是一个名字为 %PackageName:: 的散列表。散列表的键是变量名。

在默认的包(不属于任何包)里,该符号表为 %main:: 或者 %::

既然符号表是一个散列表,那么就可以遍历它。

1 while (my ($k, $v) = each %::) {
2     say "$k => %v";
3 }

仅有使用 our 定义的变量才会被记录进符号表里,而使用 my 定义的变量不会被记录,没有使用这两个关键词定义的变量默认为 our 定义的,所以会记录进符号表。

由于 Perl 包的透明性,可以在一个包里访问另一个包的符号表。

遍历系统标准库 strict 的符号表:

1 while (my ($k, $v) = each %strict::) {
2     say "$k => %v";
3 }

符号引用

创建引用

1 my $s  = 'scalar';
2 my $sr = $s;                # 标量引用
3 my $cr = "constant";        # 常量引用
4 my $lr = [1 .. 5];           # 列表引用
5 my $hr = { a => 2 };         # 哈希引用
6 my $fr = sub { "fun" };      # 函数引用

解引用需要使用正确的前缀

1 say $$sr;
2 say $$cr;
3 say @$lr;
4 say %hr;
5 say &$fr;

Perl 中还有一种引用称为 Symbolic RefSymbolic Ref 同样只对 our 定义的变量有效

 1 no strict 'refs';
 2 
 3 our $a = 1;
 4 our $b = 'a';
 5 
 6 say $a;                  # 1
 7 say $$b;                 # 1
 8 
 9 $a = 2;
10 
11 say $a;                  # 2
12 say $$b;                 # 2

上面的代码中 $b 是一个字符串,而非引用,但是当解引用时,它还有一次证明它自己的机会:用字符串的值当作它所引用的变量的名字。因此确定 $b 不是引用后,Perl 会寻找一个名为 a 的变量,正好找到了,因此 $b 就可以看作 $a 的引用,而 $$b 就是 $a

注意上面的代码中,更改 $a$$b 其中一个的值时,另一个的值也会改变。

由于最近的版本默认添加了 use strict ,无法使用符号引用,所以需要使用 no strict 'refs' 开启符号引用支持。

Typeglob

Perl 中变量分为几种类型:标量,列表,哈希等等,并以不同的前缀表示。名字相同但是变量前缀不同的变量之间是可以独立使用的。

上面说到符号表的键是变量名,由于散列表的键不能重复,那么符号表是怎么保存名字相同但独立的变量的呢?

实际上在符号表和变量值之间存在一个叫做 Typeglob 的结构,Typeglob 中保存了一组指向各种类型的值的指针,而符号表中保存了 Typeglob 结构的指针,所以上面遍历输出符号表时看到的是值为 * 开头的 Typeglob 结构。

1 stdin => *main::stdin
2 stderr => *main::stderr
3 ...
                     typeglob (*foo)
  符号表
                      +---------+
+--------+            |         | ---------> 标量变量 ($foo)
|        |            |         |
|        |            |         | ---------> 列表变量 (@foo)
|--------|            |         |
|  foo   | ---------> |         | ---------> 散列变量 (%foo)
|--------|            |         |
|        |            |         | ---------> 函数变量 (&foo)
|        |            |         |
|        |            |         | ---------> 文件句柄 (foo)
+--------+            |         |
                      +---------+

使用前缀 * 可以获取一个变量名的 Typeglob 结构,也可以在 Typeglob 结构添加不同类型的前缀解引用获得对应类型的值。

Typeglob 可以向其它类型的变量一样互相赋值。

1 our $foo = 1;
2 
3 #bar = *foo;
4 
5 say $foo;

Typeglob 赋值的效果相当于建立一个指向 Typeglob 的指针。

                       Typeglob
  符号表              (*foo *bar)
                      +---------+
+--------+            |         |
|        |            |         | ---------> 标量变量 ($foo)
|        |            |         |
|--------|            |         | ---------> 列表变量 (@foo)
|  foo   | ---------> |         |
|--------|            |         | ---------> 散列变量 (%foo)
|--------|            |         |
|  bar   | ---------> |         | ---------> 函数变量 (&foo)
|--------|            |         |
|        |            |         | ---------> 文件句柄 (foo)
+--------+            |         |
                      +---------+

Typeglob 也可以跨符号表赋值

                     typeglob (*foo *bar)
  符号表
                      +---------+
+--------+            |         | ---------> 标量变量 ($foo)
|        |            |         |
|        |            |         | ---------> 列表变量 (@foo)
|--------|            |         |
|  foo   | ---------> |         | ---------> 散列变量 (%foo)
|--------|            |         |
|        |   +------> |         | ---------> 函数变量 (&foo)
|        |   |        |         |
+--------+   |        |         | ---------> 文件句柄 (foo)
             |        |         |
             |        +---------+
             |
+--------+   |
|        |   |
|        |   |
|--------|   |
|  bar   | --+
|--------|
|        |
|        |
+--------+

一个简易的 MyExporter

到这里,Exporter 实现的原理也就很明白了:将目标包中的若干子例程对应的 Typeglob 注入到当前包中,便可以省略前缀调用了。了解了这个原理,就可以写个简易的 MyExporter 包了。

上一篇文章中说到,使用 use 加载包时,会自动调用包里的 import 子例程

1 use MyExporter qw/sub1 sub2/;
2 
3 # 等价于
4 
5 BEGIN {
6     require MyExporter;
7     MyExporter->import(qw/sub1 sub2/);
8 }

所以就可以在 import 子例程中注入 Typeglob。

 1 package MyExporter;
 2 
 3 use 5.014;
 4 use warnings;
 5 
 6 sub import {
 7     # 获取使用这个包的包名
 8     my $package = caller(0);
 9 
10     # 获取当前包名
11     my $self    = shift;
12 
13     # 开启符号引用支持
14     no strict 'refs';
15 
16     # 对每个参数对应的 Typeglob 进行注入
17     *{"$package::$_"} = &{"$self::$_"} for @_;
18 }

本文作者: zhustec

本文标题: Perl 中的 Typeglob 及 Exporter 工作原理

本文链接: https://blog.zhustec.me/posts/typeglob-and-exporter-mechanism-in-perl

发布时间: 2015-01-14T06:44:48+00:00

版权声明: 本文由 zhustec 原创 采用 CC BY-NC-ND 4.0 许可协议 转载请保留以上声明信息!