Modern-Perl-Notes

运行 Modern Perl

用于测试的模块 Test::More.

管理 Perl 版本用 App::perlbrew.

Perl 5 和 Perl 6

若要学习 Perl 6, 可浏览 http://perl6.org, 试用 Rakudo ( http://www.rakudo.org ), 以及 Using Perl 6 一书.

Perl 哲学

Perldoc

perldoc perlfaq 会显示 Perl 5 常见问题的目录.

perldoc perldiag 解释了 Perl 中警告消息的含义.

perldoc perlpod 描述了 POD 的工作方式.

表达力

Perl 黑客的口号 “TIMTOWTDI”, 发音为 “Tim Today”, 或 “There is more than one way to do it!”

另一个 Perl 的设计目标是, 尽可能不使有经验的 (Perl) 程序员吃惊.

上下文

空, 标量和列表上下文

直接调用一个函数而不对其返回值加以利用, 可以认为是空上下文. 如:

1
some_expensive_operation();

将函数的返回值赋值给单个元素使得函数在标量上下文中求值:

1
my $single_result = some_expensive_operation();

将调用函数的结果赋值给一个数组或是列表, 或者在一个列表中使用该结果, 使得函数在 列表上下文 中求值:

1
2
3
my @all_results = some_expensive_operation();
my ($single_element) = some_expensive_operation();
process_list_of_results(some_expensive_operation());

使用 scalar 操作符可以迫使其在标量上下文求值.

原本的代码如:

1
2
3
4
5
6
7
process_list_of_results( some_expensive_operation() );

my %results =
(
cheap_operation => $cheap_operation_results,
expensive_operation => some_expensive_operation(),
)

这里的 expensive_operation 位于列表上下文, 因为哈希赋值需要一个 键值对列表 , 导致哈希赋值内的所有表达式在列表上下文求值.

加上 scalar:

1
2
3
4
5
6
7
process_list_of_results( some_expensive_operation() );

my %results =
(
cheap_operation => $cheap_operation_results,
expensive_operation => scalar some_expensive_operation(),
)

数值, 字符串及布尔上下文

字符串在用作数字时值为 0.

== 操作符强制 数值上下文

eq 操作符强制 字符串上下文

布尔上下文 发生在当你在条件语句中使用某值时, 如在 if 语句中 eq== 操作的结果.

也可以在一些情况下明确强制上下文:

1
2
3
my $numeric_x =  0 + $x; # 强制数值上下文
my $stringy_x = '' . $x; # 强制字符串上下文
my $boolean_x = !!$x; # 强制布尔上下文

隐式理念

默认标量变量

$_ 是默认标量变量.

当没有明确指出参数时, 如:

1
2
print; # 将 `$_` 打印到当前所选的文件句柄
say; # 将 `$_` 打印到当前所选的文件句柄

如果在用到 $_ 的代码内调用函数, 无论是隐式还是显式, 可能导致 $_ 的值被覆盖.

Perl 5.10 允许用 my$_ 作为词法变量来声明, 避免搅乱对 $_ 的利用, 如:

1
2
3
4
while (my $_ = <STDIN>)
{
...
}

默认数组变量

有两个隐式数组变量:

  • @_ 数组, 向函数传递参数
  • @ARGV 数组, 传递命令行参数

ARGV 的一个特殊用法: 如果从空文件句柄 <> 读入, 则 Perl 会将 @ARGV 中的每一个元素当作文件的名字打开, 如果 ARGV 为空, Perl 会从标准输入读取.

Perl 和它的社区

社区网站

Perl 的主页位于 http://www.perl.org

http://www.perl.com, 每月发布若干关于 Perl 编程的文章和教程.

PerlMonks, 位于 http://perlmonks, 专门用于 Perl 编程问答及其他讨论.

提供新闻和评论的社区网站, 如 http://use.perl.org, 和 http://blogs.perl.org

展示 Perl 黑客们对 Perl 沉思的站点如 http://perlsphere.net, http://planet.perl.org/, 以及 http://ironman.enlightenedperl.org/.

