Perl-语言编程-Notes

第2章 集腋成裘

2.2 分子

Perl 识别 token 时采用的是一种贪婪方式,如果特定情况下 Perl 解析器要在识别一个短 token 还是一个长 token 之间做出选择,他肯定会选择长的那一个.

如果某一行以 = 开头,这个语句是合法的,从这一行向下,直到下一个以 =cut 开头的行,这之间的所有内容都会被 Perl 忽略,被忽略的这些文本称为 pod (plain old document, 无格式旧式文档).

Perl 的发布版本提供了一些程序,可以从 Perl 模块抽取 pod 注释,并把它转换为纯文本,手册页,LaTeX, 甚至可以转换为 HTML 或 XML 文档.

2.10 型团 (typeglob) 和文件句柄

Perl 的类型团 (typeglob) 用以保存整个符号表记录,即表示所有类型.

*foo 包括 $foo, @foo, %foo, &foo 等. (毕竟 * 这个作为通配符时表示任意字符)

类型团 (typeglob) 的一个用途是用于传递或者存储文件句柄:

1
$fh = *STDOUT;

或:

1
$fh = \*STDOUT;

其主要用途是起别名:

1
*foo = *bar;

所有叫 bar 的都可以用 foo 替换.

或只给特定类型起别名:

1
*foo = \$bar;

这样 $foo 就是 $bar 的一个别名.

所有这些都只影响全局 (包) 变量,词法不能通过符号表记录访问.

qx// (quoted excution, 即执行命令) 操作符和 `` 的作用相同, 如:

1
$test_ls = `ls`;

等价于:

1
$test_ls = qx/ls/;

2.11.2 行输入 (尖角) 操作符

<>readline 函数作用相同.

1
while (glob "*.c") { chmod 0644,$_; }

等效于:

1
while (<*.c>) { chmod 0644,$_; }

第5章 模式匹配

5.2 模式匹配操作符

5.2.4 tr/// 操作符 (转换)

第10章 包

在 Perl 中,命名空间称为包 (package)

一个文件中可能有多个包,一个包也可以跨多个文件.

Perl 遇到一个代码块时,会把它编译为我们所称的当前包 (current package), 初始的当前包名为 “main”, 不过你可以在任何时刻用 package 声明把当前包切换为另一个包,当前包确定了要使用哪个符号表来查找变量,子例程,I/O 句柄和格式.

符号表

包的内容称为符号表 (symbol table). 符号表存储在一个散列中,这个散列与包同名,不过后面附加两个冒号.

因此,main 符号表的名称就是 %main::, 由于 main 正好是默认包,Perl 提供了 %:: 作为 %main:: 的简写形式.

我们说一个符号表 “包含” 另一个符号表时,是指它包含另一个符号表的一个引用,由于 main 是顶层包,它包含它本身的一个引用, 所以 %main:: 等于 %main::main:: 等于 %main::main::main::, 以此类推. 遍历符号表时需要注意.

在一个符号表的散列中,每个键/值对将一个变量名映射到这个变量的值.

因为包是散列, 因此你可以找出该包的键字然后获取所有包中的变量. 因此该散列的数值都是类型团, 可以用好几种方法解引用:

1
2
3
4
5
6
7
foreach $symname (sort keys %main::) {
local *sym = $main::{$symname};
print "\$$symname is defined\n" if defined $sym;
print "\@$symname is nonnull\n" if @sym;
print "\%$symname is defined\n" if %sym;

}

另一个符号表的用法 是制作 “常量” 标量:

1
*PI = \3.14159265358979;

现在就不能更改 $PI.

或者用:

1
use constant PI => 3.14159;

当你执行任意下列的赋值时, 实际上只是替换了类型团里的一个引用:

1
2
3
4
*sym = \$frodo;
*sym = \@sam;
*sym = \%merry;
*sym = \&pippin;

从另一个角度考虑, 类型团本身可以看作是一种散列, 它里面有不同类型的变量记录. 在这种情况下, 键字是固定的, 因为一个类型团正好可以包含一个标量, 一个散列等等. 可以去除独立的引用, 如:

1
*pkg::sym{SCALAR} # 和 \$pkg::sym 一样

10.2 自动装载

通常, 不能调用一个没有定义的字过程. 但如果在为定义的字过程的包里有一个子过程叫做 AUTOLOAD, 那么就会调用 AUTOLOAD 子过程, 同时还传递给它原本传递给最初子过程的同样参数.

最初的子过程的全称会出现在包局部变量 $AUTOLOAD 里.
如:

1
2
3
4
sub AUTOLOAD {
our $AUTOLOAD;
warn "Attempt to call $AUTOLOAD failed.\n";
}

比如调用了一个不存在的子过程 test, $AUTOLOAD 的值则被设置为 main::test.

注意 AutoSplit, AutoLoader, SelfLoader, DynaLoader 模块。

第十一章 模块

使用 Exporter 把符号输入到当前包里, 可以不加包限制词地使用来自该模块的符号.

在模块名字中的任何双冒号都被解释成你的系统的目录分隔符, 因此如果你的模块的名字是 Red::Blue::Green, Perl 就会把它看作 Red/Blue/Green.pm

Perl 将在 @INC 数组里面列出的每一个目录里面查找模块.

模块的名字一般首字母大写.

11.2 创建模块

一个模块可以有两个方法把它的接口提供给你的程序使用:

  • 把符号输出
  • 允许方法调用
1
2
3
4
5
6
7
8
9
10
11
package Bestiary;
require Exporter;

our @ISA = qw(Exporter);
our @EXPORT = qw(camel);
our @EXPORT_OK = qw($weight);
our $VERSION = 1.00;

sub camel { print "One-hump dromedary" }

$weight = 1024;

11.2.1 模块私有和输出器

Perl 模块和其用户之间有一种约定, 常见规则部分约定是说禁止一个模块修改任何没有允许它修改的名字空间.

从模块输出符号的动作有时候被称为 污染你的名字空间.

导入列表可以使用正则表达式:

1
use Bestiary qw(/am/);

以下两个等价:

1
use Bestiary LIST;

与:

1
2
3
4
BEGIN {
require Bestiary;
import Bestiary LIST;
}

11.2.1.1 不用 Exporter 的输入方法进行输出

11.2.1.2 版本检查

如果你的模块定义了一个 $VERSION 变量. 则可以:

1
2
3
use Bestiary 3.14; # Bestiary 必须是版本 3.14 或者更新

use Bestiary v1.0.4;

11.2.1.3 管理未知符号

放在 @EXPORT_FAIL 数组中避免把这些符号输出.

如果一个程序想要输入这些符号中的任何一个, Exporter 在生成一个错误之前给模块一个处理这种情况的机会, 通过传递这个失败符号列表调用 export_fail 方法, 可以自己定义:

1
2
3
4
5
sub export_fail {
my $class = shift;
carp "Sorry, these symblos are unavailable: @_";
return @_;
}

11.2.1.4 标签绑定工具函数

%EXPORT_TAGS 里列出的符号必须同时在 @EXPORT@EXPORT_OK 中, Exporter 提供了两个函数来增加:

1
2
3
4
5
%EXPORT_TAG = ( foo => [qw(aa bb cc)], bar => [qw(aa cc dd)] );

Exporter::export_tags('foo'); # 把 aa, bb, cc 加到 @export
Exporter::export_ok_tags('bar'); # 把 aa, cc, dd 加到 @export_OK

11.3 覆盖内建函数

使用 subs:

1
2
3
4
5
use subs qw(chdir chroot chmod chown);

chdir $somewhere;

sub chdir { ... }

内建的函数的最早的版本总是可以通过伪包 CORE 来访问. 因此, CORE::chdir 将总是最初编译进 Perl 里的版本, 即使 chdir 关键字已经被覆盖.

覆盖如:

1
2
3
4
5
6
7
8
9
10
*CORE::GLOBAL::glob = sub {
my $pat = shift;
my @got;
local *D;
if (opendir D, '.') {
$got = grep /$pat/. readdir D;
closedir D;
}
return @got;
}

第十三章 重载

13.1 overload 用法

use overload 实现操作符重载, 可以向其提供一个操作符和对应性质的 键字/数值列表 :