Perl Buzz ( http://perlbuzz.com/ ) 定期收集并重新发布一些最有趣, 有用的 Perl 新闻.

开发网站

活动

YAPC – Yet Another Perl Conference (又一次 Perl 大会), 可参见 http://yapc.org

本地 Perl Monger 小组, 参见 http://www.pm.org

IRC

IRC 是一种因特网早期发展起来, 基于文本的群组聊天系统.

Perl 社区的主 IRC 服务器是 irc://irc.perl.org/

CPAN

每个 CPAN 发行版在 http://rt.cpan.org/ 上有各自的单据队列.

CPAN.pm 配置教程

App::cpanminus 是一个新兴的 CPAN 客户端.

App::perlbrew 是一个可以管理安装多版本 Perl 的系统.

local::lib CPAN 发行版让你可以在自己的用户目录下安装和管理各类 CPAN 模块.

Perl 语言

名称

utf8 编译命令生效时, 可以在标识符中使用任意合法的 UTF-8 字符.

如:

1
2
3
4
use utf8;

my $哈 = "haha";
print $哈

也是正确的.

变量名和印记 (sigil)

$, @, % 为变量提供了一些命名空间, 使得可以拥有同名不同型的变量:

1
my ($bad_name, @bad_name, %bad_name);

访问数组或哈希中的多个元素, 即分片:

1
2
my @hash_elements = @hash{ @keys };
my @array_elements = @array[ @indexes ];

包限定名称

类的完全限定名称:: 分隔的包名组成, 如: My::Fine::Package.

照惯例, 用户定义的包的名称通常以 大写字母开头. Perl 为 内建编译命令保留了小写包名 , 如 strictwarning.

当 Perl 在 Some::Package::Refinement 中查找某一符号时, 它向 main:: 符号表查找代表 some:: 名称空间的符号, 接着再在其中查找 Package:: 名称空间, 如此等等.

变量

变量作用域

大多数变量拥有词法作用域, 文件也有自己的词法作用域.

文件内部的 package 声明并不创建新的作用域.

如:

1
2
3
4
5
6
7
8
package Store::Toy;

our $discount = 0.10;

package Store::Music;

# $Store::Toy::discount 仍然可以通过 $discount 访问
say "Our current discount is $discount!";

变量印记

访问该变量所用的印记决定了所访问的值的类型.

匿名变量

Perl 5 中的变量不需要名称, Perl 能够另行分配存储空间而不必将他们存放在 lexical pad 或是符号表中, 其被称为 匿名变量 , 访问到它们的唯办法就是通过引用.

变量, 类型和强制转换

文档中记载的获得某数组中元素个数的方式就是在标量上下文中对数组进行求值:

1
my $count = @items;

字符串

字符串定义可以横跨多个逻辑行, 如:

1
2
my $literal = "two
lines";

另外的引号操作符:

  • q 操作符进行单引号操作
  • qq 操作符进行双引号操作

如:

1
2
3
my $quote       = qq{"Ouch", he said. "That hurt!"};
my $reminder = q^Didn't need to escape the single quote!^;
my $complaint = q{It's too early to be awake.};

heredoc 的语法允许另一种方式进行多行字符串赋值:

1
2
3
4
5
6
7
8
9
my $blurb =<<'END_BLURB';

He looked up. "Time is never on our side, my child. Do you see the irony?
All they know is change. Change is the constant on which they all can
agree. Whereas we, born out of time to remain perfect and perfectly
self-aware, can only suffer change if we pursue it. It is against our
nature. We rebel against that change. Shall we consider them greater
for it?"
END_BLURB

<<'END_BLURB' 语法有三个部分:

  • << 引入 heredoc
  • ', 引号决定此 heredoc 在处理变量内插和转义字符时遵循单引号还是双引号的规则
  • END_BLURB 是标识符

注意, 不管 heredoc 声明部分缩进多少, 结束分隔符不许位于一行的开头.

Unicode and Strings

Perl 5 可以按:

  • Unicode 字符序列
  • 八进制序列
    表示字符串.

默认的, Perl 将所有读入的数据按八进制序列处理.

字符编码

Unicode 字符串是一个表示一系列字符的八进制序列. Unicode 编码将八进制序列映射到字符上.

文件句柄中的 Unicode

不启用 utf8 模式时, 向某文件句柄打印 Unicode 字符串会得到一个警告 (Wide character in %s), 因为文件包含的是八进制数据而非 Unicode 字符, 打开方式:

1
open my $fh, '<:utf8', $textfile;

或:

1
binmode $fh, ':utf8';

数据中的 Unicode

Encode 核心模块的两个函数:

  • decode()
  • encode()
1
2
my $string = decode('utf8', $data);
my $latin1 = encode('iso-8859-1', $string);

程序中的 Unicode

在程序中包含 Unicode 的三种方式:

  • 使用 utf8 编译命令
  • 使用 Unicode 转义序列, \x{} 语法代表一个单独的字符, 将字符的 Unicode 数字的十六进制表示放入大括号内
  • 使用 charnames 编译命令开启使用 Unicode 字符的名称以及 \N{} 转义语法

如:

1
2
use utf8;
my $杰 = 'jie';

如:

1
my $escape_thorn = "\x{00FE}";

如:

1
2
3
4
5
6
7
use charnames ':full';
use Test::More tests => 1;

my $escape_thorn = "\x{00FE}";
my $named_thorn = "\N{LATIN SMALL LETTER THORN}";

is( $escape_thorn, $named_thorn, 'Thorn equivalence check' );

隐式转换

如果两个字符串都是八进制流, Perl 会将它们拼接为一个新的八进制字符串.

如果两者的都为编码方式相同的八进制值, 则拼接操作正常执行.

如果两者并不共用编码方式, 生成两种编码方式都无法识别的八进制序列.

参见 perldoc perluniintro.

数字

详细参考 perldoc perlnumber.

Undef

Perl 5 中由 undef 代表所有未赋值, 未定义和未知的值, 以声明但未定义的标量包含 undef.

在布尔上下文中对 undef 求值得到假.

空列表

空列表在标量上下文中求值时得到 undef.

如:

1
my $test = () # 这里的 test 就是 undef

计算一个表达式在列表上下文中返回结果的个数, 可以使用:

1
my $count = () = get_all_clown_hats();

列表

可以用 qw() 以空白符分隔一字符串字面值, 并创建一个字符串列表:

1
my @storages = qw( Larry Curly Moe Shemp Joey Kenny );

如果 qw() 包含逗号或注释符 (#), Perl 会产生一条警告.

在 Perl 中, 列表和数组的概念不可以交换 . 列表是值而数组是容器.

可以在一个数组中存放一个列表, 也可以将一个数组强转为列表, 但它们是不同的实体.

控制流程

分支语句

后缀形式的 if:

1
say 'Hello, Bob!' if $name eq 'Bob';

代码块形式的 if 要求条件两边有括号.

unless 语句是 if 的否定形式, Perl 在表达式的值为假时执行所需操作.

1
say "You're no Bob!" unless $name eq 'Bob';

unless 也可以搭配 elseelsif.

三元条件操作符

? :

条件语句相关的上下文

条件语句 if, unless 以及三元条件表达式总是在 布尔上下文 中对一个表达式进行求值.

对空哈希和数组求值得假.

Perl 5 没有单一的真值, 也没有单一的假值.

任何求值为 0 的数字为假, “0” 字符串也是假.

CPAN 模块 Want 允许你在你的函数内检测布尔上下文, 核心编译命令 overloading 允许你指定自己的数据类型在布尔上下文中求得的值.

循环语句

Perl 将 foreachfor 可互换地看待.

有经验的 Perl 程序员倾向于用 foreach 循环来指代自动迭代循环.

for 循环也有后缀形式:

1
say "$_ * $_ =", $_ * $_ for 1..10;

迭代和作用范围

如:

1
2
3
4
5
6
7
for (@values) {
some_function();
}

sub some_function {
s/foo/bar/;
}

这里, 函数也会修改 $_, 从而导致迭代值改变, 可以通过 my $_ 避免

1
2
3
4
sub some_function {
my $_;
s/foo/bar/;
}

C 语言风格的 For 循环

如:

1
2
3
4
for (my $i = 0; $i <= 10; $i += 2)
{
say "$i *$i = ", $i * $i;
}

尽量使用 foreach 风格的循环来代替 for 循环.

Tailcalls

某函数内最后一个表达式是对其他函数的调用时, 这种情况称为 尾部调用. 外层函数的返回值就是内部函数的返回值.

使用尾部调用似乎会减少有关内部控制流程记录的内存消耗.

标量

标量和类型

在字符串上下文中对 引用 求值会得到一个字符串, 在数值上下文中对引用求值会得到一个数字, 两个操作都不对引用做出修改, 但不能从得到的字符串或数字中重新创建该引用:

1
2
3
4
5
6
7
my $ref = [ qw(test1 test2 test3) ];
my $num_ref = 0 + $ref;
my $str_ref = '' + $ref;

say "Ref: $ref";
say "Num_ref: $num_ref";
say "Str_ref: $str_ref";

Perl 5 标量可以包含数值部分以及字符串部分的原因为, Perl 5 表示标量的内部数据结构中有一个数值槽和一个字符串槽.

在数值上下文中访问一个字符串最终产生一个拥有字符串和数值槽两者的标量.

Scalar::Util 模块中的 dualvar() 函数允许你对一个标量的这两个值进行改动.

标量没有单独针对布尔值的槽.

数组

字符串拼接时数组求值也是数组中所含元素的个数:

1
say 'I have ' . @cats . ' cats!';

也可以使用数组的特殊变量形式来找出最后一个下标:

1
my $last_index = $#cats;

注意这是 最后一个下标 , 而不是数组长度.

可以通过对 $# 赋值来调整数组大小:

  • 收缩, Perl 将丢弃所有不符合调整后尺寸的值
  • 扩展, Perl 将把 undef 填入多出来的位置

数组赋值

如果对超出范围的位置赋值, Perl 会将数组扩展到合适的大小, 并向夹在中间的所有空槽都填入 undef.

清空一个数组 , 用空列表对其赋值:

1
@data = ();

数组分片

如:

1
2
3
my @test  = @cats[-1, -2];
my @test2 = @cats[0 .. 2];
my @test3 = @cats[ @indexes ];

数组操作

pushpop 操作数组尾部.

ushiftshift 操作数组开头.

splice 按给出的偏移量, 列表分片长度以及替代物删除并替换数组元素.

在 Perl 5.12 中, 可以使用 each 来将某数组迭代为键值对.

1
2
3
4
while (my ($index, $value) = each @bookshelf) {
say "#$index: $value";
...
}

数组和上下文

以下创建单个数组, 而不是数组的数组:

1
my @array_of_arrays = ( 1 .. 10, ( 11 .. 20, ( 21 .. 30 ) ) );

这里的括号并不创建列表, 只是用来给表达式分组.

数组内插

数组作为字符串化的列表插入双引号时, $" 全局变量决定分隔符:

1
2
3
4
5
6
my @alphabet = 'a' .. 'z';
say "[@alphabet]";
{
local $" = ')(';
say "(@alphabet)";
}

哈希

要清空一个哈希, 可以将空列表赋值给他:

1
%favorite_flavors = ();

哈希键只能是字符串, 如果你使用某对象作为哈希键, 你将得到对象字符串化后的版本而非对象本身.

哈希分片

就是一个由哈希键值对组成的列表:

1
2
my %cats;
@cats{qw( Jack Brad Mars Grumpy )} = (1) x 4;

哈希分片可以使合并两个哈希变得容易:

1
2
3
4
my %addresses = (...);
my %canada_addresses = (...);

@addresses{keys %canada_addresses} = values %canada_addresses;

空哈希

一个空哈希不包含键和值, 他在布尔上下文中得假

1
2
3
4
5
6
7
8
9
10
11
12
13
use Test::More;

my %empty;
ok( ! %empty, 'empty hash should evaluate to false' );

my %false_key = ( 0 => 'true value' );
ok( %false_key, 'hash containing false key should evaluate to true' );

my %false_value = ( 'true key' => 0 );
ok( %false_value, 'hash containing false value should evaluate to true' );

...
done_testing();

在标量上下文中, 对哈希求值得到的是一个字符串, 表示已用哈希桶 比上 已分配哈希桶.

在列表上下文中, 对哈希求值得到类似从 each 操作符取得的键值对列表.

哈希惯用语

哈希值初始为 undef, 后缀自增操作符 ++ 将其作为零对待. 即一个键对应的值不存在, 创建一个 undef 值, 使用 ++ 则将其加 1, 即此时的值为 1.

1
2
3
4
5
6
7
8
my %ip_addresses;

while (my $line = <$logfile>)
{
my ($ip, $resource) = analyze_line( $line );
$ip_addresses{$ip}++;
...
}

利用 //= 已定义或操作符可以设置默认值:

1
2
3
4
5
6
7
8
sub make_sundae
{
my %parameters = @_;
$parameters{flavor} //= 'Vanilla';
$parameters{topping} //= 'fudge';
$parameters{sprinkles} //= 100;
...
}

或者直接赋值也是设置默认参数.

给函数传递参数:

1
2
3
4
5
6
sub make_sundae {
my %parameters = @_;
...
}

make_sundae( flavor => 'Lemon Burst', topping => 'cookie bits' );

哈希上锁

核心模块 Hash::Util 提供一些机制.

避免他人向哈希添加你不想要的键, 可以使用 lock_keys() 函数将哈希键限制在当前集合中, 任何添加不被允许的键值对的意图将引发一个异常.

可使用 unlock_keys() 去掉保护.

还有 lock_value(), unlock_value(), lock_hash, unlock_hash 等.

强制转换

引用强制转换

发生在对非引用进行解引用操作:

1
2
3
4
my %users;

$users{Bradley}{id} = 228;
$users{Jack}{id} = 229;

这里会创建哈希引用.

强制转换的缓存

Perl 5 对值的内部存储机制允许每个值拥有字符串化和数值化的结果哦.

双重变量

对数值和字符串的缓存允许你使用一个称为 双重变量 的特性. 即同时拥有数值和字符串表示的值, 需要使用核心模块 Scalar::Util 提供的 dualvar() 函数, 其允许你创建一个拥有两个不同形式的值:

1
2
3
4
5
use Scalar::Util 'dualvar';
my $false_name = dualvar 0, 'Sparkles & Blue';
say 'Boolean true!' if !! $false_name;
say 'Numeric false!' unless 0 + $false_name;
say 'String true!' if '' . $false_name;

Perl 中的 名称空间 是一种机制, 它将若干具名实体, 关联并封装于, 某具名分类之下.

Perl 中的包是单一名称空间下代码的集合, 在某种意义上, 包和名称空间是等价的.

默认包是 main 包.

除包名外, 一个包还拥有一个版本以及三个隐含的方法, 分别是 VERSION(), import()unimport(). VERSION() 返回一个包的版本.

包版本 是包含在名为 $VERSION 的包全局变量中的一系列数字. 按照惯例, 版本号倾向于写成一系列由点分隔整数的形式, 如 1.23, 1.1.10.

Perl 5.10 以及早期:

1
2
package MyCode;
our $VERSION = 1.21;

Perl 5.12 引入的简化写法:

1
package MyCode 1.2.1;

每个包都有 VERSION 方法, 他们继承自 UNIVERSAL 基类, 其返回 $VERSION 中的值:

1
say main->VERSION;

包和名称空间

每一句 package 声明都会使 Perl 完成两件任务:

  • 如果名称空间不存在则创建它
  • 告诉语法分析器将后续的包全局符号放入该名称空间下

通过使用包声明语句, 可以在任何时候向一个名称空间添加函数和变量:

1
2
3
4
5
6
7
8
9
10
11
package Pack;
sub first_sub { ... }

package main;
Pack::first_sub();

package Pack;
sub second_sub { ... }

package main;
Pack::second_sub();

或者用完全限定的名称来添加, 如:

1
sub Pack::third_sub { ... }

名称空间可以按组织需要分为多个级别, 但这并不意味着继承关系.

常见的做法是为业务或项目创建一个顶层名称空间, 如:

  • StrangeMonkey 是项目名称;
  • StrangeMonkey::UI 包含顶层用户接口的代码;
  • StrangeMonkey::Persistence 包含顶层数据管理代码;
  • StrangeMonkey::Test 包含为项目编写的顶层测试代码;

引用

Perl 5 提供了一种机制, 通过它你可以简介使用某值而不必为此创建一份拷贝.

引用是 Perl 5 中的一等共民, 是一种 内置的标量数据类型 , 它不是字符串, 不是数组, 也不是哈希. 它就是一个引用其他第一等数据类型的标量.

标量引用

解引用需要在解开每一重引用时加上额外的印记.

复杂的引用需要加上一对大括号以消除表达式断句上的歧义:

1
2
3
4
sub reverse_in_place {
my $name_ref = shift;
${ $name_ref } = reverse ${ $name_ref };
}

Perl 不提供内存位置的原生访问.

这些地址仅在大致上唯一, 因为在垃圾回收器回收某未引用的引用后, Perl 可能重用此存储位置.

数组引用

如:

1
2
3
4
5
my @cards      = qw( K Q J 10 9 8 7 6 5 4 3 2 A );
my $cards_ref = \@cards;

my $card_count = @$cards_ref;
my @card_copy = @$cards_ref

数组分片:

1
my @high_card = @{ $cards_ref }[1 .. 2, -1];

函数引用

如:

1
2
3
4
sub bake_cake { say 'Baking a wonderful cake!' };
my $cake_ref = \&bake_cake;

$cake_ref->();

注意这里需要用解引用箭头调用引用指向的函数.k

创建匿名函数, 这里使用 sub 关键字不用函数名称也可以使得函数正常编译, 但是它不会被安装到当前的名称空间中, 访问此函数的唯一方法就是通过引用.

文件句柄引用

当使用 open (opendir) 词法文件句柄形式, 其实就在处理文件句柄引用, 对此文件句柄进行字符串化可以得到类似 GLOB(0x8bda880) 形式的东西.

内部机制上, 这些文件句柄都是 IO::Handle 类的对象, 当你加载这个模块, 可以调用文件句柄上的方法:

1
2
3
4
5
use IO::Handle;
use autodie;

open my $out_fh, '>', 'output_file.txt';
$out_fh->say( 'Have some text!' );

老旧的代码用型团 (typeglob) 来获取文件句柄的引用:

1
2
3
4
5
my $fh = do {
local *FH;
open FH, "> $file" or die "Can't write to '$file': $!\n";
\*FH;
}

引用计数

Perl 5 使用一种名为 引用计数 的内存管理技术。程序中的每一个值都有附加的计数器。 每次被其他东西引用时,Perl 增加计数器的值,无论隐式还是显式。每次引用消失后,Perl 将减少计数器的值。当计数器减至零,Perl 就可以安全地回收这个值。

$fh 的回收隐式地调用了该文件句柄上的 close() 方法, 使得文件最终被关闭.

引用和函数

如果需要对引用的内容做出破坏性改动, 请将其所含的值复制到一个新的变量中:

1
2
my @new_array = @{ $array_ref };
my %new_hash = %{ $hash_ref };

如果引用更加复杂, 考虑 Storable 模块和其 dclose (deep cloning) 函数.

嵌套数据结构

Perl 中的嵌套数据结构, 例如数组的数组, 哈希的哈希, 是通过引用机制来实现的.

如:

1
2
3
4
5
6
7
8
9
10
11
my @famous_triplets = (
[qw( eenie miney moe)],
[qw( huey dewey louie)],
[qw( duck duck goose)],
)

my %meals = (
breakfast => { entree => 'eggs', side => 'hash browns' },
lunch => { entree => 'panini', side => 'apple' },
breakfast => { entree => 'steak', side => 'avocado salad' },
)

解引用之前的变量值为引用:

1
2
my $last_nephew = $famous_triplets[1]->[2];
my $breaky_side = $meals{breakfast}->{side};

因为嵌套一个数据结构的唯一方法就是通过引用, 因此这里的箭头是多余的, 如下的代码和前面等价:

1
2
my $last_nephew = $famous_triplets[1][2];
my $breaky_side = $meals{breakfast}{side};

调用存放于嵌套数据结构内的函数引用时, 使用箭头调用语法是最清晰的, 除此之外可以避开箭头的使用.

有时, 使用临时变量会更清晰:

1
2
my $breakfast_ref   = $meals{breakfast};
my ($entree, $side) = @$breakfast_ref{qw( entree side )};

自生

当你试图编写一个嵌套数据结构组件时, 如果不存在, Perl 会创建通向这部分数据结构的路径:

1
2
my @aoaoaoa;
$aoaoaoa[0][0][0][0] = 'nested deeply';

这个行为称为 自生 . 好处是减少嵌套数据结构的初始化代码.

CPAN 上的 autovivification 编译指令, 让你可以在词法作用域内对某特定类型操作禁用自生行为.

调试嵌套数据结构

核心模块 Data::Dumper 可以将任意复杂的数据结构的值字符串化为 Perl 5 代码:

1
2
3
use Data::Dumper;

print Dumper( $my_complex_structure );

一些开发人员更愿意使用 YAML::XSJSON 来调试程序.

循环引用

两个互指的引用最终形成了一循环引用, Perl 无法自行销毁它.

需要手动打断引用计数, 或者利用一个名为 弱引用 的特性. 弱引用是一个不增加被引用者引用计数的引用, 其可通过 Scalar::Util 来使用, 导出 weaken() 函数并对某引用使用它可以防止引用计数的增加:

1
2
3
4
5
6
7
8
9
10
use Scalar::Util 'weaken';
my $alice = { mother => '', father => '', children => [] };
my $robert = { mother => '', father => '', children => [] };
my $cianne = { mother => $alice, father => $robert, children => [] };

push @{ $alice->{children} }, $cianne;
push @{ $robert->{children} }, $cianne;

weaken( $cianne->{mother} );
weaken( $cianne->{father} );

操作符

操作符特征

perldoc perlopperldoc perlsyn 提供了大量有关 Perl 操作符行为的信息.

每一个操作符持有若干构成其行为的重要特征:

  • 操作数的个数
  • 和其他操作符的关系
  • 可能的用法

优先级

可以用括号将子表达式分组使某操作符在其他操作符之前求值.

perldoc perlop 包含了一张优先级表.

结合性

某操作符的结合性决定了它是从左往右求值还是从右往左.

核心模块 B::Deparse 可以重写代码片段并如实展示 Perl 究竟是如何处理操作符优先级和结合性的. 在某代码段上运行 perl -MO=Deparse,-p (-p 标志添加额外的分组括号使得求值顺序更为明显)

参数数量

操作符的参数数量就是该操作符所作用的操作数的个数.

词缀性

操作符的词缀性就是它相对其操作数的位置:

  • 中缀操作符, 出现在操作数之间, 如 $length * $width
  • 前缀操作符, 出现在其操作数之前. 如 -$x
  • 后缀操作符, 出现在其操作数之后. 如 $z++
  • 环缀操作符, 包围其操作数, 如 { ... }
  • 后环缀操作符, 接载某些操作数之后并围绕其他部分, 如 $hash{ ... }

操作符类型

数值操作符

数值操作符对其操作数强制数值上下文.

  • +
  • -
  • *
  • /
  • **
  • %
  • +=, -=, *=, /=, **=, %=
  • --

自增操作符 ++, 有特殊的字符串行为.

若干比较操作符:
==, !=, >, <, >=, <=, <=>,

字符串操作符

字符串操作符对其操作数强制字符串上下文.

  • =~
  • !~
  • .

若干比较操作符:
eq, ne, gt, lt, ge, le, cmp,

逻辑操作符

逻辑操作符在布尔上下文中处理操作数.

  • &&and
  • ||or

都为 短路测试

已定义-或 操作符, //, 测试其操作符的定义性, 在设置默认值时有用.

!not 返回其操作数的逻辑反值. not 的优先级比 ! 低.

?:, xor.

按位操作符

  • <<
  • >>
  • &
  • |
  • ^
  • &=, |=, ^=, <<=, >>=

特殊操作符

自增操作符:

  • 在数值上下文中使用则增加数值部分
  • 如果这个变量明显地是一个字符串, 则此字符串将会 带进位 地自增, 如 zz 增为 aaa

重复操作符 x.

范围操作符 ..

逗号操作符 ,

胖逗号操作符 =>

函数

别名

一个 @_ 的有用特性, 其包含了传入参数的别名, 如:

1
2
3
4
5
6
7
8
sub Test {
$_[0] = 'The value is changed..';
}

my $test = 'haha';
Test($test);

say $test;

这里 $_[0] 成了 $test 的别名.

名称空间

每一个函数存在于某名称空间中, 位于未声明名称空间中的函数, 即没有在一条明确的 package ... 语句之后声明的函数, 存在于 main 名称空间. 可以在声明时为函数指定当前之外的名称空间:

1
2
3
sub Extension::Math::add {
...
}

如果向替换一个已经存在的函数, 可以通过 no warnings 'redefine' 来禁用这类警告.

导入

当使用 use 关键字加载模块时, Perl 自动调用所提供模块的 import() 方法 (可自己定义), 以使部分或全部经过定义的符号在调用者的名称空间中可用.

所有 use 语句内模块名后的参数会传递给模块的 import() 方法:

1
use strict 'refs';

加载 strict.pm 模块, 调用 strict->import( 'refs' ).

也等价于:

1
2
3
4
5
6
BEGIN
{
require strict;
strict->import( 'refs' );
strict->import( qw( subs vars ) );
}

也可以看出, require 只是加载模块, 不导入任何名称.

报告错误

在函数之内, 可以通过 caller 操作符得到有关本次调用的上下文信息, 如果不加参数, 其返回三个元素的列表, 包括:

  • 调用包的名称
  • 本次调用的文件名
  • 调用发生的包内行号

如:

1
2
3
4
5
6
7
8
9
10
11
12
package main;

main();
sub main {
show_call_information();
}

sub show_call_information
{
my ($package, $file, $line) = caller();
say "Called from $package in $file at $line";
}

可以向 caller() 传递单个可选的整数参数, 这样, Perl 将按给出的层数回查调用者的调用者的调用者, 并提供该次调用的相关信息. 如:

1
2
3
4
5
sub show_call_information
{
my ($package, $file, $line, $func) = caller(0);
say "Called $func from $package in $file at $line";
}

验证参数

可以通过在标量上下文中对 @_ 求值来检查传递给函数的参数个数是否正确:

1
2
3
4
sub add_numbers {
croak "Expected two numbers, but received: " . @_
unless @_ == 2;
}

高级函数

上下文认知

Perl 5 内置函数了解你是在空, 标量, 还是列表上下文中调用他们.

wantarray 关键字:

  • 返回 undef 表示空上下文
  • 返回假值表示标量上下文
  • 返回真值表明列表上下文

1
2
3
4
5
6
7
8
9
10
sub context_sensitive {
my $context = wantarray();
return qw( Called in list context ) if $context;
say 'Called in void context' unless defined $context;
return 'Called in scalar context' unless $context;
}

context_sensitive();
say my $scalar = context_sensitive();
say context_sensitive();

递归

在 Perl 中每一次函数调用都创建一个新的 调用帧 . 这是一种代表调用本身的内部数据结构: 传入参数, 返回点, 步入此调用点前的所有程序控制流程.

注意先给出结束递归的条件.

词法相关

每一次对函数的新调用会创建自生词法作用域的实例.

尾部调用

递归的一个缺点就是必须将返回条件编写正确.

Perl 检测到失控的递归时, 提供的警告为 Deep recursion on subroutine. 限制是 100 次递归调用, 可以通过递归调用作用域内通过 no warning 'recursion' 来禁用这一警告.

尾部调用 就是调用一个函数然后直接返回这个函数的结果.

尾部调用消除 的特性可以解决高度递归代码创建新调用帧和存储词法变量值造成的内存使用过高问题. (因为直接返回函数的结果)

可使用 Sub::Call::Tail 模块用 tail 代替 return.

缺陷和设计失误

Perl 5 仍支持旧式函数调用, 前期版本的 Perl 要求用前置的 & 字符调用函数, Perl 1 要求使用 do 关键字.

前置 & 形式在你不明确地传递参数时, 会隐式地将 @_ 的内容不加修改地传给函数. (这里不能加括号, 不然不会传递, 加了括号貌似就是传递空参数)

作用域

在 Perl 中, 任何有名字的事物都有作用域.

词法作用域

Perl 编译器在编译期解决此类作用域.

创建一个新的词法作用域 可以编写一个由大括号分隔的代码块. (这个代码块可以是一个裸块, 或循环结构主体中的块, 一个 eval 块或是其他没有用引号引起的块)

词法作用域 管理由 my 声明的变量的可见性 , 这些变量被称作词法变量.

动态作用域

动态作用域在可见性规则上类似于词法作用域, 和在编译器确定作用域相反, 确定作用域的过程沿着调用上下文发生.

词法变量被存放在附着于作用域的 词法板 中, 每次进入到词法作用域中都需要 Perl 来创建一个包含变量值的新的专属词法板.

包变量 的存储机制称为符号表. 每个包都有一个单独的符号表, 并且每个包变量在其中占有一个条目. local 只能局部化全局和包变量而非词法变量.

local 局部化若干神奇变量的做法很常见.

“State” 作用域

state 关键字, 声明一个词法变量, 但是该变量只初始化一次, 随后一直保持.

匿名函数

匿名函数没有 Perl 可以识别的名称.

CPAN 模块 Sub::Identify 提供了一系列有用的函数来对传入函数引用的名称进行检查. 如 sub_name():

1
2
3
4
5
6
7
8
use Sub::Identify 'sub_name';

sub main {
say sub_name( \&main );
say sub_name( sub {} );
}

main();

CPAN 模块 Sub::Namesubname() 函数允许将名称附加在匿名函数上:

1
2
3
4
5
6
7
8
9
10
11
use Sub::Name;
use Sub::Identify 'sub_name';

my $anon = sub {};
say sub_name( $anon );

my $named = subname( 'pseudo-anonymous', $anon );
say sub_name( $named );
say sub_name( $anon );

say sub_name( sub {} );

隐式匿名函数

如:

1
2
3
4
5
6
7
8
use Test::More tests => 2;
use Test::Exception;

throws_ok { die "I croak!" }
qr/I croak/, 'die() should throw an exception';

lives_ok { 1 + 1 }
'simple addition should not';

throws_ok 的第一个参数是一个匿名函数, 而这里省略了 sub {}

闭包

可在 http://hop.perl.plover.com/ 在线阅读 Higher Order Perl.

闭包是 封闭于外部词法环境之上的 , 函数.

状态 VS 闭包

在可能时使用 state 代替, 否则使用闭包.

属性

Perl 中的具名实体 (变量和函数) 可按属性的形式附加额外的元信息.

属性 是名称 (通常, 也是值).

使用属性

最简单的形式: 属性是一个附加于变量或函数声明上的 前置冒号标识符 . 如:

1
2
3
my $fortress      :hidden;

sub erupt_volcano :ScienceProject { ... }

属性 可以包括一个参数列表 , Perl 将它们作为一个常量字符串列表.

属性的缺点

见书.

AUTOLOAD

调用不存在的函数是就会寻找这个函数并调用.

AUTOLOAD 的基本功能

AUTOLOAD() 函数直接在 @_ 中接到传递给未定义函数的参数.

未定义函数的名称可以从伪全局变量 $AUTOLOAD 得到:

1
2
3
4
5
6
sub AUTOLOAD {
our $AUTOLOAD;

local $" = ', ';
say "In AUTOLOAD(@_) for $AUTOLOAD";
}

一个常见用于可以用来去掉包名:

1
2
3
4
5
6
sub AUTOLOAD {
my ($name) = our $AUTOLOAD =~ /::(\w+)$/;

local $" = ', ';
say "In AUTOLOAD(@_) for $name";
}

在 AUTOLOAD() 中重分派方法

见书

正则表达式和匹配

可参考文档:

  • perldoc perlretut 教程
  • perldoc perlre 完整文档
  • perldoc perlreref 参考指南

正则表达式绑定操作符 =~ 是一个中缀操作符, 它将位于其右的正则表达式应用于左边由表达式产生的字符串, 当在标量上下文中求值时, 一个成功的匹配将得到真值.

qr// 操作符和正则表达式组合

创建正则表达式, 如:

1
2
my $hat = qr/hat/;
say 'Found a hat!' if $name =~ /$hat/;

Test::Morelike() 函数和 is() 类似, 只是第二个参数是 qr// 产生的正则表达式对象:

1
like($name, qr/$hat$filed/, 'Found a hat in a field');

量词

{n} 意味着确切匹配 n 次.

贪心性

*+ 是贪心量词.

让贪心量词变成非贪心只需在其后加上 ? 量词.

1
my $minimal_greedy_match = qr/hot.*?meal/;

正则表达式锚点

锚点的意思是, 强制在字符串某位置进行匹配.

\A 确保任何匹配都将从字符串开头开始.

\Z 确保任何匹配都将结束于字符串末尾.

(为什么是 \A\Z, 因为这是字母表的首尾)

单词边界元字符 \b.

元字符

匹配字符, 数字, 空白: \w, \d, \s

字符类

1
my $maybe_cat = qr/c${vowels}t/;

这里的 {} 有助于消除对变量名的歧义.

将连字符添加到字符类的开头或结尾可以将其包括进此字符类中 (或转义):

1
my $interesting_punctuation = qr/-!?/;

^ 作为字符类的第一个元素意味着 “除这些外的所有字符”.

1
my $not_a_vowel = qr/[^aeiou]/;

捕获

具名捕获

结构:

1
(?<capture name> ...)

(这里的 ()<> 都是有的, ... 表示正则表达式的内容)

如果正则表达式匹配该片段, Perl 将字符串被捕获部分存储在神奇变量 %+ 中: 一个以捕获缓冲区名为键, 匹配表达式的字符串部分为值的哈希.

如:

1
2
3
if ($contact_info =~ /(?<phone>$phone_number)) {
say "Found a number $+{number}";
}

编号捕获

Perl 将捕获的子字符串放在一系列以 $1 开头的神奇变量中.

捕获计数起始于捕获的开括号.

如:

1
2
3
if ($contact_info =~ /($phone_number)/) {
say "Found a number $1";
}

成组或选项

非捕获分组, 如:

1
my $starches = qr/(?:pasta|potatoes|rice)/;

其他转义序列

\Q 元字符 禁用对元字符的处理 知道它碰到 \E 序列.

断言

正则表达式的断言形式 , 指字符串需要满足此条件, 但并不实际匹配字符串中的某个字符.

如正则表达式锚点 \A, \Z. \b\B 也是断言.

还有些见书.

正则表达式修饰符

可在模式中内嵌修饰符, 如:

1
my $find_a_cat = qr/(?<feline>(?i)cat)/;

(?i) 语法仅为它所包围的组启用大小写不敏感匹配.

也可以通过前缀 - 来禁用特定的修饰符. 如:

1
my $find_a_cat = qr/(?<feline>(?-i)cat)/;

其他见书.

智能匹配

智能匹配操作符 ~~.

具体见书.

对象

Moose

Moose 是一个专为 Perl 5 提供更为完整的对象系统.

面向对象,或 面向对象程序设计,是一种程序编排方法,它将组件分块为离散的唯一 的实体。这些实体称为 对象。用 Moose 的术语来说,每一个对象是某一个 类 的实例, 类作为模版描述了对象包含的数据和专属的行为。

类可以有一个名称, 默认地, Perl 5 类使用包来提供名称空间:

1
2
3
4
5
{
package Cat;

use Moose;
}

这里 Moose 会定义此类并向 Perl 注册它.

之后就可以创建 Cat 类的对象 (或实例):

1
my $brad = Cat->new();

方法

method 就是一个和类关联的函数.

方法调用总是对执行方法 被调用者 进行的. Perl 5 中方法的被调用者是方法的第一个参数, 如, Cat 类可以拥有一个名为 meow() (喵) 的方法:

1
2
3
4
5
6
7
8
9
10
{
package Cat;

use Moose;

sub meow {
my $self = shift;
say 'Meow!';
}
}

调用:

1
2
my $alarm = Cat->new();
$alarm->meow();

按照惯例, Perl 中方法的被调用者是一个名为 $self 的词法变量, 但这仅仅是一个普遍的惯例.

也就是说, 会将调用者作为第一个参数传入.

属性

对象可以包含属性, 或者说和每一个对象关联的私有数据 (实例数据或状态).

要定义对象属性, 可以将他们描述为类的一部分:

1
2
3
4
5
6
7
{
package Cat;

use Moose;

has 'name', is => 'ro', isa => 'Str';
}

这行代码的含义为 “Cat 对象有一个 name 属性, 它可读但不可写, 并且它是字符串”, 该行代码还创建了一个访问器方法 (name()) 且允许你可以向构造函数传递一个 name 参数:

1
2
3
4
5
6
use Cat;

for my $name (qw( Tuxie Petunia Daisy )) {
my $cat = Cat->new(name => $name);
say "Created a cat for ", $cat->name();
}

属性的类型并非必须 .

Moose 的文档使用括号来分隔属性名称和它的特征:

1
has 'name' => ( is => 'no', isa => 'Str');

及:

1
2
3
4
has 'name' => (
is => 'no',
isa => 'Str',
)

若属性 标记为可读可写 is => rw , Moose 将创建一个突变方法, 即可以用这个方法来改变属性的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
package Cat;

use Moose;

has 'name', is => 'ro', isa => 'Str';
has 'age' , is => 'ro', isa => 'Int';
has 'diet', is => 'rw';
}

my $fat = Cat->new( name => 'Fatty', age => 8, diet => 'Sea Treats' );

$fat->diet( 'Low Sodium Kitty Lo Mein' );
say $fat->name(), ' now eats ', $fat->diet();

封装

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package Cat;

use Moose;

has 'name', is => 'ro', isa => 'Str';
has 'dirt', is => 'rw';
has 'birth_year', is => 'ro', isa => 'Int';

sub age {
my $self = shift;
my $year (localtime)[5] + 1900;

return $year - $self->birth_year();
}

这里隐藏了 birth_year 这个属性.

可以使用 默认属性 :

1
2
3
4
5
6
7
8
9
package Cat;

use Moose;

has 'name', is => 'ro', isa => 'Str';
has 'dirt', is => 'rw';
has 'birth_year', is => 'ro', isa => 'Int',
default => sub { (localtime)[5] + 1900 };

多态

多态 , 即可以用一个类的对象替换另一个类的对象, 只要它们以相同的方式提供相同的外部接口.

如:

1
2
3
4
5
6
7
sub show_vital_stats {
my $object = shift;

say 'My name is ', $object->name();
say 'I am', $object->age();
say 'I eat ', $object->diet();
}

这里, 任何提供 name(), age(), diet() 访问器的对象都可以使用此函数.

角色

角色 是一个具名行为和状态的集合.

类就像一个角色, 它们之间重要的区别就是你可以对一个类进行实例化, 但角色就不行.

对于对象来说, 类是将行为和状态组织为模板的主要机制, 而角色便是将行为和状态组织为具名集合的主要机制.

感觉角色就是以一些类的行为来标识的一类对象.

使用 with 关键字向类添加角色, 其语句必须出现在属性声明之后, 使得该合成过程可以识别任何生成的访问器方法, 如:

1
2
3
4
5
6
7
8
9
10
11
12
package Cat;

use Moose;

has 'name', is => 'ro', isa => 'Str';
has 'diet', is => 'rw', isa => 'Str';
has 'birth_year', is => 'ro', isa => 'Int',
default => (localtime)[5] + 1900;

with 'LivingBeing';

sub age { ... }

检查一个对象是否 能够饰演 LivingBeing 角色, 这里感觉像是创建一个角色 :

1
2
3
4
5
6
7
{
package LivingBeing;

use Moose::Role;

require qw( name age diet );
}

因此, 并不是所有的 Cat 实例会返回真:

1
2
say 'Alive!' if $fluffy->does('LivingBeing');
say 'Moldy!' if $cheese->does('LivingBeing');

饰演两个角色:

1
2
3
4
5
6
7
8
package Cat;

use Moose;

has 'name', is => 'ro', isa => 'Str';
has 'diet', is => 'rw';

with 'LivingBeing', 'CalculateAgeFromBirthYear';

角色和 DOES()

对一个类应用一个角色意味着你在调用该类和它的实例 DOES() 方法时返回真.

如:

1
say 'This Cat is alive!' if $kitten->DOES( 'LivingBeing' );

继承

在两个类间建立起一种关系, 其中子类从父类继承属性和行为.

Perl 5 对象系统最近基于角色的对象系统实验显示, 在一个系统中几乎所有用到继承的地方都可以用角色来代替.

如:

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

use Moose;

has 'candle_power', is => 'ro', isa => 'int',
default => 1;
has 'enabled', is => 'ro', isa => 'Bool',
default => 0, _writer => '_set_enabled';

sub light {
my $self = shift;
$self->_set_enabled(1);
}

sub extinguish {
my $self = shift;
$self->_set_enabled(0);
}
}

这个类有两个公共属性, 两个方法, enabled 属性的 _writer 选项创建了一个私有访问器, 可在类内部用于设置值.

继承和属性

如覆盖父类方法:

1
2
3
4
5
6
7
8
9
{
package LightSource::Glowstick;

use Moose;

extends 'LightSource';

sub extinguish {};
}

Perl 的方法分派系统将先找到这个方法并且不会再在父类中查找与此同名的其他方法.

覆盖后也需要来自父类同名方法的某些行为, 见书.

继承和 isa()

如果继承于某父类, 则调用 isa() 方法时返回真:

1
2
say 'Looks like a LightSource' if     $sconce->isa('LightSource');
say 'Monkeys do not glow' unless $sconce->isa('LightSource');

Moose 和 Perl 5 OO

来自 CPAN 的 MooseX::Declare 扩展使用了一个称为 Devel::Declare 的模块向 Perl 5 添加新语法.

Moose 和它的 元对象协议 (MOP) 为一个更好的, 用于在 Perl 5 中操作类和对象的语法提供了可能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use MooseX::Declare;

role LivingBeing { require qw( name age diet) }

role CalculateAgeFromBirthYear {
has 'birth_year', is => 'ro', isa => 'Int',
default => sub { (localtime)[5] + 1900 };

method age {
return (localtime)[5] + 1900 -$self->birth_year();
}
}

class Cat with LivingBeing with CalculateAgeFromBirthYear {
has 'name', is => 'ro', isa => 'Str';
has 'diet', is => 'rw';
}

经 bless 后的引用

Perl 5 的默认对象系统以三条简单的规则构成:

  • 一个类就是一个包
  • 一个方法就是一个函数
  • 一个 (bless 后的) 引用就是一个对象

bless 关键字将一个类的名称和一个 引用 关联起来, 使得任何在该引用上进行的方法调用由与之相关联的类来解析.

默认的 Perl 5 对象构造器是一个创建引用并对其进行 bless 的方法, 出于惯例, 构造器通常命名为 new():

1
2
3
4
sub new {
my $class = shift;
bless {}, $class;
}

bless 接受两个参数

  • 与类相关联的引用
  • 类的名称
    返回值是被 bless 的引用.

类名称不需要事先存在.

可以 bless 任何类型的引用:

1
2
3
my $array_obj = bless [], $class;
my $scalar_obj = bless \$scalar, $class;
my $sub_obj = bless \&some_sub, $class;

一个类示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package Player;

sub new {
my ($class, %attrs) = @_;

bless \%attrs, $class;
}

my $joel = Player->new(
number => 10,
position => 'center',
);

my $jerryd = Player->new(
number => 4,
position => 'guard',
);

也就是说, 属性是通过哈希来创建.

提供访问器方法:

1
2
sub number   { return shift->{number}   }
sub position { return shift->{position} }

方法查找和继承

对象的方法分派, 即一个对象调用方法时, 这个方法在类中查找, 如:

1
my $number = $joel->number();

将在与经 bless 后的引用 $joel 相关联类中查找名称. 这里的类为 number, 则在 Player 包内查找一个名为 number 的函数, 如果 Player 类从其他类继承而来, Perl 也会在父类中查找, 知道啊找到 number 方法.

每一个经 bless 后的引用的类将父类信息存放在一个名为 @ISA 的包全局变量中. 方法分派器会在一个类的 @ISA 中查找它的父类, 以在其中搜索合适的方法. 如:

1
2
3
package InjuredPlayer;

@InjuredPlayer::ISA = `Player`;

可以用 parent 编译命令替代:

1
2
3
package InjuredPlayer;

use parent 'Player';

在解析方法分派时, Perl 5 在传统上偏向于对父类使用深度优先搜索, 即, 如果 InjuredPlayerPlayerHospital::Patient 两者继承, 一个在 InjuredPlayer 实例上调用的方法将先分派到 InjuredPlayer, 然后是 Player , 接着经过所有 Player 的父类来到 Hospital::Patient.

Perl 5.10 增加了一个名为 mro 的编译指令, 具体见书.

AUTOLOAD

在调用者的类及其超类中没有可用的方法时, Perl 5 就会按照解析顺序在每个类中查找 AUTOLOAD 函数.

方法覆盖和 SUPER

要在子类中覆盖一个方法, 只需声明一个和父类方法同名的方法.

在覆盖方法内, 可以通过 SUPER:: 分派指示来调用父类方法:

1
2
3
4
5
sub overridden {
my $self = shift;
warn "Called overridden() in child!";
return $self->SUPER::overridden( @_ );
}

方法名的 SUPER:: 前缀告诉方法分派器将此方法分派到父类的具名实现.

CPAN 上有 SUPER 模块.

应付经 bless 后引用的策略

可能的话避免使用 AUTOLOAD.

使用访问器方法而非直接通过引用访问实例数据.

如果无法使用 Moose, 考虑使用 Class::Accessor 这类模块来避免重复编写样板.

不要在同一个类里混用函数和方法.

为每一个类使用单独的 .pm 文件.

考虑使用 MooseAny::Moose 来替代赤裸的 Perl 5 OO.

反射

反射 (或称 内省) 是运行期间向一个程序询问其自身情况的过程.

Class::MOP 简化了许多对象系统中的反射任务, 但很多程序需用不上 Class::MOP.

检查一个包是否存在

检查一个包是否存在, 即检查其是否从 UNIVERSAL 继承下来, 也就可以检查该包是否能够执行 can() 方法来实现:

1
say "$pkg exists" if eval { $pkg->can( 'can' ) };

暂时不知道怎么用.

检查一个类是否存在

由于 Perl 5 对包和类不加以严格区分, 检查包存在性的技巧同时可用于检查一个类是否存在.

检查一个模块是否被加载

如果知道模块的名称, 你可以通过查看 %INC 哈希来确定 Perl 是否加载了这个模块. 这个哈希是和 @INC 对应的, 当 Perl 5 用 userequire 加载代码时, 会在 %INC 中存储一个条目, 其中键是欲加载模块的路径化名称, 值是模块完整的磁盘路径.

把模块名转换为正规文件形式并测试在 %INC 内是否存在 (即把 :: 转换为 /):

1
2
3
4
sub module_loaded {
(my $modname = shift) =~ s!::!/!g;
return exists $INC{ $modname . '.pm' };
}

Test::MockObjectTest::MockModule 模块可用来修改 %INC. (似乎可以直接修改)

检查模块的版本

无法保证某给定的模块是否提供版本号.

所有模块都继承自 UNIVERSAL, 因此它们全含有 VERSION() 方法:

1
my $mod_ver = $module->VERSION();

如果给定的模块不覆盖 VERSION() 或不包含包变量 $VERSION, 这个方法返回一个未定义值.

检查一个函数是否存在

确定某个函数是否存在的最简单机制就是对包名使用 can() 方法:

1
say "$func() exists" if $pkg->can( $func );

(can() 函数应该也是继承于 UNIVERSAL)

检查一个方法是否存在

没有检查某给定函数究竟是函数还是方法的通用方法.

搜查符号表

Perl 5 符号表是一个种类特别的哈希, 其中的键是包全局符号的名称, 值则是类型团. Perl 5 内部在查找这些变量时使用类型团.

通过在包名末尾添加双冒号, 你可以将符号表当作哈希访问. 如 MonkeyGrinder 包的符号表可以通过 %MonkeyGrinder:: 访问.

可以用 exists 操作符检查特定的符号名是否存在于符号表中.

高级 Perl 面向对象

这一节具体见书

多用合成而非继承

单一职责原则 (SRP)

不要重复你自己 (DRY)

Liskov 替换原则 (LSP)

子类型和强制转换

不可变性

编程风格和效能

编写可维护的 Perl 程序

编写惯用语化的 Perl 程序

CPAN 上的发行包如 Perl::Critic, Perl::Tidy 以及 CPAN::Mini 可让你的工作更简单.

编写高效的 Perl 程序

文件

输入和输出

与程序外界交互的主要机制是通过 文件句柄.

文件句柄代表输入输出通道的某种状态.

DATA 代表当前文件. 当 Perl 完成对文件的编译, 它留着包全局文件句柄 DATA 不动, 并在编译单元尾打开它.

如果在 __DATA__ 或是 __END__ 后放置字符串, 可以从 DATA 文件句柄处读取它们. perldoc perldata 对此特性进行了详细的描述.

除了文件, 还可以在标量上打开文件句柄:

1
2
3
4
5
6
use autodie;

my $captured_output;
open my $fh, '>', \$captured_output;

do_something_awesome( $fh );

perldoc perlopentut 提供了更多有关 open 奇形怪状用法的细节, 包括它启动及控制其他进程的能力, 同时也介绍了可以对输入输出进行更加精细控制的 sysopen 用法.

读取文件

readline 也可写作 <> .

文件结尾为 eof(), 遇到文件结尾时返回 undef.

写入文件

如:

1
print $out_fh "Here's a line of text\n";

注意没有逗号.

将文件句柄包裹在大括号中是一个好习惯.

如:

1
print {$out_fh} "Here's a line of text\n";

特殊文件句柄变量

每读取一行, Perl 5 都会增加 $. 的值, 其可以用作计数器.

readline$/ 当前的内容作为行结束符.

当前活动的输出文件句柄缓冲由 $| 变量控制, 当设置为非零值时, Perl 在每次对此文件句柄进行写入操作后都会冲洗输出, 当设置为零值, Perl 仍将1采用默认的缓冲策略 (即仅在数据足够多且超出某一限制时才进行 IO 操作)

使用 FileHandleautoflush() 方法可替代 $| 全局变量:

1
2
3
4
5
use autodie;
use FileHandle;

open my $fh, '>', 'pecan.log';
$fh->autoflush( 1 );

可在 perldoc FileHandle, perldoc IO::Handle 中获取更多信息.

目录和路径

打开目录句柄:

1
2
3
use autodie;

opendir my $dirh, '/home/monkeytamer/tasks/';

Perl 5.12 新增特性, while 循环中 readdir 会设置 $_, 正如 while 中的 readline:

1
2
3
4
5
6
7
8
9
use 5.012;
use autodie;

opendir my $dirh, 'tasks/circus/';

while (readdir $dirh) {
next if /^\./;
say "Found a task $_!";
}

closedir 关闭.

操作路径

File::Spec 模块为以安全可以移植地操作文件路径提供了抽象.

CPAN 上的 Path::Class 模块为 File::Spec 提供了更好的接口. 更多信息可见 Path::Class::DirPath::Class::File 文档.

文件操作

在 Perl 5.10.1 之后, 可以用形如 perldoc -f -r 的方式来查看这些文件测试操作的文档.

Cwd 模块允许判断当前目录.

chdir 关键字可以改变当前工作目录.

rename 关键字可以重命名或在目录间移动某个文件. 接受两个操作数, 旧文件名和新文件名:

1
2
3
use autodie;

rename 'death_star.txt', 'carbon_sink.txt';

核心模块 File::Copy 提供了 copy()move() 来复制文件.

可以用 unlink 来删除一个或多个文件.

Path::Class 为检查特定文件属性和完整删除文件提供了便捷的方法.

异常

抛出异常

die() 设置全局变量 $@ 为其参数并立即退出当前函数而不返回任何值.

捕获异常

使用 eval:

1
my $fh = eval { open_log_file( 'monkeytown.log' ) };

eval 代码块参数引入了新的作用域, 如果文件打开成功, $fh 将包含此文件的文件句柄, 如果失败, $fh 将维持未定义.

检查 $@ 的值来检测异常:

1
if ($@) { ... }

$@ 的值复制出来可以避免后续代码破坏全局变量 $@ 的值.

自行编写异常处理机制的替代, 可参见 CPAN 发行模块 Exception::Class.

异常注意事项

CPAN Try::Tiny 发行模块, 更加友好:

1
2
3
4
use Try::Tiny;

my $fh = try { open_log_file( 'monkeytown.log' ) }
catch { ... };

内置异常

有些异常也许值的捕获, 语法错误则不值得.

编译命令

影响编译器行为的模块称为一条 编译命令 (pragma) , 按照惯例, 编译命令的名称为小写, 以示与其他模块的区别.

编译命令和作用域

warnings, strict 就是编译命令.

编译命令的作用域与词法变量相同.

使用编译命令

可以指定所需编译命令的版本, 也可以向编译命令传递参数列表以便更好地控制其行为:

1
use strict qw( subs vars );

在作用域内可以用 no 关键字禁用部分或全部编译命令:

1
2
3
4
5
use strict;

{
no strict 'refs';
}

常用核心编译命令

  • strict
  • warnings
  • utf8
  • autodie
  • constant, 可用 CPAN 的 Readonly 替代
  • vars
  • autobox
  • perl5i

Perl 5.10.0 新增了用纯 Perl 代码编写你自己的词法编译命令的能力, 参考 perldoc perlvar, perldoc perlvar$^H 的解释说明此特性的工作原理.

管理现实世界中的程序

测试

Test::More

Perl 测试始于核心模块 Test::More 及其 ok() 函数, ok() 接受两个参数, 一个布尔值和一个描述测试目的的字符串:

1
ok( 1, 'the number one should be true' );

Test::Moretests 参数为程序设置测试计划, 如果实际执行的测试不等于某项, 表示有错误发生:

1
2
3
4
5
6
use Test::More tests => 4;

ok( ... );
ok( ... );
ok( ... );
ok( ... );

可以在测试程序的结尾, 调用 donw_testing() 函数, 其会验证成功执行的测试数量.

执行测试

TAP (Test Anythinig Protocol) 格式.

核心模块 Test::Harness 解析 TAP 并显示最贴切的信息, 其同时提供了一个名为 prove 的程序.

更好的比较

Test::Moreis() 函数比较两个值, 如果他们匹配, 则测试通过, 否则, 测试失败并提供相关诊断信息:

1
is(  4, 2 + 2, 'addition should hold steady across the universe');

is() 对其值隐式应用标量上下文:

1
is( @cousins, 6, 'I should have only six cousins' );

这里 @cousins 表示数组长度. (也可写成 scalar @cousins)

Test::More 还提供了与 is() 相对应的 isnt() 函数.

is()isnt() 都是通过 Perl 5 操作符 eqne 进行字符串比较, 对于复杂的值时, 可使用 cmp_ok() 函数, 其允许指定自己的比较操作符:

1
cmp_ok( 100, $cur_balance, '<=', 'I should have at least $100' );

通过 isa_ok() 可以测试一个类或对象是否是其他对象的扩展:

1
2
3
my $chimpzilla = RobotMonkey->new();
isa_ok( $chimpzilla, 'Robot' );
isa_ok( $chimpzilla, 'Monkey' );

isa_ok() 在失败时会提供自己的诊断信息.

can_ok() 验证一个类或对象是否能执行所要求的 (多个) 方法:

1
can_ok( $chimpzilla, 'eat_banana' );

is_deeply() 函数比较两个引用以保证他们的内容相同:

1
2
3
4
5
6
7
use Clone:

my $numbers = [ 4, 8, 15, 16, 23, 42 ];
my $clonenums = Clone::clone( $numbers );

is_deeply( $numbers, $clonenums,
'Clone::clone() should produce identical structures' );

参见 Test::DifferencesTest::Deep 了解更多有关可配置测试的信息.

组织测试

CPAN 组织测试的标准方法是创建一个包含一个或多个以 .t 结尾程序的 t/ 目录.

所有的 CPAN 发行模块管理工具都能理解这套系统.

默认的, 当你使用 Module::BuildExtUtils::MakeMaker 构建一个发行模块时, 测试步骤将执行所有 t/*.t 文件, 综合它们的输出, 并按测试套件的总体结果决定测试通过还是不通过.

两种管理 .t 文件的策略:

  • 每个 .t 文件对应一个 .pm 文件
  • 每个 .t 文件对应一个程序功能

一种混合的管理方式较为灵活: 由一个测试验证所有模块是否能够编译, 其他测试确保每个模块都能如常工作.

其他测试模块

Test::More 依赖名为 Test::Builder 的测试后端, 后者管理测试计划并将测试结果组织为 TAP.

处理警告

产生警告

warn 将一个值列表打印至 STDERR 文件句柄, Perl 会将文件名和 warn 调用发生的行后附加其后.

可使用核心模块 Carp, 其 cluck() 输出到此调用为止的所有函数调用栈跟踪.

可以对某个程序启用 Carp 的详细模式:

1
$ perl -MCarp=verbose my_prog.pl

启用和禁用警告

使用 warnings 编译命令即可.

禁用警告类

使用 no warnings; 即可.

perldoc perllexwarn 列出了你的 Perl 5 版本在使用 warnings 编译命令时能够理解的所有类别的警告.

如: recursion, redefine, uninitialized.

致命的警告

让所有警告提升为异常:

1
use warnings FATAL => 'all';

使特定警告变得致命:

1
use warnings FATAL => 'deprecated';

捕获警告

%SIG 变量持有所有类型的信号处理器. 这些信号可由 Perl 或你的操作系统抛出. 它还包括两个专为 Perl 5 异常和警告准备的信号处理器槽, 要捕获警告, 将一个匿名函数安装到 $SIG{__WARN__}:

1
2
3
4
5
6
{
my $warning;
local $SIG{__WARN__} = sub { $warning .= shift };

say "Caught warning:\n$warning" if $warning;
}

这里的意思大概是, 当 warn 抛出警告时, 就会触发 $SIG{__WARN__} 下的匿名函数来处理, 即将警告信息作为一个参数传入这个函数.

perldoc perlvar 更为详细地讨论了 %SIG.

注册自己的警告

warnings::register 编译命令的使用可以让你创建自己的词法警告. 如:

1
2
3
4
5
package Scalar::Monkey;

use warnings::register;

1;

这段代码, 创建了一个以此包命名的新警告类别 (即 Scalar::Monkey). 可以通过 use warnings 'Scalar::Monkey 来启用. 或用 no warnings 'Scalar::Monkey 来禁用.

要报告一条警告, 需要使用 warnings::enabled()warnings::warn() 函数:

1
2
3
4
5
6
7
8
9
10
package Scalar::Monkey;

use warnings::register;

sub import {
warnings::warn( __PACKAGE__ . ' used with empty import list' )
if @_ == 0 && warnings::enabled();
}

1;

warnings::enabled() 为真才会在调用方的词法作用域启用此项警告。这里的含义是, import 是默认调用的函数, 如果没有向 import 传入任何参数或 warnings::enabled() 的值为假, 就发出警告. 这里的 __PACKAGE__ 是一个全局变量.

具体见 perldoc perllexwarn

模块

一个模块必须是合法的 Perl 5 代码, 它必须以一个求值得真的表达式结束, 使 Perl 5 语法分析器知道它已成功地加载并编译了该模块.

包通常是磁盘上的文件, 当你使用 userequire 的裸字形式加载一个模块时, perl 根据 :: 分割包名, 并将包名的组成部分转换成路径, 因此:

1
use StrangeMonkey;

使得 Perl 在 @INC 的每一个目录中一次搜索名为 StrangeMonkey.pm 的文件.

perldoc -l Module::Name 会打印相关 .pm 文件的完整路径, 并提供存在于 .pm 文件中该模块的文档.

技术上不要求此位置下的文件必须包含 package 声明, 但高度推荐此惯例.

使用 use 和 导入 import

当用 use 关键字加载模块时, Perl 从磁盘上加载它, 接着调用 import() 方法, 将你提供的参数传递进去, 这发生在编译期:

1
use strict;     # 调用 strict->import

no 关键字调用一个模块的 unimport() 方法. 如:

1
no Module::Name qw( list of arguments );

和:

1
2
3
4
BEGIN {
require 'Module/Name.pm';
Module::Name->unimport( qw( list of arguments ) );
}

Perl 5 的 userequire 是大小写敏感的.

导出 export

模块可以通过一个名为 导出 的过程使全局符号在其他包中可用.

向其他模块到处函数或变量的标准方式是通过核心模块 Exporter. Exporter 依赖于包全局变量, 特别是 @EXPORT_OK@EXPORT, 其包含了一个在请求时导出的符号列表.

如:

1
2
3
4
5
6
7
8
9
package StrangeMonkey::Utilities;

use Exporter 'import';

our @EXPORT_OK = qw( round_number translate screech );

...

1;

CPAN 模块 Sub::Exporter 为不使用包全局变量导出函数提供了一个更好的接口. 但期只能导出函数.

可以通过将符号列在 @EXPORT 中来默认导出.

当你指定要导入的符号列表时就不会默认导出, 即, 如果导入一个空列表, 就不会导入任何符号:

1
use StrangeMonkey::Utilities ();

使用模块来组织代码

Perl 5 并不要求你使用模块, 也不要求使用包或是名称空间, 可以将所有代码放在单个 .pl 文件中, 或多个 .pl 文件, 随后可以按需通过 dorequire 加载.

发行模块

发行模块 (distribution) 是一个或多个模块的集合.

发行模块的属性

包括的文件和目录如:

额外地, 一个组织良好的发行模块必须包含唯一的名称和单个版本号.

CPAN 发行模块管理工具

设计发行模块

可参见 Sam Tregar 的一本书 Writing Perl Modules for CPAN.

可从 Module::StarterDist::Zilla 模块开始.

CPAN::Mini 发行模块允许你创建你自己的本地 CPAN 镜像.

UNIVERSAL 包

就面向对象来说, 它是所有包的先祖. UNIVERSAL 包为其它类和对象提供了若干可用的方法.

isa() 方法

isa() 方法接受包含类名或内置类型名称的字符串. 可以将其作为类方法调用或用作对象上的实例方法.

如果类或对象从给出的类中衍生而来, 或者对象本身是给类型经 bless 的引用, 则此方法返回真.

(内置类型有 SCALAR, ARRAY, HASH, Regexp, IO, CODE):
如:

1
2
say $pepper->isa( 'Dolphin' );
say $pepper->isa( 'SCALAR' );

可在自己的类中覆盖 isa()

can() 方法

can() 方法接受包含 方法名 的字符串. 如果方法存在, 则返回指向实现该方法的函数引用, 否则返回假.

如:

1
if (my $meth = SpiderMonkey->can(' screech ')) { ... }

VERSION() 方法

VERSION() 方法对所有包, 类和对象都是可用的. 它返回合适包或类中 $VERSION 变量值.

它接受一个版本号作为可选参数, 当该参数大于 $VERSION 值时会抛出异常. 如:

1
2
3
use Carp;

say Carp->VERSION();

DOES() 方法

DOES() 是 Perl 5.10.0 新加的, 它的存在支持了程序中对角色的使用.

向其传递调用物和角色名称, 此方法会在合适的类饰演此角色时返回真.

如:

1
say Cappuchin->DOES( 'Monkey' );

扩展 UNIVERSAL

偶尔可以在调试或修复不正确的默认行为时扩展 UNIVERSAL

代码生成

eval

生成代码最简单的技巧是创建一个包含合法 Perl 代码片段的字符串并通过 eval 字符串操作符编译.

不像捕获异常的 eval 代码块操作符, eval 字符串在当前作用域内编译其中内容, 包括当前包和词法绑定.

如:

1
2
eval { require Monkey::Tracer } 
or eval 'sub Monkey::Tracer::log {}';

利用多行字符串的语法:

1
2
3
4
5
6
7
8
9
10
11
sub generate_accessors {
my ($methname, $attrname) = @_;

eval <<"END_ACCESSOR";
sub get_$methname {
my \$self = shift;

return \$self->{$attrname};
}
END_ACCESSOR
}

注意反斜杠.

参数闭包

这里实际上就是用函数引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sub generate_accessors {
my $attrname = shift;

my $getter = sub {
my $self = shift;
return $self->{$attrname};
};

my $setter = sub {
my ($self, $value) = @_;

$self->{$attrname} = $value;
};

return $getter, $setter;
}

向符号表安装:

1
2
3
4
5
6
7
{
my ($getter, $setter) = generate_accessors( 'homecourt' );

no strict 'refs';
*{ 'get_homecourt' } = $getter;
*{ 'set_homecourt' } = $setter;
}

编译器操控

不同于显式编写的代码, 通过 eval 字符串生成的代码于运行时生成.

Class::MOP

Class::MOPMoose 的支柱库, 它提供了 元对象协议 (Meta Object Protocol).

创建类:

1
2
3
use Class::MOP;

my $class = Class::MOP::Class->create( 'Monkey::Wrench' );

创建时添加属性和方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Class::MOP;

my $class = Class::MOP::Class->create(
'Monkey::Wrench' => (
attributes => [
Class::MOP::Attribute->new('$material'),
Class::MOP::Attribute->new('$color'),
]
methods => {
tighten => sub { ... },
loosen => sub { ... },
}
),
);

创建后添加:

1
2
$class->add_attribute( experience => Class::MOP::Attribute->new('$xp'));
$class->add_method( bash_zombie => sub { ... });

查看:

1
2
my @attrs = $class->get_all_attributes();
my @meths = $class->get_all_methods();

重载

几乎可以重载任何的对象操作.

重载常见操作

  • 字符串化
  • 数值化
  • 布尔化

overload 编译命令允许你将函数和可重载操作关联起来:

1
2
3
package Null;

use overload 'bool' => sub { 0 };

即在所有布尔上下文中, 此类的所有实例求值得假.

overload 编译命令的参数是一个键值对, 键描述了重载的类型而值则是替代 Perl 默认行为的函数引用.

可见 perldoc overload

重载和继承

重载的用途

可见 IO::All 模块

Taint

污点模式, 具体见书.

语法之外的 Perl

惯用语

将对象用作 $self

Perl 5 惯用语将 $class 用作类名而将 $self 用作实例方法的调用者.

具名参数

就是传递哈希或哈希引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
make_ice_cream_sundae(
whipped_cream => 1,
sprinkles => 1,
banana => 0,
ice_cream => 'mint chocolate chip',
)

sub make_ice_cream_sundae {
my %args = @_;

my $ice_cream = get_ice_cream( $args{ice_cream} );
...
}

import() 配合, 将多余的参数吸入哈希:

1
2
3
4
5
6
sub import {
my ($class, %args) = @_;
my $calling_package = caller();

...
}

Schwartzian 转换

从 Lisp 语言借鉴而来. 其就是 map-sort-map 的结构, 如:

1
2
3
4
5
say for 
map { " $_->[1], ext. $_->[0]" }
sort { $a->[1] cmp $b->[1] }
map { [ $_ => $extension{$_} ] }
keys %extensions;

简易文件吸入

用单个表达式将文件吸入某个标量中:

1
2
3
4
5
my $file = do { local $/ = <$fh>};

# or

my $file = do { local $/; <$fh>};

$/ 是输入记录分隔符. 用 local 将其局部化, 即将其值设为 undef, 由于 local 操作在赋值之前, 分隔符未定义, Perl 就将文件内容全部读入.

控制程序执行

了解 Perl 如何开始执行一段代码, 使用 caller 函数.

caller 的单个可选参数就是要报告的调用帧的数目. ( 调用帧 (call frame) 就是代表一个函数的调用的薄记信息 )

可以用 caller(0) 得到当前调用帧的信息.

处理 Main 函数

用一个简单的函数, main(), 来包装程序的主要代码. 将所有你不需要的变量封装为真正的全局变量.

然后将:

1
main();

写在开头.

后缀参数验证

可以使用 Params::ValidateMooseX::Params::Validate 等 CPAN 模块来验证函数接受的参数是否正确.

不使用模块的方式:

1
2
3
4
5
sub groom_monkeys {
if (@_ != 2) {
croak 'Monkey grooming requires two monkeys!';
}
}

这里验证的是参数个数.

也可写成:

1
croak 'Monkey grooming requires two monkeys!' unless @_ == 2;

Regex En Passant

假设有一个全名并且想要提取名字部分:

1
my ($first_name) = $name =~ /($first_name_rx)/;

$first_name_rx 是一个预编译的正则表达式.

在列表上下文中, 一个成功正则表达式匹配返回由所有捕获组成的列表, 这里将第一个元素赋值给 $first_name

全局变量

超级全局变量 , 不仅仅局限于任何特定的包.

两个缺点:

  • 任何直接或间接的修改就能影响到程序的其余部分
  • 过于精炼

管理超级全局变量

最佳途径是避免使用.

当必须要使用时, 在尽可能小的作用域内使用 local 来约制改动.

如文件吸入:

1
my $file = do { local $/ = <$fh> };

立刻复制 $@ 来预留它的内容.

英语名称

English 核心模块为过度使用标点的超级全局变量提供了详细的名称.

导入到名称空间:

1
use English '-no_match_vars';

名称记录在 perldoc perlvar 中.

三个正则表达式相关的超级全局变量 $&, $反引号$' 会降低程序内所有正则表达式的性能. 现代化 Perl 程序用 @- 代替前三个变量.

常用超级全局变量

要避免什么

裸字

裸字 是一个标识符, 它没有印记或其他用于说明其语法功能的附加消歧条件.

裸字的正当使用

Perl 5 中的哈希键是裸字, 有时足够用, 也可消除歧义:

1
2
3
4
5
6
my $value = $items{shift};

my $value = $items{shift @_};

# 消除歧义
my $value = $items{+shift};

constant 编译命令定义的常量可以按裸字使用:

1
use constant NAME => 'Bucky';

裸字的欠考虑使用

没有启用 strict 'subs' 而编写的代码可以使用裸字函数名.

内置的 sort 操作符以第二参数的形式接受一个用于排序的函数名, 作为代替, 提供一个用于排序函数引用 可以避免使用裸字:

1
2
3
4
5
6
# 不良用法
my @sorted = sort compare_lengths @unsorted;

# 更好的风格
my $comparison = \&compare_lengths;
my @sorted = sort $comparison @unsorted;

Perl 5 语法解析器并不理解单行版本:

1
my @sorted = sort \&compare_lengths @unsorted;

间接对象

Perl 5 中的构造器就是任何返回对象的东西.

一般将构造器函数命名为 new, 避免歧义的写法为:

1
my $q = CGI->new();

间接记法的标量限制

IO::Handle 核心模块, 其允许你调用文件句柄对象上的方法来执行 IO 操作.

原型

原型 (prototype) 是一块附加在函数生命上的可选元数据.

两个目的:

  • 给予语法分析器提示来改变对函数及其参数的语法分析
  • 修改了 Perl 5 处理函数参数的方式

如:

1
2
3
4
5
6
sub foo     (&@);
sub bar ($$) { ... };

sub foo(&@) {
...
}

括号内的代表参数内容, & 表示接受一个代码块, @ 表示接受一个列表, $ 表示接受一个标量.

该原型必须完整地出现在函数声明中.

原型最初的目的是允许用户定义它们自己的函数, 这些函数的行为和 (一些) 内置操作符一样.

内置操作符 prototype 接受一个函数名称并返回代表其原型的字符串:

1
2
3
4
$ perl -E "say prototype 'CORE::push' // 'undef';"
\@@
$ perl -E "say prototype 'CORE::keys' // 'undef';"
\%

一些内置操作符拥有你无法模拟的原型.

对于:

1
2
$ perl -E "say prototype 'CORE::push' // 'undef';"
\@@

的含义.

@ 字符代表一个列表, 反斜杠强制对对应的参数进行引用, 因此这个函数接受一个数组引用和一列值.

可参考 perldoc perlsub 文档.

原型的问题

原型可以改变后续代码的语法分析而且他们会对参数进行强制类型转换.

原型对参数的强制类型转换以一种隐晦的方式发生, 如在传入参数上强制标量上下文:

1
2
3
4
5
6
7
8
sub numeric_equality($$) {
my ($left, $right) = @_;
return $left == $right;
}

my @nums = 1 .. 10;

say "They're equal, whatever that means!" if numeric_equality(@nums, 10);

看上述代码可知.

原型的正当使用

理由一: 用于覆盖内置关键字来用户自定义函数, 需要先用 prototype 来确认有没有返回 undef 来检查是否可以覆盖内置关键字, 返回 undef 则不能.

知道了关键字的原型, 就可以声明和核心关键字同名的前置定义:

1
2
3
use subs 'push';

sub push(\@@) { ... }

理由二: 定义编译器常数: 一个由 空原型 声明且求值得单个表达式的函数将成为常数而非函数调用:

1
sub PI() { 4 * atan2(1, 1) }

毕竟此时传入参数就会报错.

CPAN 的 Readonly 模块可以将常量内插入字符串.

理由三: 见书.

方法-函数等价

Perl 自身不对存储于包中的函数和存储于包中的方法加以强制区分.

如果你试着将函数作为方法调用, Perl 自身将把任何能找到的拥有合适名称, 处于合适包中的函数当作方法.

调用方 (Caller-side)

1
2
my $name = 'apply_discount';
$o->$name();

也可运行.

被调用方 (Callee-side)

捆绑 (Tie)

使用 tie 关键字. 可查看 perldoc perltie 文档.

这个主题先见其他书.


Modern-Perl-Notes
http://example.com/2023/01/18/Modern-Perl-Notes/
作者
Jie
发布于
2023年1月18日
许可协议