1
2
3
4
5
package MyClass;

use overload '+' => \&myadd, # 代码引用
'<' => "less_then", # 命名方法
'abs' => sub { return @_; }; # 匿名字过程

最后一个后面是 ;.

这里我们自己提供的过程称为 句柄 (handler)

当你使用 < 时, Perl 会注意其被声明为一个字符串, 就会将其当作方法名称, 而不仅仅是一个子过程.

在操作一个重载了的操作符的时候, 其对应的句柄是带着三个参数调用的, 前两个参数是两个操作数, 如果该操作符只使用一个操作数, 第二个参数是 undef.

第三个参数标明前两个参数是否交换. 交换过为真.

对于单目操作符, 当该操作符应用于该类的一个对象的时候则调用为该类声明的句柄.

对于双目操作符, 当第一个操作数是该类的一个对象或当第二个操作数是该类的对象而且第一个操作数没有重载性质的时候,则调用该句柄.

唯一的三元操作符 ?: 不能被重载.

默认传入的参数就是对象, 如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package Test;
use Moose;
use 5.36.0;
use utf8;
use overload '+' => 'plus';

has 'value' => (
is => 'ro',
isa => 'Str',
);

my $fir_str = Test->new(value => 'Hello');
my $sec_str = Test->new(value => 'World');
print $fir_str + $sec_str;

sub plus {
my ($fir_str, $sec_str) = @_;
$fir_str->value() . $sec_str->value();
}

13.3 可重载操作符

只有一部分操作符可以重载.

可以在 %overload::ops 散列中找到已经被重载的操作符.

有些并不是操作符, 但都是有效键字.

13.4 拷贝构造器 =

其作为一个重载的键字有点特殊的含义. 其并不重载 Perl 的赋值操作符.

书上内容不是很理解.

13.5 当确实重载句柄的时候 (nomethod 和 fallback)

如果对一个对象使用一个没有重载句柄的操作符, Perl 会找重载 nomethod 得到的行为, 在重载操作符中相当于子过程中的 AUTOLOAD.

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package Test;
use Moose;
use 5.36.0;

use overload 'nomethod' => \&say_something;

my $obj1 = Test->new();
my $obj2 = Test->new();

print $obj1 + $obj2;

sub say_something {
print "That's it";
}

这里未定义两个对象相加, 就调用了 nomethod 对应的句柄, 其实际上传入了四个参数:

  • 操作符两侧
  • 是否交换
  • 操作符

如果 Perl 找不到 nomethod 句柄, 就会报错.

为了在没找到时得到没有重载的结果, 使用 fallback 关键字. 其有三个状态 (就是使用 overloadfallback 赋值):

  • undef, 就是未定义 fallback 时的状态, 不做出任何行为
  • defined but false, 不会进行自动生成
  • true

如:

1
use overload "fallback" => 0, # ...

具体还是不太理解.

13.6 重载常量

可以用 overload::constantoverload::remove_constant 改变 Perl 解释常量的方法.

一般放在一个包的 import 方法中使用:

如:

1
2
3
4
5
6
7
8
9
sub import {
overload::constant (
integer => \&integer_handler,
float => \&base_handler,
binary => \&base_handler,
q => \&string_handler,
qr => \&regex_handler
)
}

overload::constant 后跟参数, 参数为键值对, 键只能为:

  • integer
  • float
  • binary
  • q, 表示 string 中的
  • qr, 表示 regex 中的

值为一个子过程引用.

表示, 当 Perl 记号查找其碰到一个常量数字 (即用 use constant 声明的数字)时, 就会调用 integerfloat 提供的句柄.

Perl 会给句柄传递三个参数:

  • 第一个, 原来的常量, 形式是它原来提供给 Perl 的形式
  • 第二个, 是 Perl 实际上如何解释该常量, 如 123_456 显示为 123456
  • 第三个, 只为那些由 qqr 处理字串的句柄定义. 值为 qq, q, s, tr 之一, 含义见书

似乎直接用不太行, 需要从模块中导出时利用 import.

如, 一个模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package Orkarin;
use overload;

sub Hello {
print "Hello!";
}

sub import {
overload::constant(
integer => \&handler,
);
}

sub handler {
my $integer = shift;
return $integer * 10;
}


1;

另一个脚本:

1
2
3
4
5
6
7
use 5.36.0;
use utf8;
use lib '/home/jie/scripts/perl/';
use My::Orkarin;

my $test = 10;
print $test;

则会打印 100.

13.7 公共重载函数

见书.

13.8 继承和重载

重载过程中发生继承, 在 use overload 中提供的是字符串如 use overload '+' => 'add' 而不是函数引用, 此时能被解释为方法, 并从父类中继承.

还有见书

13.9 运行时重载

因为 use 语句是在编译时重载的, 在运行是改变重载的唯一方法是:

1
eval "use overload '+' => \&my_add";

13.10 重载诊断

见书

第十四章 捆绑 (tie) 变量

tie 可翻译成捆绑, 也可是领带. tie 感觉像是一种调用器, 它会使变量有一些默认的动作. (也就是默认调用一些动作)

Perl 内建的 dbmopendbmclose 函数, 可以将散列变量和数据库绑在一起. (现在这两个函数就是依靠 tie 来实现)

如:

1
2
tie VARIABLE, CLASSNAME, LIST;
$object = tied VARIABLE;

(这个 LIST 后面再说, 其也是传递给 TIESCALAR 之类函数的参数)

tie 可以把一个标量, 数组, 散列或者文件句柄绑到一个类上, 还要提供一个方法名称的列表. 提供的第一个方法会在绑定时调用 (即, 调用一个构造函数, 其在成功时会返回一个对象, 这个对象可以用 tied 取出)

断开变量和类的联系可以用 untie:

1
untie VARIABLE;

tie 绑定的变量可以调用预定名称的方法, 这些方法为全大写. 调用的构造函数包括:

  • TIESCALAR
  • TIEARRAY
  • TIEHASH
  • TIEHANDLE

Perl 中几乎所有预定义包, 变量和文件句柄都采用大写.

14.1 绑定标量

要实现一个捆绑的标量, 一个类必须定义以下方法:

  • TIESCALAR, 绑定时自动调用, 第一个参数是要绑定的类名.
  • FETCH, 读取时调用, 传入的第一个参数是本身
  • STORE, 给变量赋值时调用, 第一个参数是本身, 第二个参数是值
  • (可能 还有 DESTROY, 在对象的最后一个引用消失时调用, 即在程序终止或调用 untie 时调用)

在解除绑定时调用:

  • UNTIE

untie 会删除 tie 使用的引用.

Tie::ScalarTie::StdScalar 模块中定义了所有上述方法, 提供简单的用法.

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package Test;

sub TIESCALAR {
bless \my $self, shift;
}

sub STORE {
${ $_[0] } = $_[1];
}

sub FETCH {
my $value = shift;
print $$value;
}

package main;

my $test;
tie $test, "Test";
$test = 100;

print $test;

当然也有:

1
2
3
4
5
my $obj = tie $test, "Test";

# 等价于使用 `tied` 获取

my $obj = tied $test;

这里 $obj 可以调用 Test 类的其他方法. 但 tie 的返回值常常被省略. 这个对象可以用 tied 获取.

这里 TIESCALAR 中的 my $self, 这个变量叫什么名字其实无所谓, 其作用就是配合 bless 返回一个被 bless 的引用给要 tie 的变量, 所以说这个变量本质上还是一个引用, 只不过其行为被重载了. (因为其处于词法作用域, 出了作用域之后, 就会变为匿名)

STORE 这个是在赋值时调用的, 传入的第一个参数就是本身 (也就是一个引用), 第二个参数是要赋的值.

传入 FETCH 的第一个值也是本身 (也就是一个引用).

14.1.1 标量捆绑方法

如:

1
2
3
use Test;

tie $test, "Test", "/tmp/test.txt"

14.2 绑定数组

只要定义了 FETCHSTORE, 对象包含什么样的数据结构并不重要.

第18章 编译

18.1 Perl 程序的生命周期

可以把 Perl 程序分裂为四个独立的阶段, 每个阶段都有自己的独立的字阶段.

1. 编译阶段:

Perl 编译器把你的程序转换成一个叫 分析树 的数据结构. useno 实际上都是伪装的 BEGIN 块.

词法声明也做上记号, 但是还不执行给它们的赋值.

常量表达式都预先计算.

2. 代码生成阶段 (可选)

把编译完成的 ( 但还没运行的 ) 程序转换成 C 源程序或者串行的 Perl 字节码 – 一个代表内部 Perl 指令的数值序列.

3. 分析树重构阶段 (可选)

这个阶段只有在发生了代码生成并且你选择生成字节码的情况下存在.

4. 执行阶段

解释器拿来分析树 (可能是直接从编译器来的或者间接从代码生成阶段以及随后的分析树重构阶段过来的) 并且运行.

18.2 编译你的代码

Perl 总是处于两种操作模式之一: 要么它在编译你的程序, 要么是在执行它 – 从来不会同时处于两种模式.

除了处理 BEGIN 块的解释器以外, 编译器默许三个概念上的过程处理你的程序.

词法分析器 (lexer) 扫描你的程序里的每一个最小的单元 – token.

分析器 (parser) 以 Perl 语言的语法为基础. 试图通过把这些 token 组合成更大的构造, 比如表达式和语句, 来获取意义.

优化器 (optimizer) 对这些词法的组合进行重新排列并且把它们归减成更有效的序列.

这些过程并不是在相互独立的阶段进行, 而是同时发生, 并且相互之间有大量交互.

18.3 执行你的代码

Perl 里没有使用硬件程序计数器, 而是由解释器追踪当前要执行的操作数.

Perl 里也没有堆栈指针, 解释器有它自己的虚拟堆栈, 这个堆栈非常重要, 因为 Perl 虚拟机是一个基于堆栈的机器.

Perl 操作码在内部称作 PP 代码 (push-pop codes), 因为他们操作解释器的虚拟堆栈以寻找所有操作数, 处理临时数值, 还有处理所有结果.

Perl 的虚拟机替你追踪一堆动态环境.

Perl 维护不少堆栈:

  • 操作数堆栈 (operand stack)
  • 保存堆栈 (save stack)
  • 范围堆栈 (scope stack)
  • 环境堆栈 (context stack)
  • 环境跳转堆栈 (jumpenv stack)
  • 返回堆栈 (return stack)
  • 标记堆栈 (mark stack)
  • 递归词法填充堆栈 (recursive lexical pad stacks)
  • 存放所有 C 变量的 C 堆栈

18.4 编译器后端

这些后端可以把编译好的操作码串行化和存储到任何外部文件中, 甚至可以把它们转换成几种不同风格的 C 代码.

18.5 代码生成器

目前的三种把 Perl 操作码转换成其他格式的后端都是处于实验阶段的.

18.5.1 字节码生成器

B::Bytecode 模块将分析树的操作码以平台无关的编码写出.

perlcc 命令知道怎么把一个 Perl 源程序转换成一个编译成字节码的 Perl 程序.

1
$ perlcc -b -o pbyscript srcscript

ByteLoader 模块是源码过滤器, 他知道如何把 B::Bytecode 生成的串行的字节码分解并重新组合成原来的分析树.

18.5.2 C 代码生成器

B::CB::CC 模块都生成 C 代码, 而不是串行化的 Perl 操作码.

18.6 提前编译, 回头解释

一个传统: 如果一个子过程会被编译器或者解释器自动出发, 那么用大写字母为之命名.

与程序的生命期的不同阶段相连的是另外四个子过程:

  • BEGIN
  • CHECK
  • INIT
  • END

它们前面的 sub 关键字是可选的, 可能最好叫它们 “语句块”, 因为它们从某种程度上来说更像命名语句块而不像子过程.

运行顺序:

  • BEGIN, 如果在编译过程中碰到, 则在编译其他文件之前尽可能快地运行
  • CHECK, 当编译完成后, 但在程序开始之前运行
  • INIT, 在程序的主流程开始执行之前运行
  • END, 在程序执行结束之后运行

Perl-语言编程-Notes
http://example.com/2022/12/09/Perl-语言编程-Notes/
作者
Jie
发布于
2022年12月9日
许可协议