Modern Perl 中文版


file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_00.html[2011/2/21 21:21:59] Preface 今年晚些时候,Perl 就 23 岁了。 这门语言已经由介于 Shell 脚本和 C 程序之间(Perl 1)的 简易 系统管理工具,转变成为一种强大的通用编程语言。他承接着丰富的代码资产(Perl 5),并在 对 通用编程的不断反思中,续写又一个 25 年的辉煌(Perl 6)。 尽管如此,Perl 语言提供的强大特性还是未能得到现存大部分 Perl 5 程序的充分利用。你 可以 写出与Perl 4(或Perl 3或Perl 2或Perl 1)相仿的 Perl 5 程序,但用到整个 Perl 5 社区已经 发 明、发扬、发现的神奇特性的程序,会更短、更快、更强大,相比其它版本,也更易维护。 Modern Perl 仅是对老练而高效的Perl 5程序员的工作方式的一种粗略描述。他们使用惯用语。 他们充分利用CPAN。他们的程序带有明显的 “Perl 的味道”,他们展现出对事物的良好品味、 独到的匠心 以及对 Perl 通透的理解。 你也可以学着成为这样的Perl程序员。 运行 Modern Perl 除非另作说明,每一处代码片段总是假设如下基本程序框架: #!/usr/bin/perl use Modern::Perl; # 这里是示例代码 ... Modern::Perl 模块可以从 CPAN 获取。你可以自行安装它,或用如下代码代替: use strict; use warnings; use 5.010; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_00.html[2011/2/21 21:21:59] 其余用到诸如 ok(), like(), 及 is() (æµè¯) 等测试函数的代码片段,该框架则为: #!/usr/bin/perl use Modern::Perl; use Test::More; # 这里是示例代码 ... done_testing(); 书中给出的例子假设你正使用 Perl 5.10 或更新版本,理想的话至少是 Perl 5.10.1。许多例子经 适当 修改就可以在 Perl 5 的早期版本上运行,但是早于 Perl 5.10.0 的版本会给你带来更多的困 难。本书 同样描述(但不 要求 使用)Perl 5.12 中新引入的功能。 你通常可以自行安装一份最新的 Perl。Windows 用户可以从 http://www.strawberryperl.com/ 下载 Strawberry Perl。操作系统自带 Perl 5(以及 C 编译器还有其他一些开发工具)的用户, 可以以安装 CPAN 模块 App::perlbrew (footnote: 安装指南参见 http://search.cpan.org/perldoc? App::perlbrew) 作为开始. perlbrew 允许你安装并管理多个版本的 Perl 5。默认地。它将它们安装在你的家目录下。你不仅 可以 在不影响系统自带 Perl 的情况下拥有多个版本的 Perl 5,而且不用劳烦系统管理员给你特 定权限,就 可以把你喜欢的任何模块安装到这些目录中。 Perl 5 和 Perl 6 你应该学习 Perl 5 还是 Perl 6?它们有着同样的哲学和语法以及库和社区,它们有各自合适的 位置。如果: 你有现成的 Perl 5 代码要维护 你需要利用 CPAN 模块 你的部署策略要求严格的稳定性 那么学 Perl 5 吧。如果: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_00.html[2011/2/21 21:21:59] 你对频繁升级习以为常 你能够承受试验新语法和新功能 你需要的功能仅由 Perl 6 提供 你可以为它的开发做出贡献(无论是补丁、缺陷报告、文档、赞助或其他资源) 那么学 Perl 6 吧。 一般说来,Perl 5 的开发会保持有关核心语言的部分。有好有坏,变化产生得相当缓慢。Perl 6 更 具实验性质,它更注重找到最合理的设计而非保证旧的代码可以工作。所幸的是,你可以学 习并使用 两者(并且它们影响着互相进步)。 本书讨论 Perl 5。如要学习 Perl 6,请浏览 http://perl6.org/,试用 Rakudo(http://www.rakudo.org/), 并参考同样由 Onyx Neon Press 出版的 Using Perl 6 一书。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_01.html[2011/2/21 21:22:00] Perl 哲学 Perl 是一种能“干实事”的语言。它灵活、宽容、可塑。在一名编程能者的手中,它可以 完成几 乎所有的任务,从“一句话”的简单运算和自动完成任务到多人、多年的项目, 外加夹在两者之 间的部分。 Perl 功能强大,摩登 Perl────一个集最上乘知识和经验,以及来自全球 Perl 社区的 可重 用惯用语的Perl────它可维护、快速、易用。也许最重要的是,它能够帮助你无挫折、 无 繁文缛节地做你需要做的事。 Perl 是一门实用主义语言。你,程序员,完全地掌控着自己编写的程序。相对于操控你的思想 和你面对的问题来适应语言设计者认为你应该怎样写程序,Perl 允许你按你觉得合适的方式 解 决问题。 Perl 是一门伴随你成长的语言。你能够用你在一小时内阅读本书所学到的知识写出有用的程 序。 如果你还能花点时间理解语法、语义、语言设计背后的哲学,你会变得更有成效。 首先,你需要知道如何学到更多东西。 Perldoc Perl 最有用也是最不受感恩的功能之一就是 perldoc 实用工具。这个程序是每一个完整 Perl 5 安 装的一部分。 (footnote: 在免费的 GNU/Linux 的发行版或是其它类 Unix 系统下,你也许 需 要安装一项额外的软件包,Debian 和 Ubuntu 上为 perl-doc)。它能显示系统上每一个 已安装 Perl 模块的文档────无论是核心模块,或是那些来自 Comprehensive Perl Archive Network (Perl 综合典藏网,CPAN) 的部分────还包括上千页 Perl 核心文档。 如果你倾向于一份在线版本,http://perldoc.perl.org/ 存有 Perl 文档的最近版本。 http://search.cpan.org/ 可显示 CPAN 上所有模块的文档。对于 Windows 用户,ActivePerl 和 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_01.html[2011/2/21 21:22:00] Strawberry Perl 都在开始菜单提供了指向这份文档的链接。 perldoc 的默认行为是显示某模块的文档或是 Perl 核心文档的特定章节: $ perldoc List::Util $ perldoc perltoc $ perldoc Moose::Manual 第一个例子解开为 List::Util 模块所编写的文档并按适合你的显示屏的方式显示出来。 CPAN 模块 (CPAN) 的社区标准建议额外的库使用和核心模块一致的格式,使得阅读诸如 Data::Dumper 核心模块以及从 CPAN 安装的其他模块的文档并无差别。标准文档模板包 含了一份对模块的 描述,用法演示,和后续对模块的详细解释和接口介绍。虽然文档的长度 因作者而不同,其形 式却相当统一。 第二个例子显示一个纯粹的文档文件,此处是核心文档自己的目录。这个文件描述了核心文 档 的每一个独立的部分。浏览一下,简单地看看 Perl 的能力。 第三个例子是第二个例子的翻版。 Moose::Manual 是 Moose CPAN 发行版 (Moose) 的一部分。它 同样也是一个纯文档,不包含任何代码。 类似的,perldoc perlfaq 将会显示 Perl 5 常见问题的目录。仅扫一眼这些问题也会使 人获益匪 浅。 perldoc 实用工具还有不少能力(参见 perldoc perldoc)。其中两个最有用的是-q 以及 -f 参数。-q 参 数读取一个或多个关键字,在 Perl FAQ 中查找,并显示所有结 果。从而 perldoc -q sort 返回三 个常见问题:我如何按(任何条件)排序数组?, 我如何将一个哈希排序(可选的附加条件: 通过值而非键)?,以及 我如何使一个哈希总保持在有序状态?. -f 参数显示核心文档中某 Perl 内置函数的相应部分。perldoc -f sort 解释了 sort 操作符的行为。 如果你不知道你要查找函数的名称,perldoc perlfunc 可以给出一张函数列表。 perldoc perlop 和 perldoc perlsyn 记录了 Perl 中符号形式的操作符和语法结构。 perldoc perldiag 解 释了 Perl 警告消息的含义。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_01.html[2011/2/21 21:22:00] Perl 5 的文档系统是 POD,或作 Plain Old Documentation。perldoc perlpod 描述 了 POD 的工作 方式。perldoc 实用工具能显示你为你的项目创建和安装的所有 Perl 模块中 的 POD,其他诸如 podchecker 等 POD 工具,可以验证你的 POD 的格式,Pod::Webserver, 可以通过一个小型 Web 服务器以 HTML 的形式显示本地的 POD,同样也将验证其正确性。 perldoc 还有其他用途。给出 -l 命令行参数,它将显示文档文件的 路径 而非文件内容。 (footnote: 注意:一个模块除 .pm 文件外可能还有多个分离的 .pod 文件)。给出 -m 参数,它 将 显示整个模块的 内容,包括代码,并不对任何 POD 指令进行处理。 表达力 在 Larry Wall 创造 Perl 之前,他研究的是语言学和人类语言。他的经历持续地影响着 Perl 的 设计。因项目的风格、编写程序的可用时间、所期望的维护代价,甚至是你 个人的表达能力的 不同,编写一个 Perl 程序的方法是多种多样的。你可以采用直截了当 、自顶而下的风格。你 可能编写很多小而独立的函数。你可能会用类和对象对你的问题建模。 你也可以避让或拥抱 Perl 的高级功能。 Perl 黑客们对此有句口号,TIMTOWTDI,发音为“Tim Toady”, 或作 "There's more than one way to do it!" 这种表达力提供了一个大型调色盘,用它巨匠可以创造出惊人的、宏大的建筑物,仅将各 式技 巧不明智地堆砌起来只能阻碍代码的可维护性和易读性。你可以写出优秀或者一塌糊 涂的代 码。选择权在你这里。 (footnote: ……但是如果你必须弄得一塌糊涂,那么对其他人好一点)。 其他语言也许会建议说,按一条强制的指导思想走每一步路才是解决问题的正途。Perl 允许 你 针对最重要的标准进行优化。在问题的范围之内,你可以选择若干合适的方法────但是注 意一下可读性和未来的可维护性。 作为一个 Perl 的新手,你可能会觉得某些结构难易理解。Perl 社区已经总结并推出了 不少强 大的惯用语 (æ¯ç¨è¯)。别指望一下就能理解它们。Perl 的一些特性以微妙的方 式互相联系着。 学习 Perl 就好像在学习一门第二或第三口语。你会学到一些单词,然后将它们串起来构 成句 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_01.html[2011/2/21 21:22:00] 子,最终能够进行一小段简单的对话。熟能生巧,读也一样,写也一样。你不用一次性 理解本 章的所有细节就能够写出卓有成效的 Perl 程序。当你阅读本书余下部分时,请记住 这些原 则。 另一个 Perl 的设计目标是,尽可能不使有经验的 (Perl) 程序员吃惊。举例来说,用一个 数值 操作符将两个标量相加($first_num + $second_num) 很显然是一次数值的操作,该 操作符必须按数值 对待这两个标量来产生数值结果。无论 $first_num 和 $second_num 中的内容是什么,也不必用户或 程序员手动操作,Perl 会将它们强制转为数值(æ°å¼å¼ºå¶è½¬æ¢) 。 你已经通过选择数值操 作符 (æ°å¼æä½ç¬¦)表达了你想将它们用作数值的意图,因 此 Perl 很高兴地替你处理后续工 作。 一般而言,Perl 程序员可以期望 Perl 能完成他们想要 Perl 完成的事,这便是 DWIM-- do what I mean 的意思。你也能见到另一种提法 最小惊奇原则。对 Perl 有个粗略的 了解之后(特别是 它的 ä¸ä¸æ ),在读到一个不熟悉的 Perl 表达式时,就 有可能猜出它的意图。 如果你刚接触 Perl,你将渐渐培养出这种技能。Perl 表达力的反面就是,新手在学全 Perl 强大 功能之前就可以写出有用的程序。Perl 社区通常将此称作 baby Perl。虽然它可能听 上去有些轻 蔑,但请千万别生气,每个人都是这样过来的。抓住向更有经验程序员学习的机会, 并对你不 理解的惯用语和结构(向他们)索求解释。 一个 Perl 新手可能会按下面的方法把列表中所有的元素乘以三: my @tripled; my $count = @numbers; for (my $i = 0; $i < $count; $i++) { $tripled[$i] = $numbers[$i] * 3; } 一个 Perl 内行可能会这样写: my @tripled; for my $num (@numbers) { push @tripled, $num * 3; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_01.html[2011/2/21 21:22:00] } 一个经验丰富的 Perl 黑客可能会这样写: my @tripled = map { $_ * 3 } @numbers; 编写 Perl 程序的经验将会帮助你专注于要做什么而非怎么做。 Perl 是一门有意随着你对编程理解程度的增长而成长的语言。它不会因写出简单的程序而惩罚 你。 它允许你为了直接明了、富表达力、代码重用和可维护性来完善、扩展你的程序。好好利 用这条哲学。 出色地完成你的任务比写出概念上纯美的程序更为重要。 本书余下部分展示如何按对你有利的方式使用 Perl。 上下文 [当阅读这个小节的时候,我意识到 Perl 中的上下文的基础是“用什么操作符”以及“在哪里用 它”。 在本小节和 operator_types.pod 中,你决不会直接地说出来。但这是真的。我试图在这 个小节的 某处中直接地说出来,但是我还无法决定在哪里已经怎样说,因此这只是一个提 议。] 口语中有 上下文 的概念,即某词汇或短语的正确用法和含义由语境所决定。理解一下 在口语中的情况, “Please give me one hamburgers!” 中不合适的复数用法 (footnote: 名词的 复数形式和数量不符), 听上去就不对,或者 “la gato” 中不正确的性别 (footnote: 冠词为阴 性,但是名称为阳性) 使得母语人士轻声窃笑。 同样考虑代词 “you” 或者名词 “sheep” ,是单 数是复数还得由句子的余下部分决定。 Perl 中的上下文是类似的,这门语言理解对数据数量上的期望同时也知道应该提供什么样的数 据。 Perl 将高兴地尝试提供给你恰好合你心意的数据。 Perl 中上下文的每一种类型,都和你所需某操作符结果的个数(零个、一个、或许多个)相对 应, 同一个操作符在(不同的上下文中)有着不同的行为。在 Perl 中,如果你要求:“给我零 个结果, 我不在乎有没有”或是“给我一个结果”再或是“给我多个结果”,那么一个特定的结构 按这些不同的要 求做不同的事情是完全可能的。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_01.html[2011/2/21 21:22:00] 同样,特定上下文将你所需明朗化:是数值,是字符串值、或是一个为真或假的值。 如果你打算把 Perl 代码当成一系列独立于环境的单一表达式来读写,那么上下文将会变得十足 狡猾。 在一次长时间的调试后,你也许会一拍脑门,发现你对程序上下文的假设是错误的。然 而在对上下文 了如指掌后,它们会使你的代码更清晰、更简洁、更灵活。 空、标量和列表上下文 上下文之一掌控着你期望事物的 多少。这就是 数量上下文。这个上下文和英语中“主谓一致” 相当。即使尚未了解这条规则的正式定义,你很有可能理解句子 "Perl are a fun language" 中 的错误。 Perl 中的规则是,你所要求事物的数量决定了你得到的。 假设你有一个称为 some_expensive_operation() 的函数 (å½æ°),它进行一昂贵的计算并 产生许多许 多结果。如果你直接调用该函数且对返回值不加利用,那么就称你在 空上下文 中调用了 这个 函数: some_expensive_operation(); 将函数的返回值赋值给单个元素使得函数在 标量上下文 中求值: my $single_result = some_expensive_operation(); 将调用函数的结果赋值给一个数组(æ°ç»)或是列表,或者在一个列表中使用该结果,使 得函 数在 列表上下文 中求值: my @all_results = some_expensive_operation(); my ($single_element) = some_expensive_operation(); process_list_of_results( some_expensive_operation() ); 前例的第二行可能看上去有些迷惑,这里的括号给了编译器一点提示:尽管这里只有一个 标 量,该赋值应在列表上下文中发生。在语义上等同于将列表中的第一个元素赋值给一个 标量, 并将列表的其余部分赋值给一个临时数组,随即丢弃该数组。除非真的发生(类似于 后例的) 数组赋值: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_01.html[2011/2/21 21:22:00] my ($single_element, @rest) = some_expensive_operation(); 为什么对函数来说上下文是有趣的?假如 some_expensive_operation() 计算的是按优先 级排序过的 家务事。如果只有做一件事的时间,你可以在标量上下文中调用它,获得一项有用的 任务 ────也许并不一定是最重要的,但是不会是排在最底下的那一项。在列表上下文中,该函 数 能完成所有的排序、搜索和比较,可以给你一份顺序合适且详尽的列表。如果你想所有事情 都做, 但是只有那么些时间,你可以用一个一两个元素的列表(来接受该函数在列表上下文中 的返回值)。 在列表上下文中对函数或表达式────除赋值外────求值,可能造成迷惑性结果。列表 会把列表 上下文散播到其所包含的表达式中。下列对 some_expensive_operation() 的调用都发生于 列表上下文: process_list_of_results( some_expensive_operation() ); my %results = ( cheap_operation => $cheap_operation_results, expensive_operation => some_expensive_operation(), # OOPS! ); 上例 expensive_operation 位于列表上下文,因为它被赋值到一个哈希中,而哈希赋值需要一 键值 对列表,导致在哈希赋值内的所有表达式在列表上下文中求值。 后一个例子通常使期望该调用为标量上下文的新手程序员吃惊。相反,这是列表上下文,因为 该上 下文为哈希赋值所强制。使用 scalar 操作符可以迫使其在标量上下文求值: my %results = ( cheap_operation => $cheap_operation_results, expensive_operation => scalar some_expensive_operation(), ); 数值、字符串及布尔上下文 另一类型的上下文决定了 Perl 如何理解某块数据────不是你要 多少 数据,而是数据的意 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_01.html[2011/2/21 21:22:00] 义。 你也许早已觉察到 Perl 可以灵活地指出你所有的是数字还是字符串并在按需在两者之间 转换。这就是 值上下文,有助解释 Perl 是如何做这些事情的。不必明确声明(至少是跟踪) 某变量包含(或产 生自某函数)的数据的 类型,作为交换,Perl 提供了特定类型的上下文, 由它们告知编译器在某项 操作期间如何对待一个给定的值。 假设你想比较两个字符串的内容。eq 操作符能告诉你这些字符串中是否包含相同的信息: say "Catastrophic crypto fail!" if $alice eq $bob; 当 明明知道 字符串内容不同,但是比较结果却是相同时,你可能会感到莫名其妙: my $alice = 'alice'; say "Catastrophic crypto fail!" if $alice == 'Bob'; # 啊呀 eq 操作符通过强制 字符串上下文,按字符串对待它的操作数。== 操作符则强制 数值上下文。 示例代码出错的原因在于,两个字符串在用作数字时候的值是 0(æ°å¼å¼ºå¶è½¬æ¢)。 布尔上下文 发生在当你在条件语句中使用某值时。在前面的例子中,if 语句在布尔上下文中求 出 eq 和 == 操作的结果。 Perl 会尽最大努力将值强制转换成正确的类型 (强å¶è½¬æ¢),并依赖于所用的操作符。一定 要针对你所需的 上下文使用正确的操作符。 在极少数情况下,没有合适类型的操作符存在,你也许需要明确地强制上下文。强制数值上下 文,在变量前加零。 强制字符串上下文,将变量和空字符串拼接起来。强制布尔上下文,使用 双重否定操作符。 my $numeric_x = 0 + $x; # 强制数值上下文 my $stringy_x = '' . $x; # 强制字符串上下文 my $boolean_x = !!$x; # 强制布尔上下文 大体上说,相比数量上下文,类型上下文较易理解和识别。一旦你理解它们的存在,并知道什 么操作符提供什 么上下文 (æä½ç¬¦ç±»å),你很少会因为它们而犯错。 隐式理念 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_01.html[2011/2/21 21:22:00] 像不少口语一样,Perl 提供了语言学捷径。上下文即是这样一个特性。阅读代码的无论是 编译 器还是程序员,都可以通过现有信息了解到所期望结果的数量和操作符的类型,而不必 额外添 加明确信息来消歧。Perl 中也存在其他类似的特性,包括本质上是代词的默认变量。 默认标量变量 默认标量变量(也称为话题变量)$_,是 Perl 中语言学捷径的最佳例证。它最显眼的地方 就是 它的 “缺席”:当缺少一明确变量时,Perl 中许多内置操作是针对 $_ 的内容进行的。 你仍可以 将 $_ 填入所缺变量的位置,但是这通常是多此一举。 举例来说,chomp 操作符移去任何尾随字符串的换行符序列: my $uncle = "Bob\n"; say "'$uncle'"; chomp $uncle; say "'$uncle'"; 在没有指明变量时,chomp 移去 $_ 尾部的换行符,因此下列两行代码是等价的: chomp $_; chomp; $_ 在 Perl 中的功能等同于汉语中的代词“它”(英语中的代词 it)。第一行 读作 “chomp 它”,第 二行则读作“chomp”。当你不指明对什么做 chomp 操作时, Perl 理解你的意思,Perl 总是 chomp 它。 类似的,内置函数 say 和 print 在缺少参数时作用于 $_ 之上: print; # 将 $_ 打印到当前所选文件句柄 say; # 将 $_ 打印到当前所选文件句柄 # 外加结尾换行符 Perl 的这套正则表达式装备 (æ£å表达å¼åå¹é) 同样可以在 $_ 上进行匹配、 替换和转译操 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_01.html[2011/2/21 21:22:00] 作: $_ = 'My name is Paquito'; say if /My name is/; s/Paquito/Paquita/; tr/A-Z/a-z/; say; Perl 中许多标量操作符(包括 chr、ord、lc、length、reverse 及 uc) 在你不提供替代选项时,作 用于默认标量变量上。 Perl 的循环指令 (循ç¯è¯å¥) 同样设置 $_ 变量,比如用 for 遍历一个列表: say "#$_" for 1 .. 10; for (1 .. 10) { say "#$_"; } ……或者是 while: while () { chomp; say scalar reverse; } ……再或者是用 map 转换列表: my @squares = map { $_ * $_ } 1 .. 10; say for @squares; ……又或者是用 grep 过滤列表: say 'Brunch time!' if grep { /pancake mix/ } @pantry; 如果你在用到 $_ 的代码内调用函数,无论是隐式还是显式,可能导致 $_ 的值被覆盖。 相似 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_01.html[2011/2/21 21:22:00] 地,如果你编写了一个使用 $_ 的函数,就有可能搅乱调用者对 $_ 的利用。Perl 5.10 允许你用 my 将 $_ 作为词法变量来声明,这样便可以避免上述行为。明智一点。 while () { chomp; # 一例反面教材 my $munged = calculate_value( $_ ); say "Original: $_"; say "Munged : $munged"; } 在本例子中,若 calculate_value() 或它偶然调用了其他函数导致 $_ 的值改变,则在 整一次 while 循环中,此值将保持被改后的状态。用 my 来声明可以避免此类情况: while (my $_ = ) { ... } 当然,使用带有具体名字的变量会比较清晰: while (my $line = ) { ... } 你可以在书面写作中用到“它”(英语:"it")的地方使用 $_: 适度地、在小且精心圈定的范围 内。 默认数组变量 在 Perl 拥有一个隐式的标量变量的同时,它也有两个隐式数组变量。Perl 通过一个名为 @_ 的 数 组向函数传递参数。函数内部处理数组的操作符 (æ°ç») 默认影响这个数组。因此,以下二 例代码 是等价的: sub foo { my $arg = shift; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_01.html[2011/2/21 21:22:00] ... } sub foo_explicit { my $arg = shift @_; ... } 正如 $_ 对应代词 它,@_ 对应代词 它们。不同于 $_,当你调用其他函数时, Perl 自动地为你局 部化 @_。数组操作符 shift 和 pop 在没有提供其他操作数时作用于 @_。 在所有函数之外,默认数组变量 @ARGV 存有传递给程序的命令行参数。在函数 内 隐式用到 @_ 的 同一批数组操作符,在函数外隐式地使用 @ARGV。你不可以将 @_ 用作 @ARGV。 ARGV 有一个特殊的用法。如果你从空文件句柄 <> 读入,则 Perl 会将 @ARGV 中的每一个元素当作 文件的 名字 而打开。(如果 @ARGV 为空,Perl 会从标准输入读取。)这个隐含的 @ARGV 行为在编 写 短小的程序(例如将输入逆序输出的命令行过滤器)时很有用: while (<>) { chomp; say scalar reverse; } 为什么用到 scalar?say 对其操作数施加列表上下文。而 reverse 又将它的上下文传递给自己的操 作数, 在列表上下文中它将它们作为列表对待,在标量上下文中则看作拼接字符串。这听上去 有点迷糊,因为的确如此。 Perl 5 应将不同的操作用不同的操作符处理。 如果你用一列文件作参数运行这个程序: $ perl reverse_lines.pl encrypted/*.txt ……结果应该会是一系列冗长的输出。不带参数运行时,你可以提供自己的标准输入:通过管 道的方式接通其他程序, 或者直接从键盘打字。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_02.html[2011/2/21 21:22:01] Perl 和它的社区 Larry 设计 Perl 5 的一个主要目标就是鼓励核心发行版之外开发与革新。Perl 4 有几个 旁支, 因为,举例来说,没什么简单的方法能让它连接到关系数据库。Larry 希望人们能够 创建并维 护他们自己对语言的扩展而无须使 Perl 分裂为千百个不兼容的混杂语言。 你可以为扩展而引入某种技术上的机制,但是你也需站在社区角度考虑此事。那些不在人群 中 共享的扩展和增强使得每个人必须自己动手对其进行编译、测试、调试和维护。如果没人 能够 找到或者找到了也不能修正甚至根本无权使用,那么共享扩展和库同样也是毫无意义的。 幸运的是,我们有 Perl 社区的存在。它强壮且健康。它欢迎任何级别的参与者────并不只 有那些产出并共享代码的人。考虑利用无数其他 Perl 程序员的知识和经验,并同样地分享 你 自己的能力。 社区网站 Perl 的主页位于 http://www.perl.org/。这个站点提供了文档、源代码、教程、邮件列表 以及 若干重要的社区项目。如果你是 Perl 新手,那么友善的 Perl 新手邮件列表就是你提问并 得到 准确解答的地方。参见 http://beginners.perl.org/。 http://dev.perl.org/ 是一个值得一提的子域名,Perl 5、 Perl 6 (footnote: Perl 6 主站为 http://www.perl6.org/),甚至是 Perl 1 核心开发的中心站点。 Perl.com 每月发布若干关于 Perl 编程的文章和教程。其存档可追溯到20世纪。参见 http://www.perl.com/。 CPAN (CPAN) 的中心位置位于 http://www.cpan.org/,虽然有经验的用户通常在 http://search.cpan.org/ 上花去更多时间。这个承载着可重用、免费 Perl 代码的 软件发布中心 枢纽是 Perl 社区的基本组成部分。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_02.html[2011/2/21 21:22:01] PerlMonks,位于 http://perlmonks.org/,这处可敬的社区站点专门用于 Perl 编程问答及 其他 讨论。它于2009年十二月过完十周岁生日,使其成为编程语言专用 Web 社区中持续时间最长 者之一。 若干社区网站提供新闻和评论。最古老的是 http://use.perl.org/,它提供自主期刊已有 许多年 头。不少知名 Perl 黑客曾在那里执笔。http://blogs.perl.org/ 是一个用于撰写 Perl 主题博客的 新兴社区站点。 其他一些站点则集中展示了 Perl 黑客们对 Perl 的沉思,包括 http://perlsphere.net/、 http://planet.perl.org/ 以及 http://ironman.enlightenedperl.org/。后者是来自 Enlightened Perl Organization (http://enlightenedperl.org/) 倡议的一部分,以期增加 Perl 在线出版物的数 量和质量。 Perl Buzz (http://perlbuzz.com/) 定期收集并重新发布一些最有趣、有用的 Perl 新闻。 开发网站 Best Practical Solutions (http://bestpractical.com/) 为 CPAN 作者以及 Perl 5、Perl 6 的开发 维护着一份他们产品────流行的需求跟踪系统────RT的安装。每一个 CPAN 模块的 发行版有着 属于自己的 RT 队列,search.cpan.org 的模块页上有对应的链接,指向 http://rt.cpan.org/。 Perl 5 和 Perl 6 各自有一个位于 http://rt.perl.org/ 的 RT 队列。 Perl 5 Porters(也称 p5p)邮件列表是 Perl 5 开发讨论的中心点。参见 http://lists.cpan.org/showlist.cgi?name=perl5-porters。 Perl 基金会(http://www.perlfoundation.org/)为所有有关 Perl 5 的内容提供了一个 Wiki 参 见 http://www.perlfoundation.org/perl5。 许多 Perl 黑客使用 Github(http://github.com/)来存放他们的项目 (footnote: …… 包括本书 的书稿, 位于 http://github.com/chromatic/modern_perl_book/)。一个特别值得注意的 Github 页便是 Gitpan,它存有记录每一个 CPAN 模块历史的 Git 代码库。参见 http://github.com/gitpan/。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_02.html[2011/2/21 21:22:01] 活动 物理世界同样有着众多活动。Perl 社区每年举办不少会议、workshop和研讨会。特别地,社 区运营着 YAPC ──── Yet Another Perl Conference (“又一次 Perl 大会”)──── 是一 场成功的、地方性、低 成本且在多个大洲举办的会议模型。参见 http://yapc.org/。 Perl 基金会 Wiki 于 http://www.perlfoundation.org/perl5/index.cgi?perl_events 列举了其它 社区活动。 同样还有本地 Perl Monger 小组(Perl 小贩组?!)不时地碰头举办技术讲座和社会交流。参 见 http://www.pm.org/。 IRC 当 Perl 贩子们不出现在本地会议或大会上或 workshop 中,他们中的许多人会互相协助并在 IRC ────一种英特网早期发展起来、基于文本的群组聊天系统────上聊天。不少最为 出名且有用的 Perl 项目拥有自己的 IRC 频道,例如 #moose 和 #catalyst。 Perl 社区的主 IRC 服务器是 irc://irc.perl.org/。其他值得一提的频道包括 #perl-help ──── 为一般性 Perl 编程求助而设,以及 #perl-qa ──── 为测试和质量问题专用。注意 #perl 不是 一般性的求助频道,相反,它是一个为讨论其参与者想讨论的任何事情而设的通用聊天室。 (footnote: …… 而且它对提出编程基础问题的人通常不十分友好)。 CPAN Perl 5 是一门实用的语言。它帮助你完成工作。恒久务实的 Perl 社区对语言进行了 扩展,并将 成果供给全世界使用。如果你有问题需要解决,别人很可能早就为此编写 了代码并把它传到 了CPAN 上。 一个现代化的编程语言和它的库之间的界限是模糊的。是什么成就了一门语言?区区语 法吗? file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_02.html[2011/2/21 21:22:01] 还是核心库?抑或其外部库的可用性,和因在自己的项目中使用它们而带来的方 便? 不管对于其他语言你如何回答上述问题,现代化 Perl 编程大量用到 CPAN(http://www.cpan.org/)。 CPAN,或 Comprehensive Perl Archive Network,是一 项为重新分发可重用 Perl 代码 而设的上传、镜像系统。它是全世界库代码量 最大 的存档,如 果不是,加上“之一”。 CPAN 镜像模块的 发行版,意图成为可重用 Perl 代码的总集。单一发行版可能包含 一个或多 个 模块,或是自包含的 Perl 库代码。每一个发行版生存于 CPAN 上各自的 名称空间 并含有 自身的元信息。你可以对每一个发行版进行构建、安装、测试和升级。 发行版也可能互相 依 赖。为此,从 CPAN 客户端安装这些发行版会比手动安装要容易得多。 CPAN 每月 增加 数以百计的注册贡献者,和诸多发行版内数以千计的被索引的模块。 这些数 字还没有将更新算在内。2010 年八月末,search.cpan.org 报告了 8396 个上传 者,85146 个模 块, 以及 20824 个发行版。 CPAN 自身仅是一个镜像服务。作者们上传包含模块的发行版,CPAN 将它们送至镜像站点, 用户和 CPAN 客户端可以从那里下载、配置、构建、测试和安装发行版。CPAN 的成功应归 功于其简洁,还有数以千计的志愿贡献者基于 CPAN 创造更大的价值。尤其,社区标准经过 革新,能够识别一组织精良的 CPAN 发行版的属性和特点。这些标准包括: 某安装能与 CPAN 自动安装工具协作的标准; 描述每个发行版所含内容及其依赖的元信息标准; 描述发行版功能和使用条款的文档及许可标准。 额外的 CPAN 服务提供了综合自动测试和报告每个 CPAN 发行版是否遵循打包和分发要求以 及在各式平台、不同版本的 Perl 下是否工作正确。每个 CPAN 发行版在 http://rt.cpan.org/ 上 有各自的单据队列,供缺陷报告以及和其他作者合作之用。发行版的历史遗留版本、评分、 文 档注解和其他有用的信息同样可以从 CPAN 获取。所有这些都可以通过 http://search.cpan.org/ 得到。 摩登 Perl 安装版包含两个连接、搜索、下载、构建、测试和安装 CPAN 发行版的客户端, CPAN.pm 以及 CPANPLUS。他们行为相仿,使用何者只是喜好的区别。本书推荐使用 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_02.html[2011/2/21 21:22:01] CPAN.pm 仅由于其普遍性。 如果你使用 CPAN.pm 的最近版本(截止写作时,1.9402 是最新的稳定版),CPAN.pm 的 配 置无须你 做过多决定。对于任一完整的 Perl 安装来说,你可以以如下方式启动此客户端: $ cpan 安装一个发行版: $ cpan Modern::Perl 由 Eric Wilhelm 编写的优秀的 CPAN.pm 配置教程 (http://learnperl.scratchcomputing.com/tutorials/configuration/) 值得反复阅读,特别是问 题排除那一小节。 即便 CPAN 客户端是 Perl 5 发行版的核心模块,你还是需要安装例如 make 之类的标准开发 工 具还可能外加一个 C 编译器来安装任何你想安装的发行版。Windows 用户请参考 Strawberry Perl (http://strawberryperl.com/)和 Strawberry Perl Professional。Mac OS X 用户需要安 装 “开发者工具”。Unix 和类 Unix 用户请咨询你本地的系统管理员。 作为对你设置 CPAN 客户端和相应环境来构建和安装发行版的回报,你得到了可以完成任何任 务的 代码库的访问权────从数据库访问到能处理现存几乎所有网络设备的协议分析工具再 到声音和图像 处理库以及你系统上共享库的包装器。 没有了 CPAN 的摩登 Perl 仅是另一种平凡的语言。有了 CPAN 的摩登 Perl 则是惊人的。 认真的 Perl 开发人员通常会管理他们自己的 Perl 库路径甚至是完整的安装版。若干项目有助 达成这些目的。 App::cpanminus 是一个新兴的 CPAN 客户端,以速度、简洁和零配置为目标。安装它简单到: $ curl -LO http://xrl.us/cpanm $ chmod +x cpanm file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_02.html[2011/2/21 21:22:01] App::perlbrew 是一个可以安装管理多版本 Perl 的系统,允许你在版本和配置之间切换。安 装它 简单到: $ curl -LO http://xrl.us/perlbrew $ chmod +x perlbrew $ ./perlbrew install $ perldoc App::perlbrew The local::lib CPAN 发行版让你可以在自己的用户目录下安装和管理各类 CPAN 模块,而不 必将它们一股脑儿装进系统。这是既维护 CPAN 发行版又不影响其他用户的有效方法。只是安 装 过程较前两个复杂。参见 http://search.cpan.org/perldoc?local::lib 以获得更多细节。 这三个发行版项目都倾向于假设它们会用于类 Unix 环境(比如某个 GNU/Linux 版本甚至是 Mac OS X) 之下。Windows 用户可以参考 Padre 一体化下载 (http://padre.perlide.org/download.html)。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] Perl 语言 Perl 语言的语法由若干更小的部分组合而成。不像口语────说话的语气、语音、语调和直 觉 使得人们可以在略带误解或概念模糊的情况下继续沟通,计算机和源代码要求的是精确。不 需 了解语言每一项功能的每一处细节,你同样可以写出有效的 Perl 代码,但为了写好代码, 你 必须理解这些细节是如何在一起工作的。 名称 Perl 程序中,名称(或 标识符)无处不在:变量、函数、包、类甚至是文件句柄也 有名称。 这些名称都以字母或下划线开头。他们可以选择性包含任何字母、数字和下划线的 组合。当 utf8 编译命令(ç¼è¯å½ä»¤、Unicode and Strings)生效时,你可以在标识符中使用 任意合法 的 UTF-8 字符。这些都是合法的 Perl 标识符: my $name; my @_private_names; my %Names_to_Addresses; sub anAwkwardName3; # 启用 use utf8; 时 package Ingy::Döt::Net; 这些是不合法的 Perl 标识符: my $invalid name; my @3; my %~flags; package a-lisp-style-name; 这些规则仅适用于源代码中以字面形式出现的名称,就是说,直接键入 sub fetch_pie 或是 my $waffleiron。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] Perl 动态的本质使得它可以按名称引用在运行时生成或者以输入的方式提供给程序的那些 实 体。这称为 符号查找。 你可以通过这种方式获得更多的灵活性,但以牺牲安全性作 为代价。 特别地,间接调用函数或方法或是查找名称空间内的符号让你绕过 Perl 的语法 分析器 ──── Perl 中唯一强制执行语法规则的部分。请注意这样做可能会生成迷惑性的代码, 一 个哈希(åå¸)或嵌套数据结构(åµå¥æ°æ®ç»æ)(相比符号引用) 会使代码更加清晰。 变量名和印记(sigil) 变量名 的开头总有一个标明其值类型的印记。标量变量(æ é)开头是美元符 号($)。数组变 量(æ°ç»)开头是“at”符号(@)。哈希变量(åå¸) 的开头则是一个百分号(%)。 my $scalar; my @array; my %hash; 这些印记多少为变量提供了一些名称空间,使得拥有同名不同型的变量成为可能(虽然通常 具 有迷惑性): my ($bad_name, @bad_name, %bad_name); Perl 不会因此犯迷糊,但是阅读代码的人则会。 Perl 5 使用 变化印记 ──── 变量的印记可能会随使用情况的不同而不同。例如,访问数组 或哈希中的(标量)元素,印记就变成了美元符号($): my $hash_element = $hash{ $key }; my $array_element = $array[ $index ] $hash{ $key } = 'value'; $array[ $index ] = 'item'; 在最后两行中,将集合类型的标量成员用作 左值(赋值的目标,位于 = 符号的左侧)会 向 右 值(所赋之值,位于 = 符号的右侧)施加标量上下文(ä¸ä¸æ)。 类似地,访问数组或哈希中的多个元素────一个被称为 分片 的操作────使用“at”符号 (@) 作为印记并施加列表上下文: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] my @hash_elements = @hash{ @keys }; my @array_elements = @array[ @indexes ]; my %hash; @hash{ @keys } = @values; 决定一个变量────标量、数组或哈希────类型最可靠的方法是看对它进行何种操作。 标量支持所 有基本的操作,诸如字符串、数值、布尔处理。数组通过中括号支持对元素的下标 访问。哈希通 过大括号支持对元素的按键访问。 包限定名称 你偶尔会需要引用其他名称空间中的函数或变量。通常你需要通过类的 完全限定名称 来引 用 它。这些名称由双冒号(::)分隔的包名组成。就是说,My::Fine::Package 指向一个 逻辑上的函 数以及变量的集合。 虽然标准命名规则也适用于包名,照惯例,用户定义的包的名称通常以大写字母开头。Perl 核 心 为内建编译命令(ç¼è¯å½ä»¤)保留了小写包名,如 strict 和 warnings。这是由社区指南 而非 Perl 自身强制的规矩。 Perl 5 没有嵌套名称空间。Some::Package 和 Some::Package::Refinement 的关系仅仅是 存储机制上的, 并无第二重暗示指出在包关系上它们是父子还是兄弟。当 Perl 在 Some::Package::Refinement 中查找 某一符号时,它向 main:: 符号表查找代表 Some:: 名称空间的符号,接着再在其中 查找 Package:: 名称空间,如此等等。当你选择名称和组织代码时,使实体之间的 逻辑 关 系明显是你的责 任。 变量 Perl 中的 变量 是一个存放值(å¼)的地方。你可以直接处理一个值,但除了 一些琐碎的代码 之外,所有程序都在和变量打交道。变量是一个间接层,按照变量 a、 b 和 c 来解释勾股定理 较你能想象的直角三角形边长而言更为清楚。这乍看好像很 理所当然,但是要写出健壮的、良 好设计的、可测试、可组合的程序,你必须在任何可能的 地方考虑广泛性。 变量作用域 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 变量同样有可见性,取决于它们的作用域(ä½ç¨å)。你能碰到的大多数变量拥有词法作 用域 (è¯æ³ä½ç¨å)。记住,文件也有自己的词法作用域,诸如文件内部的 package 声明并不创建新 的作用域: package Store::Toy; our $discount = 0.10; package Store::Music; # $Store::Toy::discount 仍然可以通过 $discount 访问 say "Our current discount is $discount!"; 变量印记 在 Perl 5 中,无论是标量、数组或是哈希,变量声明时的印记决定了它的类型。访问该变量所 用的印记则决定了所访问的值的类型。变量上的印记因使用场合而不同,例如,你将 @values 声 明为一个数组。你可以用 $values[0] 访问它的第一个元素────一个单值。你可以通过 @values[ @indices ] 来得到其中的一部分值的列表。参见“数组”(æ°ç»)和“哈希”(åå¸) 这两个 小节以获取更多信息。 匿名变量 Perl 5 中的变量不 需要 名称,Perl 能够另行分配存储空间而不必将它们存放在词法垫板 (lexical pad) 或是符号表中。它们被称为 匿名 变量。访问到它们的唯一办法就是通过引 用 (å¼ç¨)。 变量、类型和强制转换 Perl 5 中的变量并不把类型强加给它们的值。你可以在程序的某行将某字符串存入一个变量, 然后下一行再将一个数字追加到这个变量,第三行重新将一个函数分配给一个引用(å½æ °å¼ç¨)。 这些变量的 值 是灵活的(或者说是动态的),但是 变量 的类型是静态的。标量变 量 就只能持有标量。数组就只能存放列表。哈希只能包含偶数个元素的键值对列表。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 以两种不同的措辞谈论 类型 听上去很奇怪:某类型的变量和某类型的数据,但 Perl 确实 如此 进行区分。这两种类别的类型最明确的指代方式便是 容器类型 和 值类型。 对一个变量进行赋值可能引起强制转换(强å¶è½¬æ¢)。文档中记载的获得某数组中元素个 数的 方式就是在标量上下文(ä¸ä¸æ)中对数组进行求值。因为标量变量只能存放标量, 将一 个数组赋值给标量对此操作施加标量上下文并得出该数组内元素的个数: my $count = @items; 变量类型、印记和上下文之间的关系对于正确理解 Perl 来说很重要。 值 高效的 Perl 程序依赖于对值的精确的表示方式及操作。 计算机程序包含 变量:持有 值 的容器。值是程序实际操作的数据。虽然很容易就能 描述某项 数据────你姑妈的名字和地址、你的办公室和月球上一堂高尔夫课程的距离,或者 去年你 吃的曲奇的重量────但是这些和数据格式相关的规则往往很严格。编写高效的程序 通常意 味着理解能表示该数据的最佳(最简单、最快、最紧凑或者最容易的)方式。 虽然程序的结构很大程度上依赖你用适合的变量为数据建立模型的方式,但如果不能准确地 容 下数据本身(即:值)这些变量将会是毫无意义的。 字符串 字符串 是无特定格式、无特定内容的一小片数据,对程序来说也没有其他特别的含义。它 可能 是你的名字,可能是从你的硬盘上读取的图像文件的内容,还可能是 Perl 程序本身。一 个字 符串在你赋予它意义之前对程序而言毫无意义。字符串是一块由某种形式的括号括起的固 定长 度的数据,Perl 字符串还能随着你的添加和抽减而变化。 最普通的字符串分隔符是单双引号: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] my $name = 'Donner Odinson, Bringer of Despair'; my $address = "Room 539, Bilskirnir, Valhalla"; 单引号字符串 中的字符代表它们自身的字面含义,但有两处例外。通过反斜杠转义,你可 以在 一个单引号字符串中内嵌另一个单引号字符串: my $reminder = 'Don\'t forget to escape the single quote!'; 如果反斜杠在字符串末尾,你可以用另一个反斜杠将其转义,否则 Perl 语法分析器将认为你 是在转义结尾的单引号: my $exception = 'This string ends with a backslash, not a quote: \\'; 其他所有反斜杠都是作为字面值出现在字符串中,但如果出现两个相邻的反斜杠,则前者将后 者转义。 is('Modern \ Perl', 'Modern \\ Perl', 'single quotes backslash escaping'); 双引号字符串 有着更为复杂(通常也更有用)的行为。举例来说,你可以将非打印字符编 码进 一个字符串: my $tab = "\t"; my $newline = "\n"; my $carriage = "\r"; my $formfeed = "\f"; my $backspace = "\b"; 字符串定义可以横跨多个逻辑行,如下列两个字符串是等价的: my $escaped = "two\nlines"; my $literal = "two lines"; is( $escaped, $literal, '\n and newline are equivalent' ); 你 可以 将这些字符直接输入字符串,但在视觉上通常很难将一个制表符和四个(或者两 个再 或者八个)空格区分开来。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 在双引号字符串内,你也可以将标量或数组的值 内插 入字符串,使得变量的值成为该字符串 的一部分,就好像你直接对其进行拼接操作: my $factoid = "Did you know that $name lives at $address?"; # 等同于 my $factoid = 'Did you know that ' . $name . ' lives at ' . $address . '?'; 在双引号字符串内,你可以通过 转义 插入一个字面双引号(即,在它前面加上一个反斜杠): my $quote = "\"Ouch,\", he cried. \"That hurt!\""; 如果你觉得这简直丑陋到令人发指,那你可以使用另外的 引号操作符。q 操作符进行单引号 操 作,而 qq 操作符是双引号。每种情况下,你都可以自行选择字符串分隔符。紧随操作符后的 字符决定了该字符串的开头和结尾。如果该字符是诸如大括号之类标点对中的开标点,则对应 的闭 标点为字符串结尾的分隔符。除上述之外,字符本身将作为开头和结尾的分隔符。 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 的语法让你可以以另一种方式进行多行字符串赋值: 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 自身可以是任意标识符,由 Perl 5 语法分析器用作标示结束的分隔符。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 注意,不管 heredoc 声明部分缩进多少,结束分隔符 必须 位于一行的开头: sub some_function { my $ingredients =<<'END_INGREDIENTS'; Two eggs One cup flour Two ounces butter One-quarter teaspoon salt One cup milk One drop vanilla Season to taste END_INGREDIENTS } 如果标识符以空白开始,则等数量的空白必须在结束分隔符中出现。即便你确实缩进了该标识 符, Perl 5 也 不会 从 heredoc 主体部分的每行行首移除等量的空白。 你也可以在其他上下文中使用字符串,如布尔上下文和数值上下文,它的内容将决定结果的值 (强å¶è½¬æ¢)。 Unicode and Strings Unicode 一套用于表示世界手写语言字符的系统。虽然绝大部分英语文本使用一个仅含 127 个 字符的 字符集(该字符集要求7位的存储空间,并且对于8位的字节来说正合适),但举例来 说,不相信 需要变音的人是幼稚(“naïve”,注意第三个字符)的。 Perl 5 可以按两种相关但不同的方式表示一个字符串: Unicode 字符序列 Unicode 字符集包含了绝大多数语言的书写字符及其他一些符号。每一个字符有一个对应 的 代码点, 一个在 Unicode 字符集中标识该字符的唯一数字。 八进制序列 二进制数据是一个 八进制 的序列────8位的数字,每一个都可以表示一个 0 到 255 直 接的数字。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 为什么称为 八进制数 而非 字节?将 Unicode 作为字符考虑,而不要考虑这些字符在内存中 表 示方式所占的特定长度。做出一个字符一个字节的假设只会带给你无尽的 Unicode 悲哀。 Unicode 字符串和二进制字符串看上去很像。它们都有 length() 函数,你可以对它们进行标准 字符串操作,如:拼接、分片和正则表达式处理。任何非纯二进制的字符串都是文本数据,它 应是一 个 Unicode 字符序列。 虽都是八进制序列,数据还是会因操作系统对磁盘数据的表示方式,以及其他用户、网络等原 因而不同, 因此 Perl 无从得知读取的某块数据究竟是图像文件、文本文档还是其他东西。默 认地,Perl 将所有 读入的数据按八进制序列处理。给字符串内容赋予额外的含义是你的责任。 字符编码 Unicode 字符串是一个表示一系列字符的八进制序列。Unicode 编码 将八进制序列映射到字符 上。 一些编码方式,如 UTF-8,可以编码 Unicode 字符集中的所有字符。其他编码方式只能 表示 Unicode 字符的一个子集。例如,ASCII 编码纯英语文本且不含重音字符,Latin-1 可以 表示使用拉丁字母表的 大部分语言的文本。 如果你总是以正确的方式编解码程序输入输出,你将避开许多麻烦。 文件句柄中的 Unicode Unicode 输入的一个来源就是文件句柄(æ件)。如果你告知 Perl 某特定的文件句柄对已编 码的文本进行操作,Perl 能够将数据自动转换为 Unicde 字符串。为实现此功能,请向内置 open 操作符的模式添加一个 IO 层。 IO 层 包装了输入输出并对数据进行某种形式的转换。在此 种情况下,:utf8 层将对 UTF-8 数据进行解码: use autodie; open my $fh, '<:utf8', $textfile; my $unicode_string = <$fh>; 你也可以用 binmode 对已经打开的文件句柄进行修改,无论是输入还是输出: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] binmode $fh, ':utf8'; my $unicode_string = <$fh>; binmode STDOUT, ':utf8'; say $unicode_string; 不启用 utf8 模式时,向某文件句柄打印 Unicode 字符串会得到一个警告(Wide character in %s),因为 文件包含的是八进制数据而非 Unicode 字符。 数据中的 Unicode Encode 核心模块提供了一个名为 decode() 的函数来将已知格式标量数据转换成一个 Unicode 字符 串。例如, 如果你有的是 UTF-8 数据: my $string = decode('utf8', $data); 对应的 encode() 函数将 Perl 内部编码转换为所需输出编码: my $latin1 = encode('iso-8859-1', $string); 程序中的 Unicode 在你的程序中包含 Unicode 字符有三种方式。最简易的方法是利用 utf8 编译命令 (ç¼è¯å½ä»¤),它告诉 Perl 语法分析器将后续源代码编码解释为 UTF-8。这允许你在字符串 和标识符中使用 Unicode 字符: use utf8; sub £_to_¥ { ... } my $pounds = £_to_¥('1000£'); 要 编写 这些代码,你的文本编辑器必须理解 UTF-8 并且你必须按正确的编码保存该文件。 你也可以使用 Unicode 转义序列来表示字符编码。\x{} 语法代表一个单独的字符。将该字符的 Unicode 数字的十六进制表示放入大括号内: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] my $escaped_thorn = "\x{00FE}"; 注意这些转义序列仅在双引号字符串内内插。 一些 Unicode 字符有自己的名称。虽然这相当详细,但是比起 Unicode 数字来说更加易读。你 必须使用 charnames 编译命令来启用它。用 \N{} 转义语法来指代它: use charnames ':full'; use Test::More tests => 1; my $escaped_thorn = "\x{00FE}"; my $named_thorn = "\N{LATIN SMALL LETTER THORN}"; is( $escaped_thorn, $named_thorn, 'Thorn equivalence check' ); 你可以在正则表达式内使用 \x{} 和 \N{} 形式,就像在他处合理地使用一个字符或字符串。 隐式转换 Perl 中的大多数 Unicode 问题因字符串既可以是八进制数据序列也可以是字符序列而起。Perl 允许你 通过隐式转换结合使用这些类型。当这些转换出错时,它们不会错得那么 明显。 当 Perl 拼接一个八进制序列和一个 Unicode 字符序列时,它隐式地将八进制序列按 Latin-1 编 码方式 解码。结果字符串包含 Unicode 字符。当你打印 Unicode 字符时,Perl 用 UTF-8 编码 该字符串,因为 Latin-1 无法表示完整的 Unicode 字符集。 这个不对称性可导致 Unicode 字符串在输出时编码为 UTF-8 以及在输入时候解码为 Latin-1。 更糟糕的是,当文本只包含除重音外的英文字符时,这个 bug 会隐藏起来────因为这两种 编码对每一个 字符的表示方式一致。 my $hello = "Hello, "; my $greeting = $hello . $name; 如果 $name 包含例如 Alice 之类的英文名字,你决不会察觉任何问题,因为 Latin-1 表示和 UTF- 8 表示是一样的。 。串字 面值、程序又不能按任一编码解码时发生 无法 识别的八进制序列。这种情况可能在用户以 UTF-8 编码输入姓名而问候语是 Latin-1 字符 不共用 编码方式,拼接操作将把 UTF-8 数据追加到 Latin-1 数据之后,生成两种编码方式 都 编码方式相同的八进制值────例如,全为 Lation-1,则拼接操作将正常执行。如果两者并 如果两个字符串都是八进制流,Perl 会将它们拼接为一个新的八进制字符串。如果两者的都为 如果 $hello 和 $name 都是 Unicode 字符串,拼接操作会产生另一个 Unicode 字符串。 此字符串字面值包含 Unicode 字符。 ;" ,שלום" = my $hello use utf8; 非 ASCII 字符串字面值,通过 utf8 或 encoding 编译命令指定: 此字符串字面值包含八进制数据。 my $hello = "¡Hola, "; Latin-1 字符串字面值,无明确编码,例如: 此字符串字面值包含八进制数据。 my $hello = "Hello, "; ASCII 字符串字面值 字符串字面值有如下可能的情形: $name 五个代表它们各自 Unicode 字符的八进制 UTF-8。 $name 四个代表它们各自 Unicode 字符的八进制 Latin-1; $name 四个 Unicode 字符; 又如果,$name 包含类似 José 的名字,则 $name 可能包含下列几种可能的值: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 如果仅有一值为 UTF-8 字符串,Perl 将其他部分按 Latin-1 解码。如果这不是正确的编码方 式, 作为结果的 UTF-8 字符串也会是错误的。例如,用户输入的是 UTF-8 数据并且此字符串 字面值是 一个 Unicode 字符串,那么这个姓名将被错误地解码成五个 Unicode 字符,José 而 非 José,因为 UTF-8 数据在作为 Latin-1 解码时有着不同的含义。 参见 perldoc perluniintro 以获取一份有关如下话题更为详细的解释:Unicode、编码、以及如 何 在 Unicode 世界中正确处理输入输出数据。 数字 Perl 同样提供对数字的支持,无论是整数还是浮点数。它们可以按科学计数法、二进制、 八进 制和十六进制形式给出: my $integer = 42; my $float = 0.007; my $sci_float = 1.02e14; my $binary = 0b101010; my $octal = 052; my $hex = 0x20; 加粗的字符是二进制、八进制、十六进制对应的数值前缀。注意开头的零总是意味着八进制 模 式,这偶尔会引发意想不到的迷惑。 即使你可以在 Perl 5 中明确且精准地表示一个浮点值,Perl 5 内部将它们存储为二进制格式。 某些具体情况下无法精确地比较浮点值,详情请参考 perldoc perlnumber。 你无法用逗号按千分隔数值字面值,因为语法分析器会将逗号解释为逗号操作符。为此你 可以 在数字内使用下划线。语法分析器会将其作为不可见的字符对待,但你程序的读者不会。下面 这些 是等价的: my $billion = 1000000000; my $billion = 1_000_000_000; my $billion = 10_0_00_00_0_0_0; 请考虑使用最易读的方式。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 由于强制转换(强å¶è½¬æ¢),Perl 程序员几乎不用担心将外部读取的文本转换为数字一 事。在 数值上下文中 Perl 会将所有看上去像数字的东西统一作为数字对待。虽然它几乎每每 都能正确处理 此类事项,有时知道一下某物看上去是不是像数字也不赖。核心模块 Scalar::Util 包含一个名 叫 looks_like_number 的函数,如果 Perl 将某物做数值考虑,它就会返回一个为真的 值。 来自 CPAN 的 Regexp::Common 模块同样提供了若干经过良好测试的正则表达式来识别数值的有 效 类型 (全数字、整数、浮点数值)。 Undef Perl 5 中由 undef 代表所有未赋值、未定义和未知的值。已声明但未定义的标量包含 undef: my $name = undef; # 多余的赋值 my $rank; # 同样包含 undef 在布尔上下文中对 undef 求值得到假。将 undef 内插入一个字符串────或者对其在 字符串上 下文中求值────将产生一个 uninitialized value 的警告: my $undefined; my $defined = $undefined . '... and so forth'; ……产生: Use of uninitialized value $undefined in concatenation (.) or string... 空列表 当用于一个赋值操作的右手边时,() 结构代表一个空列表。当在标量上下文中求值时得到 undef。 在列表上下文中,它在效果上就是一个空列表。 当用于一个赋值操作的左手边时,() 规定了列表上下文。不用临时变量,计算一个表达式在列 表上下 文中返回结果的个数,你可以使用如下惯用语(æ¯ç¨è¯): my $count = () = get_all_clown_hats(); file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 由于赋值操作符的右结合性(ç»åæ§),Perl 先对第二个赋值操作通过在列表上下文中调用 get_all_clown_hats() 求值,这会产生一个列表。 对空列表的赋值丢弃了该列表全部的值,但这个赋值在标量上下文中发生,将求得赋值操作右 手边的元素 的个数。结果就是 $count 包含了从 get_all_clown_hats() 返回的列表中元素的个数。 目前你不用理解这段代码中所有的隐含内容,但是它确确实实地展示了 Perl 设计中区区几个基 础功能的 组合就能产生如此有趣且有用的行为。 列表 列表是一个由逗号分隔、包含一个或多个表达式的组。 列表可能在源代码中逐字出现: my @first_fibs = (1, 1, 2, 3, 5, 8, 13, 21); ……或作为赋值的目标: my ($package, $filename, $line) = caller(); ……或作为一系列表达式: say name(), ' => ', age(); 值得一提的是 创建 列表并不需要括号,这些例子中(括号)出现的地方是为了让表达式成组出 现,以 改变这些表达式的 优先级(ä¼å级)。 你可以用范围操作符以一种紧凑的方式创建一个字面值列表: my @chars = 'a' .. 'z'; my @count = 13 .. 27; ……同时,你也可以用 qw() 操作符以空白符分隔一字符串字面值,并创建一个字符串列表: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] my @stooges = qw( Larry Curly Moe Shemp Joey Kenny ); 如果 qw() 包含逗号或注释符(#),Perl 会产生一条警告。不仅因为这些字符在 qw() 中出现的 机会很少, 而且它们的出现往往意味着疏忽。 列表可以(通常也)作为一个表达式的结果,但是这些列表不以字面形式在源代码中出现。 在 Perl 中,列表和数组概念之间不可以交换。列表是值而数组是容器。你可以在一个数组中存 放一个列表, 也可以将一个数组强转为列表,但它们是不同的实体。举例来说,按下标访问列 表通常在列表上下文中出现。 按下标访问数组通常在标量上下文(为获取单个元素)或列表上 下文中出现(为数组分片): # enable say and other features (see preface) use Modern::Perl; # you do not need to understand this all sub context { my $context = wantarray(); say defined $context ? $context ? 'list' : 'scalar' : 'void'; return 0; } my @list_slice = (1, 2, 3)[context()]; my @array_slice = @list_slice[context()]; my $array_index = $array_slice[context()]; # say imposes list context say context(); # void context is obvious context() 控制流程 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] Perl 的基本 控制流程 相当直截了当。程序执行过程起始于程序开头(被执行文件的第一行) 然后一直到结尾: say 'At start'; say 'In middle'; say 'At end'; 大多数程序需要更为复杂的控制流程。Perl 的 控制流程语句 改变了程序执行的顺序──── 程序 中接下来要发生的────依赖于任意复杂的表达式的值。 分支语句 if 语句对一条件表达式求值并仅在此条件表达式的值为真时执行相关动作: say 'Hello, Bob!' if $name eq 'Bob'; 这种后缀形式在表达式较简单时很有用。代码块形式则将多个表达式组合成单一单元: if ($name eq 'Bob') { say 'Hello, Bob!'; found_bob(); } 虽然代码块形式要求条件两边有括号,但后缀形式则相反。条件表达式也可以相对复杂: if ($name eq 'Bob' && not greeted_bob()) { say 'Hello, Bob!'; found_bob(); } ……虽然在此种情况下,采用后缀形式的括号形式可以使其稍显清晰 (footnote: 同时也会因此 对是否使用后缀形式产生争论): greet_bob() if ($name eq 'Bob' && not greeted_bob()); unless 语句是 if 的否定形式。Perl 在条件表达式的值为 假 时执行 所需操作: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] say "You're no Bob!" unless $name eq 'Bob'; 和 if 类似,unless 也有代码块形式。不同于 if,unless 的代码块形式相比其 后缀形式来说很少 见: unless (is_leap_year() and is_full_moon()) { frolic(); gambol(); } unless 很适合后缀条件,特别是函数中的参数验证(åç¼åæ°éªè¯): sub frolic { return unless @_; for my $chant (@_) { ... } } 条件一多 unless 就会变得难以阅读,这便是它很少以代码块形式出现的原因之一。 if 以及 unless 都可以搭配 else 语句,它提供了当条件表达式的值不为真(if) 或假(unless)时 运行的代码: if ($name eq 'Bob') { say 'Hi, Bob!'; greet_user(); } else { say "I don't know you."; shun_user(); } else 代码块允许你按不同于自身的方式重写 if 和 unless 条件语句: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] unless ($name eq 'Bob') { say "I don't know you."; shun_user(); } else { say 'Hi, Bob!'; greet_user(); } 如果你大声读出前面这个例子,你会发现一例不合适的伪代码用词:“除非该名字为 Bob,做这 件事。否则 做那件。”隐含的双重否定可能会很迷惑。Perl 提供了 if 和 unless 使得你可以按最 为自然、 最为上口的方式对条件语句进行组织。同样地,你可以根据比较操作符在肯定和否定 断言之间进行选择: if ($name ne 'Bob') { say "I don't know you."; shun_user(); } else { say 'Hi, Bob!'; greet_user(); } 因 else 代码块的出现而隐含着的双重否定示意此代码组织方式是不合理的。 在单个 else 之前、if 代码块之后可以跟一个或多个 elsif 语句。elsif 代码块的使用数量不限, 但你不可以改变块出现的顺序: if ($name eq 'Bob') { say 'Hi, Bob!'; greet_user(); } elsif ($name eq 'Jim') { say 'Hi, Jim!'; greet_user(); } else { file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] say "You're not my uncle."; shun_user(); } 你也可以在 unless 链内使用 elsif 块,但是结果代码会不那么清晰。不存在 elseunless 的说法。 同样也没有 else if 这一语法结构 (footnote: Larry 出于美学原因以及 Ada 编程语言的现有技术 选择了 elsif), 因此,这段代码含有语法错误: if ($name eq 'Rick') { say 'Hi, cousin!'; } # warning; syntax error else if ($name eq 'Kristen') { say 'Hi, cousin-in-law!'; } 三元条件操作符 三元条件 操作符提供了另一种方法来控制流程。它先将条件表达式求值,并由此对两不同结果 之一求值: my $time_suffix = after_noon($time) ? 'morning' : 'afternoon'; 条件表达式位于问号(?)之前,冒号(:)分隔两种选择。这两个选择分支可以是字面值或者 (带括号) 任意复杂的表达式,包括其他的三元条件表达式(以可读性为代价)。 一个有趣但晦涩的惯用语便是使用三元条件语句在两个候选 变量 之间做出选择,而非仅仅对 值: push @{ rand() > 0.5 ? \@red_team : \@blue_team }, Player->new(); 再次提醒,请对清晰和简略各自带来的利益进行权衡。 短路求值 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 当遇到由多重待求值的表达式组成的复杂表达式时,Perl 将做出名为 短路求值 的行为。如果 Perl 可以决定一个复杂的表达式整体的值是真还是假,那么它不会对后续子表达式求值。用例 子说明会更加明白: # see preface use Test::More 'no_plan'; say "Both true!" if ok(1, 'first subexpression') && ok(1, 'second subexpression'); done_testing(); ok() 的返回值(æµè¯)是对第一个参数求值得到的。 这个例子打印出: ok 1 - first subexpression ok 2 - second subexpression Both true! 当第一个子表达式────对 ok 的第一次调用────求得真值时,Perl 必须对第二个字表达 式求值。 当第一个表达式求得假值时,整个表达式不为真,因此无需检查后续子表达式。 say "Both true!" if ok(0, 'first subexpression') && ok(1, 'second subexpression'); 这个例子打印出: not ok 1 - first subexpression 虽然第二个子表达式显然为真,Perl 绝不对其求值。对于“一真即真”复杂条件表达式来说, 其 逻辑也是相似的: say "Either true!" if ok(1, 'first subexpression') || ok(1, 'second subexpression'); 这个例子打印出: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] ok 1 - first subexpression Either true! 再次,第一个子表达式为真,Perl 可以避免对第二个子表达式求值。如果第一个子表达式为 假, 则对第二个子表达式求值的结果将决定整个表达式的真假。 除了可以让你避免潜在的昂贵计算,短路求值还可以帮助你避免错误和警告: if (exists $barbeque{pork} and $barbeque{pork} eq 'shoulder') { ... } 条件语句相关的上下文 这些条件语句────if、unless,以及三元条件表达式────总是在布尔上下文(ä¸ä¸æ) 中对一个表达式进行求值。由于 eq、==、ne 和 != 这类操作符在求值时总产生布尔结果,Perl 将 其他表达式的求值结果────包括变量和值────强制转换为布尔形式。对空哈希和数组 求值得假。 Perl 5 没有单一的真值、也没有单一的假值。任何求值为 0 的数字为假。包括 0、0.0、0e0、 0x0 等等。空字符串('')以及 "0" 求值得假,但是字符串如 "0.0"、"0e0" 等则不然。 惯用语 "0 but true" 在数值上下文中求值得 0,但其值因字符串内容而为真。空列表和 undef 都 为假。空数组 和哈希在标量上下文中返回 0,因此它们在布尔上下文中为假。 带有单个元素的数组────即便该元素是 undef────在布尔上下文中求值得真,含任何元 素的哈希──── 即使是一键一值两个 undef────在布尔上下文中也得真。 CPAN 模块 Want 允许你在你的函数内检测布尔上下文。核心编译命令 overloading(éè½½、 ç¼è¯å½ä»¤)允许你指定自己的数据类型在布尔上下文中求得的值。 循环语句 Perl 也提供了供循环和迭代使用的若干语句。 foreach 风格的循环对一表达式求值而产生一个列表,接着执行某语句或代码块直到它消耗完该 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 列表: foreach (1 .. 10) { say "$_ * $_ = ", $_ * $_; } 这个例子使用范围操作符产生了一个包括一到十在内的整数列表。foreach 语句在其之上循环, 依次设置话题变量 ($_,参见 é»è®¤æ éåé)。Perl 针对每个整数执行代码块并打印该整数的 平方。 Perl 将 foreach 和 for 可互换地看待。循环余下部分的语句决定了循环的行为。尽管有经验的 Perl 程序员 倾向于用 foreach 循环来指代自动迭代循环,你可以在任何用到 foreach 的地方安全 地用 for 替代。 如同 if 及 unless,for 循环也有一个后缀形式: say "$_ * $_ = ", $_ * $_ for 1 .. 10; 有关清晰、简略的建议同样适用于此。 你可以提供一个用于赋值变量以代替话题变量: for my $i (1 .. 10) { say "$i * $i = ", $i * $i; } 如果你这样做了,Perl 将不会将话题变量($_)赋值为迭代值。注意变量 $i 的作用域仅限于循 环 内部。 如果你在循环外定义了词法变量 $i,它的值将不受循环内变量的影响: my $i = 'cow'; for my $i (1 .. 10) { say "$i * $i = ", $i * $i; } is( $i, 'cow', 'Lexical variable not overwritten in outer scope' ); file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 甚至你不将该迭代变量重新声明为词法变量,它仍会被局部化: my $i = 'horse'; for $i (1 .. 10) { say "$i * $i = ", $i * $i; } is( $i, 'horse', 'Lexical variable still not overwritten in outer scope' ); 迭代和别名 for 循环将迭代变量 别名化 为迭代中的值,以便你可以直接在迭代过程中修改: my @nums = 1 .. 10; $_ **= 2 for @nums; is( $nums[0], 1, '1 * 1 is 1' ); is( $nums[1], 4, '2 * 2 is 4' ); ... is( $nums[9], 100, '10 * 10 is 100' ); 代码块形式的 foreach 循环同样也会别名化: for my $num (@nums) { $num **= 2; } ……用话题变量迭代时也会: for (@nums) { $_ **= 2; } 你不能通过别名修改 常量 值,然而: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] for (qw( Huex Dewex Louie )) { $_++; say; } ……会抛出有关修改只读值的异常。无论如何,这样并没有什么必要。 你偶尔会碰到 for 搭配别名化为 $_ 的单个标量的用法: for ($user_input) { s/(\w)/\\$1/g; # 跳过非文字字符 s/^\s*|\s$/g; # 修剪空白 } 迭代和作用范围 迭代器的作用范围连同话题变量一起是为常见的困惑之源。这种情况下,some_function() 有意修 改 $_。如果 some_function() 调用了其他未经明确局部化 $_ 而对其加以修改的代码,则在 @values 中的迭代值将改变。 调试这样的问题很麻烦: for (@values) { some_function(); } sub some_function { s/foo/bar/; } 如果你 必须 使用 $_ 而非其他具名变量,请使用 my $_ 将话题变量词法化: sub some_function_called_later { # 曾经是 $_ = shift; my $_ = shift; s/foo/bar/; s/baz/quux/; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] return $_; } 使用具名变量同时也避免了通过 $_ 别名化的行为。 C 语言风格的 For 循环 C 语言风格的 for 循环 允许程序员手动控制迭代: for (my $i = 0; $i <= 10; $i += 2) { say "$i * $i = ", $i * $i; } 你必须手动给迭代变量赋值,因为现在已经不会自动给话题变量赋值。因此也没有别名化行 为。虽然任何 已声明的词法变量的作用范围是代码块主体部分。一个 未 在迭代控制部分中明 确声明的变量的内容 将 被覆盖: my $i = 'pig'; for ($i = 0; $i <= 10; $i += 2) { say "$i * $i = ", $i * $i; } isnt( $i, 'pig', '$i overwritten with a number' ); 此循环的循环结构中有三个子表达式。第一个子表达式是初始化部分,它在第一次执行循环体 前执行一 次。第二个子表达式是条件比较子表达式。Perl 每次在循环体执行之前对其求值。当 此子表达式值为真时 循环继续。当此子表达式为假时,循环结束。最后一个子表达式在每次完 成循环体时执行。 一个例子会使问题更加清晰: # 在外部声明以避免在条件语句中出现声明 my $i; for ( # 循环初始化子表达式 say 'Initializing' and $i = 0; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] # 条件比较子表达式 say "Iteration: $i" and $i < 10; # 迭代结尾子表达式 say 'Incrementing $i' and $i++ ) { say "$i * $i = ", $i * $i; } 请注意迭代结尾子表达式后分号的省略以及低优先级 and 的使用。这种语法真是令人惊讶地考 究。当可能时, 尽量使用 foreach 风格的循环来代替 for 循环。 所有这个三个表达式都是可选的。你可以这样编写无限循环: for (;;) { ... } While 和 Until while 循环会一直执行直到循环条件得出布尔假值。一个无限循环可以按如下方式清晰地写出: while (1) { ... } 这意味着 while 循环的迭代结束条件和 foreach 循环中的有所不同,即对表达式自身求值并不产 生任何 副作用。如果 @values 拥有一个或多个元素,下列代码也是一个无限循环: while (@values) { say $values[0]; } 为避免此类无限 while 循环,你必须通过每次迭代修改 @values 数组以对其进行 析构更新: while (my $value = shift @values) { say $value; } until 循环刚好和 while 进行相反的测试。迭代会在循环条件表达式为假时继续: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] until ($finished_running) { ... } while 循环的典型用法是从一个文件句柄中迭代读取输入: use autodie; open my $fh, '<', $file; while (<$fh>) { ... } Perl 5 对此 while 循环进行解释时,就好像你编写了如下代码: while (defined($_ = <$fh>)) { ... } 不用明确写出 defined,任何从该文件句柄读出、且求值得假────空行或只包含字符 0────的行会结束整个循 环。当完成从文件中读取行的任务后,readline(<>)操作符才返回 一个未定义的值。 一个常见的错误就是忘记从读入的每一行移除行结束符,使用 chomp 关键字可以完成这项工作。 while 和 until 都可以写成后缀形式。Perl 5 中最简单的无限循环是: 1 while 1; 任何单个表达式对后缀式的 while 和 until 来说都是合适的,例如来自八十年代早期 8 位计算机 的 经典 "Hello, world!": print "Hello, world! " while 1; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 无限循环可能看上去有点笨拙,但它们实际上很有用。一个简单的 GUI 程序或网络服务器事件 循环可以是: $server->dispatch_results() until $should_shutdown; 对于更加复杂的表达式,使用 do 代码块: do { say 'What is your name?'; my $name = <>; chomp $name; say "Hello, $name!" if $name; } until (eof); 出于语法分析的目的,虽然可以包含若干表达式,do 代码块本身却是一个单一的表达式。不像 while 循环 的代码块形式,搭配后缀式 while 或 until 的 do 代码块至少会执行它的主体一次。这 个结构相比其他 循环形式来说较为少见,但在功能上毫不逊色。 循环中的循环 你可以在循环中嵌套其他循环: for my $suit (@suits) { for my $values (@card_values) { ... } } 在这种情况下,明确地声明具名变量对可维护性来说必不可少。就迭代变量的作用范围而言, 当使用话题变量时, 引发混淆的潜在可能太大了。 嵌套使用 foreach 和 while 时,一个常见的错误是:很容易就可以用 while 循环将某文件句柄整 得 筋疲力尽。 use autodie; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] open my $fh, '<', $some_file; for my $prefix (@prefixes) { # 不要这样用,很可能是有问题的代码 while (<$fh>) { say $prefix, $_; } } 在 for 循环之外打开文件句柄使得 for 循环在两次迭代间放着文件位置不动。在第二次迭代中, while 循环无事可做且不会执行循环体。为解决此问题,你可以在 for 循环内重新打开文件(理 解 上很简单,但是是对系统资源的不恰当使用),将整个文件吸入内存(文件太大可能就不 行),或者在每次 迭代时用 seek 使文件句柄回到文件开头(通常被忽视的选择): use autodie; open my $fh, '<', $some_file; for my $prefix (@prefixes) { while (<$fh>) { say $prefix, $_; } seek $fh, 0, 0; } 循环控制 有时你需要在用尽迭代条件前跳出循环。Perl 5 的标准控制机制────异常和 return──── 可以实现这个目的,但 你也可以使用 循环控制 语句。 next 语句在下一个迭代点重新开始循环。当你已经完成本次迭代的所有任务后可以使用它。要 循环读取文件中的每 一行并跳过所有看上去像注释的内容,即以 # 开始的行,你可能会这样 写: while (<$fh>) { next if /\A#/; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] ... } last 语句立即结束循环。想在遇到结束分隔符后结束文件处理,你可能会这样写: while (<$fh>) { next if /\A#/; last if /\A__END__/ ... } redo 语句不对条件语句再次求值并重新开始本次迭代。这在少数情况下很有用:比如你想当即 修改你读到的行,接着从头 开始处理而不想影响其他的行时。例如,你可以实现一个把所有以 反斜杠结尾的行拼接起来的笨拙的文件分析器: while (my $line = <$fh>) { chomp $line; # 匹配行尾的反斜杠 if ($line =~ s{\\$}{}) { $line .= <$fh>; redo; } ... } ……虽然这是一个做作的例子。 嵌套循环可能会使这些循环控制语句的使用变得模棱两可。在这些情况下,循环标签 可以消除 歧义: OUTER: while (<$fh>) { chomp; INNER: for my $prefix (@prefixes) { file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] next OUTER unless $prefix; say "$prefix: $_"; } } 如果你发现你自己正编写需要标签来控制流程的嵌套循环,考虑简化你的代码:也许将内层循 环压缩为 函数会更清晰。 Continue continue 语法结构的行为类似于 for 循环的第三个子表达式。Perl 会在循环的每一次迭代过程执 行 该代码块,即便你使用 next 来跳出某次迭代。你可以把它和 while、until、with 或 for 循环 搭 配使用。continue 的例子比较罕见,但在想保证每一次迭代某事都会发生一次并无需顾及循环如 何结束时很有用: while ($i < 10 ) { next unless $i % 2; say $i; } continue { say 'Continuing...'; $i++; } Given/When given 语法结构是 Perl 5.10 的新特性。它将某表达式的值赋给话题变量并引入一个代码块: given ($name) { ... } 不像 for,它不对某集合类型进行迭代。它在标量上下文中求值,并总是赋值给话题变量: given (my $username = find_user()) { file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] is( $username, $_, 'topic assignment happens automatically' ); } given 同时对话题变量进行局部化来防止无意的修改: given ('mouse') { say; mouse_to_man( $_ ); say; } sub mouse_to_man { $_ = shift; s/mouse/man/; } 单独出现时,这个功能看上去没什么用。但 when 和其他功能组合时候就会非常有用。使用 given 来 话题化 某个值。 在关联的代码块之内,多个 when 语句使用 智能匹配 语义逐表达式匹配话 题。因此你可以这样编写石头剪刀布游戏: my @options = ( \&rock, \&paper, \&scissors ); do { say "Rock, Paper, Scissors! Pick one: "; chomp( my $user = ); my $computer_match = $options[ rand @options ]; $computer_match->( lc( $user ) ); } until (eof); sub rock { print "I chose rock. "; given (shift) { when (/paper/) { say 'You win!' }; when (/rock/) { say 'We tie!' }; when (/scissors/) { say 'I win!' }; default { say "I don't understand your move" }; } } sub paper file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] { print "I chose paper. "; given (shift) { when (/paper/) { say 'We tie!' }; when (/rock/) { say 'I win!' }; when (/scissors/) { say 'You win!' }; default { say "I don't understand your move" }; } } sub scissors { print "I chose scissors. "; given (shift) { when (/paper/) { say 'I win!' }; when (/rock/) { say 'You win!' }; when (/scissors/) { say 'We tie!' }; default { say "I don't understand your move" }; } } 当无一条件匹配时,Perl 执行 default 规则。 CPAN 模块 MooseX::MultiMethods 允许利用其他技术来减少这段代码。 when 语法结构甚至更为强大。它可以匹配(æºè½å¹é)其他表达式类型诸如标量、集合、 引 用、任意比较表达式甚至是代码引用。 Tailcalls 某函数内最后一个表达式是对其他函数的调用时,这种情况称为 尾部调用────外层函数的 返回值 就是内层函数的返回值: sub log_and_greet_person { my $name = shift; log( "Greeting $name" ); return greet_person( $name ); } file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 此例中,greet_person() 直接返回给 log_and_greet_person() 的调用者要比先返回 log_and_greet_person() 再立刻 从 log_and_greet_person() 中返回要来得高效。直接将 greet_person() 返回给 log_and_greet_person() 的调用者,是一种叫做 尾部调用优化 的优化手段。 Perl 5 不会自动检测它是否应该进行这种优化。 深度递归的代码(éå½),特别是互相递归的代码,会迅速消耗大量内存。使用尾部调用会减 少有关内部 控制流程记录的内存消耗,这使得某些昂贵的算法变得可行。 标量 Perl 5 的基本数据类型是 标量,它表示单个、离散的值。这个值可以是字符串、整数、浮点 数、 文件句柄或者引用────但它总是单个值。标量值和标量上下文有着深层次的联系,向 标量赋值提供了 标量上下文。 标量可以是词法、包或全局(å¨å±åé)变量。你可能只希望声明词法或包变量。标量变量的名 称 必须符合 å称 中提及的指导条款。标量变量开头总是使用美元符号($)作为印记(åéå°è® °)。 它的反面不 全 正确,标量印记可以应用对集合变量的操作以决定通过此操作可以访问到的数量 型。 (æ°ç»、åå¸) 标量和类型 Perl 5 标量并非静态类型。一个标量可以包含任何标量类型的值而不必进行特殊的转换,并且 变量内值 的类型可以改变。下列代码是合法的: my $value; $value = 123.456; $value = 77; $value = "I am Chuck's big toe."; $value = Store::IceCream->new(); file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 虽然这是 合法的,它可能会导致混乱。请给你的变量选择具有描述性、唯一的名字以避免这种 迷惑。 标量求值的类型上下文会可能会使 Perl 对该标量的值进行强制转换(强å¶è½¬æ¢)。例如, 你可以把一个 标量的内容作为字符串对待,即使你不明确地将其赋值给一个字符串。 my $zip_code = 97006; my $city_state_zip = 'Beaverton, Oregon' . ' ' . $zip_code; 你也可以对字符串进行数学操作: my $call_sign = 'KBMIU'; my $next_sign = $call_sign++; # 也可以这样 $next_sign = ++$call_sign; # 但是 不可以 这样写: $next_sign = $call_sign + 1; 这个神奇的字符串自增操作并没有对应的神奇字符串自减操作。你不能通过编写 $call_sign-- 得 到之前的字符串。 字符串自增操作针对字符集和大小写将 a 变为 b 将 z 变为 aa。同样的,ZZ9 变为 AA0,但是 ZZ09 就会成为 ZZ10─────数字会照常进位,但不会延续到字母部分。 在字符串上下文中对引用(å¼ç¨)求值会得到一个字符串,而在数值上下文对引用求值会 得到 一个数字。两个操作都不对引用做出修改,但你不能从得到的字符串或数字中重新创建该引 用。 my $authors = [qw( Pratchett Vinge Conway )]; my $stringy_ref = '' . $authors; my $numeric_ref = 0 + $authors; $authors 仍旧可以作为引用使用,但是 $stringy_ref 是一个和该引用没有关联的字符串, $numeric_ref 是一个和该引用没有关联的数字。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 使得所有这些强制转换和操作成为可能的是,因为 Perl 5 标量可以包含数值部分以及字符串部 分。 Perl 5 中表示标量的内部数据结构有一个数值槽和一个字符串槽。在数值上下文中访问一 个字符串 最终产生一个拥有字符串和数值槽两者的标量。核心模块 Scalar::Util 中的 dualvar() 函 数允许你对一个标量的这两个值进行改动。相似地,模块中的 looks_like_number() 函数在所 提供 标量值被 Perl 5 认作数字时返回真。 标量没有单独针对布尔值的槽。在布尔上下文中,空字符串('')和 '0' 为假。其他所有字 符 串为真。在布尔上下文中,求值得零的数字(0、0.0 和 0e0)为假,其他所有数字为真。 注意 字符串 '0.0' 和 '0e0' 是真值,这是 Perl 5 将看上去像数字的事物和数字区别对 待的地方之 一。 另有一个总是为假的值:undef。这是未定义变量和它本身的值。 数组 Perl 5 数组 是存放零个或多个标量的数据结构。它们是 一等 数据结构,意味着 Perl 5 在语言 级别提供了 单独的数据类型。数组支持下标访问,即,你可以按整数下标访问某数组中每一个 独立的成员。 @ 印记是数组的标识。要想声明一个数组: my @items; 数组元素 在 Perl 5 中 访问 数组中独立的元素要求标量印记。Perl 5(和你)可以识别出 $cats[0] 指的是 @cats 数组 而不必理会印记的改动,因为中括号([])始终标明对集合变量的下标访问。用简单 的话来说,意思就是“通过一个整数 在一组东西里查找某件东西”。 数组中第一个元素的下标为零: # @cats 包含一列 Cat(猫)对象 my $first_cat = $cats[0]; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 数组中最后一个元素的下标取决于数组中元素的个数。标量上下文中的数组(因标量赋值、字 符串拼接、加法或布尔上下文) 求值得出数组中所含元素的个数: # 标量赋值 my $num_cats = @cats; # 字符串拼接 say 'I have ' . @cats . ' cats!'; # 加法 my $num_animals = @cats + @dogs + @fish; # 布尔上下文 say 'Yep, a cat owner!' if @cats; 如果你需要得到最后一个元素指定的下标,将数组元素的个数减去一即可(因为数组下标从零 开始): my $first_index = 0; my $last_index = @cats - 1; say 'My first cat has an index of $first_index, ' . 'and my last cat has an index of $last_index.' 你也可以使用数组的特殊变量形式来找出最后一个下标,将 @ 数组印记替换为更为笨拙的 $#: my $first_index = 0; my $last_index = $#cats; say 'My first cat has an index of $first_index, ' . 'and my last cat has an index of $last_index.' 然而,读上去可能不那么好。大多数时间你不会需要那种用法,因为你还可以使用负偏移量从 末尾而不是 开头访问一个数组。数组的最后一个元素可以由下标 -1 取到。倒数第二个元素可 以用下标 -2,等等。 例如: my $last_cat = $cats[-1]; my $second_to_last_cat = $cats[-2]; 你可以通过对 $# 赋值来调整数组大小。如果你对数组进行收缩,Perl 将丢弃所有不符合调整后 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 尺寸的值。 如果你扩展一个数组,Perl 将把 undef 填入多出来的位置。 数组赋值 你可以通过对某下标位置直接对数组赋值: my @cats; $cats[0] = 'Daisy'; $cats[1] = 'Petunia'; $cats[2] = 'Tuxedo'; $cats[3] = 'Jack'; $cats[4] = 'Brad'; Perl 5 的数组是可变的。它没有静态尺寸,它会按需扩展和收缩。 你不也必按顺序赋值。如果你对超出范围的位置赋值的话,Perl 会将数组扩展到合适的大小, 并向夹在中间的所有空槽都填入 undef。 多行赋值太过冗长。你可以用列表对一个数组进行一步到位的初始化: my @cats = ( 'Daisy', 'Petunia', 'Tuxedo', 'Jack', 'Brad' ); 注意括号 并不 创建一个列表。没有了括号,根据操作符优先级(ä¼å级),Daisy 将赋 值成为 数组的第一个也是唯一一个元素。 任何在列表上下文中产生列表的表达式可以对数组进行赋值: my @cats = get_cat_list(); my @timeinfo = localtime(); my @nums = 1 .. 10; 对数组的标量元素赋值将施加标量上下文,对数组整体进行赋值则施加列表上下文。 要清空一个数组,用空列表对其赋值: my @dates = ( 1969, 2001, 2010, 2051, 1787 ); ... file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] @dates = (); 由于新声明的数组起始为空,my @items = (); 便成为了 my @items 的加长版。请使用后者。 数组分片 你也可以通过一个名为 数组分片 的语法结构在列表上下文中访问一个数组。不同于对某数组元 素 进行标量访问,此操作使用一列下标以及数组印记(@): my @youngest_cats = @cats[-1, -2]; my @oldest_cats = @cats[0 .. 2]; my @selected_cats = @cats[ @indexes ]; 你也可以对一个数组分片进行赋值: @users[ @replace_indices ] = @replace_users; 一个分片可以包含零个或多个元素────包括一个: # 单元素数组分片,列表 上下文中的函数调用 @cats[-1] = get_more_cats(); # 单元素数组访问,标量 上下文中的函数调用 $cats[-1] = get_more_cats(); 数组分片和单元素访问之间唯一的语法区别就是开头的印记。语义 区别就比较大了: 一个数组 分片总是强制列表上下文。任何在标量上下文中求值的数组分片都会引发错误: Scalar value @cats[1] better written as $cats[1] at... 数组分片对用于下标的表达式强制列表上下文(ä¸ä¸æ): # 函数调用于列表上下文 my @cats = @cats[ get_cat_indices() ]; 数组操作 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 管理数组下标会是个麻烦。因为 Perl 5 可以按需扩展或收缩数组,语言同时提供了若干操作 以 将数组作为栈、队列,等对待。 push 和 pop 操作符各自在数组尾部添加和删除元素: my @meals; # 这里有什么可以吃的? push @meals, qw( hamburgers pizza lasagna turnip ); # ……但是侄儿不喜欢蔬菜 pop @meals; 你可以用 push 向数组添加任何数量的元素。它的第二个参数是一个值列表。但你只能用 pop 一 次 删除一个元素。push 返回的是更新后数组中元素的个数。pop 返回删除的元素。 类似的,unshift 和 shift 向数组开头添加和删除元素: # 扩展我们的烹饪视野 unshift @meals, qw( tofu curry spanakopita taquitos ); # 对豆类重新考虑 shift @meals; unshift 将一个含零个或多个元素的列表前置于某数组开头并返回数组中元素最新个数。shift 删 除 并返回数组的第一个元素。 很少有程序使用 push 和 unshift 的返回值。编写本章催生了一个优化 push 在空上下文中使用 的 Perl 5 补丁。 splice 是另一个重要的────可能较少使用────数组操作符。它按给出偏移量、列表分片 长度以及 替代物删除并替换数组元素。替换和删除都是可选的,你可以忽略这些行为。perlfunc 中对 splice 的描述演示了它和 push、pop、shift 及 unshift 的等价性。 数组元素通常在循环中处理。有关 Perl 5 控制流程和数组处理的更多信息,请参见 循ç¯è¯å ¥。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 在 Perl 5.12 中,你可以使用 each 来将某数组迭代为键值对: while (my ($index, $value) = each @bookshelf) { say "#$index: $value"; ... } 数组和上下文 在列表上下文中,数组平整为列表。如果你将多个数组传递给一个常规 Perl 5 函数,它们会平 整为 单个列表: my @cats = qw( Daisy Petunia Tuxedo Brad Jack ); my @dogs = qw( Rodney Lucky ); take_pets_to_vet( @cats, @dogs ); sub take_pets_to_vet { # 不要这样用! my (@cats, @dogs) = @_; ... } 在此函数内,@_ 会包含七个元素而非二。与此类似,数组的列表赋值是 贪心的。一个数组会从 列表 中消耗尽可能多的元素。赋值之后,@cats 将包含传递给函数的 每一个 参数。而 @dogs 为 空。 平整行为有时会给想要在 Perl 5 中创建嵌套数组的新手带来疑惑: # 创建单个数组,不是数组的数组 my @array_of_arrays = ( 1 .. 10, ( 11 .. 20, ( 21 .. 30 ) ) ); 有些人起初会期望这段代码产生一个数组,它的头十个元素是一到十的数字,第十一个元素 是 一个数组,包含数字十一到二十以及内嵌另一个包含二十一到三十的数组。注意在这种情 形下 括号并不 创建 列表────它只用来给表达式分组。 解决平整问题的方法和给函数传递参数、创建嵌套数组的一样(å¼ç¨)。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 数组内插 数组会作为字符串化的列表内插入双引号字符串,它的每一个元素由神奇的全局变量 $" 的当前 值分隔。此变量的默认值是一个空格。它的 English.pm 助记形式是 $LIST_SEPARATOR。 因此: my @alphabet = 'a' .. 'z'; say "[@alphabet]"; [a b c d e f g h i j k l m n o p q r s t u v w x y z] 临时局部化并将另外值赋给 $" 是一个很好的调试用法。 (footnote: 致谢:Mark-Jason Dominus 在几年前演示了本例中的用法): # 这个数组中有什么? { local $" = ')('; say "(@sweet_treats)"; } ……这个例子产生如下结果: (pie)(cake)(doughnuts)(cookies)(raisin bread) 哈希 哈希 是 Perl 数据结构中的一等公民,它将字符串键和标量值之间一一联系起来。 在其他编程 语言中,它们可能被称为 表格、关联数组、字典 或是 映射。 正如变量名和一个存储位置相对 应,哈希中的一个键对应一个值。 一个备受推崇但老旧的说法,便是将哈希比方成电话簿:你可以按朋友的名字来查找 她的电话 号码。 哈希有两个重要的属性。第一,它们将一个标量以唯一的键存储。第二,它们不提供特定 的键 顺序。哈希就是一个大尺寸键值对容器。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 声明哈希 哈希使用 % 印记。按如下方式声明一个词法哈希: my %favorite_flavors; 哈希初始为空,没有键也没有值。在布尔上下文中,不含键的哈希求值得假。除此之外, 它返 回一个求值为真的字符串。 你可以对哈希的每一个独立的元素进行赋值和访问: my %favorite_flavors; $favorite_flavors{Gabi} = 'Mint chocolate chip'; $favorite_flavors{Annette} = 'French vanilla'; 当访问独立元素时,哈希将使用标量印记 $ 且将大括号 { } 用于字符串键。 你可以在单一的表达式内将一个键值对列表赋值给一个哈希: my %favorite_flavors = ( 'Gabi', 'Mint chocolate chip', 'Annette', 'French vanilla', ); 如果你将奇数个元素赋值给一个哈希,你将收到警告说得不到预想的结果。通常 用 胖逗号 操 作符来关联键和值会更加明显,因为它能使“成对”的需求更加突出。 请比较: my %favorite_flavors = ( Gabi => 'Mint chocolate chip', Annette => 'French vanilla', ); ……和: my %favorite_flavors = ( 'Jacob', 'anything', 'Floyd', 'Pistachio', ); file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 胖逗号操作符表现得和常规逗号一样,但是它使得 Perl 词法分析器将其前的裸字(裸å 加了引号般对待。编译命令 strict 不会对这些裸字发出警告,并且如果你有一个和此哈希键 同 名的函数,胖逗号 不 会调用此函数: sub name { 'Leonardo' } my %address = ( name => '1123 Fib Place', ); 哈希的键是 name 而不是 Leonardo。如果你想调用该函数以得到键,请明确地使用函数调用 来消 除歧义: my %address = ( name() => '1123 Fib Place', ); 要清空一个哈希,可以将空列表赋值给它 (footnote: 一元 undef 也可以,但相对比较少用): %favorite_flavors = (); 哈希下标 由于哈希是一个集合,你可以通过下标操作访问其中独立的值。把键用作下标(按键访问 操 作) 可以从一个哈希中取得对应的值: my $address = $addresses{$name}; 在这个例子中,$name 包含用作哈希键的字符串。和访问数组中的单个元素一样,按照键访问一 个 标量值时哈希的印记也会从 % 改为 $。 你也可以将字符串字面值用作哈希的键。Perl 会按胖逗号的规则自动为裸字加上引号: # 自动加引号 my $address = $addresses{Victor}; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] # 需要加引号,不是一个合法的裸字 my $address = $addresses{'Sue-Linn'}; # 函数调用需要消歧 my $address = $addresses{get_name()}; 你也许会发现加上引号的哈希字符串字面值键会比较清晰,但是自动加引号的行为已经在 Perl 5 文化中根深蒂固,因此最好将引号留给一些“词不达意”的特殊情况。 甚至连 Perl 5 的关键字也会做自动加引号的处理: my %addresses = ( Leonardo => '1123 Fib Place', Utako => 'Cantor Hotel, Room 1', ); sub get_address_from_name { return $addresses{+shift}; } 一元加号(ä¸å强å¶ç±»å转æ¢、æ°å¼æä½ç¬¦)使得原本将成为裸字的 shift 跳过自动加引 号 行为而变成表达式。隐含的意思是,你可以使用任意表达式────不仅是函数调用 ────作为哈希的键: # 虽然可以,不要真的这样 做 my $address = $addresses{reverse 'odranoeL'}; # 可以使用字符串内插 my $address = $addresses{"$first_name $last_name"}; # 方法调用也可以 my $address = $addresses{ $user->name() }; 任何求值为字符串的事物都是一个可接受的哈希键。当然,哈希键只能是字符串,如果你使用 某对象作为哈希键,你将得到对象字符串化后的版本而非对象本身: for my $isbn (@isbns) { my $book = Book->fetch_by_isbn( $isbn ); file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] # 不太会是你想要的 $books{$book} = $book->price; } 哈希键的存在性 exists 操作符返回布尔值,指示某哈希是否含有给定的键: my %addresses = ( Leonardo => '1123 Fib Place', Utako => 'Cantor Hotel, Room 1', ); say "Have Leonardo's address" if exists $addresses{Leonardo}; say "Have Warnie's address" if exists $addresses{Warnie}; 不直接访问哈希键而使用 exists 避免了两个问题。第一,它不对哈希值的布尔本质 做检查:一 个哈希键可能对应到一个求值得布尔假的值(包括 undef): my %false_key_value = ( 0 => '' ); ok( %false_key_value, 'hash containing false key & value should evaluate to a true value' ); 第二,在处理嵌套数据结构时,exists 避免值的自生现象(èªç)。 哈希键对应的操作是 defined。如果一个哈希键存在,它对应是值可能是 undef。 你可以用 defined 检查这个值: $addresses{Leibniz} = undef; say "Gottfried lives at $addresses{Leibniz}" if exists $addresses{Leibniz} && defined $addresses{Leibniz}; 访问哈希的键和值 哈希是集合变量,但是它的行为和数组不太一样。尤其是,你可以迭代一个哈希的所有键和值 或者是键值对。keys 操作符返回由哈希键组成的列表: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] for my $addressee (keys %addresses) { say "Found an address for $addressee!"; } values 操作符返回由哈希的值组成的列表: for my $address (values %addresses) { say "Someone lives at $address"; } each 操作符返回一个列表,由一个个键值二元列表组成: while (my ($addressee, $address) = each %addresses) { say "$addressee lives at $address"; } 不像数组,哈希没有明确的键值列表排序方式。其顺序依赖于哈希的内部实现,实现又依赖 于 你使用的特定 Perl 版本、哈希的尺寸以及一个随机的因素。遵循上述条件,哈希中元素 的顺 序对于 keys、values 和 each 来说是一致的。修改哈希可能改变这个顺序, 但只要哈希不改变, 你可以依赖此顺序。 每一个哈希对于 each 操作符来说只有 单一 的迭代器。通过 each 对哈希进行多 次迭代是不可靠 的,如果你在一次迭代过程中开始另一次迭代,前者将过早地结束而后者将 从半路开始。 在空上下文中使用 keys 或 values 可以重置一个哈希的迭代器: # 重置哈希迭代器 keys %addresses; while (my ($addressee, $address) = each %addresses) { ... } 你也应该确保不调用尝试使用 each 来迭代哈希的函数。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 单个哈希迭代器是一个众所周知的注意点,但是它不如你想象的那么常见。小心一些, 但在你 需要时,也请大胆地使用 each。 哈希分片 和数组一样,你也可以在一个操作内访问一列哈希元素。哈希分片 就是一个由哈希键值对组成 的列表。最简单的解释就是使用无序列表初始化多个哈希元素: my %cats; @cats{qw( Jack Brad Mars Grumpy )} = (1) x 4; 这和下列初始化是等价的: my %cats = map { $_ => 1 } qw( Jack Brad Mars Grumpy ); ……除了上例中的哈希分片初始化不会 替换 哈希的原有内容。 你可以用分片一次性从哈希中取出多个值: my @buyer_addresses = @addresses{ @buyers }; 正如数组分片,哈希印记的改变反映了列表上下文。通过使用大括号进行按键访问, 你仍然可 以得知 %addresses 的哈希本质没有变。 哈希分片可以使合并两个哈希变得容易: my %addresses = ( ... ); my %canada_addresses = ( ... ); @addresses{ keys %canada_addresses } = values %canada_addresses; 这样做和手动对 %canada_addresses 的内容进行循环等价,但是更为短小。 对两种方式的选择取决于你的合并策略。如果两个哈希中出现相同的键怎么办?哈希分片 方式 总是覆盖已存在于 %addresses 中的键值对. file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 空哈希 一个空哈希不包含键和值。它在布尔上下文中得假。含有至少一个键值对的哈希在布尔 上下文 得真,哪怕其所有的键或值或两者同时在布尔上下文中求值得假。 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(); 在标量上下文中,对哈希求值得到的是一个字符串,表示已用哈希桶比上已分配哈希桶。 这个 字符串并不怎么有用,因为它仅仅表示有关哈希的内部细节且对于 Perl 程序来说 基本没有意 义。你可以安全地忽略它。 在列表上下文中,对哈希求值得到类似从 each 操作符取得的键值对列表。然而,你 不能 按迭 代产生自 each 的列表的方式迭代此列表,因为这将无限循环,除非此 哈希为空。 哈希惯用语 哈希有若干用途,诸如查找列表或数组中唯一的元素。因为每个键只在哈希中存在一份, 对哈 希中相同的键赋值多次仅存储最近的值: my %uniq; undef @uniq{ @items }; my @uniques = keys %uniq; 对哈希分片使用 undef 操作符可以将哈希的值设置为 undef。这是判定某元素 是否存在于集合中 成本最低的方法。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 在对元素计数时哈希也很有用,比如在日志文件中的一列 IP 地址: my %ip_addresses; while (my $line = <$logfile>) { my ($ip, $resource) = analyze_line( $line ); $ip_addresses{$ip}++; ... } 哈希值初始为 undef。后缀自增操作符(++)将其作为零对待。这个对值即时修改 增加某个键对 应的值。如果该键对应的值不存在,它创建一个值(undef)并立刻将其 加一,因为数值化的 undef 产生值 0。 此策略的一个变种很适合缓存,即你愿意付出一点存取代价来存放某昂贵计算的结果: { my %user_cache; sub fetch_user { my $id = shift; $user_cache{$id} ||= create_user($id); return $user_cache{$id}; } } 如果已经存在,这个 Orcish Maneuver (footnote: “Or-cache”,如果你喜欢双关语的话) 会 从哈 希中返回值。否则,计算该值,存入缓存,再返回它。注意布尔或赋值操作符(||=) 作用于布 尔值之上,如果你的缓存值在布尔上下文中求值得假,则可以使用“已定义-或” 赋值操作符 (//=)来代替: sub fetch_user { my $id = shift; $user_cache{$id} //= create_user($id); return $user_cache{$id}; } 这个惰性 Orcish Maneuver 检查缓存值是否已被定义,而非其布尔真假。“已定义-或”赋值 操 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 作符是 Perl 5.10 中的新功能。 哈希也可以收集传递给函数的具名参数。如果你的函数接受若干参数,你可以使用吸入式哈 希 (åæ°å¸å¥)来把键值对收集在单个哈希中: sub make_sundae { my %parameters = @_; ... } make_sundae( flavor => 'Lemon Burst', topping => 'cookie bits' ); 你甚至用如下方式设置默认参数: sub make_sundae { my %parameters = @_; $parameters{flavor} //= 'Vanilla'; $parameters{topping} //= 'fudge'; $parameters{sprinkles} //= 100; ... } ……或者将它们包含在最初的声明和赋值中: sub make_sundae { my %parameters = ( flavor => 'Vanilla', topping => 'fudge', sprinkles => 100, @_, ); ... } ……因为后续对同一键的不同值声明会覆盖前面的值。 哈希上锁 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 哈希的一个缺点就是它们的键是几乎不提供打字错误保护的裸字(特别是将其和受 strict 编译 命令保护的函数、变量名相比)。核心模块 Hash::Util 提供了一些机制来对哈希的 修改和允许 的键做出限制。 为避免他人向哈希添加你不想要的键(假设一个打字错误或是不受信任的输入),你可以使用 lock_keys() 函数将哈希键限制在当前集合中。任何添加不被允许的键值对的意图将引发 一个异 常。 当然,其他想达到此目的的人总是可以使用 unlock_keys() 函数来去掉保护,因此不要 将此作为 防止其他程序员误用的安全保障来信任。 类似的你可以对哈希中给定的键对应已存在的值进行上锁和解锁(lock_value() 和 unlock_value()) 以及利用 lock_hash() 和 unlock_hash() 使得或不使整个哈希变为只读。 强制转换 不像其他语言中一个变量只能存放一个特定类型的值(字符串、浮点数、对象),Perl 依赖 操 作符的上下文来决定如何对值进行解释(æ°å¼ãå符串åå¸å°ä¸ä¸æ)。如果你将数字作为字符 串, Perl 将尽最大努力将该数字转换为字符串(反之亦然)。这个过程就是 强制转换。 植根于设计,Perl 就试图按你的意思办事(DWIM 代表 do what I mean),虽然你必须对 你的 意图进行较为具体的描述。 布尔强制转换 布尔强制转换发生于测试某值 为真性 的时候 (footnote: 为真性和真假性有些类似,设想你 斜 着眼睛说“啊,那是真的,但是……”),就是 if 或 while 语句中的测试条件。数 值 0 是假。未定 义值是假。空字符串和字符串 '0' 是假。然而字符串 '0.0' 和 '0e' 是 真。 所有其他的值是真的,包括惯用语字符串 '0 but true'。对于诸如有着字符串和数值 两面的标量 这种情况(åéåé),Perl 5 选择字符串部分作为检查布尔真假的依据。 '0 but true' 在数值上下 文中求值得零,但不是一个空字符串,因此它在布尔上下文 中求值得真。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 字符串强制转换 字符串强制转换发生于使用字符串操作符之时,例如比较(eq 和 cmp,比方说)、拼 接、split、substr 以及正则表达式。它也发生在将某值作为哈希键时。未定义值字 符串化得到 空字符串,但会引发 “use of uninitialized value(使用未初始化值)” 的警告。 数值 字符串化 得到包含其值的字符串,就是说,值 10 字符串化得到字符串 10, 如此你便可以用 split 将某数 值分割成组成它的各个数字: my @digits = split '', 1234567890; 数值强制转换 数值强制转换发生于使用数值比较操作符(诸如 == 和 <=>)、进行数学计算操作以 及将某值作 为数组或列表的下标时。未定义值 数值化 得零,虽然这会产生一个“Use of uninitialized value(使用未初始化的值)”的警告。不以数值部分开头的字符串数值化为零,并产 生“Argument isn't numeric(参数不是数值)” 的警告。以数值字面值中允许出现的字符开头 的字符串将数值化为对应的值, 就是说 10 leptons leaping 数值化为 10,同样地,6.022e23 moles marauding 数值化为 6.022e23。 核心模块 Scalar::Util 包含一个名为 looks_like_number() 的函数,它使用和 Perl 5 语法一样的语法 分析规则从字符串中提取数字。 字符串 Inf 和 Infinity 表示无穷值,并且出于在数值化时不会产生“Argument isn't numeric(参 数不是数值)”的警告,它们行为和数字一致。字符串 NaN 表示 “不是一个数字(英语:not a number)”的概念。除非你是数学家,否则一般不会关心 这些。 引用强制转换 在某些的情况下,将一个值作为引用对待将使该值 转变为 一个引用。这个自生 (èªç)的过程 对于嵌套数据结构来说比较有用。它发生在当你对 非引用进行解引用操作时: my %users; $users{Bradley}{id} = 228; $users{Jack}{id} = 229; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 虽然上例中的哈希从未包含对应 Bradley 和 Jack 的值,Perl 5 热心地为这 些值创建了哈希引用, 接着将以 id 为键的键值对赋值给它们。 强制转换的缓存 Perl 5 对值的内部存储机制允许每个值拥有字符串化和数值化的结果 (footnote: 这是一种简 化,但 残酷的现实真的很残酷)。字符串化一个数值并不将数值替换为字符串。取而代之的 是,它将 字符串化后的值作为对数值的补充 附着 到该值上。类似的操作还发生于数值化一个 字符 串值时。 你几乎不必知道发生了这种转换────如果传闻证据可信的话,也许十年内会有个一两次。 Perl 5 也许会偏向于使用某种形式的转换。如果一个值拥有一个缓存了的、但不是你 期望的表 示方式,依赖于隐式的转换可能会产生令人惊讶的结果。你几乎不必明确提出 你的期望,但是 明白这种缓存确实存在,你可以对某些奇怪的情况做出诊断。 双重变量 对数值和字符串值的缓存允许你使用一个“罕见但有用”的特性,称为 双重变量, 或者说一个 同时拥有数值和字符串表示的值。核心模块 Scalar::Util 提供了一个名为 dualvar() 的函数,它允 许你创建一个拥有两种不同形式的值: 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 中的 名称空间 是一种机制,它将若干具名实体关联并封装于某具名分类之下。它就 像你 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 的姓或是某种品牌,只能反映出命名归类上的关系而非其他。(这类关系可以存在,但不 是必 须的。) Perl 5 中的 包 是单一名称空间下代码的集合。在某种意义上,包和名称空间是等价的; 包代 表源代码而名称空间代表当 Perl 分析这段代码时创建的实体。 (footnote: 这是个微妙的区别) package 关键字声明一个包和一个名称空间: package MyCode; our @boxes; sub add_box { ... } 所有在包声明语句之后的对全局变量和函数的声明或引用,都指向位于 MyCode 名称空间 内的符 号。如果代码是这样写的,那么你只能通过 完全限定 名称────@MyCode::boxes ────来从 main 名称空间访问 @boxes 变量。相似地,你仅可以通过 MyCode::add_box() 来调用 add_box() 函数。一 个完全限定名称包括了完整包名。 默认包是 main 包。如果你不明确声明一个包,无论是在命令行 one-liner 或在独立的 Perl 程序 甚至是磁盘上的 .pm 文件,那么当前包就是 main 包。 除包名(main 或是 MyCode 或其他任何允许的标识符)外,一个包还拥有一个版本以及 三个隐含 的方法,分别是 VERSION()、import()(导å¥)和 unimport()。 VERSION() 返回一个包的版本。 包版本是包含在名为 $VERSION 的包全局变量中的一系列数字。按照惯例,版本号倾向于写 成一 系列由点分隔整数的形式,例如 1.23 和 1.1.10,其中每一段是一个整数,但通常 就这样写。 Perl 5.12 引入了一种简化版本号的新语法。如果你编写的代码不会在早先版本的 Perl 5 上 运 行,你可以避免不少不必要的复杂性: package MyCode 1.2.1; 在 5.10 以及早期的版本中,声明包版本最简单的方式是: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] package MyCode; our $VERSION = 1.21; 每个包都有 VERSION() 方法;它们继承自 UNIVERSAL 基类。它返回 $VERSION 中 的值。虽然没有什么 理由这样做,但你还是可以按需重写此方法。使用 VERSION() 方法 是获取一个包版本号最简便的 办法: my $version = Some::Plugin->VERSION(); die "Your plugin $version is too old" unless $version > 2; 包和名称空间 每一句 package 声明都会使 Perl 完成两件任务。如果名称空间不存在则创建它。 它还告诉语法 分析器将后续的包全局符号(全局变量和函数)放入该名称空间下。 Perl 有 开放式名称空间。通过使用包声明语句,你在任何时候向一个名称空间添 加函数和变 量: package Pack; sub first_sub { ... } package main; Pack::first_sub(); package Pack; sub second_sub { ... } package main; Pack::second_sub(); ……或者在声明时使用完全限定的函数名称: # 隐式 package main; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] sub Pack::third_sub { ... } Perl 5 是那么的开放以至于你可以在编译期、运行时的任何时刻或从其他文件向其中 添加内 容。当然,这样做使人迷惑,因此应尽量避免。 名称空间可以按组织需要分为多个级别。这并不意味着继承关系,包与包之间也没有什 么技术 上的联系────仅对于这段代码的 阅读者 来说有语义上的关系罢了。 常见的做法是为业务或项目创建一个顶层名称空间。这对于阅读代码和发现组件间关系 来说变 得方便,同时也为代码和包在磁盘上的组织提供便利。因此: StrangeMonkey 是项目名称; StrangeMonkey::UI 包含顶层用户接口的代码; StrangeMonkey::Persistence 包含顶层数据管理代码; StrangeMonkey::Test 包含为项目编写的顶层测试代码; ……等等。 引用 即便你的期望相当微妙,Perl 通常也能实现它。考虑当你将值传递给函数时候会发生什么: sub reverse_greeting { my $name = reverse shift; return "Hello, $name!"; } my $name = 'Chuck'; say reverse_greeting( $name ); say $name; 你也许会期望,虽然该值曾传递给函数并且逆序为 kcuhC,但在函数之外,$name 仍包含值 Chuck────确实,这就是所发生的事。在函数之外的 $name 和函数内部的 $name 是两个分离的 标量,各有一份独立的字符串拷贝。修改一个不会影响另一个。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 这是有用且合意的默认行为。如果你每次执行可能引发修改的操作之前都必须明确地复制 这些 值,将导致编写大量多余的、不必要的代码来抵抗善意而错误的修改。 有些时候,直接对值进行修改也有不少用处。试想仅为更新某值或删除一个键值对就将一 个装 满数据的哈希传递给函数,导致每次更改都要创建和返回新哈希────这是一个麻烦的 过程 (毫无效率可言)。 Perl 5 提供了一种机制,通过它你可以间接使用某值而不必为此创建一份拷贝。任何对该 引用 做出的修改将就地对值进行更新,如此,所有 对该值的引用都将见到最新的值。 引用是 Perl 5 中的一等公民,是一种内置的标量数据类型。它不是字符串、不是数组、也 不是哈希。它就是 一个引用其他第一等数据类型的标量。 标量引用 引用操作符是反斜杠(\)。在标量上下文中,它创建对另一个值的单一引用。在列表上 下文 中,它创建一个引用列表。因此,你可以按如下方式引用前例中的 $name: my $name = 'Larry'; my $name_ref = \$name; 要访问引用指向的值,你必须对其 解引用。解引用需要你在解开每一重引用时加上额外 的印 记: sub reverse_in_place { my $name_ref = shift; $$name_ref = reverse $$name_ref; } my $name = 'Blabby'; reverse_in_place( \$name ); say $name; 双标量印记对一个标量引用进行解引用。 这个例子看上去不怎么有用,为什么不让函数直接返回修改后的值?标量引用在处理 大型 标量 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 时很有用────复制这些标量的内容会花去大量的时间和内存。 复杂的引用需要加上一对大括号以消除表达式断句上的歧义。这对于简单解引用来说是可选 的,加 上它会变得很臃肿: sub reverse_in_place { my $name_ref = shift; ${ $name_ref } = reverse ${ $name_ref }; } 如果你忘记对一个标量引用解引用,它将会字符串化或数值化。字符串值将拥有 SCALAR(0x93339e8) 的形式,数值则为 0x93339e8 这部分。该值将引用的类型(此例为标量,即 SCALAR)以及引用在 内存中的位置编码在一起。 Perl 不提供对内存位置的原生访问。因为引用不一定有名称,引用的地址是一个可用作唯一标 识符的值。 不像一些语言(如:C)中的指针,你不能修改该地址或将其作为对应于内存的地 址。 这些地址仅在 大致上 唯一, 因为在垃圾回收器回收某未引用的引用后, Perl 可能重用此存储 位置。 数组引用 你也可以创建对数组的引用,或称 数组引用。说它有用有如下几个理由: 不加平整地向函数传递及从函数中返回数组; 创建多维数据结构; 避免不必要的数组复制; 持有匿名数据结构。 对已声明的数组创建引用,使用引用操作符: my @cards = qw( K Q J 10 9 8 7 6 5 4 3 2 A ); my $cards_ref = \@cards; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 现在 $cards_ref 包含了对该数组的引用。所有通过 $cards_ref 做出的修改 将同样影响 @cards,反之 亦然。 你可以通过 @ 已经对数组进行整体访问,可能会使数组平整为列表,或者得到其中包含 的元素 个数: my $card_count = @$cards_ref; my @card_copy = @$cards_ref; 你也可以通过解引用箭头(->)访问数组中的独立元素: my $first_card = $cards_ref->[0]; my $last_card = $cards_ref->[-1]; 访问单个元素时候,为区分名为 $cards_ref 的标量和名为 @cards_ref 数组,箭头是必需的。 还有另外一种写法,你可以在数组引用之前加上标量印记。它更简短,但是可读性较差: my $first_card = B<$$cards_ref[0]>;. 通过大括号解引用和分组语法,可以通过引用对数组分片: my @high_cards = @{ $cards_ref }[0 .. 2, -1]; 这种情况下,你 可以 忽略大括号,但是它(和空白)带来的视觉上的分组有助于可读性的提 高。 你可以不用命名就地创建匿名数组。在一个值或表达式列表的周围加上中括号: my $suits_ref = [qw( Monkeys Robots Dinosaurs Cheese )]; 这个数组引用的行为和具名数组引用一样,除了匿名数组引用 总是 创建新引用,而具名数组的 引用总是在作用域内指向 同一个 数组。就是说: my @meals = qw( waffles sandwiches pizza ); my $sunday_ref = \@meals; my $monday_ref = \@meals; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] push @meals, 'ice cream sundae'; ……$sunday_ref 和 $monday_ref 现在包含了一份甜点,但是: my @meals = qw( waffles sandwiches pizza ); my $sunday_ref = [ @meals ]; my $monday_ref = [ @meals ]; push @meals, 'berry pie'; ……无论 $sunday_ref 还是 $monday_ref 都不包含甜点。在用于创建匿名数组引用的中括号内, @meals 数组在列表上下文中被展开。 哈希引用 要创建一个 哈希引用,可以在具名哈希上使用引用操作符: my %colors = ( black => 'negro', blue => 'azul', gold => 'dorado', red => 'rojo', yellow => 'amarillo', purple => 'morado', ); my $colors_ref = \%colors; 按如下方法在引用前加上哈希印记 % 可以访问其中的键和值: my @english_colors = keys %$colors_ref; my @spanish_colors = values %$colors_ref; 你可以通过解引用箭头访问哈希中独立的值(存储、删除、检查存在性或取值): sub translate_to_spanish { my $color = shift; return $colors_ref->{$color}; } file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 虽然箭头有时更为清晰,和数组引用一样,你可以避开解引用箭头转用前缀标量印记: $$colors_ref{$color}。 你也可以通过引用对哈希分片: my @colors = qw( red blue green ); my @colores = @{ $colors_ref }{@colors}; 注意大括号标示了哈希下标操作,数组印记标示了对引用进行的列表操作。 你可以通过大括号就地创建匿名哈希: my $food_ref = { 'birthday cake' => 'la torta de cumpleaños', candy => 'dulces', cupcake => 'pancuecitos', 'ice cream' => 'helado', }; 和匿名数组一样,每次执行都会创建一个匿名哈希。 一个常见的新手错误就是将匿名哈希赋值给标准哈希。这会产生一个有关哈希元素个数为 奇数 的警告。请对具名哈希使用小括号,对匿名哈希使用大括号。 函数引用 Perl 5 支持 第一级函数。至少在你使用 函数引用 时,函数和数组、哈希一样,是 一种数据类 型。这个特性启用了许多高级功能(éå)。和其他数据类型一样,你可 以通过在函数名称上使 用引用操作符来创建函数引用: sub bake_cake { say 'Baking a wonderful cake!' }; my $cake_ref = \&bake_cake; 没有了 函数印记(&),你将得到的是函数返回值的引用。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 你也可以创建匿名函数: my $pie_ref = sub { say 'Making a delicious pie!' }; 使用 sub 关键字 不用 函数名称也可以使得函数正常编译,但是它不会被安装到当前 的名称空间 中。访问此函数的唯一方法就是通过引用。 你可以通过解引用箭头调用引用指向的函数: $cake_ref->(); $pie_ref->(); 将空小括号的解引用作用想成和中括号对数组进行下标查找、大括号进行哈希查找一样。你可 以 在小括号内将函数的参数传递进去: $bake_something_ref->( 'cupcakes' ); 你也可以将函数引用用作对象的方法(Moose),当你已经完成方法查找时它最有用: my $clean = $robot_maid->can( 'cleanup' ); $robot_maid->$clean( $kitchen ); 你会看到另外一种调用函数引用的语法,它使用函数印记(&)而非解引用箭头。请避免使用这 种语法; 它暗示隐式参数传递。 文件句柄引用 文件句柄也可以是引用。当你使用 open 的(以及 opendir 的)词法文件句柄形式,你就在处理文 件句柄引用。对此文件句柄进行字符串化可以得到类似 GLOB(0x8bda880) 形式的东西。 说到内部机制,这些文件句柄都是 IO::Handle 类的对象。当你加载这个模块时,你可以调用文件 句柄 上的方法: use IO::Handle; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] use autodie; open my $out_fh, '>', 'output_file.txt'; $out_fh->say( 'Have some text!' ); 你会碰到使用型团(typeglob)引用的陈旧代码,例如: my $fh = do { local *FH; open FH, "> $file" or die "Can't write to '$file': $!\n"; \*FH; }; 这个惯用语出现于词法文件句柄之前,是在 2000 年 3 月作为 Perl 5.6.0 的一部分引入的 (footnote: ……你现在知道那段代码有多老了)。你仍可以在型团上使用引用操作符得到包全局 文件句柄 如 STDIN、STDOUT、STDERR 和 DATA 的引用────无论如何,这些仅代表全局数 据,对于 其他情况,请使用词法文件句柄。 除了使用词法作用域代替包或全局作用域所带来的好处,词法文件句柄允许你管理文件句柄的 生存期限。这是一个 Perl 5 如何管理内存和作用域的优秀特性。 引用计数 Perl 怎么知道它何时可以安全地释放一个变量所占内存以及何时需要继续保留?Perl 怎么知 道 它何时可以安全地关闭在内层作用域内打开的文件: use autodie; use IO::Handle; sub show_off_scope { say 'file not open'; { open my $fh, '>', 'inner_scope.txt'; $fh->say( 'file open here' ); } say 'file closed here'; } file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] Perl 5 使用一种名为 引用计数 的内存管理技术。程序中的每一个值都有附加的计数器。 每次 被其他东西引用时,Perl 增加计数器的值,无论隐式还是显式。每次引用消失后,Perl 将减少 计数器的值。当计数器减至零,Perl 就可以安全地回收这个值。 上例中,在内层代码块内,有一个 $fh。(源代码中有多行提及它,但只有一处 引用 它,即 $fh 自己。)$fh 的作用域仅限于此代码块,并且没有被赋值到代码块外,因 此,当此代码块结束 时候,它的引用计数减为零。对 $fh 的回收隐式地调用了该文件句柄 上的 close() 方法,使得文 件最终被关闭。 你不需要了解全部这些是如何工作的。你只需理解你对值的引用和传递将影响 Perl 如何管理 内存。(请参考 循ç¯å¼ç¨ 获得有关循环引用的告诫。) 引用和函数 当你使用引用作为函数的参数时,请小心地记录你的意图。在函数内部修改引用指向的值可能 会是调用它的代码感到意外,因为调用者其实并不期望这种修改。 如果你需要对引用的内容做出破坏性改动,请将其所含的值复制到一个新的变量中: my @new_array = @{ $array_ref }; my %new_hash = %{ $hash_ref }; 这仅在少数情况是必须的,同时为了避免调用者感到诧异,在这些情况下最好明确地复制。如 果 引用更加复杂(åµå¥æ°æ®ç»æ),请考虑使用核心模块 Storable 和它的 dclone (deep cloning)函数。 嵌套数据结构 Perl 的集合数据类型────数组和哈希────允许你按整数下标或字符串键存储标 量。Perl 5 的引用(å¼ç¨)则允许你通过特殊标量间接访问集合数据类型。Perl 中的嵌套数 据 结构,例如数组的数组、哈希的哈希,是通过引用机制来实现的。 声明嵌套数据结构 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 一个对数组的数组简单声明可能是: 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' }, dinner => { entree => 'steak', side => 'avocado salad' }, ); Perl 允许在结尾添加逗号,但并非必须,这样做只是为了方便以后添加元素。 访问嵌套数据结构 访问嵌套数据结构中的元素需要用到 Perl 的引用语法。印记标示了欲取得数据的数量, 解引 用箭头表明数据结构中的这部分值是一个引用: my $last_nephew = $famous_triplets[1]->[2]; my $breaky_side = $meals{breakfast}->{side}; 对于嵌套数据结构这种情况,嵌套一个数据结构的唯一方法就是通过引用,因此箭头是 多余 的。下面的代码和前面的等价,并且更清楚: my $last_nephew = $famous_triplets[1][2]; my $breaky_side = $meals{breakfast}{side}; 调用存放于嵌套数据结构内的函数引用时,使用箭头调用语法是最清晰的,除此之外, 你可以 避开箭头的使用。 将嵌套数据结构作为第一等数组或哈希访问时,需要消歧代码块: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] my $nephew_count = @{ $famous_triplets[1] }; my $dinner_courses = keys %{ $meals{dinner} }; 类似的,对嵌套数据结构分片也需要额外的标点: my ($entree, $side) = @{ $meals{breakfast} }{qw( entree side )}; 空白的使用有助于,但不能完全消除这个语法结构的噪音。一些时候,使用临时变量会 更清 晰: my $breakfast_ref = $meals{breakfast}; my ($entree, $side) = @$breakfast_ref{qw( entree side )}; perldoc perldsc,数据结构的“食谱”,给出了有关如何使用 Perl 中各式数据结构 丰富的实例。 自生 Perl 的表达力同样也扩展到了嵌套数据结构。当你试图编写一个嵌套数据结构组件时, 如果不 存在,Perl 会创建通向这部分数据结构的路径: my @aoaoaoa; $aoaoaoa[0][0][0][0] = 'nested deeply'; 第二行代码之后,这个数组的数组的数组的数组包含了对数组的引用的引用的引用的 引用。每 一个引用包含一个元素。类似的,在嵌套数据结构中将未定义值作为哈希引 用会创建以合适的 值作为键的中间哈希。 my %hohoh; $hohoh{Robot}{Santa}{Claus} = 'mostly harmful'; 这个行为称为 自生,并且很有用。它的好处是减少嵌套数据结构的初始化代码。它的 坏处是无 法区分创建嵌套数据结构中所缺元素究竟是有意还是无意。 CPAN 上的 autovivification 编译命令(ç¼è¯å½ä»¤)让你可以在词法作用域内对某 特定类型操 作禁用自生行为。在多人参与的大型项目中很值得考虑这些问题。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 你也可以在对复杂的数据结构进行层层解引用前检查特定的哈希键是否存在以及获取数组 中元 素的个数,但是这会导致许多程序员都不愿碰的冗长代码。 你也许会考虑利用自生和对代码启用 strictures 这对矛盾。这是一个权衡问题。以禁用 针对几 个严实封装的符号引用错误检查为代价来捕获错误是不是更方便?让数据结构自行增长 是不是 比指定它们的大小和允许的键来得方便? 后一个问题的答案取决于特定的项目。刚开始开发时,你可能想要严格要求编码以防止意外 的 副作用。好在有了 strict 和 autovivification 编译命令的词法作用域,你可以按 需要启用禁用它 们。 调试嵌套数据结构 Perl 5 的解引用语法的复杂结合多级引用潜在的迷惑性,使得调试嵌套数据结构变得困难。所 幸 有两种可视化它们的好选择。 核心模块 Data::Dumper 可以将任意复杂的数据结构的值字符串化为 Perl 5 代码: use Data::Dumper; print Dumper( $my_complex_structure ); 在识别数据结构所含内容以及找出应该访问到和实际访问到什么时很有用。Data::Dumper 可以转 储对象和函数引用(如果你将 $Data::Dumper::Deparse 设置为真)。 Data::Dumper 是核心模块,并且打印出 Perl 5 代码,但它也给出详细的输出。一些开 发人员更愿 意使用 YAML::XS 和 JSON 来调试程序。为理解它们的输出,你必须学习 不同的格式,但它们的输 出更易阅读也更易理解。 循环引用 Perl 5 的引用计数(å¼ç¨è®¡æ°)内存管理系统对于用户代码来说有一个明星的 坏处。两个互 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 指的引用最终形成了一 循环引用,Perl 无法自行销毁它。考虑生物模型, 每一个实体有父方 母方,并可以有子代: 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; 因为 $alice 和 $robert 都包含了一个指向 $cianne 数组引用,并且由于 $cianne 是一个包含 $alice 和 $robert 的哈希引用,Perl 始终无法将这 三者的引用计数减为零。它无法认识到循环引用的存 在,并且无法管理这些实体的生存 期限。 你必须手动打断引用计数(通过清除 $alice 和 $robert 的子代或 $cianne 的 亲代),或者利用一个 名为 弱引用 的特性。弱引用是一个不增加被引用者引用计数的 引用。弱引用可以通过核心模 块 Scalar::Util 来使用。导出 weaken() 函数并对某 引用使用它可以防止引用计数的增加: 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} ); 完成之后,$cianne 仍持有对 $alice 和 $robert 的引用,但是这些引用不会主动 阻拦 Perl 的垃圾回 收器回收这些数据结构。经过正确设计的数据结构一般不会用到弱引用, 但在极少数情况下它 们仍可能被用到。 嵌套数据结构的替代选择 不管数据结构嵌套得多深,Perl 都愿意处理,但是理解这些数据结构、理顺边边角角关系所花 的人力代价,别提访问数据结构各个部分所用的代码,就已经够高了。除了两三层嵌套的数据 结构,其他情况就应该考虑是不是应该用类和对象(Moose)来对系统的各个组件进行建模, file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_03.html[2011/2/21 21:22:04] 它会更清楚地表达你的数据。 有的时候,将数据和合适的行为绑定在一起会使代码更清晰。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_04.html[2011/2/21 21:22:07] 操作符 一个准确但不礼貌的形容:Perl 是一种“面向操作符的语言”。正是操作符之间的互动 以及它们 的操作数给与了 Perl 表达力和力量。要理解 Perl 必须理解操作符和它们的 行为。出于本次讨 论,一个 Perl 操作符 的确切定义是:“它由一系列符号组成, 用作语言语法的一部分”。每一 个操作符接受零个或多个 操作数;这个定义是循环的, 因为操作数也是由操作符操作的值。 对操作符最准确的定义是“perlop中的那个”,但它还是将一些操作符遗漏在 perlsyn 中并且把内置 函数也算入其中。不要太执着于单个定义。 操作符特征 perldoc perlop 和 perldoc perlsyn 提供了大量有关 Perl 操作符行为的信息。即 使如此,其中 没有 提到的部分对于理解这些操作符来说更为重要。这部分文档假定你对 语言设计中的若干概念有 着一定程度的熟悉。起初,这些概念也许听起来有些生硬,但是其实 是直截了当的。 每一个操作符持有若干构成其行为的重要特征:操作数的个数,和其他操作符的关系,以及 可 能是用法。 优先级 某操作符的 优先级 有助 Perl 在对其在表达式中求值时做出决定。求值顺序从高到低。 例如, 因乘法的优先级较加法高,7 + 7 * 10 求值得 77,而非 140。你可以用 括号将子表达式分组使某 操作符在其他操作符之前求值;(7 + 7) * 10 确实 求值得 140,因为加法操作符已经成为一个整 体,并且必须在乘法发生之前完全求值。 在平手的情况下────两个操作符拥有同样的优先级────则由其他因素(如词缀性 è¯ç¼æ§ 和结 合性 ç»åæ§)来决定。 perldoc perlop 包含了一张优先级表。几乎没有人记得住这张表。掌控优先级的最佳途径 就是让 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_04.html[2011/2/21 21:22:07] 表达式尽量简单。另一种好方法就是在复杂的表达式中用括号来澄清优先级。如果你发 现自己 淹没在括号的海洋中,再请参考第一条规则。 结合性 某操作符的 结合性 决定了它是从左往右求值还是从右往左。加法是左结合的,因此 2 + 3 + 4 先求出 2 + 3,然后再在此结果上加 4。指数操作是右结合的,因 此 2 ** 3 ** 4,先进行 3 ** 4 这部分运算,然后再求得 2 的 81 次方。 简化复杂的表达式并用括号来展示你的意图远比记住结合性表来得重要。虽说如此,记住 结合 性表的数学操作符部分还是值得的。 核心模块 B::Deparse 可以重写代码片段并如实展示 Perl 究竟是如何处理操作符优先 级和结合性 的;在某代码片段上运行 perl -MO=Deparse,-p。(-p 标志添加额外的 分组括号使得求值顺序更为 明显。)注意 Perl 的优化器会如前面的例子般简化数学操作, 你可以用变量替代,就像 $x ** $y ** $z。 参数数量 操作符的 参数数量 就是该操作符所作用的操作数的个数。空元 操作符没有操作数。 一元 操作 符有一个操作数。二元 操作符有两个操作数。三元 操作符有三个操作 数。列元 操作符对一个 操作数列表进行操作。 除了大多数操作符都接受两个、多个或一个操作数这一事实,没有其他什么好的规则来决 定一 个操作符的参数个数。操作符的文档会把这些交待清楚。 举例说来,算术运算符是二元操作符,它们通常是左结合的。2 + 3 - 4 先对 2 + 3 求值,加法和 减法的优先级一致,但是它们是左结合并且是二元的,因此正确的求值顺序将 最左端的操作符 (+)应用到最左端的两个操作数(2 和 3)上,接着,将最右端 的操作符(-)应用在第一个操 作符的结果和最右端的操作数(4)上。 一个 Perl 新手疑问的常见来源就是嵌套表达式中的列元操作符(特别是函数调用)。请使用 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_04.html[2011/2/21 21:22:07] 括号表明你的意图,并且留意如下代码中的问题: # 可能是一段有缺陷的代码 say ( 1 + 2 + 3 ) * 4; ……由于 Perl 5 高兴地将括号解释为后环缀(è¯ç¼æ§)操作符并认为括号中的内容是 say 的参 数,而并非为改变优先级而对表达式分组的环缀括号。换句话说,这段代码打印出 6 然 后求值 say 的返回值乘以 4 后的结果。 词缀性 操作符的 词缀性 就是它相对其操作数的位置: 中缀 操作符出现在操作数之间。大部分数学操作符是中缀操作符,例如 $length * $width 中 的乘法操作符; 前缀 操作符出现于其操作符之前而 后缀 操作符出现于其操作符之后。这 些操作符通常是 一元的,如数学上的负数符号(-$x)、布尔取反(!$y)以及后缀 自增($z++); 环缀 操作符包围其操作数。例子包括匿名哈希构造符({ ... })和引述操 作符(qq[ ... ]); 后环缀 操作符接在某些操作数之后并环绕其他部分,就向访问哈希或数组的元 素那样 ($hash{ ... } 和 $array[ ... ])。 操作符类型 Perl 无孔不入的上下文────特别是值上下文(æ°å¼ãå符串åå¸å°ä¸ä¸æ)────大大扩 展了操作符 的行为。Perl 操作符对它们的操作数提供了值上下文。为给定的情况选择最合适的 操作符 要求你对所求值和所用值的类型都要有所理解。 数值操作符 数值操作符对其操作数强制数值上下文。它们由标准算术操作符构成:加(+)、减 (-)、乘 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_04.html[2011/2/21 21:22:07] (*)、除(/)、指数(**)、取模(%)以及变种(+=、 -=、*=、/=、**= 和 %=)外加前后缀自 减(--)。 虽然自增操作符看上去像数值操作符,它有着特殊的字符串行为(ç¹æ®æä½ç¬¦)。 若干比较操作符对其操作数强制数值上下文。它们是数值等于(==)、数值不等于(!=)、 大 于(E)、小于(E)、大于等于(E=)、小于等于(E=), 以及排序比较操作符(E=E)。 字符串操作符 字符串操作符对其操作数强制字符串上下文。它们由肯定和否定正则表达式绑定操作符 (=~ 和 !~,对应地),和拼接操作符(.)组成。 若干比较操作符对其操作数强制数值上下文。它们是字符串等(eq)、字符串不等于(ne)、 大于(gt)、小于(lt)、大于等于(ge)、小于等于(le)以及字符串排序比较操 作符 (cmp)。 逻辑操作符 逻辑操作符在布尔上下文中处理其操作数。&& 和 and 操作符测试两边表达式是否都为 逻辑 真,|| 和 or 操作符则测试两边的表达式是否有一个为真。这四个全部是中缀操作 符。这四个 全部执行 短路 测试:如果一个表达式的求值结果使得整个表达式为假,Perl 不 会继续对其他 表达式求值。单词形式比符号形式的优先级低。 已定义-或操作符,//,测试其操作数的 定义性。不像 || 测试其操作数的为真性,如 果操作数 求值为数值零或空字符串,// 还是会返回真值。这对设置默认参数的值尤其有用。 sub name_pet { my $name = shift // 'Fluffy'; ... } 三元条件操作符,?:,接受三个操作数。它对第一个操作符在布尔上下文中求值,并在 结果为 真时对第二个操作数求值,否则求值第三个: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_04.html[2011/2/21 21:22:07] my $truthiness = $value ? 'true' : 'false'; ! 和 not 操作符返回其操作数的逻辑反值。not 的优先级比 ! 低。它们是前 缀操作符。 xor 操作符是中缀操作符,对其操作数进行异或操作。 按位操作符 按位操作符在位级别按数值形式处理它们的操作数。这些操作符在大多数 Perl 5 程序中 并不常 见。它们由左移(<<)、右移(>>)、按位与(&)、按位或(|)、 以及按位异或(^)以及它 们“就地”变种(&=、|=、^=、<<= 和 >>=) 组成。 特殊操作符 自增操作符有一个特例。如果任何变量在数值上下文中使用过(强å¶è½¬æ¢çç¼å 操作符将增加它的数值部分。如果这个变量明显地是一个字符串(且尚未在数值上下文中求 值)则此字符串将会带进位地自增,因此 a 增加为 b、zz 增为 aaa,以及 a9 增为 b0。 my $num = 1; my $str = 'a'; $num++; $str++; is( $num, 2, 'numeric autoincrement should stay numeric' ); is( $str, 'b', 'string autoincrement should stay string' ); no warnings 'numeric'; $num += $str; $str++; is( $num, 2, 'adding $str to $num should add numeric value of $str' ); is( $str, 1, '... but $str should now autoincrement its numeric part' ); 重复操作符(x)是一个中缀操作符。在列表上下文中,它的行为按它的第一个操作数改变。 当给与列表时,它的求值结果是一个重复列表,次数由第二个操作数给出。当给以一标量,它 产生的值是将第一个操作数的字符串部分重复拼接到自身,次数由第二个操作数给出。在标量 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_04.html[2011/2/21 21:22:07] 上下文中操作符总是产生重复拼接合适次数的字符串。 例如: my @scheherazade = ('nights') x 1001; my $calendar = 'nights' x 1001; is( @scheherazade, 1001, 'list repeated' ); is( length $calendar, 1001 * length 'nights', 'word repeated' ); my @schenolist = 'nights' x 1001; my $calscalar = ('nights') x 1001; is( @schenolist, 1, 'no lvalue list' ); is( length $calscalar, 1001 * length 'nights', 'word still repeated' ); 范围 操作符(..)是一个中缀操作符,它在列表上下文中产生一个列表: my @cards = ( 2 .. 10, 'J', 'Q', 'K', 'A' ); 它可以产生简单的、递增的范围(无论是整数还是自增字符串),但是它不能感知模式 和更复 杂的范围。 在布尔上下文中,范围操作符变为 翻斗 (flip-flop)操作符。如果它的左操作数为假, 这个操 作符返回假值,然后当它的右操作符一直为真时返回真。因此你可以这样引述一封格 式迂腐邮 件的正文: while (/Hello, $user/ .. /Sincerely,/) { say "> $_"; } 逗号 操作符(,)是一个中缀操作符。在标量上下文中它先求值它的左操作数然后返回 对其右 操作数求值的结果。在列表上下文中,它按从左到右的顺序对两边的操作数求值。 胖逗号操作符(=>)的行为与此类似,除了它自动对用做其左操作数的裸字加上引号。 (参见 åå¸)。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 函数 Perl 中的 函数(或者 子过程),是行为的一个离散又封闭的单元。它有也可能没 有名称。它 处理也可能不处理外来信息。它产出也可能不产出信息。它代表了一种类型的控 制流程,即程 序的执行过程转到源代码中的另一个点上。 函数是 Perl 5 中抽象和封装以及可重用代码的首选机制;其余不少机制是基于函数这一理 念建 立的。 函数 可以使 sub 关键字定义一个函数: sub greet_me { ... } 现在起 greet_me() 可以在程序的其他任何地方被调用,假如本符号────即函数名────是 可见的。 你不必在声明函数时 定义 它。你可以使用这种 预先声明 的方式告知 Perl 你希望此 函数存在 的意图,接着延后它的定义: sub greet_sun; 你不必在使用 Perl 5 函数之前声明它,除非它修改了语法分析器分析它的 方式。参见 å±æ§。 函数调用 要调用一个函数,请在源代码中提及它的名称并将可选的参数列表传递给它: greet_me( 'Jack', 'Brad' ); file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] greet_me( 'Snowy' ); greet_me(); 如果你的程序在启用 strict 后运行正常,则你 通常 可以忽略用于分组参数的括号,但 是它们 会将代码意图清晰地展现给给语法分析器────以及更重要的────阅读者。 当然,你可以将多种 类型 的参数传递给函数: greet_me( $name ); greet_me( @authors ); greet_me( %editors ); ……但也请参考引用 å¼ç¨ 一小节以获取更详细的内容。 函数参数 在函数内部,所有参数存在于一个数组中,即 @_。如果 $_ 对应英语单词 it(“它”), 那么 @_ 对应的就是单词 them(“它们”)。Perl 将所有传入的参数 展开 为单个列表。 函数必须自行将 参数解开为所需变量或着直接操作 @_: sub greet_one { my ($name) = @_; say "Hello, $name!"; } sub greet_all { say "Hello, $_!" for @_; } @_ 表现得和 Perl 中其他数组一样。你可以用下标引用单个元素: sub greet_one_indexed { my $name = $_[0]; say "Hello, $name!"; # or, less clear file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] say "Hello, $_[0]!"; } 你也可以 shift、unshift、push、pop、splice 并且在 @_ 上使用 列表分片。在函数内部,shift 和 pop 隐含地对 @_ 进行操作,就像在函数外 操作 @ARGV 一样: sub greet_one_shift { my $name = shift; say "Hello, $name!"; } 虽然起初写下 shift @_ 看上去更清晰,但利用 shift 的隐含操作数是 Perl 5 中 的惯用语。 注意从 @_ 中赋值一个标量变量需要 shift、或者对 @_ 的下标访问、再或者左 值列表上下文括 号。否则,Perl 5 会高兴地替你在标量上下文中对 @_ 求值,并将参数 个数赋值给此标量: sub bad_greet_one { my $name = @_; # buggy say "Hello, $name; you're looking quite numeric today!" } 多参数的列表赋值通常比多行 shift 要来得清爽。请比较: sub calculate_value { # multiple shifts my $left_value = shift; my $operation = shift; my $right_value = shift; ... } ……和: sub calculate_value { my ($left_value, $operation, $right_value) = @_; ... } file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 偶尔会需要提取 @_ 中的部分参数并把其余的传递给另外函数: sub delegated_method { my $self = shift; say 'Calling delegated_method()' $self->delegate->delegated_method( @_ ); } 主导用法就是仅在函数需要访问单个参数使用 shift,访问多个参数时使用列表赋值。 说明性的参数处理方法,请参见如下 CPAN 模块:signatures、Method::Signatures、 MooseX::Method::Signatures。 参数展开 参数展开为 @_ 发生在调用方。把哈希作为参数传递给函数将产生一个键值对列表: sub show_pets { my %pets = @_; while (my ($name, $type) = each %pets) { say "$name is a $type"; } } my %pet_names_and_types = ( Lucky = > 'dog', Rodney = > 'dog', Tuxedo = > 'cat', Petunia = > 'cat', ); show_pets( %pet_names_and_types ); show_pets() 函数的工作原理就是因为 %pet_names_and_types 哈希展开为列表 'Lucky', 'dog', 'Rodney', 'dog', 'Tuxedo', 'cat', 'Petunia', 'cat'。show_pets() 函数内的哈希赋值工作原理基本上就和更为明确 的 %pet_names_and_types 赋值一样。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 通常很有用,但是当你将一部分参数作为标量传递、一部分参数作为展开列表传递时,你 必须 明确你的目的。如果你想编写一个 show_pets_of_type() 函数,它的一个参数是 要显示的宠物的 类 型,你必须将其作为 第一个 参数(或使用 pop 把它从 @_ 的末尾移出): sub show_pets_by_type { my ($type, %pets) = @_; while (my ($name, $species) = each %pets) { next unless $species eq $type; say "$name is a $species"; } } my %pet_names_and_types = ( Lucky = > 'dog', Rodney = > 'dog', Tuxedo = > 'cat', Petunia = > 'cat', ); show_pets_by_type( 'dog', %pet_names_and_types ); show_pets_by_type( 'cat', %pet_names_and_types ); show_pets_by_type( 'moose', %pet_names_and_types ); 参数吸入 就像所有对集合的左值赋值,在函数内对 %pets 的赋值会 吸入 所有 @_ 中剩下 的值。如果 $type 参数位于 @_ 的末尾,Perl 将试图将奇数个元素赋值给哈希,并 会产生警告。你 可以 绕过它: sub show_pets_by_type { my $type = pop; my %pets = @_; ... } ……但会以清晰为代价。当然,同样的原理也适用于赋值给一个数组并将其作为参数。在传递 集合参数时,避免展开和吸入,请参考“引用”(å¼ç¨)一节。 别名 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 一个 @_ 的有用特性可能会使得那些粗心的人感到惊讶:在你将解开 @_ 为属于自己 的变量之 前,它包含了传入参数的别名。这个行为最容易用如下例子说明: sub modify_name { $_[0] = reverse $_[0]; } my $name = 'Orange'; modify_name( $name ); say $name; # prints egnarO 如果你直接修改了 @_ 中的某个元素,你将同时对原始参数进行直接修改。请谨慎些。 名称空间 每一个函数存在于某名称空间中。位于未声明名称空间中的函数函数────即,没有在一条 明确的 package ... 语句之后声明的函数────存在于 main 名称空间。你可以在声 明时为函数 指定当前之外的名称空间: sub Extensions::Math::add { ... } 任何符合包命名规则的函数名前缀会创建此函数并将其插入合适的名称空间中,而非 当前的名 称空间。由于 Perl 5 中包可以在任何时刻被修改,你甚至可以在名称空间 未存在前、或该函数 已在目标名称空间中声明时这样做。 你仅可以在同一名称空间下为某名称声明一个函数。否则,Perl 5 将就子过程重定义 发出警 告。如果你确实想 替换 一个已经存在的函数,可以通过 no warnings 'redefine' 来禁用这类警告。 通过使用完全限定名称,你可以在其他名称空间内调用函数: package main; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] Extensions::Math::add( $scalar, $vector ); 名称空间中的函数对外部是 可见的,因为它们可以被直接地引用,但仅在它们可以在所 定义的 名称空间内使用其短名时称其为 可调用────除非你通过导入导出机制使这些函数 在当前名 称空间内可用(导åºï¼âexportâï¼)。 导入 当使用 use 关键字加载模块时(模å),Perl 自动调用所提供模块的 import() 方法。带有过程式 接口的模块可以提供它们自己的 import(),以使部分或全部经过定义的符 号在调用者的名称空 间中可用。所有 use 语句内模块名后的参数会传递给模块的 import() 方法。即: use strict; ……加载 strict.pm 模块并不带参数地调用 strict->import(),且: use strict 'refs'; use strict qw( subs vars ); ……加载 strict.pm 模块,调用 strict->import( 'refs' ),接着再调用 strict->import( 'subs', vars' )。 你可以直接调用某模块的 import() 方法。前面的代码示例等价于: BEGIN { require strict; strict->import( 'refs' ); strict->import( qw( subs vars ) ); } 注意,use 关键字向这些语句添加了一个隐式的 BEGIN 块使得 import() 调用 在语法分析器编译完 整个语句后 立即 执行。这样便保证在编译后续部分时所有导入符 号可见。否则,任何从其他 模块导入而未在当前文件定义的函数将看上去像是为声明的裸字 使 strict 发出抱怨。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 报告错误 在函数之内,你可以通过 caller 操作符得到有关本次调用的上下文信息。如果不加 参数,它返 回三个元素的列表,包括调用包的名称、包含本次调用的文件名、本次调用发 生的包内行号: 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 将按给出的层数回查调用 者的调用者 的调用者,并提供该次调用的相关信息。换句话说,如果 show_call_information() 中用到 caller(0),它将收到来自 main() 的调用信息。如果它使用 caller(1), 则会收到程序开始之初的调 用信息。 提供此可选参数可以让你检查调用者的调用者,同时它也会返回更多值,包括函数 的名称,和 调用的上下文: sub show_call_information { my ($package, $file, $line, $func) = caller(0); say "Called $func from $package in $file at $line"; } 标准 Carp 模块使用这个技巧来增强错误报告和函数警告的效果,其中的 croak() 抛出由调用者在 某文件某行报告的异常。当在库代码中代替 die 使用,croak() 会 因不正确的使用方法而就地抛 出异常。Carp 中的 carp() 函数从文件中发出警告并 报告有问题的行号(产çè¦å)。 此行为在验证参数或函数先决条件时候最为有用,例如意图指出调用代码的错误时: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] use Carp 'croak'; sub add_two_numbers { croak 'add_two_numbers() takes two and only two arguments' unless @_ == 2; ... } 验证参数 防御性编程通常得益于针对参数类型和值的检查,以便做出后续处理。Perl 5 为此 提供了一些 默认内置机制(别指望 åå 对此有所帮助)。你可以通过在 标量上下文中对 @_ 求值来检查传递 给函数的参数 个数 是否正确: sub add_numbers { croak "Expected two numbers, but received: " . @_ unless @_ == 2; ... } Perl 面向操作符的类型转换(参见“上下文哲学”一节 ä¸ä¸æ)使得类型 检查更为困难。如果你 确实想变得更加严格,请考虑 CPAN 模块 Params::Validate。 高级函数 函数也需看上去简单,但你可以做的还有很多很多(参见闭包 éå 和匿名函数 å¿åå½æ° 以获得 更多细节)。 上下文认知 Perl 5 内置函数了解你是在空、标量还是列表上下文中调用它们。你编写的函数也可以知 晓它 们各自的调用上下文。被错误命名的 (footnote: 参见 perldoc -f wantarray 以确认) wantarray 关键字 返回 undef 表明空上下文,假值表明标量上下文,真值则表明列表上下文。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 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(); 这在避免可能产生昂贵返回值的函数在空上下文中返回结果时很有用。一些惯用语函数在列 表 上下文中返回列表并在标量上下文中返回数组引用(或列表中的第一个元素)。即便如此, 也 没有什么针对使用 wantarray 与否的好建议;一些时候还是单独编写返回所需类型值 的函数来得 清楚。 话虽如此,CPAN 上的 Robin Houston 的 Want 和 Damian Conway 的 Contextual::Return 发行版 为编写强大而可用的接口提供了种种可能。 递归 在 Perl 中每一次函数调用都创建一个新的 调用帧。这是一种代表调用本身的内部数据 结构: 实际上就是,传入参数、返回点、步入此调用点前的所有程序控制流程。它同样捕获 了本次函 数调用的特定词法环境。这意味着一个函数可以 递归,它可以调用自己。 递归是一个带迷惑性但又简单的概念,如果之前你没有碰到过,它看上去令人望而却步。考 虑 你如何在一个经过排序的数组中查找某个元素。你 可以 迭代整个数组中的每个元素, 在其中 查找目标,但是平均起来,你每次只需查找数组中半数元素。 另一中方法就是将数组分开两半。选取中间值,比较,看接下去是否应该将较小的那部分进 行 折半还是对较大部分,接着继续。你可以用循环来编写上述算法,或者,你可以通过递归 让 Perl 接管所有状态和跟踪所需变量。看上去可能会像这样: use Modern::Perl; use Test::More tests => 8; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] my @elements = ( 1, 5, 6, 19, 48, 77, 997, 1025, 7777, 8192, 9999 ); ok elem_exists( 1, @elements ), 'found first element in array'; ok elem_exists( 9999, @elements ), 'found last element in array'; ok ! elem_exists( 998, @elements ), 'did not find element not in array'; ok ! elem_exists( -1, @elements ), 'did not find element not in array'; ok ! elem_exists( 10000, @elements ), 'did not find element not in array'; ok elem_exists( 77, @elements ), 'found midpoint element'; ok elem_exists( 48, @elements ), 'found end of lower half element'; ok elem_exists( 997, @elements ), 'found start of upper half element'; sub elem_exists { my ($item, @array) = @_; # 如果没有元素可以查找则跳出递归 return unless @array; # 折半,如果有奇数个元素则向下取整 my $midpoint = int( (@array / 2) - 0.5 ); my $miditem = $array[ $midpoint ]; # 如果当前元素就是目标则返回真 return 1 if $item == $miditem; # 如果只剩一个元素则返回假 return if @array == 1; # 划分数组并用较小分支递归 return elem_exists( $item, @array[0 .. $midpoint] ) if $item < $miditem; # 划分数组并用较大分支递归 return elem_exists( $item, @array[$midpoint + 1 .. $#array] ); } 这并非是搜索已排序列表的最好的算法,但它演示了递归。再次说明,你 可以 按过程 式方法 编写这段代码,但是某些算法采用递归时更清晰。 词法相关 每一次对函数的新调用创建自身词法作用域的 实例。拿递归为例,虽然 elem_exists() 的声明为 $item、@array、$midpoint 和 $miditem 创建的单独的词法作用域,对 elem_exists() 的每一次 调用,即 便是递归,也将词法变量的值分开存放。你可以通过 添加如下调试代码来展示这一特性: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] use Carp 'cluck'; sub elem_exists { my ($item, @array) = @_; cluck "[$item] (@array)"; # other code follows ... } 这段代码的输出显示了 elem_exists() 不仅可以安全地调用自身,而且词法变量之间也不会 冲 突。 尾部调用 递归的一个 缺点 就是你必须将返回条件编写正确,否则函数将无限次调用自身。这 就是为什 么 elem_exists() 函数拥有若干 return 语句。 当它检测到失控的递归时,Perl 提供了有用的警告:Deep recursion on subroutine。 限制是 100 次 递归调用,对某些情况可能太少而其他情况下又太多。你可以在递归调用作 用域内通过 no warnings 'recursion' 来禁用这一警告。 因为对函数的每一次调用都需要新建调用帧,外加自身词法变量值的存储,高度递归的代码 比 迭代用去的内存更多。一个名为 尾部调用消除 的特性有助解决此问题。 尾部调用消除也许在编写递归代码时最为明确,但它在含尾部调用的任何情况下都有用处。 许 多编程语言实现支持自动进行这个过程。 尾部调用 就是调用一个函数然后直接返回这个函数的结果。下列几行: # split the array down and recurse return elem_exists( $item, @array[0 .. $midpoint] ) if $item < $miditem; # split the array up and recurse return elem_exists( $item, @array[$midpoint + 1 .. $#array] ); file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] ……直接返回递归调用 elem_exists() 的结果,就是尾部调用消除的候选代码。这个消除 的过程 避免了先将结果返回给本次调用再返回父调用。取而代之的是它直接将结果返回给父 调用。 Perl 5 支持手动进行尾部调用消除,但如果你觉得高度递归的代码可以很好地利用尾部调 用消 除的优势,那么 Yuval Kogman 的 Sub::Call::Tail 值得研究。Sub::Call::Tail 对非递归的尾部调用 同样适用: use Sub::Call::Tail; sub log_and_dispatch { my ($dispatcher, $request) = @_; warn "Dispatching with $dispatcher\n"; return dispatch( $dispatcher, $request ); } 本例中,你可以将 return 改为新的 tail 关键字并无须做出函数层面的修改(它 更加清晰,也提 升了性能): tail dispatch( $dispatcher, $request ); 如果你确实 必须 手动进行尾部调用消除,则你可以使用 goto 关键字的一个特殊形 式。不像通 常导致通心粉式代码的那种形式,goto 的函数形式将当前函数调用替换为对 另一个函数的调 用。可以使用函数名称或引用。如果你想传递不同的参数,则必须手动设置 @_: # 划分数组并递归(较小部分) if ($item < $miditem) { @_ = ($item, @array[0 .. $midpoint]); goto &elem_exists; } # 划分数组并递归(较大部分) else { @_ = ($item, @array[$midpoint + 1 .. $#array] ); goto &elem_exists; } 相比较而言,代码 CPAN 版本的清爽程度可见一斑。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 缺陷和设计失误 所有 Perl 5 函数的特性并非都是有用的。特别的,原型 åå 很少完成你想让它做的事。 它们有 它们的用途,但是除了个别情况下,你可以避免使用它们。 从早期 Perl 版本直接接手,Perl 5 仍支持旧式函数调用。虽然现在你可以直接通过名称调用函 数, 但前期版本的 Perl 要求你用前置的 & 字符调用函数。Perl 1 要求你使用 do 关键字: # 过时风格;避免使用 my $result = &calculate_result( 52 ); # Perl 1 风格 my $result = do calculate_result( 42 ); # 疯狂的混杂调用,真的应该避免 my $result = do &calculate_result( 42 ); 相比退化的语法,除了看上去是噪音外,前置 & 形式的其他行为偶尔会使你惊奇。第一, 它 禁用原型检查(好像这常常很重要)。第二,如果你不明确地传递参数,它会 隐式地 将 @_ 的 内容不加修改地传给函数。两者都可导致意外的结果。 最后一个缺陷来自于函数调用时不写括号。Perl 5 的语法分析器使用若干启发式方法解决 裸字 的歧义以及传递给函数参数的个数,但是这些猜测偶尔会出错。虽然移去无关括号通常 很明 智,但请比较下列两行代码的可读性: ok( elem_exists( 1, @elements ), 'found first element in array' ); # 警告;包含一细微的缺陷 ok elem_exists 1, @elements, 'found first element in array'; 第二行中细微的缺陷就是对 elem_exists() 的调用将本应作为 ok() 第二个参数的 测试描述贪婪地 吞入。因为 elem_exists() 的第二个参数是吸入式的,这种情况可能一 直不为人知直到 Perl 产生 有关将非数字(测试描述,它不能被转换为数字)和数组中某元 素进行比较的警告。 这诚然是一个特例,但说明了正确地加括号可以使代码清晰并使得细微的缺陷在阅读者面前 展 露无遗。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 作用域 Perl 中的 作用域 指的是符号的生存期限和可见性。在 Perl 中,任何有名字的事物 (比如:变 量、函数)都有作用域。作用域的制定有助于强制 封装————将相关的概念 放在一块并防止 它们的泄漏。 词法作用域 在现代的 Perl 编程环境中,最常见的作用域形式是词法作用域。Perl 编译器在编译期解 决此 类作用域。这类作用域在你 阅读 一段程序时是可见的。 要创建一个新的词法作用域,可以编写一个由大括号分隔的代码块。这个代码块可以是一个 裸 块、或循环结构主体中的块、一个 eval 块、或是其他任何没有用引号引起的块: # 外层词法作用域 { package My::Class; # 内层词法作用域 sub awesome_method { # 最内层词法作用域 do { ... } while (@_); # 内层词法作用域的兄弟 for (@_) { ... } } } 词法作用域管理由 my 声明的变量的可见性;这些变量被称作 词法 变量。在某块内 声明的词法 变量对块本身及嵌套块可见,但对此块兄弟或外层块是不可见的。因此,在如下 代码中: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] # 外层词法作用域 { package My::Class; my $outer; sub awesome_method { my $inner; do { my $do_scope; ... } while (@_); # 兄弟内层词法作用域 for (@_) { my $for_scope; ... } } } …… $outer 对全部四个作用域都是可见的。$inner 在方法内部、do 代码块和 for 循环内都是可 见。$do_scope 仅在 do 代码块内可见,$for_scope 仅 在 for 循环内可见。 在内层词法作用域里声明一个和外部词法作用域同名的词法变量将隐藏,或者说 遮盖 外层的词 法变量: { my $name = 'Jacob'; { my $name = 'Edward'; say $name; } say $name; } 这段程序在 Edward 之后接着打印 Jacob。即使在单一词法作用域内重新声明一个有 着相同名称相 同类型的词法变量会产生一个警告,在嵌套作用域内遮盖一个词法变量则不会; 这是一个词法 遮盖的特性。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 词法遮盖可能会意外地发生,但是通过限制变量的作用域和嵌套的层数————这是一个良好 的设计————你可以减小此风险。 词法变量的声明有着自身的微妙性。例如,一个用作 for 循环迭代器变量的词法变量的 作用域 是循环代码块 内部。它对循环体外部是不可见的: my $cat = 'Bradley'; for my $cat (qw( Jack Daisy Petunia Tuxedo )) { say "Iterator cat is $cat"; } say "Static cat is $cat"; 类似地,given 语法结构在其块内部创建了 词法话题 (近似于 my $_): $_ = 'outside'; given ('inner') { say; $_ = 'whomped inner'; } say; ……先不管块内对 $_ 的赋值。你可以显式地词法化话题,虽然这在考虑动态作用域时 更为有 用。 最后,词法作用域助于构造闭包(éå)。注意不要意外地创建闭包。 “Our”作用域 在给出的作用域内,你可以用 our 关键字声明一个包变量的别名。就像 my 一样, our 强制了别 名的词法作用域。完全限定名称随处可用,但是词法别名仅在自身作用 域内可见。 对 our 的最好使用就是声明那些你 不得不 有的变量,诸如 $VERSION。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 动态作用域 动态作用域在可见性规则上类似于词法作用域,和在编译期确定作用域相反,确定作用域 的过 程沿着调用上下文发生。考虑如下例子: { our $scope; sub inner { say $scope; } sub main { say $scope; local $scope = 'main() scope'; middle(); } sub middle { say $scope; inner(); } $scope = 'outer scope'; main(); say $scope; } 这段程序由声明一个 our 变量————$scope,和三个函数开始。它于赋值 $scope 并调用 main() 处 结束。 在 main() 内,这个程序打印出 $scope 当前的值,即 outer scope,接着用 local 局部化了这个变量。 这样,符号在当前词法作用域内的可见性 连同 该符号 在此词法作用域内调用的函数内部的可 见性一同被改变。因此,$scope 在 middle() 和 inner() 两者的代码体包含 main() scope 这个值。在 main() 返回后———— 在该点流程退出了那个代码块,块中包含 local 后的 $scope,Perl 恢复了 变量的 原始值。最后的 say 再次打印出 outer scope。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 虽然此变量在这些作用域内都是 可见的,但变量 值 的变化却取决于用 local 局部化和赋值操 作。这个特性既狡猾又微妙,但改变那些神奇变量时还是相当有用的。 包变量和词法变量在可见性上的区别用 Perl 5 自身存储这些变量的机制来解释,就会变得 非常 明显。词法变量被存放在附着于作用域的 词法板 中。每次进入到词法作用域中都需 要 Perl 来 创建一个包含变量值的新的专属词法板。(这就是为什么函数可以调用自身而不会 弄坏现有同 名变量的值。) 包变量的存储机制称为符号表。每个包都含有一个单独的符号表,并且每个包变量在其中占有 一个条目。你可以用 Perl 检查并修改这个符号表;这就是导入的工作原理(导å¥)。 这也是 为什么你只能用 local 局部化全局和包变量而非词法变量。 用 local 局部化若干神奇变量的做法很常见。举例来说,$/,输入记录分隔符,决 定了 readline 操作从一个文件句柄读入数据的量。$!,系统错误变量,包含了最近 一次系统调用的错误 号。$@,Perl eval 错误变量,包含最近一次 eval 操作中 发生的任何错误。$|,自动冲洗变量, 决定了 Perl 是否应该在每次写操作之后,自动 冲洗当前由 select 选定的文件句柄。 这些全部是特殊的全局变量;在最窄小的作用域内用 local 局部化它们可以避免其余代 码远距 离修改所用全局变量而引发的问题。 “State”(状态)作用域 最后一种作用域类型的年纪和 Perl 5.10 一样。这是 state 关键字的作用域。状态作用域 类似词 法作用域的地方在于,它声明一个词法变量,但是该变量的值只初始化 一次,随后便 一直保 持: sub counter { state $count = 1; return $count++; } say counter(); say counter(); say counter(); file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 在第一次调用计数函数的地方,$count 从未被初始化过,因此 Perl 执行赋值操作。这个 程序打 印出 1、2 和 3。如果你把 state 改为 my,则会打印 1、1、1。 你也可以使用传入参数来初始化 state 变量的值: sub counter { state $count = shift; return $count++; } say counter(2); say counter(4); say counter(6); 虽然这段代码粗粗一读让人感觉输出应该是 2、4、6,但实际上是 2、3 和 4。第一次对 counter 子过程的调用设置了 $count 变量。后续调用不会改变它的 值。这个行为是意料之中并如同记载 的一样,但这种实现方式会导致令人惊讶的结果: sub counter { state $count = shift; say 'Second arg is: ', shift; return $count++; } say counter(2, 'two'); say counter(4, 'four'); say counter(6, 'six'); 程序中计数器按预想地打印出 2、3 和 4,但是成为 counter() 调用的第 二个参数却依次为 two、4 和 6————并非因为这些整数确实变成了传递的第二 个参数而是因为第一个参数的 shift 只发 生在第一次调用 counter() 的时候。 state 在创建默认值和准备缓存时非常有用,但在使用它前,请确信你已经理解了它 的初始化行 为。 匿名函数 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 匿名函数 就是没有名字的函数。它的行为和那些有名字的函数一样————你可以调用它, 也 可以把参数传递给它,从其中返回结果,复制引用给它————它可以做到任何具名函数可以 做到的事。区别就是它没有名字。你可以用引用来处理匿名函数(参见引用 å¼ç¨ 和函数引用 å½æ°å¼ç¨)。 声明匿名函数 你不能独立地声明一个匿名函数;你必须在构造完成后将它复制给一个变量,或直接调用它, 再就是将它作为参数传递给另一个函数,显式或是隐式地。使用 sub 关键字而不加命名 将显式 地创建一个匿名函数: my $anon_sub = sub { ... }; 一个名为 分派表 的 Perl 5 惯用语,使用哈希将输入和行为关联起来: my %dispatch = ( plus => sub { $_[0] + $_[1] }, minus => sub { $_[0] - $_[1] }, times => sub { $_[0] * $_[1] }, goesinto => sub { $_[0] / $_[1] }, raisedto => sub { $_[0] ** $_[1] }, ); sub dispatch { my ($left, $op, $right) = @_; die "Unknown operation!" unless exists $dispatch{ $op }; return $dispatch{ $op }->( $left, $right ); } dispatch() 函数以 (2, 'times', 2) 的形式接受参数并且返回对操作求值后的 结果。 你可以在使用函数引用的地方用匿名函数。对于 Perl 来说,两者是等价的。没有什么 迫使 使 用匿名函数来进行这些数学操作,但对这类短小的函数来说,写成这样也没 有什么不好。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 你可以将 %dispatch 重写为: my %dispatch = ( plus => \&add_two_numbers, minus => \&subtract_two_numbers, # ……等等 ); sub add_two_numbers { $_[0] + $_[1] } sub subtract_two_numbers { $_[0] - $_[1] } ……相比因语言特性而做出这样的决定,到不如说是出于对代码可维护性,或是安全,再或 是 团队编程风格的考虑。 因间接通过分派表而带来的一个好处是,它对未经验证调用函数提供了一定的保护————调 用 这些函数安全多了。如果你的分派函数盲目地假设那些字符串直接对应到某操作应该调用的 函数名,那么可以想象通过将 'Internal::Functions::some_malicious_function' 修 整为操作名,一个恶 意用户可以调用任何其他名称空间的任何函数。 你也可以在将匿名函数作为参数传递的过程中创建它们: sub invoke_anon_function { my $func = shift; return $func->( @_ ); } sub named_func { say 'I am a named function!'; } invoke_anon_function( \&named_func ); invoke_anon_function( sub { say 'I am an anonymous function' } ); 匿名函数名称 存在可以鉴别一个引用是指向具名函数还是匿名函数的特例————匿名函数(正常情况下) 没 有名称。这听上去很玄乎很傻也很明显,内省可以现实这个区别: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] package ShowCaller; use Modern::Perl; sub show_caller { my ($package, $filename, $line, $sub) = caller(1); say "Called from $sub in $package at $filename : $line"; } sub main { my $anon_sub = sub { show_caller() }; show_caller(); $anon_sub->(); } main(); 结果可能令人惊讶: Called from ShowCaller::main in ShowCaller at anoncaller.pl : 20 Called from ShowCaller::__ANON__ in ShowCaller at anoncaller.pl : 17 输出第二行中的 __ANON__ 展示了匿名函数没有 Perl 可以识别的名称。即使这样会难以调试, 但 还是有方法可以绕过它的隐匿性。 CPAN 模块 Sub::Identify 提供了一系列有用的函数来对传入函数引用的名称进行检查。 sub_name() 便是不二之选: use Sub::Identify 'sub_name'; sub main { say sub_name( \&main ); say sub_name( sub {} ); } main(); 正如你想象的那样,名称的缺少使得调试匿名函数更加复杂。CPAN 模块 Sub::Name 可以 帮助 你。它的 subname() 函数允许你将名称附加在匿名函数上: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 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 {} ); 这个程序产生如下输出: __ANON__ pseudo-anonymous pseudo-anonymous __ANON__ 注意这两个引用都指向同一个底层函数。用 $anon 调用 subname() 并且将结果 返回给 $named 将修改 该函数,因此其他指向这个函数的引用将见到相同的名字,即 pseudo-anonymous。 隐式匿名函数 所有这些匿名函数声明都是显式的。Perl 5 通过原型(åå)允许隐式匿名函数。 虽然这个特性 的存在名义上是为了让程序员为诸如 map 和 eval 编写自己的语法,一 个有趣的例子就是对 延迟 函数的使用看上去不像函数那样。考虑 CPAN 模块 Test::Exception: 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'; lives_ok() 和 throws_ok() 都接受一个匿名函数作为它们的第一个参数。这段代码 等价于: throws_ok( sub { die "I croak!" }, qr/I croak/, 'die() should throw an exception' ); file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] lives_ok( sub { 1 + 1 }, 'simple addition should not' ); ……只不过更加易读罢了。 注意隐式版本中匿名函数最后的大括号后 没有 逗号。相比其他一些好用的语法来说, 有时候 这是一个令人疑惑的疙瘩,是 Perl 5 语法分析器古怪的好意。 这两个函数的实现都不关心你是用何种机制传递函数引用的。你也可以按引用传递一个具 名函 数: sub croak { die 'I croak!' } sub add { 1 + 1 } throws_ok \&croak, qr/I croak/, 'die() should throw an exception'; lives_ok \&add, 'simple addition should not'; ……但你 不 能将他们当作标量引用传递: sub croak { die 'I croak!' } sub add { 1 + 1 } my $croak = \&croak; my $add = \&add; throws_ok $croak, qr/I croak/, 'die() should throw an exception'; lives_ok $add, 'simple addition should not'; ……因为原型改变了 Perl 5 语法分析器解释这段代码的方式。在对 throws_ok() 或是 lives_ok() 的 调用进行求值时,它不能 100% 清楚地确定 $croak 和 $add 含 有 什么内容,因此它会产生一条错 误信息: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] Type of arg 1 to Test::Exception::throws_ok must be block or sub {} (not private variable) at testex.pl line 13, near "'die() should throw an exception';" 不提缺点,这个特性偶尔也有其用处。虽然通过将裸代码块提升为匿名函数带来了语法上 的清 晰,但请有节制地使用并小心编写好 API 文档。 闭包 你已经见过了函数(å½æ°)和作用域(ä½ç¨å)是如何工作的。你知道了每次 控制流程进入函 数后,该函数将得到代表本次调用的词法作用域的新环境。你现在可以使 用函数引用(å¼ç¨) 以及匿名函数(å¿åå½æ°)了。 你已经学到了理解闭包所需的全部知识。 Mark Jason Dominus 的著作 Higher Order Perl 是有关第一等函数、闭包和利用它们 完成令人 惊奇的事物的公认参考书。你可以在 http://hop.perl.plover.com/ 在线阅读 这本书。 创建闭包 闭包 是封闭于外部词法环境之上的函数。你也许早已创建并使用了闭包,只是没有意识 到: { package Invisible::Closure; my $filename = shift @ARGV; sub get_filename { return $filename; } } 这段代码的行为平淡无奇。你也许并未觉察有何特殊之处。显然 函数 get_filename() 可以在词法 层面见到 $filename。作用域就是这样工作的!闭包还可以封闭于 转瞬即逝 词法环境上。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 假设你想迭代一个列表,而不愿自行管理迭代器。你可以创建一个返回函数的函数,当它被 调 用时,将返回迭代过程中的下一个元素: sub make_iterator { my @items = @_; my $count = 0; return sub { return if $count == @items; return $items[ $count++ ]; } } my $cousins = make_iterator(qw( Rick Alex Kaycee Eric Corey )); say $cousins->() for 1 .. 5; 即使 make_iterator() 已经返回,但此匿名函数仍将引用词法变量 @items 和 $count。它们的值会一直 保持(å¼ç¨è®¡æ°)。这个匿名函数,存放于 $cousins, 在调用 make_iterator() 的特别词法环境 中闭合于这些值上。 很容易演示词法环境是独立于对 make_iterator() 的调用: my $cousins = make_iterator(qw( Rick Alex Kaycee Eric Corey )); my $aunts = make_iterator(qw( Carole Phyllis Wendy )); say $cousins->(); say $aunts->(); say $cousins->(); say $aunts->(); 因为对 make_iterator() 的每次调用都为词法量创建分离的词法环境,匿名子过程 就此产生且返回 唯一的词法环境。 因为 make_iterator() 并非按值或按引用返回这些词法量,其他闭包外的 Perl 代 码无法访问它 们。他们和其他词法量一样被有效地封装。 多个闭包可以闭合于同一批词法变量上,这是一个偶尔会用到的惯用语,可以为本将全 局可见 的变量提供一个更好的封装: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] { my $private_variable; sub set_private { $private_variable = shift } sub get_private { $private_variable } } ……但注意你不可以 嵌套 具名函数。具名函数有着包全局作用域。任一在嵌套函数间 共享的 词法变量,在外层函数销毁它的第一层词法环境时,将变为非共享 (footnote: 如果你还是 不太 清楚,可以想像一下它的实现)。 CPAN 模块 PadWalker 可以让你打破词法封装,但是所有利用该模块破坏你代码的人 应有权在没 有你帮助的情况下修复所有伴随产生的软件缺陷。 闭包的使用 闭包可以构造针对定长列表的高效迭代器,但是他们在迭代那些不适合直接引用其元素的 列表 时更胜一筹,那写列表不是一次性计算全部元素的代价过于昂贵,就是尺寸太大以至 于无法直 接塞进内存中去。 考虑一个按需创建 Fibonacci 序列的函数。不必递归地重计算这个序列,而应使用缓存并 惰性 地按需要求出各个元素: sub gen_fib { my @fibs = (0, 1, 1); return sub { my $item = shift; if ($item >= @fibs) { for my $calc ((@fibs - 1) .. $item) { $fibs[$calc] = $fibs[$calc - 2] + $fibs[$calc - 1]; } } return $fibs[$item]; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] } } 每次调用由 gen_fib() 返回的函数需提供一个参数,即 Fibonacci 序列的第 n 个 元素。该函数按 需要生成序列中所有前导值,缓存起来,并且返回所需的元素。它将延后 计算过程直到不得不 这样做。 如果你所需的全部就是计算 Fibonacci 数的话,这个方法也许太过复杂。然而,考虑到 函数 gen_fib() 可以变得惊人地通用:它初始化一个数组,用于缓存,执行一些定制 的代码来填充缓 存的各类值,并从缓存中返回已计算的结果。抽掉计算 Fibonacci 值的 部分,你可以使用这段 代码来实行所有带缓存的、惰性迭代器的行为。 换句话说,你可以提取出一个函数,generate_caching_closure(),并按该函数的方 式重写 gen_fib(): sub gen_caching_closure { my ($calc_element, @cache) = @_; return sub { my $item = shift; $calc_element->($item, \@cache) unless $item < @cache; return $cache[$item]; }; } sub gen_fib { my @fibs = (0, 1, 1); return gen_caching_closure( sub { my ($item, $fibs) = @_; for my $calc ((@$fibs - 1) .. $item) { $fibs->[$calc] = $fibs->[$calc - 2] + $fibs->[$calc - 1]; } }, file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] @fibs ); } 该程序的行为和以往一致,但对高阶函数和闭包的使用允许从 Fibonacci 序列的计算中有 效分 离出缓存的初始化过程。代码行为的定制————就此而言,gen_caching_closure() ————通过 传递高阶函数,带来了极大的抽象性和灵活性。 在某种意义上,你可以将内置的 map、grep 以及 sort 比作高阶函数,特别是 在你拿他们和 gen_caching_closure() 比较时。 闭包与部分应用 闭包除了抽象结构化细节外还可以做更多事。它允许你定制特定的行为。从某种意义上讲, 它 还可以 去掉 不必要的泛化。考虑一例接受若干参数的函数: sub make_sundae { my %args = @_; my $ice_cream = get_ice_cream( $args{ice_cream} ); my $banana = get_banana( $args{banana} ); my $syrup = get_syrup( $args{syrup} ); ... } 所有这些定制也许和你那位于购物中心内商品齐全的主力店正相称,但如果你在天桥附近有 一 小辆冰淇淋专卖车,那里只出售安在卡文迪什香蕉上的法式香草冰淇淋,那样在调用 make_sundae() 时,你必须每次都传递一个恒久不变的参数。 一个名为 部分应用 的技巧将一部分参数绑定给函数以便你可以在后续的调用过程中 填入其他 值。用闭包来模拟再简单不过了: my $make_cart_sundae = sub { return make_sundae( @_, ice_cream => 'French Vanilla', banana => 'Cavendish', file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] ); }; 现在不必调用 make_sundae() 了,你可以直接使用 $make_cart_sundae->() 并只将相关的参数传入,而 无需顾及忘传或错传不变量。 (footnote: 你还可以使用来自 CPAN 的 Sub::Install 把这个函数直 接安装到你的名称空间中。)。 状态 VS 闭包 闭包(éå)是除使用全局变量外,在函数调用间保证数据持续性的简易、有效 且安全的的方 法。如果你需要在具名函数间共享变量,你可以仅为这些函数的声明引入 一个新作用域 (ä½ç¨å): { my $safety = 0; sub enable_safety { $safety = 1 } sub disable_safety { $safety = 0 } sub do_something_awesome { return if $safety; ... } } 对安全性开关函数进行封装让这三个函数可以共享状态而不必将词法变量直接地暴露给外 部代 码。在外部代码可以更改内部状态时,这个惯用语可以很好的起到作用,但在状态仅 由单个函 数维护时,它就显得有些笨拙了。 假设你想统计一下你的冰淇淋小摊接待了多少客人。逢百的客人可以免费加料: { my $cust_count = 0; sub serve_customer { $cust_count++; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] my $order = shift; add_sprinkles($order) if $cust_count % 100 == 0) ... } } 这个方法 行得通,但是为单个函数创建新的词法作用域没有那么得必要,它带来了意外的 复杂 度。state 关键字允许你声明一个词法作用域变量,它的值在调用之间是连续的: sub serve_customer { state $cust_count = 0; $cust_count++; my $order = shift; add_sprinkles($order) if $cust_count % 100 == 0) ... } 你必须通过使用类似 Modern::Perl 的模块、feature 编译命令、或是要求 Perl 的版本必须新于 5.10(例如,use 5.010; 或 use 5.012;)来明确地启用这个特 性。 你也可以在匿名函数内使用 state,就像这个计数器的典型例子一样: sub make_counter { return sub { state $count = 0; return $count++; } } ……虽然没有什么明显的优势让人采用这种方式。 Perl 5.10 不推荐为之前版本所用的一种技术,通过它你可以有效地模拟 state。在 my 声明中使 用求值得假的后缀式条件表达式可以避免将一个词法变量 重新初始化 为 undef 或初始化过的 值。通过肆意使用这个实现方式,一个具名函数可以闭合于它 之前的词法作用域。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 现在,任何对改动词法变量声明的后缀条件表达式的使用都会因不推荐而产生警告。采用 这种 技巧时,很容易在无意中写出错误百出的代码;请在可能时用 state 代替,否则 使用闭包。避 免使用这个惯用语,但在遇到时也应该能读懂它: sub inadvertent_state { # 不推荐;请勿使用 my $counter = 1 if 0; ... } 属性 Perl 中的具名实体————变量和函数————可按 属性 的形式附加额外的元信息。属性是 名称(通常,也是值),它允许某种形式的元编程(代ç çæ)。 定义属性可能有点别扭,对其有效地使用是科学更是艺术。它们有充足的理由不在大多数 程序 中出现,虽然它们 可以 在代码维护和清晰度上带来引人注目的好处。 使用属性 用最简单的形式来说,属性是一个附加于变量或函数声明上的前置冒号标识符: my $fortress :hidden; sub erupt_volcano :ScienceProject { ... } 如果合适类型(分别是标量和函数)的属性处理器存在,这些声明将引发对名为 hidden 和 ScienceProject 的属性处理器的调用。如果合适的处理器不存在,Perl 会抛出编译 期异常。这些 处理器可以做 任何事。 属性可以包括一个参数列表;Perl 将它们作为一个常量字符串列表,即使它们可能会类似于 其 他值,如,数字或变量。来自 CPAN 的 Test::Class 模块很好地利用了这一参数形式: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] sub setup_tests :Test(setup) { ... } sub test_monkey_creation :Test(10) { ... } sub shutdown_tests :Test(teardown) { ... } Test 属性标识了包括测试断言的方法,并且可选地标识了意图执行的断言方法的数量。 虽然对 这些类的内省(åå°)可以发现合适的测试方法,当给出经良好设计的启发 式方法,:Test 属性 使得代码及其意图毫无歧义。 setup 和 teardown 参数允许测试类定义它们自己的支持方法而不必担心重名或其他 因继承或其余 设计上的考虑而引发的冲突。你 可以 强制施行某种设计,其中所有测试类 必须自行覆盖名为 setup() 和 teardown() 的方法,但是属性方式给与实现更多的灵 活性。 Catalyst Web 框架也采用属性来决定 Web 应用内方法的可见性和行为。 属性的缺点 属性确实有它们的缺点: 属性的正式编译命令(即 attributes)已经将其接口列为“实验性质”很多年了。 Damian Conway 的核心模块 Attribute::Handlers 简化了它们的实现。请在可能时采用 它而非 attributes; 任何声明属性处理器的模块必须 继承 Attribute::Handlers 以使处理器对 所有用到它们的包可 见 (footnote: 你也 可以 将它们存放在 UNIVERSAL,但这是对全局的污染, 还是一种糟糕的设 计。)。此缺点归根于 Perl 5 自身对属性的实现方式; 属性处理器生效于 CHECK 代码块,使得它们不适合用于那些修改语法分析及编 译顺序的项 目,例如 mod_perl; 任何提供给属性的参数是一列常量字符串。Attribute::Handlers 进行部分数据 转换,但偶尔 你需要禁用它。 属性中最差的一点就是它们会远距离产生奇怪的语法动作。给出一段使用属性的代码,你 能否 预言它的行为?良好而准确的文档可以帮助你,但如果一段看上去很无辜的词法变量 声明将此 变量的引用存放他处,则你对销毁该变量内容的期待将是错误的,除非你非常仔 细地阅读文 档。类似的,处理器可能会用一个函数包装另一个函数并在你不知情的情况下 在符号表里替换 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 了它————考虑一个自动调用 Memoize 的 :memoize 属性。 复杂的特性可以产生紧凑和惯用语化的代码。Perl 允许开发者实验多种设计以便找到他们 想法 的最佳表示。属性和其他高级 Perl 特性可以帮助你解决复杂的问题,但是他们也会混 淆原本 简单的代码意图。 大多数程序绝对不会用到这个特性。 AUTOLOAD 你没有必要为调用而定义 每一个 函数和方法。Perl 提供了一种机制,通过它你可以 截获不存 在方法的调用。这样你就可以只定义所需的函数或提供有趣的错误信息和警告。 考虑如下程序: #! perl use Modern::Perl; bake_pie( filling => 'apple' ); 当运行它时,Perl 将因调用未定义的函数 bake_pie() 而抛出一个异常。现在添加一个 名为 AUTOLOAD() 的函数: sub AUTOLOAD {} 除了错误不再出现,没有发生任何明显改变。在某个包中,名为 AUTOLOAD() 函数的出现 告诉 Perl 无论是正常分派或方法缺失时都调用它。可以将 AUTOLOAD() 稍作修改,显示 一个消息来演 示这一点: sub AUTOLOAD { say 'In AUTOLOAD()!' } AUTOLOAD 的基本功能 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] AUTOLOAD() 函数直接在 @_ 中接到传递给未定义函数的参数。你可以按喜好修改 这些参数: sub AUTOLOAD { # 将参数美化输出 local $" = ', '; say "In AUTOLOAD(@_)!" } 未定义函数的 名称 可以从伪全局变量 $AUTOLOAD 得到: sub AUTOLOAD { our $AUTOLOAD; # 将参数美化输出 local $" = ', '; say "In AUTOLOAD(@_) for $AUTOLOAD!" } our 声明(âOurâä½ç¨å)将此变量的作用域限制在 AUTOLOAD() 的代码体内。这个变量 包含了未定 义函数的完全限定名称。就此例来说,这个函数是 main::bake_pie。一个 常见的惯用语可以用来 去掉包名: sub AUTOLOAD { my ($name) = our $AUTOLOAD =~ /::(\w+)$/; # 将参数美化输出 local $" = ', '; say "In AUTOLOAD(@_) for $name!" } 最终,无论 AUTOLOAD() 返回什么,最初的调用都会收到: say secret_tangent( -1 ); sub AUTOLOAD { return 'mu' } 目前为止,这些例子只是截获了对未定义函数的调用。还有其他的路可走。 在 AUTOLOAD() 中重分派方法 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 面向对象编程中一个常见的模式就是将某方法 委托 或 代理 给另一个对象,通 常包含在前者内 或可以从中访问到。这是一个记录日志有趣且有效的方法: package Proxy::Log; sub new { my ($class, $proxied) = @_; bless \$class, $proxied; } sub AUTOLOAD { my ($name) = our $AUTOLOAD =~ /::(\w+)$/; Log::method_call( $name, @_ ); my $self = shift; return $$self->$name( @_ ); } 这段 AUTOLOAD() 的代码记录方法调用。它的神奇之处就在于一个简单的模式;它从 经 bless 的标 量引用中对被代理对象进行解引用,提取未定义方法的名称,然后调用被 代理对象中的方法, 传递参数给它。 在 AUTOLOAD() 中生成代码 那个双重分派的技巧很有用,但比要求的要慢一些。每一次代理的方法调用必须通过常规 分 派,失败,最后落入 AUTOLOAD()。因为程序需要它们,作为代替,你可以将新方法 安装到代理类 中: sub AUTOLOAD { my ($name) = our $AUTOLOAD =~ /::(\w+)$/; my $method = sub { Log::method_call( $name, @_ ); my $self = shift; return $self->$name( @_ ); } file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] no strict 'refs'; *{ $AUTOLOAD } = $method; return $method->( @_ ); } 之前 AUTOLOAD() 的代码体变为了一个匿名函数。这段代码创建绑定未定义函数 名称 的闭包(é­ å)。接着它在合适的符号表内安装该闭包,使得方法的后续分派能够找 到已经创建的闭包而 避开 AUTOLOAD()。最后,它直接调用该方法,返回结果。 虽然这个方法更为清爽且几乎总是比直接在 AUTOLOAD() 处理调用行为来得更透明,但被 AUTOLOAD() 调用 的代码也许会检测到分派过程经过了 AUTOLOAD()。简单说来,caller() 将反映出目前两种双 重分派的技巧。这便可能成为一个问题;诚然你可以争辩这是一种打破闭 包的行为,必须被关 注,但是让一个对象 如何 提供方法的内幕泄露到宽广的世界中去,也 算得上是违反闭包原则 了。 另一种惯用语就是使用尾部调用(Tailcalls)从 caller()(调用者的) “记忆”中把当前对 AUTOLOAD() 的调用 替换 为目标方法: sub AUTOLOAD { my ($name) = our $AUTOLOAD =~ /::(\w+)$/; my $method = sub { ... } no strict 'refs'; *{ $AUTOLOAD } = $method; goto &$method; } 这样做和直接调用 $method 等效。AUTOLOAD() 不会出现在 caller() 的调用列表中, 因此看上去就像 生成的方法被直接调用一般。 AUTOLOAD 的缺点 在某些情况下,AUTOLOAD() 的确是一个有用的工具,但它也会变得难以正确使用。请考虑使 用其 他的技巧,比如 Moose 及其他抽象来代替。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] 运行时生成方法的朴素手段意味着 can() 方法将不能正确地报告有关类和对象能力的相关信 息。你可以用几种方法解决这个问题;其中最简单的一个就是用 subs 编译命令预定义所有 打算 让 AUTOLOAD 处理的函数: use subs qw( red green blue ochre teal ); 这个技巧的好处是可以让你记录下你的意图,但坏处则是你必须维护一个静态的函数、方法名 称表。 你也可以提供你自己的 can() 方法来生成合适的函数: sub can { my ($self, $method) = @_; # 是用上级 can() 方法的结果 my $meth_ref = $self->SUPER::can( $method ); return $meth_ref if $meth_ref; # 添加过滤器 return unless $self->should_generate( $method ); $meth_ref = sub { ... }; no strict 'refs'; return *{ $method } = $meth_ref; } sub AUTOLOAD { my ($self) = @_; my ($name) = our $AUTOLOAD =~ /::(\w+)$/;> return unless my $meth_ref = $self->can( $name ); goto &$meth_ref; } 根据需求的复杂程度,你也许会发现维护一个类似包含所缺方法名称的包作用域哈希之 类的数 据结构更为简单。 注意某些原本不欲提供的方法也可能通过 AUTOLOAD()。一个常见的罪犯便是 DESTROY(), 对象的析 构器。最简便的方法就是提供一个不含实现的 DESTROY() 方法;Perl 将高兴地分派 此方法并将 AUTOLOAD() 一起忽略: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_05.html[2011/2/21 21:22:09] # 跳过 AUTOLOAD() sub DESTROY {} 如 import()、unimport() 和 VERSION() 等特殊方法绝不会通过 AUTOLOAD()。 如果你在一个继承某自行提供 AUTOLOAD() 的包的名称空间内混用函数和方法,你可能会 得到一 个怪异的错误消息: Use of inherited AUTOLOAD for non-method slam_door() is deprecated 这会发生在当你尝试调用一个包内不存在的方法而所继承的类有包含它自己的 AUTOLOAD() 时。 这绝不是你的意图。这个问题由多个原因混合而成:在单个名称空间内混用函数和方法通常是 一个 设计缺陷,继承以及 AUTOLOAD() 会很快地变得复杂,并且,在你不知道对象可以执行何种 方 法时,推理一段代码是很困难的。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_06.html[2011/2/21 21:22:11] 正则表达式和匹配 Perl 操作文本的强大能力部分来源于对一个名为 正则表达式 的计算概念的囊括。正则 表达式 (通常简化为 regex 或 regexp)是一个 模式,它描述了某文本字符串的 特征。正则表达式引擎 解释一个模式并将其应用到文本字符串上,以识别何者匹配。 Perl 的核心文档丰富而详细地描述了 Perl 的正则表达式;请参考 perldoc perlretut 教程、perldoc perlre 完整文档和 perldoc perlreref 参考指南。Jeffrey Friedl 的著作 精通正则表达式 解释了正则 表达式工作背后的理论和机制。即便这些参考书看上 去令人望而生畏,正则表达式却很像 Perl ———— 你可以在懂得很少的情况下完成很多任务。 字面值 最简单的正则表达式就是简单的子字符串模式: my $name = 'Chatfield'; say "Found a hat!" if $name =~ /hat/; 匹配操作符(// 或者,更为正式的 m//)中包含一正则表达式————在这个例子中是 hat。即 使读上去像一个单词,它的意思其实是“h 字符,后接 a 字符,再接 t 字符,出现在该字符串的 任何地方”。在 hat 中的每一个字符都是一个正则表达式中的 原子:该模式独立的单元。正则表 达式绑定操作符(=~)是一个中缀操作符(è¯ç¼æ§) ,它将位于其右的正则表达式应用于左边 由表达式产生的字符串。当在标量上下文中求值时, 一个成功的匹配将得到真值。 绑定操作符的否定形式(!~)在匹配成功时得到一个假值。 qr// 操作符和正则表达式组合 在现代化的 Perl 中,当由 qr// 操作符创建时,正则表达式是第一级实体: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_06.html[2011/2/21 21:22:11] my $hat = qr/hat/; say 'Found a hat!' if $name =~ /$hat/; 来自 Test::More 的 like() 函数的工作就像 is() 一样,除了它的第 二个参数是一个由 qr// 产生的正 则表达式对象。 你可以通过内插和组合将他们变为更大更复杂的模式: my $hat = qr/hat/; my $field = qr/field/; say 'Found a hat in a field!' if $name =~ /$hat$field/; # 或 like( $name, qr/$hat$field/, 'Found a hat in a field!' ); 量词 正则表达式比前例演示的更为强大;你可以使用 index 操作符在字符串内搜索 子字符串字面 值。但用正则表达式引擎达成这项任务就像驾驶自治区战斗直升机去 拐角小店购买备用奶酪。 通过使用 正则表达式量词,正则表达式可以变得更为强大,使你能够指定一个正则表达式 组件 在匹配字符串中出现的频率。最简单的量词是 零个或一个量词,或者说 ?: my $cat_or_ct = qr/ca?t/; like( 'cat', $cat_or_ct, "'cat' matches /ca?t/" ); like( 'ct', $cat_or_ct, "'ct' matches /ca?t/" ); 在正则表达式中,任意原子后接 ? 字符意味着“匹配此原子零次或一次”。这个正则表 达式匹配 c 后立即跟随零个 a 字符再接一个 t 字符。它同时也匹配在 c 和 t 字符间有一个 a。 一个或多个量词,或者说 +,在字符串中只匹配量词前原子至少出现一次的情况: my $one_or_more_a = qr/ca+t/; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_06.html[2011/2/21 21:22:11] like( 'cat', $one_or_more_a, "'cat' matches /ca+t/" ); like( 'caat', $one_or_more_a, "'caat' matches /ca+t/" ); like( 'caaat', $one_or_more_a, "'caaat' matches /ca+t/" ); like( 'caaaat', $one_or_more_a, "'caaaat' matches /ca+t/" ); unlike( 'ct', $one_or_more_a, "'ct' does not match /ca+t/" ); 能匹配多少原子没有什么理论上的限制。 零个或多个量词 是 *;它匹配量化原子在字符串中出现的零个或多个实例: my $zero_or_more_a = qr/ca*t/; like( 'cat', $zero_or_more_a, "'cat' matches /ca*t/" ); like( 'caat', $zero_or_more_a, "'caat' matches /ca*t/" ); like( 'caaat', $zero_or_more_a, "'caaat' matches /ca*t/" ); like( 'caaaat', $zero_or_more_a, "'caaaat' matches /ca*t/" ); like( 'ct', $zero_or_more_a, "'ct' matches /ca*t/" ); 这看上去可能不很有用,但是它和其他正则表达式功能可以组合得很好,让你不必关心在 特定 位置是否出现某模式。即便如此,大多数 正则表达式从使用 ? 和 + 量词 中获益远多于 * 量词, 因为它们可以避免昂贵的回溯,并将你的意图表达得更为清晰。 最后,你可以通过 数值量词 指定某原子匹配的次数。{n} 意味着确切匹配 n 次。 # equivalent to qr/cat/; my $only_one_a = qr/ca{1}t/; like( 'cat', $only_one_a, "'cat' matches /ca{1}t/" ); {n,} 意味着匹配次数必须至少为 n 次,同时可以匹配更多次数: # equivalent to qr/ca+t/; my $at_least_one_a = qr/ca{1,}t/; like( 'cat', $at_least_one_a, "'cat' matches /ca{1,}t/" ); like( 'caat', $at_least_one_a, "'caat' matches /ca{1,}t/" ); like( 'caaat', $at_least_one_a, "'caaat' matches /ca{1,}t/" ); like( 'caaaat', $at_least_one_a, "'caaaat' matches /ca{1,}t/" ); {n,m} 意味着必须至少匹配 n 次且不多于 m 次: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_06.html[2011/2/21 21:22:11] my $one_to_three_a = qr/ca{1,3}t/; like( 'cat', $one_to_three_a, "'cat' matches /ca{1,3}t/" ); like( 'caat', $one_to_three_a, "'caat' matches /ca{1,3}t/" ); like( 'caaat', $one_to_three_a, "'caaat' matches /ca{1,3}t/" ); unlike( 'caaaat', $one_to_three_a, "'caaaat' does not match /ca{1,3}t/" ); 贪心性 就 + 和 * 自身来说,它们是 贪心量词;它们尽可能多地匹配输入字符串。在利 用 .* 来匹配“任 何数量的任何字符”时尤其有害: # a poor regex my $hot_meal = qr/hot.*meal/; say 'Found a hot meal!' if 'I have a hot meal' =~ $hot_meal; say 'Found a hot meal!' if 'I did some one-shot, piecemeal work!' =~ $hot_meal; 贪心量词总是试图 先行 匹配尽可能多的输入字符串,仅在匹配明显不成功时回退。如 果你用 如下方式查找单词“loam(土壤)”,你将无法把所有结果塞进 7 Down 的四个盒子 里面: my $seven_down = qr/l${letters_only}*m/; 作为新手,你将得到 Alabama、Belgium 以及 Bethlehem。那里的土壤也许不错, 但是它们全都太长 了————并且,匹配是从单词的中间开始的。 让贪心量词变成非贪心量词只需在其后加上 ? 量词: my $minimal_greedy_match = qr/hot.*?meal/; 当给予非贪心量词,正则表达式引擎将偏向 最短的、可能的潜在匹配,并仅在目前 数目无法满 足匹配要求时,增加由 .*? 标记组合识别的字符数量。由于 * 匹配 零或更多次,对应这个标记 组合的最小潜在匹配是零个字符: say 'Found a hot meal' if 'ilikeahotmeal' =~ /$minimal_greedy_match/; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_06.html[2011/2/21 21:22:11] 使用 + 量词可以匹配某项一次或多次: my $minimal_greedy_at_least_one = qr/hot.+?meal/; unlike( 'ilikeahotmeal', $minimal_greedy_at_least_one ); like( 'i like a hot meal', $minimal_greedy_at_least_one ); ? 量词修饰符也可以应用于 ?(零或一次匹配)和范围量词。在每种情况下,它使 得正则表达 式尽可能地少匹配。 贪心修饰符 .+ 和 .* 是诱人但危险的。如果你编写了贪心匹配正则表达式,请用 综合自动化测 试套件和代表性数据完整地测试它,以将产生令人不快结果的可能性降到最 小。 正则表达式锚点 正则表达式锚点 强制在字符串某位置进行匹配。字符串开头锚点(\A)确保任 何匹配都将从字 符串开头开始: # 也匹配 "lammed"、"lawmaker" 和 "layman" my $seven_down = qr/\Al${letters_only}{2}m/; 字符串末尾锚点(\Z)确保任何匹配都将 结束 于字符串末尾: # 也匹配 "loom",它足够接近 my $seven_down = qr/\Al${letters_only}{2}m\Z/; 单词边界元字符(\b)仅匹配一单词字符 (\w) 和另一非单词字符 (\W) 之间的边界。因此,查找 loam 而非 Belgium,可以使用加锚点的正则表达式: my $seven_down = qr/\bl${letters_only}{2}m\b/; 与 Perl 类似,达成某个目的的正则表达式也有许多写法。请考虑从中挑出最有表达力 也是最 易维护的一个。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_06.html[2011/2/21 21:22:11] 元字符 正则表达式随着原子的一般化而变得更为强大。举例来说,在正则表达式内,. 字符 的意思 是“匹配除换行外的任意字符”。玩填字游戏时,如果你想在一个单词列表里查找每 一个匹配的 7 Down(“Rich soil”),你可以这样写: for my $word (@words) { next unless length( $word ) == 4; next unless $word =~ /l..m/; say "Possibility: $word"; } 当然,如果你的候选匹配列表由单词外的东西构成,这个元字符可能导致假阳性,因为它同时 匹配标点符号、空格、数字以及其他的非单词字符。\w 元字符代表所有字母数字符(按 Unicode 处理————Unicode and Strings)还有下划线: next unless $word =~ /l\w\wm/; \d 元字符匹配数字————不是你预期的 0-9,而是 Unicode 数字: # 并非一个健壮的电话号码匹配器 next unless $potential_phone_number =~ /\d{3}-\d{3}-\d{4}/; say "I have your number: $potential_phone_number"; 可以使用 \s 元字符匹配空白,无论是字面空格、制表符、硬回车、换页符或者换行: my $two_three_letter_words = qr/\w{3}\s\w{3}/; 这些元字符也有否定形式。要匹配 除 单词外的其他字符,使用 \W。要匹配非数字 字符,使用 \D。要匹配非空白字符,使用 \S。 正则表达式引擎将所有元字符作为原子对待。 字符类 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_06.html[2011/2/21 21:22:11] 如果允许字符的范围在上述四组里不够具体,通过把它们用中括号围起,你可以自行指定 字符 类: my $vowels = qr/[aeiou]/; my $maybe_cat = qr/c${vowels}t/; 标量变量名 $vowels 外的大括号帮助对变量名称消歧。如果没有它,语法分析器将把 变量名解释 为 $vowelst,这样不是因未知变量导致编译期错误就是把已存在 $vowelst 变量的内容内插进正则 表达式。 如果字符集内的字符构成了一个连续的范围,你可以使用连字符(-)作为表达该范围的 快捷 方式。 my $letters_only = qr/[a-zA-Z]/; 将连字符添加到字符类的开头或结尾可以将其包括进此字符类中: my $interesting_punctuation = qr/[-!?]/; ……或对其进行转义: my $line_characters = qr/[|=\-_]/; 就像单词和数字类元字符(\w 和 \d)有自己的否定形式,你也可以否定一个字 符类。用插入符 号(^)作为字符类的第一个元素意味着“除 这些外的所有字符”: my $not_a_vowel = qr/[^aeiou]/; 在此之外使用插入符号使其成为该字符类的一个成员。要在否定字符类里包含一个连字符, 可 以将它放在插入符号后,或者在字符类的最后,再或者对其转义。 捕获 通常的做法是先匹配字符串的一部分并在稍后对其进行处理;也许你想从一个字符串中提取 一 个地址或美国电话号码: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_06.html[2011/2/21 21:22:11] my $area_code = qr/\(\d{3}\)/; my $local_number = qr/\d{3}-?\d{4}/; my $phone_number = qr/$area_code\s?$local_number/; 正则表达式中的括号是元字符;$area_code 对它们进行了转义。 具名捕获 给出一个字符串,$contact_info,包含有联系信息,你可以将 $phone_number 正则表 达式应用其上 并通过 具名捕获 来将任何匹配结果 捕获 并存入变量中: if ($contact_info =~ /(?$phone_number)/) { say "Found a number $+{phone}"; } 捕捉结构可能看上去像是一大个摇晃的标点,当你可以将其作为整体认读时,它还是比较 简单 的: (? ... ) 括号包围了整个捕获。?< name > 结构必须紧跟左括号。它为捕获缓冲区提供了名 称。此结构位 于括号内的其余部分是一个正则表达式。如果当正则表达式匹配该片段,Perl 将字符串被捕获 的部分存储在神奇变量 %+ 中:一个以捕获缓冲区名为键、匹配正则表 达式的字符串部分为值 的哈希。 对于 Perl 5 正则表达式来说,括号是特殊的;默认和常规的 Perl 代码一样,它们的行为 就是 进行分组。它们也将匹配部分组成的一个或多个原子包围在内。要在正则表达式内使用 字面括 号,你必须添加反斜杠,就像 $area_code 变量里那样。 编号捕获 具名捕获是 Perl 5.10 的新功能,但捕获早已在 Perl 中存在了许多年头。你也会碰到 编号捕 获: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_06.html[2011/2/21 21:22:11] if ($contact_info =~ /($phone_number)/) { say "Found a number $1"; } 括号把要捕获片段包围在内,但是没有正则表达式元字符给出捕获的 名称。作为代替, Perl 将捕获的子字符串存放在一系列以 $1 开头的神奇变量中,并延续至正则表达式 中提供所有捕 获组。Perl 找到的 第一个 匹配捕获存放在 $1,第二个存放在 $2, 等等。捕获计数起始于捕获 的 开 括号;因而第一个左括号将捕获存入 $1,第二个 存入 $2,等等。 虽然具名捕获的语法比编号捕获来得长一些,但它提供了额外的清晰度。你不需要统计开 括号 的的个数来指出某捕获会被存入 $4 还是 $5,并且基于较短的正则表达式编写 更长的正则表达 式相对容易一些,因为它们通常对位置的变更或是否出现在单个原子中不 那么敏感。 具名捕获中仍可能发生名称冲突,虽然和发生在编号捕获中的编号冲突相比较少。考虑避免 在 正则表达式片段中使用捕获;将它们留给顶层正则表达式。 当你将一处匹配在列表上下文中求值时,编号捕获相对不那么令人沮丧: if (my ($number) = $contact_info =~ /($phone_number)/) { say "Found a number $number"; } Perl 将按捕获顺序赋值给左值: 成组和选项 前面的例子将全部量词应用于简单原子上。它们也可以应用于一个更为复杂的子模式整体: my $pork = qr/pork/; my $beans = qr/beans/; like( 'pork and beans', qr/\A$pork?.*?$beans/, 'maybe pork, definitely beans' ); 如果手动扩展该正则表达式,结果可能令你感到惊讶: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_06.html[2011/2/21 21:22:11] like( 'pork and beans', qr/\Apork?.*?beans/, 'maybe pork, definitely beans' ); 这样仍然匹配,但考虑一个更为具体的模式: my $pork = qr/pork/; my $and = qr/and/; my $beans = qr/beans/; like( 'pork and beans', qr/\A$pork? $and? $beans/, 'maybe pork, maybe and, definitely beans' ); 一些正则表达式不是匹配这项就是匹配另一项。使用 选项 元字符 (|) 即可: my $rice = qr/rice/; my $beans = qr/beans/; like( 'rice', qr/$rice|$beans/, 'Found some rice' ); like( 'beans', qr/$rice|$beans/, 'Found some beans' ); 选项元字符意味着匹配前述任一片段。但请注意解释为正则表达式片段的内容: like( 'rice', qr/rice|beans/, 'Found some rice' ); like( 'beans', qr/rice|beans/, 'Found some beans' ); unlike( 'ricb', qr/rice|beans/, 'Found some weird hybrid' ); 模式 rice|beans 可能会解释为 ric,后接 e 或 b,再跟上 eans ————但是,这是不正确的。选项 总是将离正则表达式分隔符最近的算作 整个 片 段,无论该分隔符是模式开头和结尾,外围的 括号,还是另一个选项字符或者中括号。 为了减少迷惑性,可以像变量 ($rice|$beans) 这样使用具名片段,或者将候选项 包括在 非捕获分 组 中: my $starches = qr/(?:pasta|potatoes|rice)/; (?:) 序列将一系列原子成组但跳过捕获行为。此例中,它包括了三个选项。 如果你打印一个编译后的正则表达式,你将看到它的字符串化形式包含在一个非捕获分组 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_06.html[2011/2/21 21:22:11] 内;qr/rice|beans/ 字符串化为 (?-xism:rice|beans)。 其他转义序列 Perl 将正则表达式内的若干字符解释为 元字符,它们代表不同于他们字面形式的意义。 中括 号总是标示一个字符类,括号则将片段成组且可选地进行捕获。 要匹配一个元字符的 字面 实例,可以用反斜杠(\)对其进行 转义。因此,\( 意指单个左括号 而 \] 意指单个右中括号。\. 指的是一个字面点号,而非“匹配除换行符 外所有字符”的原子。 其他通常需要转义的有用的元字符是管道符(|)和美元符号($)。同时不要忘记量词: *、+ 和 ?。 为避免处处转义(和担心忘记转义内插的值),可以使用 元字符禁用字符。\Q 元字符 禁用对 元字符的处理直到它碰到 \E 序列。当取用你无法控制的正则表达式来匹配文本时, 这个个功 能尤其有用: my ($text, $literal_text) = @_; return $text =~ /\Q$literal_text\E/; $literal_text 参数可以包含任何内容————例如字符串 ** ALERT **。使用 \Q 和 \E,Perl 不会 将“零或多个”量词解释为量词。相反,它会将此正则表达式解释为 \*\* ALERT \*\* 并试图匹配字 面星号。 在处理来自不可信任的用户输入时须特别小心。构造一个恶意正则表达式对你的程序进行 有效 的拒绝服务攻击是完全可以办到的。 断言 正则表达式锚点(\A 和 \Z)是一种 正则表达式断言 的形式,字符串需要满足 此条件,但并不 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_06.html[2011/2/21 21:22:11] 实际匹配字符串中的某个字符。就是说,正则表达式 qr/\A/ 将 一直 匹配,无论字符串内容为 何。元字符 \b 和 \B 也是断言。 零宽度断言 匹配一个 模式,不仅仅是一个字符串中的条件。最重要的是,它们不消耗 它们匹 配模式中的位置。例如,你只要找一只“cat(猫)”,你可以是用单词边界断言: my $just_a_cat = qr/cat\b/; ……但如果想找一非灾难性的“cat”,你也许会用到 零宽度否定前瞻断言: my $safe_feline = qr/cat(?!astrophe)/; (?!...) 结构仅在 astrophe 不紧随其后时匹配短语 cat。 零宽度否定前瞻断言: my $disastrous_feline = qr/cat(?=astrophe)/; ……仅在短语 astrophe 紧随其后时匹配 cat。这看上去不怎么有用,一个普通的正 则表达式就能 完成同样的任务,但考虑下述情况,如果你想在字典中查找所有非“catastrophic” 但以 cat 开头 的单词。一种可能的情况是: my $disastrous_feline = qr/cat(?!astrophe)/; while (<$words>) { chomp; next unless /\A(?$disastrous_feline.*)\Z/; say "Found a non-catastrophe '$+{some_cat}'"; } 因为断言宽度为零,它不消耗源字符串。因此,带锚点的 .*\Z 模式片段必须出现;否则 就只将 捕获源字符串中的 cat 部分。 零宽度后顾断言也是存在的。不像前瞻断言那样,这些断言的模式的长度必须固定;你不可 以 在这些模式中使用量词。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_06.html[2011/2/21 21:22:11] 要对你的猫绝不会出现在行首做出断言,你可以使用 零宽度否定后顾断言: my $middle_cat = qr/(?cat/; ……此处的 (? 结构包含定长模式。特别的,你可以用 零宽度肯定后顾断言 表达 cat 必须总是 立即出现在空格符之后: my $space_cat = qr/(?<=\s)cat/; ……此处的 (?<=...) 结构包含定长模式。这种方式在用 \G 修饰符进行全局正则表 达式匹配时非 常有用,但这是一个你不会经常用到的高级特性。 正则表达式修饰符 正则表达式操作符允许若干修饰符改变匹配的行为。这些修饰符出现在匹配、替换和 qr// 操作 符的结尾。例如,要启用大小写不敏感的匹配: my $pet = 'CaMeLiA'; like( $pet, qr/Camelia/, 'You have a nice butterfly there' ); like( $pet, qr/Camelia/i, 'Your butterfly has a broken shift key' ); 第一个 like() 会失败,因为这些字符串包含不同的字母。第二个 like() 将通过,因 为 /i 修饰符 使得正则表达式忽略大小写的区别。因修饰符的关系,M 和 m 在第二 例中是等价的。 你也可以在模式中内嵌修饰符: my $find_a_cat = qr/(?(?i)cat)/; (?i) 语法仅为它所包围的组启用大小写不敏感匹配:此例中,即整个 feline 捕获组。 你可以以 此形式使用多个修饰符(在模式合适的部分)。你也可以通过前缀 - 来禁用特定 的修饰符: my $find_a_rational = qr/(?(?-i)Rat)/; 多行操作符,/m,允许 ^ 和 $ 锚点匹配字符串中任意行开头和结尾。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_06.html[2011/2/21 21:22:11] /s 修饰符将源字符串作为一行对待,如此 . 元字符便匹配换行符。Damian Conway 对助记符 提出建议,/m 修改 多个(multiple) 正则表达式元字符的行为,而 /s 修改 单个(single) 正则表达 式元字符的行为。 /x 修饰符允许你在模式中内嵌额外的空白和注释而不会改变它们的原意。此修饰符生效时, 正 则表达式引擎将空白和注释字符(#)及其后的内容统统作为注释并忽略它们。这允许你编 写 更可读的正则表达式: my $attr_re = qr{ ^ # 行首 # 杂项 (?: [;\n\s]* # 空白和伪分号 (?:/\*.*?\*/)? # C 注释 )* # 属性标记 ATTR # 类型 \s+ ( U?INTVAL | FLOATVAL | STRING\s+\* | PMC\s+\* | \w* ) }x; 这个正则表达式不 简单,但注释和空白提高了它的可读性。即便你利用已编译的片段一起 编写 正则表达式,/x 修饰符还是能够帮助提高你的代码质量。 /g 修饰符对字符串从头到脚执行某正则表达式行为。它和替换一起使用时候比较合理: # appease the Mitchell estate my $contents = slurp( $file ); $contents =~ s/Scarlett O'Hara/Mauve Midway/g; 当和匹配一起使用时────并非替换────\G 元字符允许你在循环中按块处理字符串。\G 在最近一次匹配结束的位置进行匹配。为了按逻辑块处理一个全是美国电话号码的不正确编码 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_06.html[2011/2/21 21:22:11] 文件,你可以编写: while ($contents =~ /\G(\w{3})(\w{3})(\w{4})/g) { push @numbers, "($1) $2-$3"; } 注意 \G 锚点将从字符串中前一次迭代匹配的那一点开始着手。如果前一次匹配以诸如 .* 之类 的贪心匹配结束,则接下来的可以用于匹配的部分将减少。前瞻断言的使用在这里很重要, 因 为它们不消耗欲匹配的字符串。 /e 修饰符允许你在替换操作右边写入任意 Perl 5 代码。如果成功匹配,正则表达式引擎将运 行 这段代码,并用它的返回值作为替换的值。前面的全局替换例子中,替换不幸主角姓名的部分 可 以变得更加健壮: # appease the Mitchell estate my $contents = slurp( $file ); $contents =~ s{Scarlett( O'Hara)?} { 'Mauve' . defined $1 ? ' Midway' : '' }ge; 你可以向一次替换操作添加任意多的 /e 修饰符。每一处额外的修饰符将对表达式的结果进行又 一次的求值,通常只有 Perl 高尔夫手才会使用 /ee 以及更加复杂的语句。 智能匹配 智能匹配操作符,~~,对两个操作符进行比较并在互相匹配时返回真值。定义的模糊恰 好反映 了此操作符的智能程度:比较操作由操作数两者共同决定。之前你已经见识了这种行 为 ────given(Given/When)进行的就是隐式智能匹配。 参见 perldoc perlsyn 中“智能匹配细节”一段以了解更多细节。一些智能匹配的语义 在 Perl 5.10.0 和 Perl 5.10.1 之间已经做出修改,因此在可能时,仅在 5.10.1 及更 高版本中使用智能匹配。 智能匹配操作符是一个中缀操作符: say 'They match (somehow)' if $loperand ~~ $roperand; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_06.html[2011/2/21 21:22:11] 比较的类型大致先由右操作符的类型决定然后再是左操作符。例如,如果右操作符是一个 带数 值成分的标量,则比较将使用数值等于。如果右操作符是一个正则表达式,则比较将 是一个 grep 操作或模式匹配。如果右操作符是一个数组,比较将是 grep 操作或递归的 智能匹配。如 果右操作符是一个哈希,比较操作将检查一个或多个键是否存在。 例如: # 标量数值比较 my $x = 10; my $y = 20; say 'Not equal numerically' unless $x ~~ $y; # 标量类数值比较 my $x = 10; my $y = '10 little endians'; say 'Equal numeric-ishally' if $x ~~ $y; ……以及: my $needlepat = qr/needle/; say 'Pattern match' if $needle ~~ $needlepat; say 'Grep through array' if @haystack ~~ $needlepat; say 'Grep through hash keys' if %hayhash ~~ $needlepat; ……再及: say 'Grep through array' if $needlepat ~~ @haystack; say 'Array elements exist as hash keys' if %hayhash ~~ @haystack; say 'Array elements smart match' if @strawstack ~~ @haystack; ……又及: say 'Grep through hash keys' if $needlepat ~~ %hayhash; say 'Array elements exist as hash keys' if @haystack ~~ %hayhach; say 'Hash keys identical' if %hayhash ~~ %haymap; 这些比较操作在某操作数是给出数据类型的 引用 时也能正常工作。举例来说: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_06.html[2011/2/21 21:22:11] say 'Hash keys identical' if %hayhash ~~ \%hayhash; 你可以在对象上重载(éè½½)智能匹配操作符。如果你不这样做,智能匹配 操作符在你尝试 将某对象用作操作数时会抛出异常。 你也可以使用如 undef 等其他数据类型,以及函数引用作为智能匹配操作数。请参 考 perldoc perlsyn 中的表格来获取更多细节。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] 对象 鉴于同时掌控所有程序细节的难度,编写大型程序要比编写小程序更需要规范。抽象(找到 并 利用相似性和近似之处)和封装(组织特定细节并按其所属来访问)是掌控细节之必需。 函数有益于此,但函数本身对庞大的程序来说远远不够。面向对象是一种流行的技术,它将 函 数组织为可执行相关行为的对象。 Perl 5 的默认对象系统极其简单。它相当灵活────你可以在它之上创建几乎所有其他对象 系统────但它几乎不对完成最基本任务提供任何简易的辅助。 Moose Moose 是一个专为 Perl 5 提供的更为完整的对象系统。它基于现有的 Perl 5 对象系统 创建, 并提供了更为简单的默认操作、更好的集成、以及来自其它语言────包括 Smalltalk、 Common Lisp 和 Perl 6────的更为先进的特性。如果你只想编写一些简单的程序、维护陈 年 代码或者 Moose 不适用时,Perl 5 的对象系统仍然值得一学,但 Moose 是目前为止在现代 化 Perl 5 中编写面向对象程序的最佳途径。 面向对象,或 面向对象程序设计,是一种程序编排方法,它将组件分块为离散的唯一 的实体。 这些实体称为 对象。用 Moose 的术语来说,每一个对象是某一个 类 的实例, 类作为模版描述 了对象包含的数据和专属的行为。 类 Perl 5 中的类存储类数据。类可以有一个名称。默认地,Perl 5 类使用包来提供名称空间: { package Cat; use Moose; } file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] Cat 类看上去没有做任何事,但 Moose 却做这做那以定义此类并向 Perl 注册它。完成 之后,你 可以创建 Cat(猫)类的对象(或 实例): my $brad = Cat->new(); my $jack = Cat->new(); 箭头语法看上去不会那么陌生。就像解引用,此处箭头调用一个类或对象的方法。 方法 method 就是和一个类关联的函数。表面上看起来,它就像一个完全限定的函数调用,但 有两点 重要的区别。第一,方法调用总是对执行方法 被调用者 进行的。按创建两个 Cat 对象的例子, 类的名称(Cat)就是被调用者: my $fuzzy = Cat->new(); 第二,一处方法调用总是引发 分派 策略。分派策略描述了对象系统如何决定调用 哪些 方法。 在只有一个 Cat 对象时看上去很显然,但方法分派是对象系统设计的基本要求。 Perl 5 中方法的被调用者是方法的第一个参数。举例来说,Cat 类可以拥有一个名为 meow() (喵)的方法: { package Cat; use Moose; sub meow { my $self = shift; say 'Meow!'; } } 现在所有 Cat 实例都会因尚未进食而在清晨苏醒: my $alarm = Cat->new(); $alarm->meow(); file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] $alarm->meow(); $alarm->meow(); 按照惯例,Perl 中方法的被调用者是一个名为 $self 的词法变量,但这仅仅是一个 普遍的惯 例。meow() 方法的示例实现没有用到被调用者,因此一旦方法分派完毕就 毫不相关了。按 此,meow() 就像 new();你可以安全地用类名(Cat)作为被 调用者。这是一个 类方法: Cat->meow() for 1 .. 3; 属性 Perl 5 中每一个对象都是唯一的。对象可以包含 属性,或者说和每一个对象关联的私 有数据。 你也可能听到它被描述为 实例数据 或 状态。 要定义对象属性,可以将它们描述为类的一部分: { package Cat; use Moose; has 'name', is => 'ro', isa => 'Str'; } 在英语中,那行代码的意思是“Cat 对象有一个 name 属性。它可读但不可写,并 且它是字符 串。”该行代码创建了一个访问器方法(name())且允许你可以向构造函数 传递一个 name 参数: use Cat; for my $name (qw( Tuxie Petunia Daisy )) { my $cat = Cat->new( name => $name ); say "Created a cat for ", $cat->name(); } 属性的类型并非 必需,在此情况下,Moose 将为你跳过核查和验证的部分: { package Cat; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] use Moose; has 'name', is => 'ro', isa => 'Str'; has 'age', is => 'ro'; } my $invalid = Cat->new( name => 'bizarre', age => 'purple' ); 这样更为灵活,但在某人尝试对属性提供无效数据时便会导致奇怪的错误。灵活性和正确性 之 间的平衡取决于当时的编码标准以及欲捕获的错误类型。 Moose 的文档使用括号来分隔属性名称和它的特征: has 'name' => ( is => 'ro', isa => 'Str' ); Perl 对此形式和本书所用的形式做相同的语法分析。你 可以 编写如下任一代码来实现 相同的 功能: has( 'name', 'is', 'ro', 'isa', 'Str' ); has( qw( name is ro isa Str ) ); ……但在这种情况下,额外的标点使程序更加清晰。Moose 文档中使用的方法在处理多项特征 时尤其有用: has 'name' => ( is => 'ro', isa => 'Str', # 高级 Moose 选项;perldoc Moose init_arg => undef, lazy_build => 1, ); ……出于介绍的简易性,本书倾向于使用标点符号较少的形式。Perl 给你以灵活性,让你 可以 自由选择使代码更为清晰的编码方式。 如果你将某属性标记为可读 且 可写(用 is => rw),Moose 将创建一个 突变 方法────你 可以用它改变属性的值: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] { 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' ); say $fat->name(), ' eats ', $fat->diet(); $fat->diet( 'Low Sodium Kitty Lo Mein' ); say $fat->name(), ' now eats ', $fat->diet(); 尝试将 ro 访问器用作突变方法将抛出一个异常: Cannot assign a value to a read-only accessor at .... 何时使用 ro 何时使用 rw?这是关系到设计、便捷和代码纯度的事情。有一派意见 (ä¸å¯åæ§) 认为所有实例数据都应该采用 ro 并将相关数据都传递给构造函 数。在 Cat 一例中,age() 仍可以 是一个访问器,但构造函数可以取得猫出生的 年份 并根据当前日期自行计算出年龄,而非依赖 于手动更新猫的年龄。 此方式有助于巩固验证代码的效果并保证所有创建的对象只包含有效的数据。这些设计目标 值 得考虑,尽管在这方面 Moose 并不强制采用特定的哲学。 既然独立的对象可以各自拥有实例数据,面向对象的价值就更加明显了。一个对象是其包含 数 据,同时也是适用于这些数据的行为的书签。一个对象是具名数据和行为的集合。一个类 是对 该类实例持有的数据和行为的一个描述。 封装 Moose 允许你声明类的实例可以持有 哪些 属性,以及如何处理这些属性。目前为止的 例子尚 未描述 如何 存储这些属性。如果真的需要了解,这些信息是可以获取的,但是 直接说明的方 式可以实际地改进你的程序。由此,Moose 鼓励 封装,或者说对对象的 外界使用隐藏内部细 节。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] 考虑对 Cat 做出年龄修改;并不直接对构造函数请求,而是通过出生年份来计算一只 Cat (猫) 的年龄: package Cat; use Moose; has 'name', is => 'ro', isa => 'Str'; has 'diet', is => 'rw'; has 'birth_year', is => 'ro', isa => 'Int'; sub age { my $self = shift; my $year = (localtime)[5] + 1900; return $year - $self->birth_year(); } 虽然 创建 Cat 对象的语法变了,但 使用 Cat 对象的语法却没有。age() 方法 始终完成它应该完成 的任务,至少就 Cat 类之外的代码而言可以理解其行为。它 如何 完 成任务的方式已经改变,但 这属于 Cat 类的内部细节────由该类封装于自身内。 之前用于 创建 Cat 对象的语法仍可以原地不动;定制生成的 Cat 构造函数以允许向其 传递 age 参数,并由此计算正确的 birth_year。请参看 perldoc Moose::Manual::Attributes。 这个计算 Cat 年龄的新方式有着另外的优势;你可以使用 默认属性值 来减少创建 Cat 类所需的 代码量: package Cat; use Moose; has 'name', is => 'ro', isa => 'Str'; has 'diet', is => 'rw', isa => 'Str'; has 'birth_year', is => 'ro', isa => 'Int', default => sub { (localtime)[5] + 1900 }; 属性上的 default 关键字接受一函数引用,该引用在对象构造之时返回此属性的默认 值。如果构 造函数并未接收此属性某一合适的值,则该对象获得替代的默认值。现在你可 以这样创建一只 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] 小猫: my $kitten = Cat->new( name => 'Bitey' ); ……并且在明年之前这只小猫的年龄都将为 0。 你也可以使用一个简单的值,诸如数字或字符串,来作为默认值。当你需要为每一个对象 计算 某些唯一值(包括哈希或数组引用)时,使用函数引用。 多态 那行仅处理单一类型数据以及基于此数据之上单一行为的程序从对象的使用中获益不多。 一个 经过良好设计的面向对象程序应有能力处理各种类型的数据。当设计精良的类在合 适处封装特 定对象的细节时,会神奇地影响后续程序:一旦有机会,它可以选择变得 不那么 具体。 换句话说,将程序了解的 Cat 对象个体特征(属性)和它能做的事(方法)这类具 体细节从程 序中移入 Cat 类中意味着处理 Cat 实例的代码可以快乐地忽略 Cat 对象 如何 做它该做的事。 用例子说话。考虑一个描述对象的函数: sub show_vital_stats { my $object = shift; say 'My name is ', $object->name(); say 'I am ', $object->age(); say 'I eat ', $object->diet(); } 很明显(在上下文中),你可以向此函数传入一个 Cat 对象并得到合理的结果。不 那么明显的 是,你也可以传入其他对象并得到合理的结果。这是一个称为 多态 的重 要面向对象属性,你 可以用一个类的对象替换另一个类的对象,只要它们以同样的方式 提供相同的外部接口。 任何提供 name()、age() 和 diet() 访问器的对象都可以正常使用此函数。 这个函数足够通用,每 一个遵循接口的对象都是一个合法的参数。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] 一些语言和环境在程序互换实例之前要求两者间建立正式的关系。Perl 5 提供了多个方法 来强 制进行这类检查,但并不要求这样做。它默认的特殊系统让你可以足够等同地对待两个 拥有同 名方法的实例。一些人称其为 duck typing,此说法认为任何对象只要能 quack() (“叫”)就足够 像鸭子,因而你可以将其作为鸭子对待。 在 show_vital_stats() 中体现的泛型的好处不是特定的 类型 也非对象的实现起 决定作用。任何被 调用者只要提供三个方法 name()、age() 和 diet()────它们 不带参数,它们的返回值可以在字 符串上下文中拼接,那么它就是一个合法的参数。你的代 码中也许会包含成上百个类,它们之 间也可以没有任何明显的关系,但如果它们符合预期的 行为,那么此方法就可以正常工作。 对于为那么多类(哪怕是其中一部分)编写特定的函数提取并显示这类信息来说,这是一种 质 的提升。这个泛化的方法只需更少的代码,并使用经过良好定义的接口作为访问这些信息 的机 制,意味着上百个类可以各自采用任何可行的办法计算这些信息。计算方法的细节则位 于它们 的岗位上:在各个类各自的方法体内。 当然,仅出现名为 name() 或是 age() 的方法并不暗示对象的行为。一个 Dog 对象也可以包含 age() 访问器,它可以让你了解 $rodney 8 岁了而 $lucky 是 3 岁。Cheese 对象也可以含有 age() 方法,让 你控制堆放 $cheddar 多久使得 奶酪味道更重;换句话说,age() 可以在一个类中是访问器而在另 一个类中不是: # 这只猫几岁了? my $years = $zeppie->age(); # 把这批奶酪在仓库里存六个月 $cheese->age(); 有时候了解一个对象 做什么 很有用。就是说,你要了解它的类型。 角色 角色 是一个具名行为和状态的集合。类就像是一个角色,它们之间重要的区别就是你 可以对一 个类进行实例化,但角色就不行。对于对象来说,类是将行为和状态组织为模版 主要机制,而 角色便是将行为和状态组织为具名集合的主要机制。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] 简单说来,角色和类是类似的。 某些 Animal(动物)────有 name()(名称)、有 age()(年龄)和偏好的 diet() (食物)和 Cheese(奶酪)────可以在仓库 age()(陈化)────的区别在于 Animal 可以饰演 LivingBeing 的角色,而 Cheese 则饰演 Storable 的角色。 虽然你 可以 检查传入 show_vital_stats() 的每一个对象是否是 Animal 类的实例, 这样你便失去了 部分泛型的特质。你可以用检查该对象是否 饰演 LivingBeing 角色来代 替: { package LivingBeing; use Moose::Role; requires qw( name age diet ); } 任何饰演此角色的对象必须提供 name()、age() 和 diet() 方法。这并不会自动发 生;Cat 类必须明 确标明它可以饰演此角色: 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 { ... } 那行代码有两个左右。第一,它告知 Moose 该类饰演具名角色。第二,它将角色合成到类中。 该步骤检查类是否 以某种方式 提供了所需的所有方法和属性并避免了潜在的冲突。 Cat 类为具名属性提供了作为访问器的 name() 和 diet() 方法。它同时声明了自 身的 age() 方法。 使用 with 关键字向类添加角色,其语句必须出现在属性声明 之后,使得该合成过 程可以识别 任何生成的访问器方法。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] 当被问及它们是否提供 LivingBeing 角色时,并非所有的 Cat 实例会返回真,并且 Cheese 对象不应 如此: say 'Alive!' if $fluffy->does('LivingBeing'); say 'Moldy!' if $cheese->does('LivingBeing'); 这个设计手法看上去可能像是多余的记账,但它从类和对象的 实现 中分离出了它们的 能 力。Cat 类的特殊行为,即存储动物的出生年份并直接计算年龄,本身就可以是一 个角色: { package CalculateAgeFromBirthYear; use Moose::Role; has 'birth_year', is => 'ro', isa => 'Int', default => sub { (localtime)[5] + 1900 }; sub age { my $self = shift; my $year = (localtime)[5] + 1900; return $year - $self->birth_year(); } } 将这段代码从 Cat 类中移出到一个分离的角色中使其也可以用于其它类中。现在, Cat 对象可以 饰演两个角色: package Cat; use Moose; has 'name', is => 'ro', isa => 'Str'; has 'diet', is => 'rw'; with 'LivingBeing', 'CalculateAgeFromBirthYear'; 由 CalculateAgeFromBirthYear 提供的 age() 方法的实现满足 LivingBeing 角色的需要,使之成功合成。 无论对象 如何 扮演此角色,对对象是否扮演 LivingBeing 角色的检查结果仍然不变。一个类可以 选择提供它自己的 age() 方法或从其它角色中获 得一份,这并不重要。重要的是它包含此方 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] 法。这就是 同质异晶性. 角色和 DOES() 对一个类应用一个角色意味着你在调用该类和它的实例的 DOES() 方法时返回真: say 'This Cat is alive!' if $kitten->DOES( 'LivingBeing' ); 继承 Perl 5 对象系统的另一个特性就是 继承,即一个类将另一个类专门化。这在两个类间 建立起一 种关系,其中子类从父类继承属性和行为。就两个提供同一角色的类来说,你可以 用子类替换 父类。在某种意义上说,一个子类通过其父类的存在隐式地提供了某角色。 Perl 5 中最近基于角色的对象系统实验显示,在一个系统中几乎所有用到继承的地方都可 以用 角色来替代。决定使用何者大体上是熟悉程度的事。角色提供了合成时安全,更好的类 型检 查,组织良好且更低耦合的代码,可对名称和行为做出细粒度的控制,但继承对其他语 言的用 户来说更为熟悉。设计上的问题就是一派是否确实扩展了另一派或者说它是否提供了 额外(或 者,至少是,不同)的行为。 考虑 LightSource(光源)类,它提供了两个公共属性(candle_power 和 enabled) 以及两个方法 (light 和 extinguish): { 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); } file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] sub extinguish { my $self = shift; $self->_set_enabled(0); } } enabled 属性的 _writer 选项创建了一个私有访问器,可在类内部用于设置值。 继承和属性 创建 LightSource 的子类使得定义一支行为和 LightSource 相似、但提供亮度百倍 于常的超级蜡烛 成为可能: { package LightSource::SuperCandle; use Moose; extends 'LightSource'; has '+candle_power', default => 100; } extends 语法结构接受一个类名称列表作为当前类的父类。位于 candle_power 属性 名称前的 + 指出 当前类扩展了此属性的定义。在这种情况下,超级蜡烛覆盖了光源的默 认值,因此任何新建的 SuperCandle 的亮度值为 100 支蜡烛。另一个属性以及两个方法 对 SuperCandle 实例也是可用的;当 你在这样一个实例上调用 light 或 extinguish Perl 会先在 LightSource::SuperCandle 内查找这些方法, 然后是父类列表。最终它在 LightSource 里找到了它们。 属性继承的工作方式与此类似,除了 构造 此实例的行为使得所有适当的属性按正确的方式 提 供(参见 perldoc Class::MOP)。 单重继承的方法分派顺序理解起来比较简单。当一个类拥有多个父类时(多重继承),分派 便 不那么显然了。默认的,Perl 5 提供了深度优先的方法解析策略。它先搜索类的 首个 具 名父 类,接着在搜索后续具名父类之前递归地搜索此类的所有父类。这个行为通常令人迷惑;在 你 理解多重继承之前请避免使用它,并尽可能采用其他替代手段。参见 perldoc mro 以获取 有关方 法解析和分派策略的更多细节。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] 继承和方法 你可以在子类中覆盖父类的方法。设想一个你无法熄灭的光源: { package LightSource::Glowstick; use Moose; extends 'LightSource'; sub extinguish {}; } 所有对此类的 extinguish 方法的调用将毫无作用。Perl 的方法分派系统将先找到这个 方法并且 不会再在父类中查找与此同名的其他方法。 有时候覆盖后方法也需要来自父类同名方法的某些行为。override 命令告诉 Moose,该 子类故 意覆盖此具名方法。super() 函数可以用来从覆盖方法分派到被覆盖方法: { package LightSource::Cranky; use Carp; use Moose; extends 'LightSource'; override light => sub { my $self = shift; Carp::carp( "Can't light a lit light source!" ) if $self->enabled; super(); }; override extinguish => sub { my $self = shift; Carp::carp( "Can't extinguish an unlit light source!" ) file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] unless $self->enabled; super(); }; } 这个子类在点亮和熄灭一个已经处于当前状态的光源时增加了一条警告。super() 函数 在遵守正 常 Perl 5 方法解析顺序的同时将当前方法分派到最近的父类实现中。 你可以用 Moose 的方法修饰符实现相同的行为。参见 perldoc Moose::Manual::MethodModifiers。 继承和 isa() 从父类继承意味着子类及其所有实例在调用其上 isa() 方法时返回真: say 'Looks like a LightSource' if $sconce->isa( 'LightSource' ); say 'Monkeys do not glow' unless $chimpy->isa( 'LightSource' ); Moose 和 Perl 5 OO Moose 提供了许多原本需要你自行在 Perl 5 的默认对象模型中实现的特性。虽然你 可以 自行 构建来自 Moose 的全部特性(参见 ç» bless åçå¼ç¨),或者用一系列 CPAN 发行 包修修补补 地实现,但 Moose 是一个合适且连贯的包,包括了优秀的文档,且是许多成功项目 的一部 分,还有,它正由一个善解人意且有才的社区积极地开发着。 默认地,Moose 对象不需要你担心构造器、析构器、访问器和封装。Moose 对象可以扩展并 和来自平淡无奇的 Perl 5 对象系统的对象协同工作。你同时也得到了 元编程────一种 通 过系统自身访问系统实现的方式────以及随附的扩展性。如果你曾考虑过某类或对象上可 以调用什么方法或者对象支持什么属性,这类元编程信息可以通过 Moose 得到: my $metaclass = Monkey::Pants->meta(); say 'Monkey::Pants instances have the attributes:'; say $_->name for $metaclass->get_all_attributes; say 'Monkey::Pants instances support the methods:'; say $_->fully_qualified_name for $metaclass->get_all_methods; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] 你甚至可以知道那些类扩展了一个给定的类: my $metaclass = Monkey->meta(); say 'Monkey is the superclass of:'; say $_ for $metaclass->subclasses; 请分别参阅 perldoc Class::MOP::Class、perldoc Class::MOP 以获取有关元类操作符、 Moose 元编程的 更多信息。 Moose 和它的 元对象协议(或称 MOP)为一个更好的、用于在 Perl 5 中操作类和对象的 语法 提供了可能。如下是合法的 Perl 5 代码: use MooseX::Declare; role LivingBeing { requires 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'; } 来自 CPAN 的 MooseX::Declare 扩展使用了一个称为 Devel::Declare 的聪明的模块 向 Perl 5 添加新 语法,特别是为了 Moose。class、role 和 method 关键字减少 了在 Perl 5 中编写良好的面向对象 代码所需的样版数量。特别注意本例称述性的本质,还有 现在 age 方法开头的 my $self = shift; 代码行不是必须的。 自 Perl 5.12 起,Perl 5 核心提供了对 Devel::Declare 的支持,但这个模块不是核心 模块。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] 采用这整个方案的一个缺点是你必须能够安装 CPAN 模块(或使用诸如 Strawberry Perl 或 Strawberry Perl Professional 之类定制的 Perl 5 发行版,它们会替你包括这些),但和 Perl 5 的内置面向对象比较,Moose 在清晰和简洁方面的优势应是明显的。 参见 perldoc Moose::Manual 以获取有关使用 Moose 的更多信息。 经 bless 后的引用 Perl 5 的默认对象系统故意最小化。以下三条简单的规则相组合构成了简单────但有效的 ────基本对象系统: 一个类就一个包; 一个方法就是一个函数; 一个(bless 后的)引用就是一个对象。 你已经在 Moose 里见过了前两条规则。第三条规则是新出现的。bless 关键字将一个类 的名称 和一个引用关联起来,使得任何在该引用上进行的方法调用由与之相关联的类来解析。 它听上 去比实际的要复杂一些。 虽然这些规则很好地解释了 Perl 5 的底层对象系统,它们在实际应用时显得捉襟见肘,特 别是 对于较大型的项目来说。特别地,它们几乎没有提供元编程(使用 API 操作程序本身) 的组 件。 Moose(Moose)对于正式而现代化的、且大于几百行 Perl 程序来说是更好的选择,但你 很可 能在现存代码中遇见赤裸裸的 Perl 5 OO。 默认的 Perl 5 对象构造器是一个创建引用并对其进行 bless 的方法。出于惯例,构造器通 常命 名为 new(),但并非一定如此。构造器几乎总是 类方法: sub new { file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] my $class = shift; bless {}, $class; } bless 接受两个参数,与类相关联的引用以及类的名称。虽然程序抽象建议使用一个单 独的方法 来处理,但你仍可以在构造器或类之外使用 bless。类名称不需要事先存在。 设计上,构造器以被调用者的形式接收类名。直接硬编码类名称也是可能的,但不推荐。 参数 式的构造器使得此方法可以通过继承、委托或导出来重用。 引用的类型对对象上的方法调用没有影响。它只掌控对象如何存储 实例数据────对象 自身 的信息。哈希引用最为常见,但你可以 bless 任何类型的引用: my $array_obj = bless [], $class; my $scalar_obj = bless \$scalar, $class; my $sub_obj = bless \&some_sub, $class; Moose 中创建的类由它们自己声明式地定义各自的对象属性,Perl 5 默认的 OO 则非常 宽松。 一个存储球衣号和位置、代表篮球运动员的类可能会使用如下构造器: 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', ); file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] 在类的内部,方法可以直接访问哈希元素: sub format { my $self = shift; return '#' . $self->{number} . ' plays ' . $self->{position}; } 类之外的方法也可以这样做。这样便违反了封装────特别是,它意味着你绝不能在不 破坏 外部代码的情况下改变对象的内部表示,除非投机取巧────因此,保险起见还应 提供访问 器方法: sub number { return shift->{number} } sub position { return shift->{position} } 即便只有两个属性,Moose 在那些非必须代码方面显得更具吸引力。 Moose 的创建访问器的默认行为鼓励你在注重封装和泛型的情况下编写正确的代码。 方法查找和继承 除实例数据外,对象的另一部分就是方法分派。给定一个对象(一个 bless 后的引用), 如下 形式的方法调用: my $number = $joel->number(); ……将查找与经 bless 后的引用 $joel 相关联类的名称。在此例中,该类就是 Player。 接下 来,Perl 在 Player 包内查找一个名为 number 的函数。如果 Player 类从其 它类继承而来,Perl 也 会在父类查找(如此继续)直到它找到 number 方法为止。如果存 在的话,Perl 以 $joel 作为调用 物调用它。 Moose 类在元模型中存放各自的继承信息。每一个经 bless 后的引用的类将父类信息存放在 一 个名为 @ISA 的包全局变量中。方法分派器会在一个类的 @ISA 中查找它的父类,以 在其中搜索合 适的方法。因此,一个 InjuredPlayer 类会在其 @ISA 中包含 Player。 你可以这样编写这重关系: package InjuredPlayer; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] @InjuredPlayer::ISA = 'Player'; 许多现存的 Perl 5 项目都这样做,但用 parent 编译命令来替代会更容易些: package InjuredPlayer; use parent 'Player'; Perl 5.10 为替换增加于 Perl 5.004_4 的 base 编译命令而添加了 parent。如果 你无法使用 Moose,请使用 parent。 可以从多个父类继承: package InjuredPlayer; use parent qw( Player Hospital::Patient ); 在解析方法分派时,Perl 5 在传统上偏向于对父类使用深度优先搜索。这就是说,如果 InjuredPlayer 从 Player 和 Hospital::Patient 两者继承,一个在 InjuredPlayer 实例上调用的方法将先分 派到 InjuredPlayer,然后是 Player,接着经过所有 Player 的父类来到 Hospital::Patient。 Perl 5.10 增加了一个名为 mro 的编译命令,它允许你另行使用称作 C3 的方法解析策略。 虽然 特定的细节可能在处理复杂的多重继承布局时变得复杂,关键的区别是,方法解析过程将 在访 问父类之前访问所有的子类。 虽然其他技巧(诸如 角色 è§è² 和 Moose 方法修饰符)允许你避开多重继承,但 mro 编译命令 可以帮助你避免方法分派时令人惊讶的行为。可以这样在类中启用: package InjuredPlayer; use mro 'c3'; 除非你在编写具有互操作插件的复杂框架,你几乎不会用到它。 AUTOLOAD file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] 如果在调用者及其超类的类定义内没有可用的方法,Perl 5 接下来将按照所选方法解析顺序 在 每个类中查找 AUTOLOAD 函数。Perl 会调用它找到的任何 AUTOLOAD,由此提供或 谢绝所需方法。参 加 AUTOLOAD 以获取更多细节。 如你所想的那样,在面对多重继承和多个候选 AUTOLOAD 目标时会变得很复杂。 方法覆盖和 SUPER 与 Moose 中一样,你可以在默认的 Perl 面向对象系统中覆盖方法。不幸的是,Perl 5 核心 并 没有提供指出你覆盖父类方法 意图 的机制。更糟糕的是,任何预声明、声明或导入子 类的函 数都可能因重名而覆盖父类方法。你可以忘记使用 Moose 的 override 系统,但在 Perl 5 默认的 面向对象系统中你根本没有这样(甚至是可选的)一重保护。 要在子类中覆盖一个方法,只需声明一个和父类方法同名的方法。在覆盖的方法内,你可以通 过 SUPER:: 分派指示来调用父类方法: sub overridden { my $self = shift; warn "Called overridden() in child!"; return $self->SUPER::overridden( @_ ); } 方法名的 SUPER:: 前缀告诉方法分派器将此方法分派到 父类 的具名实现。你可以向 其传递任何 参数,但最好还是重用 @_。 注意当重分派到父类方法时,这个分派器依赖于覆盖方法最初被编译的包。这长期以来是一个 错误的特性,只是为了向后兼容而保留着。如果你向其它类或角色导出方法或手动合成类和角 色,你会和此项特性正面冲突。CPAN 上的 SUPER 模块可以为你绕过它。Moose 同样能够出 色 地处理该问题。 应付经 bless 后引用的策略 可能的话避免使用 AUTOLOAD。如果 必须 用到,你应该用它来转发函数(å½æ°) 定义以帮助 Perl 知道哪个 AUTOLOAD 会提供方法的实现。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] 使用访问器方法而非直接通过引用访问实例数据。甚至在类内部的方法体内也应该这样做。 由 你自己来生成这些方法相当乏味;如果你无法使用 Moose,考虑使用诸如 Class::Accessor 这类模 块来避免重复编写样板。 准备好某人某时最终将继承你的类(或委托或重新实现接口)。通过不对代码内部细节做出假 定、 使用两参数形式的 bless 和把类分割为最小职责单元,可以使得他人的工作更加轻松。 不要在同一个类里混用函数和方法。 为每一个类使用单独的 .pm 文件,除非该类是一个仅用于某处的小型自包含辅助类。 考虑使用 Moose 和 Any::Moose 来替代赤裸的 Perl 5 OO;它们可以轻松和 Perl 5 对象系统 的类 和对象互动,还减轻了几乎所有因类声明而带来的无趣,同时提供了更多及更好的特性。 反射 反射(或称 内省)是运行期间向一个程序询问其自身情况的过程。即便你可以在不 使用反射的 情况下编写不少有用的程序,一些诸如元编程(代ç çæ)等技术 从深刻了解系统中的实体获 益良多。 Class::MOP(Class::MOP)简化了许多对象系统中的反射任务,但是很多有用的程序 并非彻底面 向对象,很多程序也不会用到 Class::MOP。因为没有一个正规的系统,Perl 中存在着一些有效进 行反射的惯用语(æ¯ç¨è¯)。 检查一个包是否存在 为检查一个包是否存在在系统之中────即,如果一段代码在某一点执行了一条带包名称的 package 指令────就是检查它是否从 UNIVERSAL 继承下来,这可以通过检查该包是 否能够执行 can() 方法来实现: say "$pkg exists" if eval { $pkg->can( 'can' ) }; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] 虽然你 可以 使用名为 0 或 '' (footnote: ……仅仅在你符号化地定义它们时,因为这些 并非 Perl 5 语法分析器禁止的标识符。) 的包,can() 方法会在你将其作为调用 物时抛出一个方法调 用异常。eval 代码块可以捕获这类异常。 你也 可以 对符号表的条目进行一一礼拜,但上述方法更加快速同时也更易理解。 检查一个类是否存在 由于 Perl 5 对包和类不加以严格区分,检查包存在性的技巧同时可用于检查一个类是否 存在。 尚没有方法可以判断一个包是否是一个类。你 可以 用 can() 来检查一个包 是否可以执行 new(), 但这样无法保证任何找到的 new() 是方法,更不要说是构造 器了。 检查一个模块是否被加载 如果知道模块的名称,你可以通过查看 %INC 哈希来确定 Perl 是否相信它已经从 磁盘加载了这 个模块。这个哈希和 @INC 是对应的;当 Perl 5 用 use 和 require 加载代码时,会在 %INC 中存储一 个条目,其中键是欲加载模块的路径化名称,值是 模块完整的磁盘路径。就是说,加载 Modern::Perl 等效于: $INC{'Modern/Perl.pm'} => '/path/to/perl/lib/site_perl/5.12.1/Modern/Perl.pm'; 路径的细节大部分取决于安装,但出于测试 Perl 是否成功加载一个模块目的,你可以把 模块 名转换为正规文件形式并测试在 %INC 内是否存在: sub module_loaded { (my $modname = shift) =~ s!::!/!g; return exists $INC{ $modname . '.pm' }; } 没有什么阻止其它代码修改 %INC。按你偏执的程度,你可以自行检查路径和预期的包 内容,但 是拥有充足利用修改此变量的模块(诸如 Test::MockObject 和 Test::MockModule) 可以这样做。修改 %INC 但理由不足的代码应该被替换。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] 检查模块的版本 无法保证某给定的模块是否提供版本号。即便如此,所有模块都继承自 UNIVERSAL(UNIVERSAL å) ,因此它们全含有 VERSION() 方法以供调用: my $mod_ver = $module->VERSION(); 如果给定的模块不覆盖 VERSION() 或不包含包变量 $VERSION,这个方法将返回一个未定义值。 类似 地,如果该模块不存在,则对方法的调用将会失败。 检查一个函数是否存在 确定函数是否存在最简单的机制就是对包名使用 can() 方法: say "$func() exists" if $pkg->can( $func ); 除非 $pkg 是合法的调用物,否则 Perl 将会抛出异常;如果对其合法性有任何怀疑, 可以将此 方法调用包装在一个 eval 块内。注意,若函数所在的包没有正确地覆盖 can(), 使用 AUTOLOAD()(AUTOLOAD)实现的函数可能会导致错误结果。这在其他包里便会 导致代码缺 陷。 你可以用这个技巧决定一个模块的 import() 是否向当前名称空间导入某函数: say "$func() imported!" if __PACKAGE__->can( $func ); 你也可以检查符号表和类型团来决定函数是否存在,但这种机制更简单也更好解释。 检查一个方法是否存在 没有检查某给定函数究竟是函数还是方法的通用办法。一些函数身兼双职,既是函数 也是方 法,尽管听上去过于复杂并且同时是失误,但这确实是一个允许的特性。 搜查符号表 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] Perl 5 符号表是一个种类特别的哈希,其中的键是包全局符号的名称,值则是类型团。 类型团 是一种核心数据结构,它可以包含一个标量、一个数组、一个哈希、一个文 件句柄和一个函 数。Perl 5 内部在查找这些变量时使用类型团。 通过在包名末尾添加双冒号,你可以将符号表当作哈希访问。例如,MonkeyGrinder 包 的符 号表可以通过 %MonkeyGrinder:: 访问。 你 可以 用 exists 操作符检查特定的符号名是否存在于符号表中(或随你喜好 修改符号表来 添 加 或 or 删除 符号)。还应注意到一些对 Perl 5 核心的变更 已经修改了默认出现的类型团条 目。特别是,Perl 5 早期版本总是为每个类型团默认提 供一个标量变量,现代化的 Perl 5 已经 取消。 参见 perldoc perlmod 中的“Symbol Tables”小节以获取更多细节,最好采用不同于 本章介绍的反 射技巧。 高级 Perl 面向对象 利用 Moose (Moose) 在 Perl 5 中创建并使用对象是容易的。设计 一个好的对象 系统则不那么 简单。额外的抽象能力同时也为混乱提供了可能。只有实践经验才能帮助你 理解最重要的设计 技能……还有一些原理可以作为指导。 多用合成而非继承 初级 OO 设计通常过度地使用继承。常见的类结构尝试将所有实体的行为建模于单类系统之 中。 由于你必须理解整个层次结构,这样就给理解该系统增加了概念上的开销;同时这也向每 一个 类增加了技术上的份量,因为冲突的职责和方法可能成为所需行为和进一步修改的绊脚 石。 由类提供的封装为代码组织提供了更好的方法。你并不需要从超类继承以向对象的用户提供 所 需行为。Car(车)对象无需从 Vehicle::Wheeled 对象继承,它可以实例属性的 形式包含若干 Wheel 对象。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] 将复杂的类分解为较小的、专一的实体(无论类或是角色)可提升封装性并降低类或角色身 兼 数职的可能。更小更简单、封装更好的实体无论是从理解、测试、维护上说都更为简易。 单一职责原则(SRP) 当你设计你的对象系统时,应按职责给问题建模,或按每一个特定实体改变的理由。举例说 来, 一个 Employee 对象也许会代表有关人名、联系方式以及其他个人数据的特定信息,而 Job 对 象可能代表的是业务职责。一个简单的设计也许会将这两者合并为单一的实体,但职责分离允 许 Employee 类仅考虑管理有关此是何人的问题,且 Job 类只需代表其人的工作。(例如, 两个 Employees 也许实行工作(Job)分担制。) 当每个类都有单一的职责时,你可以更好地封装特定于类的数据和行为,并降低类间的耦合。 不要重复你自己(DRY) 复杂性和重复使开发和维护活动更加纷繁。DRY 原则(“Don't Repeat Yourself”)提醒你挑出 并减少系统内的重复。重复的形式是多种多样的,既出现在数据也出现在代码中。如果你发现 自 己正重复配置信息、用户数据以及系统内其它人为数据;作为替代,找一个正式的单一的信 息表 示形式,接着从此表示生成其它人为数据。 这个原则有助于减少系统中重要部分不同步的可能,并帮助你找到系统及其数据的优化表达。 Liskov 替换原则(LSP) Liskov 替换原则提出给出某类型(类或角色或子类的特化形式),其子类型应能在不窄化其接 收数据的类型、不扩张其产出数据类型的情况下替换其父类型。换句话说,它们应该尽可能将 接收数据一般化并将产出数据专一化。 理解该原则最简单的方式是想象两个类,Dessert 和 PecanPie,后者是前者的子类。如果 这两个类 遵循 Liskov 替换原则,则在测试套件中每一个使用 Dessert 对象的地方用 PecanPie 对象替换后, 测试仍应通过。 (footnote: 更多细节参见 Reg Braithwaite 的 "IS-STRICTLY-EQUIVALENT- TO-A", http://weblog.raganwald.com/2008/04/is-strictly-equivalent-to.html。)。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_07.html[2011/2/21 21:22:13] 子类型和强制转换 Moose 允许你声明及使用类型,并通过子类型对其扩展,以进一步对数据代表的事物及其行为 作出更为专一的描述。你可以使用这些类型注解验证特定函数或方法处理的是否为合适的数 据, 甚至凭此指定从一个类型强制转换到另一个类型的机制。 更多信息请参见 Moose::Util::TypeConstraints 和 MooseX::Types。 不可变性 一个常见于面向对象程序设计新手的模式便是将对象视为一捆记录,并使用方法获得和设置内 部的值。虽然容易实现和理解,但是它可能引发不幸的诱惑,使行为上的职责散播到整个系统 各个独立的类中。 高效处理对象最为有用的技巧便是告诉它们做什么而非怎么做。如果你发现自己正访问对象的 实例数据(即使通过访问器方法)都有过度访问类职责之嫌。 避免这种行为的一种方法是将对象视为不可变。向对象的构造器传入所有相关的配置数据,接 着禁止所有来自该类外部对此类信息的修改。不要暴露任何改变实例数据的方法。 一些设计仅制止类 内部 对实例数据的修改,虽然这更加难以做到。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_08.html[2011/2/21 21:22:15] 编程风格和效能 编程和编好程是两个相关但有所区分的技能。如果我们仅编写一次性程序并不再需要修 改或维 护它们,如果我们的程序毫无缺陷,如果我们的无需在耗内存和耗时间之间做出选择, 并且如 果我们永远不用和他人一起工作,我们就不必担心我们的程序写得有多差。要写好程 序,你必 须基于特定的时间、资源侧重点理解候选解决方案和进一步计划间的区别。 写好 Perl 程序意味着理解 Perl 是如何工作的。同时也意味这个培养良好的编程品味。要 培养 这种技能,你必须不断练习编写和维护代码,并阅读优秀的代码。此处并无捷径可走──── 但通过遵守如下指导,你可以提高练习效率。 编写可维护的 Perl 程序 程序越易理解和修改越好。这就是 可维护性。假设将你现在正编写的程序放一边, 六个月以后 回来修改缺陷或是添加功能。代码越一样维护,修改是遇到的人工复杂度 就越小。 要编写可维护的 Perl 程序,你必须: 去掉重复 Perl 提供了不少使用抽象消去重复的机会。函数、对象、角色和 模块,举例来 说,允许你定义程序和解决方案的模型。 程序中重复越多,做出必要修改时花的精力越多,并且很可能会忘记修改每一必要处。 重 复越少,说明你很可能找到了问题的有效解决。最佳设计让你在添加功能的同时减少 整体 代码量。 正确命名实体 系统中每一样由你命名的事物────函数、类、方法、变量、模块 ────可以有助也可以妨害代码的清晰程度。好的一面是,你可以通过命名这些实体来 揭示你 对问题的理解以及你设计的内聚力。你的设计就是在讲述一个故事,其中经斟酌的 一词一字 都有助于在日后维护代码时帮你记起故事的来龙去脉。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_08.html[2011/2/21 21:22:15] 避免小聪明 新手有时候误将小聪明认作简明。简明的代码避免非必要的结构和 复杂性。 耍小聪明的代码通常倾向于展示聪明而非简明。Perl 提供了许多解决相似问题的 手段。通 常其中之一更加可读。有时候某中形式的解更加快速或简单,通常某一解法的上下 文特征 更加明显。 你无法总是避开 Perl 中黑暗的角落,而且有部分问题需要高效解决的小聪明。仅有良好的 代码品味和经验能帮助你估计小聪明的合适程度。按经验来看,如果你认为在你的同事面 前 解释你的解法更使你感到骄傲,你的代码更可能包含不必要的复杂性。 如果你 确实 需要编写小聪明代码,请将其封装在简单的接口之后并详尽地用文档记下你 的聪明才智。 拥抱简洁 给出两个解决相同问题的程序,简洁的那个几乎总是更易于维护。简 洁并非让 你避开高级 Perl 知识,或是避免使用库,或是扫清几百行过程式代码。 简洁意味着你高效地解决手边的问题而不用增加任何你不需要的东西。没有任何理由避开 错误 检查或验证数据或不注重安全性。相反,应该重点思考究竟什么是重要的。有时候你 不需要框 架、对象或复杂的数据结构。有时候你需要。简洁意味着你了解其中的区别。 编写惯用语化的 Perl 程序 Perl 从其他语言及编程以外的大千世界借鉴各式思想。Perl 倾向于使其 Perl 化来占有这些思 想。 要写好 Perl 程序,你必须了解有经验的 Perl 程序员是如何写程序的。 理解社区的智慧 Perl 社区通常就技巧进行辩论,有时非常激烈。甚至这些反对的声音 也 会给特点设计取舍和风格带来启示。你了解自身特定的需求,但 CPAN 作者,CPAN 开 发人员,本 地的 Perl 贩子小组以及其他程序员拥有解决类似问题的经验。和他们聊聊。 阅读他们公开的代码。 提问。并互相学习。 遵循社区标准 Perl 社区并不总是正确的,特别是在你的需求特别专一或独特时,但社 区 本身一直持续运作以尽可能广泛地解决各类问题。Perl 的测试和打包工具在代码符合 CPAN 发行 规则时可最高效地工作。遵守编码、文档、打包、测试、代码发布的各项标 准,利用好这些工具。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_08.html[2011/2/21 21:22:15] 类似地,CPAN 上的发行包如 Perl::Critic、Perl::Tidy 以及 CPAN::Mini 可让你的工作 更简单 更轻松。 阅读代码 加入诸如 Perl Beginners (http://learn.perl.org/faq/beginners.html) 之类的邮件 列表,注册一个 PerlMonks (http://perlmonks.org/) 帐号,使自己沉浸在 Perl 社区 (http://www.perl.org/community.html 包括了丰富的链接)。你将会有非常多的机会见识 他人是如 何解决问题的(无论方法是好是坏)。学习优秀的方法(通常很明显),并从不 那么好的方法中汲取教 训。 就他人发贴提出的问题编写几行代码给出自己的解答,这是一种学习的好方法。 编写高效的 Perl 程序 了解 Perl 的语法和语义只是一个起步。你之能通过 鼓励 良好设计的习惯达成良好 的设计。 编写可测试的代码 也许确保你可以维护一段代码的最佳方法就是编写一个高效 的测试套 件。编写良好的测试代码和设计程序一样,都锻炼了设计技能;绝对不要忘记,测 试代码 仍是代码。即便如此,一个良好的测试套件会给你带来信心,让你知道你可以修改程 序并 不会打破你关心的程序行为。 模块化 将你的代码分割为单独的模块强制推行封装和抽象边界。将此培养成一 种习惯后 你就能认出那些功能过于臃肿的代码单元。你也将识别出结合过于紧密的多个模块。 模块化同时强制你处理各个层面的抽象;你必须考虑系统中的各个实体如何协作。没有比 将系统修改为高效抽象更能学到抽象的价值了。 利用 CPAN 使任何 Perl 5 程序能力倍增的是这个唾手可得、令人惊叹的可重用 代码库。 数千开发人员已经编写了几万个模块,可以解决的问题超乎你的想象,CPAN 仍在继 续 成长。社区有关文档、打包、安装、测试的规范保证了代码质量,并且,以 CPAN 为中 心的 现代化 Perl 已经帮助 Perl 社区在知识、智慧和效能上发展壮大。 当可能时,请先搜索 CPAN────并询问你的社区伙伴────征询解决问题的建议。 你甚至可以报 告缺陷,或提交补丁,再或自己编写 CPAN 模块发行版。没有什么比帮助 解决他人的问题更 能展示你是一个高效的 Perl 程序员了。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_08.html[2011/2/21 21:22:15] 建立合理的编码标准 有效的指导为错误处理、安全性、封装、API 设计、 项目布局以及 其他可维护性考虑建立对策。出色的指导随着你和你的团队互相理解及项目跟进 而革新。 编程的目的是解决问题,建立编码标准的目的是帮助你清晰地表达意图。 文件 大部分程序都以某种方式与外界交互,其中交互又多于文件中发生:读、写、修改。Perl 早期 作为系统管理和文本处理语言的历史,使其非常适用于文件修改。 输入和输出 与程序外界交互的主要机制是通过 文件句柄。文件句柄代表输入输出通道的某种状态, 例如程 序的标准输入输出、程序读取或写入的某个文件,给定文件的位置。每个 Perl 5 程序都有三个 文件句柄可用,STDIN(程序的输入)、STDOUT(程序的输出),以及 STDERR (程序的错误输 出)。 默认地,用 print 或 say 打印的内容将流向 STDOUT,同时,错误和警告和用 warn() 打印的内容将流 向 STDERR。这种输出分离允许你将有用的输出及错误重定 向到两个不同的地方────例如, 输出文件和错误日志。 尚有其它可用的文件句柄;DATA 代表当前文件。当 Perl 完成对文件的编译,它留着 包全局文件 句柄 DATA 不动,并在编译单元尾打开它。如果你在 __DATA__ 或是 __END__ 后放置字符串,你可以 从 DATA 文件句柄处读取它们。对于小型自包含程序来说非常实用。 perldoc perldata 对此特性进 行了详细的描述。 除标准文件句柄之外,你可以用 open 关键字打开自己的文件句柄。为读取打开某文件: open my $fh, '<', 'filename' or die "Cannot read '$filename': $!\n"; 第一个操作数是存放文件句柄的词法变量。第二个操作数是 文件模式,它决定了文件 句柄操作 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_08.html[2011/2/21 21:22:15] 的类型。最后的操作数是文件名。如果 open 失败,die 分句将抛出异常, 用 $! 的内容给出了打 开失败的原因。 Table: 文件模式 符号 说明 < 以读方式打开 > 以写方式打开,如果文件存在则截断内容,如不存在则创建文件。 >> 以写方式打开,追加到现存内容之后,不存在则创建文件。 +< 以读 和 写的方式打开。 除了文件,你还可以在标量上打开文件句柄: use autodie; my $captured_output; open my $fh, '>', \$captured_output; do_something_awesome( $fh ); 这类文件句柄支持所有现存的文件模式。 你也许会碰到使用双参数式 open() 的早期代码: open my $fh, "> $some_file" or die "Cannot write to '$some_file': $!\n"; 文件模式和文件名之间清晰界限的缺失,在将不受信任的输入内插入第二个操作数时,会使 意 外有机可乘 (footnote: 当你阅读这句时,训练自己进行如下思考:“这段代码是不是会导致安全 问题?”) 。 你可以安全地将任何双参数式 open 替换为三参数式,不用担心有任何功能上的损 失。 perldoc perlopentut 提供了更多有关 open 奇形怪状用法的细节,包括它启动及控制其他 进程的能 力,同时也介绍了可以对输入输出进行更加精细控制的 sysopen 的用法。perldoc perlfaq5 包含处 理许多常规 IO 任务的实用代码。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_08.html[2011/2/21 21:22:15] 读取文件 给出为读打开的文件句柄,可用 readline 操作符从中读取内容,此操作符也写作 <>。 最为常见 的惯用语是用 while() 循环从文件中一次读取一行: use autodie; open my $fh, '<', 'some_file'; while (<$fh>) { chomp; say "Read a line '$_'"; } 在标量上下文中,readline 遍历可以通过该文件句柄读取的每一行,直到它遇到文件结尾 (eof())。每次迭代都会返回下一行。在遇到文件结尾后,每次迭代都返回 undef。此 while 惯用 语明确地检查迭代所用变量的是否定义,以便确保只在文件结尾处结束循环。 从 readline 读取的每一行包含标示行结束的一个或多个字符。大多数情况下,这是一个平 台相 关的序列,可由换行(\n),硬回车(\r),或者两者组合(\r\n)构成。使用 chomp 移除平台相 关的换行序列。 综合上述,Perl 5 中读取文件最清晰的方式是: use autodie; open my $fh, '<', $filename; while (my $line = <$fh>) { chomp $line; ... } 如果读取的不是 文本────而是 二进制 数据────在读写之前请在文件句柄上启用 binmode。这个关键字告诉 Perl 将来自该文件句柄的数据视作纯数据。出于对平台 可移植性等考 虑,Perl 不会以任何方式修改它。虽然在这种情况下类 Unix 平台不那么 需要使用 binmode,可 移植的程序无论如何都应使用它。更多有关 binmode 的特 性请参见 Unicode and Strings。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_08.html[2011/2/21 21:22:15] 写入文件 给定一个为写打开的文件句柄,你可以使用 print 或者 say 来写文件: use autodie; open my $out_fh, '>', 'output_file.txt'; print $out_fh "Here's a line of text\n"; say $out_fh "... and here's another"; 注意文件句柄和后续操作数之间是没有逗号的。 由 Damian Conway 所著的 Perl 最佳实践 建议养成将文件句柄包裹在大括号中的习惯。 这在 对包含在集合变量内的文件句柄进行语法分析并解歧时是必须的,无论如何这是一个值 得培养 的好习惯。 你可以用 print 或 say 写入整列值,在这种情况下,Perl 5 使用特殊全局变量 $, 作为 列表值的分 隔符。Perl 同时用 $\ 的值作为 print 和 say 结尾的参数。 关闭文件 当你完成文件操作后,你可以用 close 明确地关闭它,或者让文件句柄自行超出作用域,在这种 情况下 Perl 会替你关闭它。明确调用 close 的好处是,你可以检查特定的错误────同时也 可以 从其中恢复,例如,存储设备容量不足或是网络状况欠佳。 和往常一样,autodie(autodie ç¼è¯å½ä»¤)会为你处理这类检查: use autodie; open my $fh, '>', $file; ... close $fh; 特殊文件句柄变量 读取每一行,Perl 5 都会增加 $. 的值,它可以用作行计数器。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_08.html[2011/2/21 21:22:15] readline 将 $/ 当前的内容用作行结束序列(详例参见 å¨æä½ç¨å)。此变量 的值默认为当前平台 上最合适的文本文件行结束符序列。事实上,“行” 一字用法不当。你可 以将 $/ 设置为任意字 符序列 (footnote: ……但别设置成正则表达式,因为 Perl 5 并不支持这样做。)。 这在一次读 取高度结构化数据的一条 记录 时很有用。 默认地,Perl 使用 缓冲式输出,即仅在数据足够多且超出某一限制时才进行 IO 操作。这使 得 Perl 可以批量处理昂贵的 IO 操作而不必每次只写非常少量的数据。有时你想尽快发送手边的 数据而不想等待缓冲────特别是你在编写一些连接至其他程序或行式网络服务的命令行过 滤器时。 当前活动的输出文件句柄缓冲由 $| 变量控制。当设置为非零值时,Perl 在每次对此文件句柄 进行写入操作后都会冲洗输出。当设置为零值,Perl 仍将采用默认的缓冲策略。 作为对全局变量的代替,可以在词法文件句柄上调用 autoflush() 方法。请确认加载 FileHandle 在 先,否则你将不能在词法文件句柄上调用此方法: use autodie; use FileHandle; open my $fh, '>', 'pecan.log'; $fh->autoflush( 1 ); ... 当加载完 FileHandle,你还可以使用其中的 input_line_number() 和 input_record_separator() 方法,作为 和 $.、$/ 对应的替代。参考 perldoc FileHandle 和 perldoc IO::Handle 以获取 更多信息。 如果你使用的是 Perl 5.12 或更新版本,IO::File 已经被 FileHandle 所替代。 目录和路径 你也可以用 Perl 5 修改目录和文件路径。操作文件夹和操作文件差不多,除了你不可以 写 目 录之外 (footnote: 作为代替,你可以保存、移动、重命名和删除其中的文件。)。你可以 用 opendir 打开目录句柄: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_08.html[2011/2/21 21:22:15] use autodie; opendir my $dirh, '/home/monkeytamer/tasks/'; 从文件夹中读取的语言关键字是 readdir。就像 readline,你可以一一遍历目录 内容,或者将它们 一举赋值给数组: # 迭代 while (my $file = readdir $dirh ) { ... } # 展开为列表 my @files = readdir $otherdirh; 作为 5.12 新增特性,while 循环中 readdir 会设置 $_,正如 while 中 的 readline: use 5.012; use autodie; opendir my $dirh, 'tasks/circus/'; while (readdir $dirh) { next if /^\./; say "Found a task $_!"; } 例子中可笑的正则表达式在 Unix 和类 Unix 系统上跳过所谓 隐藏文件,即前缀点号默认 防止 它们出现在文件列表中。它同时也跳过了每次 readdir 调用返回的最初两个文件,特 别是 . 和 ..。这两个文件代表了当前目录和上级目录。 注意由 readdir 返回的名称是 相对于 目录自身的。换句话说,如果 tasks/ 目录 包含三个名为 eat、drink 和 be_monkey 的文件,readdir 将返回 eat、drink 以及 be_monkey 而 非 tasks/eat、tasks/drink 和 task/be_monkey。相反地, 绝对 路径是一个完全限定于文件系统的路 径。 可以通过越过作用域或用 closedir 关键字关闭一个目录句柄。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_08.html[2011/2/21 21:22:15] 操作路径 Perl 5 用 Unix 式的视角看待世界,至少对于文件系统部分是这样的。即便不使用类 Unix 平 台,Perl 仍将解析适合于你操作系统和文件系统的 Unix 式路径。换句话说,如果你使用 Microsoft Windows,你可以和使用 C:\My Documents\Robots\Caprica Six\ 一般方便地 使用 C:/My Documents/Robots/Bender/ 这样的路径。 即便如此,以安全的、跨平台的行为操作文件路径意味着你必须避免字符串内插和拼接。 核心 模块 File::Spec 系列为以安全可以移植地操作文件路径提供了抽象。即便如此, 理解并正确使 用它们并不总是容易的。 CPAN 上的 Path::Class 模块为 File::Spec 提供了更好的接口。可以使用 dir() 函数创建代表目录 的对象,以及使用 file() 函数创建代表文件的对象: use Path::Class; my $meals = dir( 'tasks', 'cooking' ); my $file = file( 'tasks', 'health', 'exoskeleton_research.txt' ); ……并且你可以这样得到目录中的文件对象: my $lunch = $meals->file( 'veggie_calzone.txt' ); ……反之亦然: my $robots_dir = $robot_list->dir(); 你甚至可以打开文件和目录的文件句柄: my $dir_fh = $dir->open(); my $robots_fh = $robot_list->open( 'r' ) or die "Open failed: $!"; 更多信息请参见 Path::Class::Dir 和 Path::Class::File 文档。 文件操作 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_08.html[2011/2/21 21:22:15] 除了读写文件,你还可以像在命令行或是文件管理器中那样直接操作它们。-X 文件测试 操作符 可以提供有关系统上文件和目录的属性信息。例如,要测试某文件是否存在: say 'Present!' if -e $filename; -e 操作符只有一个操作数,即文件名或者文件、目录的文件句柄。如果文件存在,此表达式 求 值得真。perldoc -f -X 列出了所以其他的文件测试;最著名的有: -f, 如果操作数是普通文件,返回真值 -d, 如果操作数是目录,返回真值 -r, 如果操作数的文件属性对当前用户可读,返回真值 -z, 如果操作数是非空文件,返回真值 在 Perl 5.10.1 之后,你可以用形如 perldoc -f -r 的方式察看这些文件测试操作的文档。 Perl 同时允许你改变当前目录。默认地,当前目录就是你启动程序的目录。核心模块 Cwd 允许 你判断当前目录。chdir 关键字可以用于改变当前工作目录。这在用相对────而非绝对 ────路径 操作文件时非常有用。 rename 关键字可以重命名或在目录间移动某个文件。它接受两个操作数,旧文件名和 新文件 名: use autodie; rename 'death_star.txt', 'carbon_sink.txt'; 复制文件没有对应的核心关键字,但是核心模块 File::Copy 提供了 copy() 和 move() 函数两者。可 以使用 unlink 来删除一个或多个文件。这些函数和关键字在操作成功时候返回真 值,出错则设 置 $!。 Path::Class 为检查特定文件属性和完整删除文件提供了便捷的方法,并且是跨平台的。 异常 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_08.html[2011/2/21 21:22:15] 如果一切都像预期的那样工作,编程就会变得简单些。不幸的是,要处理的文件可不一定。 有 时候你还会碰到磁盘空间不足、网络连接不稳、数据库拒绝写入数据等等。 异常总会发生,健壮的软件必须处理这些异常状况。如果可以从中回复,那太好了!如果不 行,有时候能做的也只能是重试或至少为进一步调试将相关信息记入日志。Perl 5 以 异常 的方 式处理非正常状况:一个动态作用域的控制流程语法形式,让你可以在最合适的地方处理 错 误。 抛出异常 考虑你需要为记日志打开文件这种情况。如果你不能打开该文件,就说明有错误发生。你 可以 用 die 来抛出一个异常: sub open_log_file { my $name = shift; open my $fh, '>>', $name or die "Can't open logging file '$name': $!"; return $fh; } die() 设置全局变量 $@ 为其参数并立即退出当前函数而 不返回任何值。如果函数调用方 不明确 地处理此异常,则此异常将向上传播至所有的调用者,直到有东西处理它,或者程序以一条错 误信息退出。 异常抛出和处理的动态作用域与 local 符号(å¨æä½ç¨å)一致。 捕获异常 未捕获的异常最终将结束程序。有时候这是有用的;一个从 cron (一个 Unix 作业调度器) 运 行的系统管理程序可以在填写错误日志后抛出异常;这样便可以将错误通知给系统管理员。 而 另一些异常并非致命的;优秀的程序可以从中恢复,或者至少保存状态并更为体面地退出。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_08.html[2011/2/21 21:22:15] 要捕获一个异常,可以使用 eval 操作符的代码块形式: # 也许无法打开日志文件 my $fh = eval { open_log_file( 'monkeytown.log' ) }; 就像所有的代码块,eval 的代码块参数引入了新的作用域。如果文件打开成功,$fh 将 包含此文 件的文件句柄。如果失败,$fh 将维持未定义,且 Perl 将继续执行程序中的下一 行语句。 如果 open_log_file() 调用了一个调用了其他函数的函数,如果其中某函数抛出了自身的异 常,这 条 eval 语句会捕获到,否则什么也不做。没什么要求异常处理器只处理你希望处理的 异常。 要检查捕获的内容(或检查究竟有没有捕获到异常),检查 $@ 的值: # 也许无法打开日志文件 my $fh = eval { open_log_file( 'monkeytown.log' ) }; # 捕获异常 if ($@) { ... } 当然,$@ 是一个 全局 变量。为了安全起见,你应该在意图捕获异常之前,用 local 本地化的它 的值: local $@; # 也许无法打开日志文件 my $fh = eval { open_log_file( 'monkeytown.log' ) }; # 捕获异常 if ($@) { ... } 你可以按可能的异常逐条检查 $@ 字符串值,看看是否能够处理或者是否应该重新抛出 这条异 常: if (my $exception = $@) { die $exception unless $exception =~ /^Can't open logging file/; $fh = log_to_syslog(); } file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_08.html[2011/2/21 21:22:15] 将 $@ 复制到 $exception 可以避免后续代码破坏全局变量 $@ 值的可能。你绝对不会知道 有什么在 其他地方用 eval 代码块重置了 $@ 的值。 你可以通过向 die() 传入 $@ 再次抛出异常。 你会发现对 $@ 的值使用正则表达式令人讨厌;你还可以对 die 使用 对象。诚然,这比较 少 见。$@ 可能 包含任意引用,但是就实际来说差不多 95% 是字符串 5% 是对象。 作为自行编写异常处理机制的替代,参见 CPAN 发行模块 Exception::Class。 异常注意事项 正确使用 $@ 很讲究技巧;全局变量的本质为其带来了不少微妙的缺陷: 动态作用域下使用未经 local 处理的变量值可能会导致其被重置 在异常捕获作用域内的对象析构可能调用 eval 并改变它的值 它可能包含一个覆盖自身布尔值的对象,使其返回假 一个信号处理器(特别是 DIE 信号处理器)可能在你不经意时修改它的值 编写非常安全和合理的异常处理器非常困难。来自 CPAN Try::Tiny 发行模块非常简短,易于安 装, 易于理解,同时也易于使用: use Try::Tiny; my $fh = try { open_log_file( 'monkeytown.log' ) } catch { ... }; 不仅是语法比 Perl 5 默认的更加友好,而且该模块在你不知情的情况下处理了所有边边角角的 情况。 内置异常 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_08.html[2011/2/21 21:22:15] Perl 5 有部分异常状况可以通过 eval 块捕获。perldoc perldiag 把它们列为 “trappable fatal errors”。 大多数是在编译期抛出的语法错误。其他一些是运行时错误。有些异常也许值得捕 获;语法错误则不值得。 最有趣或者很可能因如下原因发生异常: 在上锁的哈希里使用不允许的键 (åå¸ä¸é) 对非引用进行 bless (ç» bless åçå¼ç¨) 在非法的调用物上调用方法 (Moose) 在调用物上无法找到对应名称的方法 以不安全的方式使用污点(taint)值 (Taint) 修改只读值 在类型错误的引用上使用错误的操作 (å¼ç¨) 如果你启用了致命词法警告(注åèªå·±çè¦å),你可以捕获因它们而起的异常。 对来自 autodie 的异常同样适用(autodie ç¼è¯å½ä»¤)。 编译命令 Perl 5 的扩展机制是模块(模å)。大部分模块提供了函数可供调用的函数,或定义 了一些类 (Moose),但有部分模块则不同,它们对语言自身的行为产生了影响。 影响编译器行为的模块称为一条 编译命令(pragma)。按照惯例,编译命令的名称为小写, 以示与其他模块的区别。你已经听说过的一些编译命令有:strict 和 warnings。 编译命令和作用域 编译命令的工作方式是向闭合的静态作用域导出特定行为或信息。编译命令的作用域与词法变 量相同。在某种程度上你可以将词法变量声明看做是一种带有可笑语法的编译命令。用例子说 明编译命令的作用域会更加清楚: { # $lexical 不 可见;strict 未 生效 { use strict; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_08.html[2011/2/21 21:22:15] my $lexical = 'available here'; # $lexical 可见;strict 有效 ... } # $lexical 再次 不 可见;strict 不 起作用 } 一个潜能被充分激发的 Perl 大师可以实现一个无视作用域的行为不良的编译命令, 但这太不 友善了。 如同词法声明影响内层作用域那样,编译命令也会如此: # 整个文件范围 use strict; { # 内层作用域;但是仍在 strict 生效的范围内 my $inner = 'another lexical'; ... } 使用编译命令 编译命令与模块的使用机制相同。和使用模块时相同,你可以指定所需编译命令的版本,也 可 以向编译命令传递参数列表以便更好地控制其行为: # 要求变量声明;防止裸字函数名称 use strict qw( subs vars ); 在作用域内你可以用 no 关键字禁用全部或部分编译命令: use strict; { # 准备修改符号表 no strict 'refs'; ... } 常用核心编译命令 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_08.html[2011/2/21 21:22:15] Perl 5 包含了一些有用的核心编译命令: strict 编译命令启用编译器对符号引用、裸字、变量声明的检查 warnings 编译命令为不推荐的、出人意料的、古怪的行为启用可选的警告。 它们 不一定 是 错误,但可能产生多余的行为 utf8 编译命令对源代码启用 UTF-8 编码 autodie 编译命令(5.10.1 新增)对系统调用和关键字启用自动错误检查, 减少手动检查之 需 constant 编译命令允许你创建编译期常量(也请参见来自 CPAN 的 Readonly 替代) vars 编译命令允许你声明包全局变量,诸如 $VERSION、导出变量(导åºï¼âexportâï¼) 以 及来自 Perl 文档的 OO(ç» bless åçå¼ç¨) 另有一些来自 CPAN 的实用编译命令。两个值得细看的是 autobox,它为 Perl 5 核心类型 (标 量、引用、数组和哈希)启用了类对象行为;以及 perl5i,它组合并启用了许多实验性 质的语 言扩展,使之成为一个有机的整体。这两个编译命令不经大量测试和慎重考虑可能不会出 现在 你的产品代码中,但它们展示了编译命令的实用和强大。 Perl 5.10.0 新增了用纯 Perl 代码编写你自己的词法编译命令的能力。perldoc perlpragma 阐述了如 何去做,同时,perldoc perlvar 内对 $^H 的解释说明了此特性的工作原理。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] 管理现实世界中的程序 编写简单的示例程序来解决本书中的示例问题有助你从小处学习一门语言。编写现实世界 中的 程序则提出了比学习语言语法、或其设计原理,甚至是查找并使用语言库更高的要求。 实际编程要求你管理自己的代码:组织代码、了解其如何运作、让它在错误的意图和逻辑面 前 更为健壮、并以一种理性、清晰、可维护的方式完成以上目的。所幸的是,现代化的 Perl 为编 写实际应用────从测试到组织源码────提供了许多工具和技巧。 测试 测试 是编写并运行自动验证套件的过程,以保证软件整体或局部按预期的方式工作。从 根本上 来说,这是你已经无数次手动执行过程的自动化:编写一段代码,运行并检查是否正 常。区别 在与整个过程是否 自动化。相比手动执行这些步骤并依靠人力保证每次都完美 无缺,还是让计 算机来处理这些重复部分。 Perl 5 提供了上佳的工具来帮助你编写良好且实用的自动化测试。 Test::More Perl 测试始于核心模块 Test::More 及其 ok() 函数。ok() 接受两个参数, 一个布尔值和一个描述测 试目的的字符串: ok( 1, 'the number one should be true' ); ok( 0, '... and the number zero should not' ); ok( '', 'the empty string should be false' ); ok( '!', '... and a non-empty string should not' ); 最终,任何能够在程序中测试的条件将会变为一个布尔值。代码是否如期工作?一个复杂 的程 序也许有上千条独立的测试条件。通常,粒度越细越好。编写独立断言的目的是将一 个个功能 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] 进行隔离以便了解哪些功能不正常以及做出进一步改动后哪些部分罢工了。 然而,上述代码片段并不是一个完整的测试脚本。Test::More 以及相关的模块要求写 明 测试计 划,它代表了欲进行的独立测试个数: use Test::More tests => 4; ok( 1, 'the number one should be true' ); ok( 0, '... and the number zero should not' ); ok( '', 'the empty string should be false' ); ok( '!', '... and a non-empty string should not' ); Test::More 的 tests 参数为此程序设置测试计划。这向测试增加了一项额外的断 言。如果实际执 行的测试少于四项,表示有错误发生。如果多于四项,还是不对。在这种 简单的情形下,该断 言不那么有用,但它 能够 捕捉到代码中的简单到不太可能出错 的那种缺陷 (footnote: 作为一 条规则,任何你吹嘘简单到不可能出错的代码会不幸地时候包含错误。)。 你不必以 import() 参数的形式提供 tests => ...。你还可以在测试程序的 结尾,调用 done_testing() 函数。虽然在程序开头包含固定的测试数目能保证只执行 预期数量的测试,但有时候确认这个 数量是非常痛苦的一件事。在这里情况下,done_testing() 将验证成功执行的测试数量──── 否则,你怎么可能会 知道 呢? 执行测试 结果就是一个功能齐全的 Perl 5 程序,它产生如下输出: 1..4 ok 1 - the number one should be true not ok 2 - ... and the number zero should not # Failed test '... and the number zero should not' # at truth_values.t line 4. not ok 3 - the empty string should be false # Failed test 'the empty string should be false' # at truth_values.t line 5. ok 4 - ... and a non-empty string should not # Looks like you failed 2 tests of 4. file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] 此格式遵循名为 TAP,即 Test Anything Protocol (http://testanything.org/) 的测试输出标准。 作为此协议的一部分,失败的测试输出诊断信息。这对于调试来说是莫大的 帮助。 测试文件输出的各类断言(特别是多种 失败 断言)可能会很详细。在大多数情况下,你 希望 了解测试是全部通过了或是其中 x、y、z 失败了。核心模块 Test::Harness 解析 TAP 并显示最贴 切的信息。它同时提供了一个名为 prove 的程序,它接手了所有这些繁重的工 作: $ prove truth_values.t truth_values.t .. 1/4 # Failed test '... and the number zero should not' # at truth_values.t line 4. # Failed test 'the empty string should be false' # at truth_values.t line 5. # Looks like you failed 2 tests of 4. truth_values.t .. Dubious, test returned 2 (wstat 512, 0x200) Failed 2/4 subtests Test Summary Report ------------------- truth_values.t (Wstat: 512 Tests: 4 Failed: 2) Failed tests: 2-3 有很大部分显示的是一些很显然的内容:第二、三两个测试因为零和空字符串求值得假而失 败。进行双重否定布尔转换(å¸å°å¼ºå¶è½¬æ¢)即可很方便地修正这些错误: ok( ! 0, '... and the number zero should not' ); ok( ! '', 'the empty string should be false' ); 有了这些修改,prove 现在显示: $ prove truth_values.t truth_values.t .. ok All tests successful. 更好的比较 即使所有自动测试归根究底只是一些“是真是假”的布尔条件,将所有这些规约为一条条布 尔条 件仍显乏味且没有提供作进一步诊断的可能。Test::More 提供了若干方便函数来 确保你的代码按 你的意图行事。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] is() 函数比较两个值。如果它们匹配,则测试通过。否则,测试失败并提供相关诊断信息: is( 4, 2 + 2, 'addition should hold steady across the universe' ); is( 'pancake', 100, 'pancakes should have a delicious numeric value' ); 按你预期的,第一项测试通过而第二项会失败: t/is_tests.t .. 1/2 # Failed test 'pancakes should have a delicious numeric value' # at t/is_tests.t line 8. # got: 'pancake' # expected: '100' # Looks like you failed 1 test of 2. ok() 只听过失败测试的行号,is() 显示未能匹配的值。 is() 对其值应用隐式的标量上下文。这意味着,例如,你可以不用明确地在标量上 下文中对数 组求值而检查其中元素的个数: my @cousins = qw( Rick Kristen Alex Kaycee Eric Corey ); is( @cousins, 6, 'I should have only six cousins' ); ……虽然有些人考虑清晰度更倾向于编写 scalar @cousins。 Test::More 还提供了对应的 isnt() 函数,仅在所提供值不相等时通过测试。除此 之外,它与 is() 的行为相同,也遵循标量上下文和比较类型。 is() 和 isnt() 都是通过 Perl 5 操作符 eq 及 ne 进行 字符串比较。 这样几乎总是正确的,但是对 于复杂的值,如,重载对象(éè½½)或是双重变 量(åéåé),你会更倾向使用明确的比较测 试。cmp_ok() 函数允许你指定自己 的比较操作符: cmp_ok( 100, $cur_balance, '<=', 'I should have at least $100' ); cmp_ok( $monkey, $ape, '==', 'Simian numifications should agree' ); 类和对象自身会以有趣的方式和测试互动。通过 isa_ok() 可以测试一个类或对象是否是 其它类 的扩展(继æ¿): file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] my $chimpzilla = RobotMonkey->new(); isa_ok( $chimpzilla, 'Robot' ); isa_ok( $chimpzilla, 'Monkey' ); isa_ok() 在失败时会提供自己的诊断信息。 can_ok() 验证一个类会对象是否能够执行所要求的(多个)方法: can_ok( $chimpzilla, 'eat_banana' ); can_ok( $chimpzilla, 'transform', 'destroy_tokyo' ); is_deeply() 函数比较两个引用以保证它们的内容相同: 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::More 将尽力做出合理的诊断指明结构间首处不等的位置。参见 CPAN 模块 Test::Differences 和 Test::Deep,了解更多有关可配置测试的信息。 Test::More 还有另外一些测试函数,但上面介绍的这些最为有用。 组织测试 CPAN 组织测试的标准方法是创建一个包含一个或多个以 .t 结尾程序的 t/ 目录。 所有的 CPAN 发行模块管理工具(包括 CPAN 基础设施自身)都能理解这套系统。默认地, 当你使 用 Module::Build 或 ExtUtils::MakeMaker 构建一个发行模块时,测试步 骤将执行所有 t/*.t 文件,综合 它们的输出,并按测试套件的总体结果决定测试通过 还是不通过。 目前没有什么有关管理独立 .t 文件内容的建议,但有两种策略比较常见: 每个 .t 文件对应一个 .pm 文件 每个 .t 文件对应一个程序功能 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] 由于大型文件较小文件难以维护,并且测试套件的粒度也是如此,对与测试文件组织方式 的重 要考虑之一便是可维护性。一种混合的管理方式较为灵活:由一个测试验证所有模块 是否能够 编译,其他测试确保每个模块都能如常工作。 通常只对当前在开发功能执行测试。如果你正向 RobotMonkey 添加喷火功能,那么你 可能会希望 执行 t/breathe_fire.t 测试文件。当你已经对此功能非常满意了,就可以 运行全套测试以保证程 序整体未受局部改动的影响。 其他测试模块 Test::More 依赖与名为 Test::Builder 的测试后端。后者管理测试计划并将测试结果 组织为 TAP。 这种设计允许多个测试模块共享同一 Test::Builder 后端。因此,CPAN 有数 以百计的测试模块 可供使用────并且,它们可以在同一程序中协同工作。 Test::Exception 提供了保证你代码正确(不)抛出异常的函数。 Test::MockObject 和 Test::MockModule 允许你通过 模拟(mocking) (模仿但产出不同结果)测 试难以测试的接口。 Test::WWW::Mechanize 允许你测试线上的 Web 应用。 Test::Database 提供测试对数据库使用及误用情况的函数。 Test::Class 另行提供组织测试套件的机制。它允许你按特定的测试方法组来 创建类。你可 以像一般对象继承那样从测试类继承。这是一种降低测试套件重复的好方法。 参见由 Curtis Poe 编写的 Test::Class 系列,位于 http://www.modernperlbooks.com/mt/2009/03/organizing-test-suites-with- testclass.html。 Devel::Cover 分析测试套件的执行情况并报告经由实际测试代码的数量。一般 说来,覆盖率 越高越好────虽然不是总能达到 100% 覆盖率,但 95% 要比 80% 好上不少。 Perl QA 项目(http://qa.perl.org/)是测试模块的主要源头,也是使 Perl 测试简单 高效的智慧 和实用经验的来源。 处理警告 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] Perl 5 会针对各类令人困惑、不甚清楚、带有歧义的情况产生可选的警告。虽然你应该 在几乎 任何时候无条件地启用警告,但某些情形使我们谨慎地做出禁用警告的决定──── 并且 Perl 支持这样做。 产生警告 可使用 warn 关键字发出警告: warn 'Something went wrong!'; warn 将一个值列表打印至 STDERR 文件句柄(è¾å¥åè¾åº)。Perl 会将文件名和 warn 调用发生 的行号附加其后,除非列表最后一个元素以换行结尾。 核心模块 Carp 提供了其他产生警告的机制。其中的 carp() 函数把警告以调用方 的角度汇报。就 是说,你可以按如下方式检查一个函数是几元函数(åæ°æ°é): use Carp; sub only_two_arguments { my ($lop, $rop) = @_; Carp::carp( 'Too many arguments provided' ) if @_ > 2; ... } ……阅读错误消息的每一个人将得知 调用方 代码文件名和行号,而非 only_two_arguments()。 类 似地,Carp 中的 cluck() 输出到此调用为止的所有函数调用栈跟踪。 为在系统范围内全面跟进怪异的警告或异常,可以对整个程序启用 Carp 的详细模式: $ perl -MCarp=verbose my_prog.pl 这样便将所有 croak() 调用改为 confess() 的行为而所有 carp() 调用改为 cluck() 的行为。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] 启用和禁用警告 对警告的词法封装如同对变量的词法封装一样重要。旧式代码可能会使用 -w 命令行参数对 整 个文件启用警告,即便其它代码没有明确压制警告产生的意图。它要么有要么没有。如果你有 消除警告及潜在警告的资本,它很实用。 现代化的方法是使用 warnings 编译命令。在代码中对 use warnings; 或其等价 (footnote: 诸如 use Modern::Perl;) 的使用意味着作者认为对此代码的常规操作不会引发警告。 -W 命令行参数单方面对整个程序启用警告,而不顾 warnings 编译命令的词法启、禁用。-X 参数 对整个程序单方面 禁用 警告。没有哪个参数是常用的。 所有 -w、-W 和 -X 影响全局变量 $^W 的值。在 warnings 编译命令出现(2000 年 春季,Perl 5.6.0) 之前编写的代码,也许会 local 化 $^W 以在给定的作用域内压制某些警告。 新编写的代码应使用 编译命令。 禁用警告类 要在作用域内选择性禁用警告,使用 no warnings; 并提供一列参数。忽略参数列表则在该作用域 内禁用所有警告。 perldoc perllexwarn 列出了你的 Perl 5 版本在使用 warnings 编译命令时能够理解的所 有类别的警 告。它们中的大多数代表了你的程序可能会陷入的一些非常有意思的条件,并且可由 Perl 认 出。另有一小部分在特定条件下不那么有用。举例来说,如果 Perl 检测到一个函数自行调用在 一百次以上,则产生 recursion 警告。如果你对你编写递归函数的能力有信心,可以在递归的 作 用域内禁用次警告(另行参见 å°¾é¨è°ç¨。) 如果你正生成代码(代ç çæ)或局部地重定义符号,你或许想禁用 redefine 警告。 一些有经验的 Perl 黑客在从众多来源拼接值的字符串处理代码内禁用 uninitialized 值的警告。 仔细地初始化变量可以免除禁用此警告的需要,但局部风格和简明的代码可使这种警告没有实 际意义。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] 致命的警告 如果你的项目视警告如错误般繁重,你可以使其词法致命。让 所有 警告提升为异常: use warnings FATAL => 'all'; 你也许会想使特定类别的警告变得致命,例如对不推荐语法结构的使用: use warnings FATAL => 'deprecated'; 捕获警告 就像你可以捕获异常,你也可以捕获警告。%SIG 变量持有所有类型的信号处理器,这些 信号可 由 Perl 或你的操作系统抛出。它还包括两个专为 Perl 5 异常和警告准备的信号处 理器槽。要 捕获警告,将一个匿名函数安装到 $SIG{__WARN__}: { my $warning; local $SIG{__WARN__} = sub { $warning .= shift }; # 做一些冒险的事 say "Caught warning:\n$warning" if $warning; } 在警告处理器内,第一个参数是警告消息。不可否认,这个技巧不像在词法范围内禁用 警告那 么有用────但它在如来自 CPAN 的 Test::Warnings 等测试模块内得到良好的 利用,这些时 候重要的是警告消息的实际文本内容。 perldoc perlvar 更为详细地讨论了 %SIG。 注册自己的警告 对 warnings::register 编译命令的使用可以让你创建自己词法警告,使你代码的用户 可以按合适 启用、禁用词法警告。很容易就可以做到────在某模块内,用 use 引入 warnings::register 编 译命令: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] package Scary::Monkey; use warnings::register; 1; 这将创建一个按此包命名的新警告类别(此例中是 Scary::Monkey)。用户可以显式地用 use warnings 'Scary::Monkey' 启用或用 no warnings 'Scary::Monkey' 显式禁用它。 要报告一条警告,结合 warnings::enabled() 使用 warnings::warn() 函数: package Scary::Monkey; use warnings::register; sub import { warnings::warn( __PACKAGE__ . ' used with empty import list' ) if @_ == 0 && warnings::enabled(); } 1; 如果 warnings::enabled() 为真,那么调用方词法作用域即启用此项警告。你也可 以报告已存在类 别的警告,诸如对不推荐语法结构的使用: package Scary::Monkey; use warnings::register; sub import { warnings::warnif( 'deprecated', 'empty imports from ' . __PACKAGE__ . ' are now deprecated' ) unless @_; } 1; warnings::warnif() 函数检查具名警告类别并在其活动时报告。 更多细节参见 perldoc perllexwarn。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] 模块 模块 就是一个包含于自身文件中、可用 use 或 require 加载的包。一个模块必须是 合法的 Perl 5 代码。它必须以一个求值得真的表达式结束,使 Perl 5 语法分析器知道它已成 功地加载并编译 了该模块。 除一些普遍使用的惯例外,没有其他要求。 包通常对应于磁盘上的文件,当你使用 use 或 require 的裸字形式加载一个模块时,Perl 根据双 冒号(::)分割包名,并将包名的组成部分转换成路径。因此: use StrangeMonkey; ……使得 Perl 在 @INC 的每一个目录中依次搜索名为 StrangeMonkey.pm 的文件,直到找到 该文 件或完成对列表的遍历。同样地: use StrangeMonkey::Persistence; ……使得 Perl 在 @INC 中所有目录下存在的 StrangeMonkey/ 子目录中查找名为 Persistence.pm 的文 件,如此等等。最后: use StrangeMonkey::UI::Mobile; ……使得 Perl 在相对每个 @INC 中目录的 StrangeMonkey/UI/Mobile.pm 路径处寻找文件。 换句 话说,如果你想加载你的 StrangeMonkey::Test::Stress 模块,你必须拥有一个名为 StrangeMonkey/Test/Stress.pm,且它可以顺着 @INC 所列出的目录找到。 perldoc -l Module::Name 会打印出相关 .pm 文件的完整路径,并提供存在于 .pm 文 件中该模块的 文档。 技术上 不要求此位置下的文件必须包含 package 声明,更不用说匹配文件名的 package 声明了。 然而,出于维护上的考虑,高度推荐此惯例。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] 使用(“use”)和导入(“import”) 当你用 use 关键字加载模块时,Perl 从磁盘上加载它,接着调用它的 import() 方法,将 你提供 的参数传递进去。这发生在编译期: use strict; # 调用 strict->import() use CGI ':standard'; # 调用 CGI->import( ':standard' ) use feature qw( say switch ) # 调用 feature->import( qw( say switch ) ) 你不必要提供一个 import() 方法,你也可以将其用于任何目的,但标准 API 期望 它接受一个由 符号组成的参数列表(通常是函数)使其在调用方名称空间内可用。这不 是一个强制的要求, 诸如 strict 等编译命令(ç¼è¯å½ä»¤)改变了它们的行为而非 导入符号。 no 关键字调用一个模块的 unimport() 方法,如果它存在,则传入参数。虽然可能 移除已经导入 的符号,但通常它用于禁用特定编译命令特性以及其他通过 import() 引 入新特性的模块: use strict; # 禁用符号引用,要求变量声明,不允许裸字 ... { no strict 'refs'; # 允许符号引用 # 仍要求变量声明,禁止裸字 } 就像 use 和 import(),no 在编译期调用 unimport()。在效果上: use Module::Name qw( list of arguments ); ……和下面的代码效果一样: BEGIN { require 'Module/Name.pm'; Module::Name->import( qw( list of arguments ) ); } file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] 类似的: no Module::Name qw( list of arguments ); ……和下面的代码等效: BEGIN { require 'Module/Name.pm'; Module::Name->unimport( qw( list of arguments ) ); } ……包括对模块的 require。 你可以直接调用 import() 和 unimport(),虽然在 BEGIN 块之外反导入(“unimport”) 一个编译命 令有些说不通,通常它们对编译期另有影响。 如果 import() 或 unimport() 不存在于模块中,Perl 不会给出错误消息。它们事实上 是可选的。 Perl 5 的 use 和 require 是大小写敏感的,然而底层的文件系统不是。虽然 Perl 知道 strict 和 Strict 之间的区别,你使用的操作系统和文件系统也许并不知道。如 果你写的是 use Strict;,一 个大小写敏感的文件系统不会去查找 strict.pm。一个大 小写不敏感文件系统则将找到 Strict.pm。然而,当 Perl 尝试在已加载的模块上调用 Strict->import() 时,不会产生任何效果, 因为包名是 strict。 可移植的程序在虽然不必的情况下也会严格对待大小写。 导出(“export”) 模块可以通过一个名为 导出 的过程使全局符号在其它包中可用。这是通过 use 语句 向 import() 传递参数的反面。 向其它模块导出函数或变量的标准方式是通过核心模块 Exporter。Exporter 依赖于包 全局变量 ────特别是 @EXPORT_OK 和 @EXPORT────它们包含了一个在请求时导出的符号 列表。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] 考虑一个提供若干全系统可用的独立函数的 StrangeMonkey::Utilities 模块: package StrangeMonkey::Utilities; use Exporter 'import'; our @EXPORT_OK = qw( round_number translate screech ); ... 1; 任何人都可以使用这个模块,并且,可选地,导入任一或全部三个导出函数。你也可以导出变 量: push @EXPORT_OK, qw( $spider $saki $squirrel ); CPAN 模块 Sub::Exporter 为不使用包全局变量导出函数提供了一个更好的接口。它同时提供 了 更多强大的选项。然而,Exporter 可以导出变量,而 Sub::Exporter 只可以导出函数。 你 可以 通过将符号列在 @EXPORT 而非 @EXPORT_OK 中来默认地导出它们: our @EXPORT = qw( monkey_dance monkey_sleep ); ……因此,任何 use StrangeMonkey::Utilities; 语句将导入两个函数。注意指定要导入 的符号并 不 导入默认的符号。同时,你可以通过显式地提供一个空列表来加载一个模 块而不导入任何符 号: # 是模块可用,但不用 import() 导入符号 use StrangeMonkey::Utilities (); 不理会任何导入列表,你总是可以通过完全限定名称来调用其它包中的函数: StrangeMonkey::Utilities::screech(); 使用模块来组织代码 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] Perl 5 并不要求你使用模块,也不要求你使用包或是名称空间。你可以将所有代码放在单 个 .pl 文件中,或多个 .pl 文件,随后你可以按需通过 do 或 require 加 载。你拥有灵活性来按合适的方 式管理代码,给出开发风格,控制项目的条框、风险和回报、 增加经验,以及 Perl 5 部署的舒 适程度。 还有一条经验之谈来自有经验的 Perl 5 程序员,就是,一个上百行代码的项目可从创建模 块中 获得多重益处。 模块有助于强制对系统中不同实体进行逻辑上的隔离; 模块提供 API 边界,无论是过程式还是面向对象; 模块使源代码自然组织; Perl 5 生态系统有许多工具专门创建、维护、组织、部署模块和发行版; 模块提供了一种代码重用机制。 即便你不采用面向对象的手法,为系统中不同实体或职责建立模块保持相关代码内聚、不相关 代码隔离。 发行模块 发行模块(“distribution”) 是一个或多个模块(模å)的集合,由此组成单 个可重分发、可测 试、可安装的单元。效果上即是模块和元数据的集合。 管理软件配置、构建、分发、测试和安装────甚至是在你的工作组织中────最为简便 的方 法就是创建和 CPAN 兼容的模块。CPAN 的惯例────如何打包发行、如何解决其依 赖、将其 安装至何处、如何验证它是否正常、如何显示文档、如何管理代码仓库────这些 问题已经 由维护万个项目的几千贡献者所共同提出。 特别是,由 CPAN 实现的测试、报告、依赖检查功能非常齐全,已大大超出同类语言社区 所 能提供的信息范围,品质也在其余之上。一个按 CPAN 标准构建的发行模块可以上传后 几小 时内在若干 Perl 5 版本及不同硬件平台上被测试────所有这些无需人工干预。 你可以选择永远不像公共 CPAN 发行模块那样发布你的代码,但你可以在可能时重用现存 的 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] CPAN 工具和设计。智能的默认设置和可定制性的组合多少都可以满足你特定的需求。 发行模块的属性 一个发行模块显然会包括一个或多个模块。它同时也包含其他的文件和目录: Build.PL 或是 Makefile.PL,这些程序用于配置、构建、测试、捆绑及 安装模块; MANIFEST,发行模块中包含的所有文件的列表。这有助于打包工具生成完整的 tar 压缩 包并帮助验证压缩包使用者得到所有必需的文件; META.yml 和/或 META.json,一个包含有关发行模块及其依赖的元数据文件; README,对发行模块的描述、意图、版权和许可信息; lib/,含有 Perl 模块的目录; t/,包含测试文件的目录; Changes,模块变更的日志。 额外地,一个良好组织的发行模块必须包含唯一的名称和单个版本号(通常从其主要模块而 来)。从公共 CPAN 上下载的组织良好的发行模块应遵循这些标准。(CPANTS 服务将评估 所 有 CPAN 发行模块的 “kwalitee” (footnote: 质量难以启发式地衡量。Kwalitee 是“质量”的 自动化亲 属。) 并提出改进之处使它们更加易于安装和管理。) CPAN 发行模块管理工具 Perl 5 核心包含若干工具来管理发行模块────不仅仅是从 CPAN 安装它们,还有开发和管 理 属于自己的模块: CPAN.pm 是官方的 CPAN 客户端。虽然它默认从公共 CPAN 安装模块,但你可以 将其指向 自己的 CPAN 仓库以代替或作为公共仓库的补充; CPANPLUS 是有着不同设计手法的 CPAN 客户端替代品。很多方面它完成得比 CPAN.pm 更为出 色,但是它们的大部分功能相同。可以按你的使用偏好进行选择。 Module::Build 是一个由纯 Perl 编写的工具套件,它可以配置、构建、安装和 测试发行模 块。它和早些提到的 Build.PL 文件配合工作。 ExtUtils::MakeMaker 是一个更老的遗留工具,它是 Module::Build 意图替 代的目标。虽然它已经 进入维护阶段、仅接纳最紧急的缺陷修复,但它仍在大规模地使用中。 它和早些提到的 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] Makefile.PL 文件配套。 Test::More(æµè¯)是基本也是被广泛使用的测试模块,用于为 Perl 软 件编写自动化测 试; Test::Harness 和 prove(æ§è¡æµè¯)是用于运行测试和解析、报告 测试结果的工具。 作为附加,若干非核心 CPAN 模块是你的开发生涯更为轻松: App::cpanminus 是一个新兴的实用工具,它对公共 CPAN 提供了几乎免配 置的使用方式。它 满足了你查找安装模块需求的 90%。 CPAN::Mini 和 cpanmini 命令运行你创建自己(私人的)公共 CPAN 镜 像。你可以在此仓库中 插入你自己的发行模块,并管理在你的组织内可用的公共模块版本。 Dist::Zilla 是通过自动化常规任务来管理发行模块工具集。很多情况下它 可以替代 对Module::Build 或 ExtUtils::MakeMaker 的使用。 Test::Reporter 允许你报告对你安装的发行模块运行自动化测试套件的结果, 向模块作者提 供更详细的失败数据。 设计发行模块 设计发行模块的过程可以填满一整本书(参见 Sam Tregar 的著作 Writing Perl Modules for CPAN) ,但是有不少设计原理可以帮助你。以来自 CPAN 的诸如 Module::Starter 或 Dist::Zilla 等实用工具开始。最初学习配置和规则的代价也许看上去像一笔不合理的投资,但是按部就班 地 配置所有事项(就 Dist::Zilla 一例来说,永不 过时)将你从冗长的记录中释放出来。 接着考虑若干规则。 每一个发行模块应拥有单一、经良好定义的目的。 该目的也许是处理某一类 型的数据文 件或是将若干相关发行模块集成为单一的可安装捆绑。将你的软件解耦为单个 部分允许你 正确地管理它们各自依赖并使其更好地封装。 每一个发行模块需要单一的版本号。 版本号必须递增。使用语义版本规则 (“semantic version policy”(http://semver.org/)是明智的,而且它兼容 Perl 5 的版本惯例。 每一个发行模块必须包含经良好定义的 API。 一个综合自动化测试套件可以 验证你是否 在版本间保持 API 一致。如果你使用本地 CPAN 镜像来安装你自己的发行模块 ,你可以 重用 CPAN 基础设施来测试模块及其依赖。对可重用的组件进行持续测试是很方 便的。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] 自动化你的发行模块测试并使它们可重复进行且有价值。 有效管理软件要求 你明白其工 作原理,和在它出错时知道为什么。 提供一个有效的简洁的接口。 避免使用全局符号和默认导出;允许人们按需 使用并不对 他们的名称空间进行污染。 来自 CPAN 的 CPAN::Mini 发行模块允许你创建你自己的本地 CPAN 镜像,你可以向其 中插入你 自己的发行模块。 UNIVERSAL 包 Perl 5 提高了一个特殊的包,就面向对象来说,它是所有包的先祖。UNIVERSAL 包为其 它类和对 象提供了若干可用的方法。 isa() 方法 isa() 方法接受包含类名或内置类型名称的字符串。你可以将其作为类方法调用或用作对 象上的 实例方法。如果类或对象从给出的类中衍生而来,或者对象本身是给定类型经 bless 的引用, 则此方法返回真。 给出对象 $pepper,一个 bless 为 Monkey 类(继承 Mammal 类)的哈希引用: say $pepper->isa( 'Monkey' ); # 打印 1 say $pepper->isa( 'Mammal' ); # 打印 1 say $pepper->isa( 'HASH' ); # 打印 1 say Monkey->isa( 'Mammal' ); # 打印 1 say $pepper->isa( 'Dolphin' ); # 打印 0 say $pepper->isa( 'ARRAY' ); # 打印 0 say Monkey->isa( 'HASH' ); # 打印 0 内置类型为 SCALAR、ARRAY、HASH、Regexp、IO 和 CODE。 你可以在你自己的类中覆盖 isa()。这在处理模拟对象(Mock Object)(示例参见 CPAN 上的 Test::MockObject 和 Test::MockModule)或不使用角色(è§è²) 的代码时非常有用。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] can() 方法 can() 方法接受包含方法名称的字符串(参见 æ¹æ³-å½æ°çä»· 中的免责声明)。 它如果它存 在,则返回指向实现该方法的函数引用。否则,返回假。你可以在类、对象或包名称 上调用 它。在后一种情况下,它返回函数引用,而非方法。 给出一个名为 SpiderMonkey 并带有名为 screech 方法的类,你可以这样得到方法的引用: if (my $meth = SpiderMonkey->can( 'screech' )) { ... } if (my $meth = $sm->can( 'screech' ) { $sm->$meth(); } 给出一个插件式结构,你可以用类似方法测试出一个包是否实现了特定的函数: # 一个有用的 CPAN 模块 use UNIVERSAL::require; die $@ unless $module->require(); if (my $register = $module->can( 'register' ) { $register->(); } 如果你使用了 AUTOLOAD(),可以(并且应该)在你自己的代码中覆盖 can()。篇幅更 长的说明请参 见 AUTOLOAD ç缺ç¹。 已知在 一种 的情况下将 UNIVERSAL::can() 作为函数而非方法调用是错误的:决定某 个类是否存在 于 Perl 5 中。如果 UNIVERSAL::can( $classname, 'can' ) 返回真,说明 某人于某处定义了一个名为 $classname 的类。除此之外,Moose 的内省更强大也更易 于使用。 VERSION() 方法 VERSION() 方法对所有包、类和对象都是可用的。它返回合适的包或类中 $VERSION 变量值。 它接受 一个版本号作为可选参数。如果你提供了版本号,此方法将会在目标 $VERSION 大于等于 所提供 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] 参数时抛出异常。 给出 1.23 版的 HowlerMonkey 模块: say HowlerMonkey->VERSION(); # 打印 1.23 say $hm->VERSION(); # 打印 1.23 say $hm->VERSION( 0.0 ); # 打印 1.23 say $hm->VERSION( 1.23 ); # 打印 1.23 say $hm->VERSION( 2.0 ); # 抛出异常 你可以在代码中覆盖 VERSION(),但这样做并没有什么很好的理由。 DOES() 方法 DOES() 是 Perl 5.10.0 新加的。它的存在支持了程序中对角色(è§è²)的使用。 向其传递调用物 和角色名称,此方法会在合适的类饰演此角色时返回真。(类也可以通过 继承、委托、合成、 角色应用或其他机制饰演此角色。) DOES() 的默认实现仍回到 isa() 上,因为继承是类饰演某角色的一种机制。给出 一个名为 Cappuchin 的类: say Cappuchin->DOES( 'Monkey' ); # 打印 1 say $cappy->DOES( 'Monkey' ); # 打印 1 say Cappuchin->DOES( 'Invertebrate' ); # 打印 0 如果你手动提供了角色或其他同质异晶行为,你可以(也应该)在自己的代码中覆盖 DOES()。 除此之外,可以使用 Moose 并无需关心细节。 扩展 UNIVERSAL 在 UNIVERSAL 存储另外方法以使其在 Perl 5 中的其余类和对象中可用是一种诱惑。 但请拒绝这种 诱惑;这个全局行为可因其自身不受约束而引发隐晦的副作用。 话虽如此,出于 调试 目的或修复不正确的默认行为而偶尔滥用 UNIVERSAL 尚可以 宽恕。例 如,Joshua ben Jore 的 UNIVERSAL::ref 发行模块使几乎无用的 ref() 操作符可用。UNIVERSAL::can 和 UNIVERSAL::isa 发行模块能够帮助你找到并除去 阻止使用多态的陈旧惯用语(但请参见 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] Perl::Critic 以了解其他带有其它优势的另一些 手段)。 UNIVERSAL::require 模块增加了有助于在运行时加载模块和类的有用行为────虽然使用 如 Module::Pluggable 的模块更安全也更不带入侵性。 在小心控制之下的代码和非常特殊非常现实的情况之外,确实没有什么直接向 UNIVERSAL 中添加 代码的理由。几乎总是有其他更好的替代设计可以选择。 代码生成 程序员的进步需要你去找寻更好的抽象。越少代码要写越好。解决方案越通用越好。当你 可以 删代码加功能的时候,你已经达成了某种完美的目标。 新手程序员常会写出多于要求的代码,其原因部分基于对语言、库、惯用语的不熟悉,同 时也 归咎于无法熟练地创建和维护良好的抽象。他们以编写长篇的过程式代码起步,接着 发现函 数,再是参数,然后是对象,还有────可能的话────高阶函数和闭包。 元编程(或 代码生成)────编写编写程序的程序────是另一种抽象技巧。它可以如 发 掘高阶函数能力般清晰,也可能如鼠洞一般让你身陷其中,困惑而恐惧。然而,这种技巧 强 大、实用────其中一些还是 Moose(Moose)这类强大工具的基础。 处理缺少函数和方法的 AUTOLOAD(AUTOLOAD)技巧展示了此技巧勉强的一面;Perl 5 的函数 和方法分派系统允许你定制常规查找失败后的行为。 eval 生成代码最简单的 (footnote: 至少是 概念上....) 技巧莫过于创建一个包含合法 Perl 代码片段 的 字符串并通过 eval 字符串操作符编译。不像捕获异常的 eval 代码块操作符,eval 字符串在当前 作用域内编译其中内容,包括当前包和词法绑定。 此技巧的常用于提供后备,如果你不能(或不想)加载某个可选的依赖: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] eval { require Monkey::Tracer } or eval 'sub Monkey::Tracer::log {}'; 如果 Monkey::Tracer 不可用,其中的 log() 函数仍将存在,只是不做任何事。 这不一定是处理这种特性的 最佳 途径,空对象(Null Object)模式通常提供更好的封装, 但 这是完成任务的 一种 方法。 这个简单的例子可能有点靠不住。为在 eval 代码中包含变量,你必须处理引号问题。 这增加了 内插的复杂度: sub generate_accessors { my ($methname, $attrname) = @_; eval <<"END_ACCESSOR"; sub get_$methname { my \$self = shift; return \$self->{$attrname}; } sub set_$methname { my (\$self, \$value) = \@_; \$self->{$attrname} = \$value; } END_ACCESSOR } 对忘记加反斜杠的你表示悲哀!祝你调教语法高亮器好运!更糟糕的是,每次对 eval 字符串的 调用都将创建一个代表整段代码的全新数据结构。编译代码也不是免费的────也许, 比IO操作便宜些,但并非免费。 即便如此,此技巧简单合理、易于理解。 参数闭包 eval file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] 虽然使用 构建访问器和增变器时很直接,但闭包(éå)允许你在编译期 向已生成的代码添 加参数而无需进行额外的求值: 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; } 这段代码避免了不愉快的引号问题。由于只有一道编译过程,性能也更好,无论你有多少要 创 建的访问器。通过重用 相同的 已编译代码作为两个函数的主体,它甚至使用更少的内 存。所 有的区别来自对词法变量 $attrname 的绑定。对于长期运行的进程或是包含大量访 问器的程序 中,此技巧非常有用。 向符号表安装比较容易,但很丑陋: { my ($getter, $setter) = generate_accessors( 'homecourt' ); no strict 'refs'; *{ 'get_homecourt' } = $getter; *{ 'set_homecourt' } = $setter; } 这一古怪的、哈希那样的语法指向当前 符号表 中的一个符号,它是当前名称空间内存 放诸如 包全局变量、函数、方法等全局可见符号的地方。将引用赋值给符号表某项将安装或 替换对应 的条目。要将一个匿名函数提升为方法,可把函数引用赋值到符号表中的对应条目。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] 这个操作是一个符号引用,因此应该禁用 strict 对此操作的引用检查。许多程序在类似 的代码 中有不少隐晦的缺陷,它们在单个步骤内进行赋值和生成: { no strict 'refs'; *{ $methname } = sub { # 隐晦的缺陷:strict refs # 在此处也被禁用 }; } 这个例子在外部块、内部块和函数体中都禁用严格检查。只有赋值违反了严格的引用检查, 因 此只要对该操作禁用即可。 如果在你编写的代码中,方法名称是一个字符串字面值,而非变量的内容,你可以不用通过 符 号引用而直接向相关符号赋值: { no warnings 'once'; (*get_homecourt, *set_homecourt) = generate_accessors( 'homecourt' ); } 这没有违反严格检查,但是会引发一条“used only once”警告,除非你已经在作用域内部 显式 地抑制它的产生。 编译期操控 不同于显式编写的代码,通过 eval 字符串生成的代码于运行时生成。虽然你预计一个 常规函数 在你程序的生命周期内都是可用的,但(运行时)生成的函数也许直到你要求时才 是可用的。 在编译期强制 Perl 运行代码────生成其他代码────的方法是将其包装于 BEGIN 块内。 当 Perl 5 语法分析器遇到标有 BEGIN 的代码块时,它将对整个代码块进行语法分析。 证实其不 含任何语法错误后,代码块将立即执行。执行完毕,语法分析过程就好像未曾中断 一般继续。 实际点说,编写: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] sub get_age { ... } sub set_age { ... } sub get_name { ... } sub set_name { ... } sub get_weight { ... } sub set_weight { ... } ……和: sub make_accessors { ... } BEGIN { for my $accessor (qw( age name weight )) { my ($get, $set) = make_accessors( $accessor ); no strict 'refs'; *{ 'get_' . $accessor } = $get; *{ 'set_' . $accessor } = $set; } } ……之间的区别主要是可维护性。 由于 Perl 隐式地将 require 和 import(导å¥)用 BEGIN 包装起来, 在模块内,任何函数外部的代 码都会在你 use 它时执行。任何处于函数外、模块内的代 码会在 import() 调用发生 之前 执行。 如果你 require 该模块,则不含隐式的 BEGIN 代码块。函数外部代码的执行将放在语法分析的 结 尾。 同时也请注意词法 声明(名称和作用域间的联系)和词法 赋值 之间的交互。前者 发生于编译 期,而后者发生于执行点处。如下代码隐含一处缺陷: use UNIVERSAL::require; # 有缺陷;不要使用 my $wanted_package = 'Monkey::Jetpack'; BEGIN { $wanted_package->require(); file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] $wanted_package->import(); } ……因为 BEGIN 块在对 $wanted_package 的字符串值赋值 前 执行。结果将是 意图在未定义值上调用 require() 方法而引发的异常。 Class::MOP 不像安装函数引用来填充名称空间及创建方法,目前没有简易的内置途径在 Perl 5 中创建 类。 所幸的是,一个成熟且强大的 CPAN 发行模块恰好可以完成此项工作。Clas::MOP 是 Moose(Moose)的支柱库。它提供了 元对象协议(Meta Object Protocol)────一 种用于对 象系统创建操控自身的机制。 相比自行编写脆弱的 eval 字符串代码或是尝试手动干涉符号表,你可以通过对象和方法操 控程 序中的实体和抽象。 要创建一个类: use Class::MOP; my $class = Class::MOP::Class->create( 'Monkey::Wrench' ); 在你创建它时,你可以添加属性和方法到该类中: 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 { ... }, } file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] ), ); ……或在创建后,把它们添加到 元类(Metaclass)(代表类的对象)中: $class->add_attribute( experience => Class::MOP::Attribute->new( '$xp' ) ); $class->add_method( bash_zombie => sub { ... } ); ……你可以对元类进行内省: my @attrs = $class->get_all_attributes(); my @meths = $class->get_all_methods(); 使用 Class::MOP::Attribute 和 Class::MOP::Method,你可以类似地创建、操作 并内省属性和方法。此 元对象协议及其带来的灵活性是 Moose(Moose)强大的根源。 重载 Perl 5 不是一个彻头彻尾面向对象的语言。其核心数据类型(标量、数组和哈希)也非有 方法 让你重载的对象。即便如此,你还是 能够 控制你编写的类和对象的行为,特别是 当它们在各 类上下文中强制类型转换或求值。这称为 重载(Overloading)。 重载隐晦而强大。一个有趣的例子就是重载对象在布尔上下文中的行为,特别是在你使用如 空 对象(Null Object)模式(http://www.c2.com/cgi/wiki?NullObject)时。在布尔上 下文中, 对象为真……但仅在你不对布尔化操作进行重载的情况下。 你可以重载几乎所有的对象操作:字符串化、数值化、布尔化、迭代、调用、数组访问、哈 希 访问、算术操作、比较操作、智能匹配、按位操作甚至赋值。 重载常见操作 最为有用的通常也是最为常见的:字符串化、数值化以及布尔化。overload 编译命令允许你 将 函数和可重载操作关联起来。下面就是一个重载布尔求值的类: package Null; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] use overload 'bool' => sub { 0 }; 在所有布尔上下文中,此类所有实例求值得假。 overload 编译命令的参数是一个键值对,键描述了重载的类型而值则是替代 Perl 默认行为 的函 数引用。 添加字符串化也是很容易的: package Null; use overload 'bool' => sub { 0 }, '""' => sub { '(null)' }; 重载数值化操作则更为复杂,因为算术操作符倾向于执行二元操作(åæ°æ°é)。给出 两个重 载了加法的操作数,如何确定优先级?答案应是一致的、易于解释、便于未阅读 实现源码的人 理解的。 perldoc overload 意图在标有 Calling Conventions for Binary Operations 和 MAGIC AUTOGENERATION 的两个小节中解释这一切,但最简单的解决方法是重载数值化 操作并告 诉 overload 在可能时将提供的重载操作用作后备: package Null; use overload 'bool' => sub { 0 }, '""' => sub { '(null)' }, '0+' => sub { 0 }, fallback => 1; 将 fallback 设为真值使 Perl 在可能的情况下使用其他已定义的重载操作来合成所 要求的操作。 如果不行,Perl 将表现得好像未经任何重载那样。这通常是你想要的。 没有 fallback,Perl 将仅使用由你提供的特定重载操作。如果某人尝试进行未经你 重载的操 作,Perl 将会抛出异常。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] 重载和继承 子类继承祖先重载的操作。它们可以以两种方法中的一种覆盖这个行为。如果父类如所示 使用 重载操作,函数引用直接提供,则子类 必须 直接通过 overload 覆盖父类的 重载行为。 父类可通过指定执行重载操作所调用的方法 名称 来允许其子代更为灵活,而非将函数 硬性编 码: package Null; use overload 'bool' => 'get_bool', '""' => 'get_string', '0+' => 'get_num', fallback => 1; 子类在不直接用 overload 时只能覆盖指定的方法。 对方法名的使用可以产生更加灵活的代码,但是开发人员对代码引用的使用更加频繁。 在这种 情况下,请使用开发小组定下的编码规范。 重载的用途 重载也许看上去像是一种富有诱惑力的工具,用于产生新操作的符号快捷方式。IO::All CPAN 发行模块极尽此特性之能事,为简明、可复合的代码提供聪明的点子。还有各类通过 合理利用 重载而精炼的出色 API,使众多混乱凝固下来。有时最好的代码会出于简单直接的 设计而避开 灵巧。 因为加法、乘法还有拼接操作记法已经到处都是,针对 Matrix(矩阵) 类重载这些操作 还可以说 得通。一个全新的问题领域则无需刻意重载这些操作────你没必要因此将现存 Perl 操作符 弄得一词二义。 Damian Conway 的著作 Perl 最佳实践(Perl Best Practices) 建议另行将重载用于防 止对对象 的意外滥用。举例来说,将无法用单一数值表达的对象上的数值化操作重载为 croak() 可以帮助 你在实际程序中找到真正的缺陷。Perl 5 中重载还是比较少见的,但是这条建议可以 提升程序 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] 的可靠性和安全性。 Taint Perl 给予你编写安全代码的工具。这些工具并非认真思考和计划的替代品,但它们对 谨慎和深 入理解做出 回报,帮助你避免隐晦的失误。 使用 Taint 模式 一个名为 taint(“污点”)模式 或 taint 的特性向所有来自程序之外的数据添 加一小部分元数 据。任何衍生自污点数据的数据同样带有污点,你可以在你的程序内使 用污点数据,但如果你 用其影响外部世界────如果你不安全地使用它────Perl 将抛出一 条致命异常。 perldoc perlsec 中对污点模式进行翔实地解释,并带有其他的安全指导。 要启用污点模式,可以用 -T 参数启动你的程序。你可以在将文件设置为可执行且不 使用 perl 启动的情况下于 #! 行中使用此参数;如果你像 perl mytaintedappl.pl 这样运行它并忽略 -T 参 数,Perl 会以异常退出。当 Perl 遇到位于 #! 行的参数 时,它已经错过了污染 %ENV 中环境数据 的时机,此即一例。 Sources of Taint 污点数据来自两个位置:文件输入和程序的操作环境。前者是你从文件中读取或从网页或 网络 编程收集的用户数据。后者则更为隐晦。它包括命令行参数、环境变量以及来自系统 调用的数 据。即便是如读取目录句柄(用 opendir() 打开)的操作产出污点数据。 来自核心模块 Scalar::Util 的 tainted() 函数在其参数带污点时返回真: die "Oh no!" if Scalar::Util::tainted( $some_suspicious_value ); 从数据中除去污点 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] 要除去污点,你必须用正则表达式捕获操作从数据中提取已知良好的部分。捕获的数据将是 不 带污点的。如果你的用户输入由美国电话号码组成,你可以向这样去污: die "Number still tainted!" unless $tainted_number =~ /(\(/d{3}\) \d{3}-\d{4})/; my $safe_number = $1; 提供的模式对你所需部分描述得越具体,你的程序就越安全。相反,拒绝 特定条目或 形式的手 段则有可能过度看重有害数据。就安全来说,Perl 更希望你禁止不需要的安全数 据而非允许有 害但看上去安全的数据。即便如此,没有什么阻止你编写捕获变量所有内容 的模式────既 然如此,为什么还要使用污点模式呢? 从环境中除去污点 一个污点数据的来源就是超级全局变量 %ENV,它代表了系统中的环境变量。这部分数据 带有污 点,因为来自程序以外的力量可以操控这些值。任何修改 Perl 或 Shell 查找文件和 目录的环境 变量都是攻击的目标。一个污点敏感的程序应该从 %ENV 中删除若干键并将 $ENV{PATH} 设置为具体 且相当安全的路径: delete @ENV{ qw( IFS CDPATH ENV BASH_ENV ) }; $ENV{PATH} = '/path/to/app/binaries/'; 如果你不恰当地设置 $ENV{PATH},你将收到有关其不安全性的消息。 如果这一环境变量包含当前工作目录,或者它包含相对路径,再或者指定的目录有着全局 可写 的属性,一个聪明的攻击者可以通过劫持系统调用执行不安全的操作。 基于相似的理由,污点模式下的 @INC 并不包含当前工作目录。Perl 也将忽略 PERL5LIB 以及 PERLLIB 环境变量。如果你需要添加库目录,可以使用 lib 编译命令或是 perl 的 -I 参数。 Taint 陷阱 污点模式要么起作用要么不起。它只有开和关两种状态。有时候这会导致程序员使用宽松的 模 式除去数据污点,并留下安全的错觉。请仔细审查去污代码。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_09.html[2011/2/21 21:22:17] 不幸的是,并非所有模块都能正确处理污点数据。这是 CPAN 作者应该严肃对待的缺陷。如果 你必须使遗留代码污点安全(taint-safe),请考虑使用 -t 参数,它启用污点模式但把违 反条 件从异常改为警告。这不是完全污点模式的替代,但允许你在不使用“一 -T 独大”的情 况下让 现有程序更加安全。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_10.html[2011/2/21 21:22:20] 语法之外的 Perl Perl 5 就像现实世界中其余解决问题的语言那样,是一门庞大的语言。高效的 Perl 程 序不仅要 求对语法的深刻理解;你也必须开始体会 Perl 的特性间互动的方式,以及那些 用 Perl 解决已 被充分理解问题的常见方法。 请为 Perl 的第二条学习曲线做好准备:通过对常见行为模式和内置捷径的高效使用,以 Perl 的方式思考,这些惯用语在恰当使用时能让你写出清晰、强大的代码。 惯用语 任何语言────无论编程或是自然语言────都将发展出 惯用语,或者说表达的通用模 式。 地球每天自转,但我们称之为太阳东升西落。我们也谈论聪明的 hack、下流的 hack 和不 靠谱的代码。 随着你在学习中更加清楚地了解到 Perl 5 的本质,你将碰到并理解常见惯用语。它们并 非语言 特性────你不是 必须 使用它们────而且它们也没有庞大到使你能用函数和方法 封装 它们。相反,它们是一种习惯。它们是用 Perl 的腔调来编写 Perl 代码的方式。 将对象用作 $self Perl 5 的对象系统(Moose)将方法的调用者视作一个俗套的参数。类方法的调用者──── 一个包含类名称的字符串────就是该方法的第一个参数。对象或实例方法的调用者 ────对象自身 ────就是该方法的第一个参数。你可以随意按需使用或忽略它。 Perl 5 惯用语将 $class 用作类名而将 $self 用做实例方法的调用者。这不是一个被 语言自身强制 的惯例,但这是一个强大到连诸如 MooseX::Method::Signatures 这类有用的 扩展都默认假设你将 $self 用作调用者的惯例。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_10.html[2011/2/21 21:22:20] 具名参数 不谈 signatures 和 MooseX::Multimethods 这类模块,Perl 5 的参数传递机制 还是比较简单的:所有参 数展开为可以通过 @_(å½æ°åæ°)访问的单 个列表。然而这种简单偶尔也会显得太过简单 ────具名参数时常很有用────它没有排除使 用惯用语来提供具名参数。 列表上下文求值和 @_ 赋值允许你用自然且 Perl 味十足的方式将具名参数一对对展开 。即便这 种函数调用方式等同于传入逗号分隔或由 qw// 创建的列表,把参数像真正的 键值对排列使得函 数调用方看起来像是支持具名参数一般: 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}) ); ... } Perl 最佳实践(Perl Best Practices) 建议传入哈希引用代替。这样 Perl 就可以在 调用方检查所 构造的哈希是否合法。它比其他方式也少用些内存。 这个技巧能很好地和 import()(导å¥)一起工作;在将余下部分吸入哈希之前 你可以处理任意 数量的参数: sub import { my ($class, %args) = @_; my $calling_package = caller(); ... } file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_10.html[2011/2/21 21:22:20] Schwartzian 转换 作为表达式求值基本成分的列表和列表处理,Perl 新人通常会忽视其重要性。说得更加简单 些,Perl 程序员串联求值得变长列表表达式的能力提供了无数高效操作数据的机会。 Schwartzian 转换(Schwartzian transform) 是一个展示上述原则的重要惯用语,是从 Lisp 语言 家族中随手借鉴过来的。 假设你有一个将同事名及其分机号关联起来的 Perl 哈希: my %extensions = ( 4 => 'Jerryd', 5 => 'Rudy', 6 => 'Juwan', 7 => 'Brandon', 10 => 'Joel', 21 => 'Marcus', 24 => 'Andre', 23 => 'Martell', 52 => 'Greg', 88 => 'Nic', ); 又假设你想打印出按名字而非分机号排序的同事-分机号列表。换句话说,你需要对此哈希按值 排序。 按字符串顺序给哈希的值排序还是很容易的: my @sorted_names = sort values %extensions; ……但是这样就失去了姓名和分机号之间的联系。相反,采用 Schwartzian 转换对数据在排序 前后 进行处理以保留所需信息。第一步,将该哈希转化为一个数据结构列表,此数据结构按可 排序的方 式保留重要信息。此例子中,哈希转化为一个二元素匿名数组: my @pairs = map { [ $_, $extensions{$_} ] } keys %extensions; 将此哈希 就地 反转仅在没有重名的情况下才是可行的。这个特别的数据集并没有这个问题, file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_10.html[2011/2/21 21:22:20] 但还是带防御性地编写代码。 sort 接受一个匿名数组列表并照对第二个元素(姓名)的字符串比较结果对其排序: my @sorted_pairs = sort { $a->[1] cmp $b->[1] } @pairs; 得到 @sorted_pairs,又一个 map 操作将此数据结构转化为一个更可用的形式: my @formatted_exts = map { "$_->[1], ext. $_->[0]" } @sorted_pairs; ……现在你可以把它整个打印出来了: say for @formatted_exts; 当然,这些步骤用到了很多(命名不佳的)临时变量。这是一个值得花时间来好好理解的 技 巧,但真正的魔法在于组合: say for map { " $_->[1], ext. $_->[0]" } sort { $a->[1] cmp $b->[1] } map { [ $_ => $extensions{$_} ] } keys %extensions; 从右往左按求值顺序阅读整个表达式。对分机号哈希中的每一个键,把来自原哈希的键值组合 为一个两个元素的匿名数组。对这个由匿名数组构成的列表按第二个元素,就是原哈希的值, 排序。把排序后的数组格式化为一个个输出字符串。 Schwartzian 转换就是 map-sort-map 的管道,用于将一种数据结构转换为易于排序 的另一种形式 接着将排序后的结果转换回用于进一步操作的形式。 这种转换是简单的。考虑一种情况,计算用于排序的值既费时又费内存,比方说,对一个个大 文件计算密码学散列值。这种情况下 Schwartzian 转换也很有用,因为这样的昂贵操作可以只 进行一次(位于最右边的 map 内),在用 sort 排序时,实际上是从缓存中取出值比较, 随后可 以在最左边的 map 中去掉用于比较的中间散列值。 简易文件吸入 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_10.html[2011/2/21 21:22:20] Perl 5 神奇的全局变量在很多情况下是真正全局的。在其他地方搞砸它们的值真是太容易了, 除非你在每一处使用 local。这种要求还衍生出若干有趣的惯用语。举例来说,你可以用 单个表 达式将文件吸入某个标量中: my $file = do { local $/ = <$fh> }; # 或 my $file = do { local $/; <$fh> }; $/ 是输入记录分隔符。用 local 将其局部化即将其值设为 undef,并等候赋值。 该 local 操作在赋 值 之前 发生。由于分隔符的值未定义,Perl 高兴地将文件句柄 的所有内容用一句话读入并将 此值赋给 $/。因为 do 代码块求值为此代码块内最后求 值语句的值,也即赋值操作的值,或者 说,文件的内容。即便在代码块末尾 $/ 立刻恢复 为先前的状态,$file 现在包含文件的所有内 容。 第二个例子是类似的,除了它不执行赋值操作而仅返回从文件句柄读这一句代码的结果。你 可 能见过这些例子中的一个,它们在此情况下一相同的方式工作。 如果你没有从 CPAN 安装 File::Slurp 时,此惯用语非常有用(并且,诚然,对不熟悉 这个 Perl 5 功能特定组合的人来说尤其恼火)。 控制程序执行 程序和模块之间的显著差异在于它们预计的用途。用户直接地调用程序,而程序在执行流程 开 始之后才加载模块。程序和模块技术上的区别就是对其直接调用是否有意义。 在希望使用 Perl 测试工具(æµè¯)来测试独立程序中的函数或创建用户可以直接的 模块时碰 到这个问题。你要做的全部就是了解 Perl 如何 开始执行一段代码。对于这个 问题,使用 caller。 caller 的单个可选参数就是要报告的调用帧的数目。(调用帧(call frame) 就是 代表一次函数 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_10.html[2011/2/21 21:22:20] 的调用的簿记信息。)你可以用 caller(0) 得到当前调用帧的信息。要允 许一个模块正确地作为 程序 或 模块运行,你可以编写一个合适的 main() 函数并在 模块开始处添加这样一行: main() unless caller(0); 如果此模块 没有 调用者,用户就可以作为程序直接调用它(使用 perl path/to/Module.pm 而非 use Module;)。 检查 caller 在列表上下文返回的列表中的第八个元素也许在多数情况下更加精确,但 很少见。 这个值在调用帧表示 use 或 require 为真,否则为 undef。 处理 Main 函数 Perl 在创建闭包(éå)时不要求任何特殊语法;你可能不经意间使闭包闭合于某 词法变量之 上。在实践中,这通常 很少 会导致问题,除一些特定于 mod_perl 的情形 ……以及 main() 函 数。 许多程序通常会在将执行流程交付其他函数前设置若干文件作用域的词法变量。相比向其他 函 数传递或从中返回值,直接使用这些变量是一种诱惑,特别是当程序扩展以提供更多功能 时。 更糟糕的是,这些程序也许会依赖于发生在 Perl 5 编译期间的隐晦作用;一个你 本以为 会赋 为某特定值的变量也许要到很晚才能得到初始化。 这里有一个简单的解决办法。用一个简单的函数,main(),来包装程序的主要代码。将 所有你不 需要的变量封装为真正的全局变量。接着在你使用完所有需要的模块和编译命令后, 向程序的 开头加上如下行: #!/usr/bin/perl use Modern::Perl; use autodie; ... main( @ARGS ); 在执行程序其他部分 之前 调用 main() 强制你显式安排编译的初始化顺序。同时也 有助提醒你 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_10.html[2011/2/21 21:22:20] 将程序行为封装为函数和模块。(这样可以和既是程序又是库的代码工作得很好 ────æ§å¶ç¨åºæ§è¡。) 后缀参数验证 即使不使用诸如 Params::Validate 和 MooseX::Params::Validate 等 CPAN 模块来验证 函数接受的参数 是否正确,你仍可以从对偶尔的正确性检查中获益。unless 控制流程修饰符 是一个在函数开始 处断言你期望参数的简单易读的方式。 假设你的函数接受两个参数,不多也不少。你 原可以 这样写: use Carp; sub groom_monkeys { if (@_ != 2) { croak 'Monkey grooming requires two monkeys!'; } } ……但从语言学角度来看,后果通常比检查更为重要,因而值得放在表达式的 开头: croak 'Monkey grooming requires two monkeys!' if @_ != 2; ……这种形式,按你阅读后缀条件的偏好,可以简化为: croak 'Monkey grooming requires two monkeys!' unless @_ == 2; 如果你专注于消息的文字内容("You need to pass two parameters!")和测试条件(@_ 应包含 两个元素),则这样读起来更加顺口。这在真值表中几乎占了一行。 Regex En Passant 许多 Perl 5 惯用语依赖于表达式求值得值这一语言设计,就像在: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_10.html[2011/2/21 21:22:20] say my $ext_num = my $extension = 42; 这样编写代码不太好,但它阐述如下观点:你可以在其他表达式中使用另一个表达式的值。 这 也不是什么新鲜事;你很可能早就在列表中使用过函数的返回值,或者把它传递给另一 个函 数。你也许并没有意识到这一隐含点。 假设你有一个全名并且你想提取名字部分。用正则表达式来做很简单: my ($first_name) = $name =~ /($first_name_rx)/; ……其中 $first_name_rx 是一个预编译的正则表达式。在列表上下文中,一个成功正则 表达式匹 配返回由所有捕获组成的列表,并且 Perl 将第一个元素赋值给 $first_name。 现在想像你打算修改这个名字,就说去掉所有非单词字符以作为系统帐号的用户名。你可以 这 样写: (my $normalized_name = $name) =~ tr/A-Za-z//dc; 不像前一个例子,此例子从右往左读。第一,将 $name 赋值给 $normalized_name。接 着,对 $normalized_name 进行直译 (footnote: 这里的括号影响优先级使得赋值先行发生。)。赋值 表达式 求值为 $normalized_name 变量。这个技巧对所有就地改动操作符都是可用的: my $age = 14; (my $next_age = $age)++; say "Next year I will be $next_age"; 一元强制类型转换 Perl 5 的类型系统通常能正确完成任务,至少在你选对操作符的情况下是如此。要拼接字符 串, 使用字符串拼接操作符,Perl 将两个标量都作字符串对待。把两个数相加,使用加法操作 符,Perl 将两个标量都作数值对待。 有时候你必须就你的意图给 Perl 一点提示。若干 一元强制类型转换(unary coercions) 惯 用 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_10.html[2011/2/21 21:22:20] 语可用,通过它们你可以使用 Perl 5 操作符强制对某值在指定的方法求值。 要确保 Perl 将某个值用作数值,加零: my $numeric_value = 0 + $value; 要确保 Perl 将某个值作为布尔值对待,双重否定: my $boolean_value = !! $value; 要确保 Perl 将某个值作字符串对待,将其和空字符串拼接: my $string_value = '' . $value; 虽然对这些强制类型转换的需求是难以察觉得稀少,在遇见这类惯用语时,你应该能够正 确理 解它们。 全局变量 Perl 5 提供了若干 超级全局变量,它们是真正的全局变量,而不仅仅局限于任何特定的 包。这 些超级全局变量有两个缺点。第一,它们是全局变量;任何直接或间接的修改就能影响 到程序 的其余部分。第二,它们太过精炼。经验丰富的 Perl 5 程序员早就记住了它们中的一 部分。很 少有人能够记全这些变量。而且,它们中也只有几个是常用的。perldoc perlvar 包含这类变量的 详尽列表。 管理超级全局变量 要管理这些超级全局变量的全局行为,最佳途径就是避免使用它们。当你必须用到它们时, 请 在尽可能小的作用域内使用 local 来约制改动。所 调用 的代码对这些全局变量 做出的修改仍然 会影响到你,但你已经降低了在你能力范围 之外 出现令人惊奇的代码 的可能。 对于这些全局变量的行为目前有一些变通的方法,但是很多变量从 Perl 1 开始就存在了, 并将 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_10.html[2011/2/21 21:22:20] 作为 Perl 5 的一部分直至其生命周期的结束。就像文件吸入惯用语(ç®ææ件å¸å¥) 所展示 的那样,通常会这样写: my $file = do { local $/ = <$fh> }; 用 local 本地化 $/ 的效果仅持续到代码块的末尾。以从文件句柄中读取所有行 (footnote: 一个 tie 文件句柄就是屈指可数的可能情况中的一种。)为目的并在 do 代码块内改变 $/ 的 值的 Perl 代码出现的机会不多。 并非所有对超级全局变量的使用管理起来都那么容易,但是这个方法通常很有效。 其他时候你需要 读取 超级全局变量的值并期望没有代码会修改它。用 eval 代码块捕获 异常的 方式容易受竞争条件的感染 (footnote: 用 C 代替!),在这种情况下,因词法变量 超出作用域 而调用其上的 DESTROY() 方法可能会重置 $@: local $@; eval { ... }; if (my $exception = $@) { ... } 立刻 复制 $@ 可以预留它的内容。 英语名称 English 核心模块为过度使用标点的超级全局变量提供了详细的名称。用如下方式将其 导入名称 空间: use English '-no_match_vars'; 随后你就可以在该名称空间作用域内使用记录在 perldoc perlvar 中的详细名称。 三个正则表达式相关的超级全局变量($&、$` 和 $')一同降低了程序内 所有 正则表达式的性 能。如果你因疏忽而没有提供导入参数,则你的程序性能仍会下降即便你没有 显式地从这些变 量中读取。出于向后兼容考虑,这并非默认行为。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_10.html[2011/2/21 21:22:20] 现代化的 Perl 程序应使用 @- 代替前三个可怕的变量。 常用超级全局变量 大部分现代化的 Perl 5 程序只用到一部分超级全局变量。有部分全局变量只是为一些 你不太可 能碰到的特殊情况而存在的。虽说 perldoc perlvar 是这些变量正规文档, 有部分变量还是值得在 此提一下。 Table: 超级全局变量 全局变 量 英语名称 说明 $/ $INPUT_RECORD_SEPARATOR 一个由零或多个字符组成的字符串,用于按行读取时标记记录的结尾。默认地,这是 对应平台相关的换行符 序列。如果此值未定义,Perl 将试图将整个文件读入内存。如果将其 值设置为某整数的 引用,Perl 将尝试对 每一条记录读取该整数指出 字节 数(因此 请注意 Unicode)。 $. $INPUT_LINE_NUMBER 从最近访问的文件句柄中读取当前行(更精确地说,记录)的行号。你可以从此变 量中读,向其写入内容则 没有任何效果。局部化此变量也将局部化和此变量关联的文件句柄。 $| $OUTPUT_AUTOFLUSH 此变量的布尔值管辖着 Perl 是否应该在向当前选中的文件句柄写入后立即冲洗缓冲区 或仅在 Perl 的缓冲区 填满时才这样做。非缓冲输出在写管道、套接字或终端时很有用,这些 都不应等待程序写入。 @ARGV (none) 传递给程序的命令行参数。 $! $ERRNO 一个包含 最近一次 系统调用结果的双重变量(åéåé)。在数值上下文中, 它对应于 C 的 errno 值,非零值 表明有错误发生。在字符串上下文中,返回合适的系统 错误字符。在进行系统调用之前本地化该变量(显式 或隐式地)以避免其余代码覆写它的值。 Perl 5 的很多地方都在你不知情的情况下执行系统调用。该变量的 值也会在你眼皮底下改变, 因此你应该在系统调用之后 立即 复制它的值。 $" $LIST_SEPARATOR 此字符串用于在数组和列表内插为字符串时分隔其中元素。 %+ (none) 此变量包含来自成功的正则表达式匹配的具名捕获(å·åæè·)。 $@ $EVAL_ERROR 此值有最近一次异常(æè·å¼å¸¸)抛出。 $0 $PROGRAM_NAME 当前执行的程序的名称。在一些类 Unix 系统上,你可以通过修改此值来改变程序 显示给系统上其他程序的名 称,诸如 ps 或是 top。 $$ $PID 当前运行程序实例的进程号,按操作系统的理解。它会在随 fork() 程序而不同 在同一程序的不同线程内可 能也不同。 @INC (none) 一个文件系统路径列表,在使用 use 或 require 时,Perl 将从中查找要加载 的文件。参见 perldoc -f require 以了解此数值中的其它内容。 %SIG (none) 此哈希将 OS 和底层 Perl 信号映射为用于处理对应信号的函数引用。例如,可以通过 捕获 INT 信号捕获标 准的 Ctrl-C 中断。参见 perldoc perlipc 以了解有关信号特别 是安全信号的信息。 超级全局变量的替代 超距作用(Action at a distance)的罪魁祸首与IO和异常条件有关。使用 Try::Tiny (å¼å¸¸æ³¨æäºé¡¹)有助于把你和正确异常处理所含狡猾的语义隔开。用 local 局部化并复制 $! 的值可以帮助你避免因 Perl 执行隐式系统调用时的古怪行为。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_10.html[2011/2/21 21:22:20] 你可以通过 use 引入 IO::Handle 来执行词法文件句柄(æ件å¥æå¼ç¨)上 的方法而非 IO 相关的 超级全局变量。就地 select 一个文件句柄,接着修改 $|,即是 直接调用词法文件句柄上的 autoflush() 方法。调用特定文件句柄上的 input_line_number() 方法可以得到和 $. 等价的结果。有关 其他适用方法的信息请参见 IO::Handle 文档。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_11.html[2011/2/21 21:22:21] 要避免什么 Perl 5 并不完美。一些看起来像是个好主意的特性却难以正确使用。其他一些特性的行为 不合 常理。还有一部分就是些坏主意。这种种特性将持续存在────从 Perl 中删除一个特性 是 一个严肃的过程并且只为滔天罪行而备────然而你可以并应该避免在几乎任何地方使用它 们。 裸字 Perl 到处使用印记(sigil)和其他标点来同时帮助语法分析器和程序员区分具名实体的 类别。 即便如此,Perl 仍是一门可塑的语言。你可以按偏好用最具创意的、可维护的、 混乱的、奇形 怪状的风格来编写程序。可维护性是每一个优秀程序员都必须考虑的,但 Perl 开发者本身并不 会冒昧地对 你 认为的可维护性做出任何假设。 Perl 语法分析器理解内置的 Perl 关键字和操作符;它知道 bless() 意味着你在创建 对象(ç» bless åçå¼ç¨)。这些不太会产生歧义……但是 Perl 程序员可以通过使用 裸字(barewords) 添 加语法分析的复杂度。裸字是一个标识符,它没有印记或其他用 于说明其语法功用的附加消歧 条件。因为 Perl 5 没有名为 curse 的关键字,则出现在 源代码中的字面词 curse 就是有歧义的。 你是想操作一个变量 $curse 还是调用函数 curse()?strict 编译命令有充分的理由对这类带歧义裸 字的使用作出警告。 即便如此,基于充分的理由,裸字仍允许在 Perl 5 的若干地方使用。 裸字的正当使用 Perl 5 中的哈希键是裸字。键的用法已经足够让语法分析器将其识别为单引号字符串 的等价 物,因此它们不会产生歧义。还要注意意图通过对函数调用或内置操作符(例如 shift)求值而 产生 一个哈希键的做法并不如你想象的那样,除非你消除歧义。 # 'shift' 字面值是键 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_11.html[2011/2/21 21:22:21] my $value = $items{shift}; # 由 shift 产出的值是键 my $value = $items{shift @_} # 一元加号足够用于消除歧义 my $value = $items{+shift}; 从某种意义上说,Perl 5 中的包名是裸字。良好的包命名习惯(首字母大写)有助于避免 意外 情况,但是语法分析器使用启发式(heuristic)方法决定 Package->method() 是否意味着先调用一 个名为 Package() 的函数接着调用结果的 method() 方法,或者 是否应该将 Package 做包名对待。你 可以通过后缀包分隔符(::)对此消歧,但这种 情况比较罕见,显然也很丑陋: # 可能是类方法 Package->method(); # 一定是类方法 Package::->method(); 特殊的具名代码块提供了独有的裸字。AUTOLOAD、BEGIN,、CHECK、DESTROY、 END、INIT 和 UNITCHECK 声 明 了函数,但无需 sub 关键字。你可能熟悉 了编写不带 sub 的 BEGIN 惯用语: package Monkey::Butler; BEGIN { initialize_simians( __PACKAGE__ ) } 你 可以 不在 AUTOLOAD() 声明时使用 sub,但不常见。 用 constant 编译命令定义的常量可以按裸字使用: # 不要在正式验证时这样做 use constant NAME => 'Bucky'; use constant PASSWORD => '|38fish!head74|'; ... return unless $name eq NAME && $pass eq PASSWORD; 注意这些常量 不会 在内插上下文,如双引号字符串,中内插。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_11.html[2011/2/21 21:22:21] 常量是原型函数(åå)的特例。如果你预声明了函数的原型,你就可以将函数 用作裸字;Perl 5 知道它要知道的每件事来正确的地对函数的每一处出现进行语法分析。 原型的其他缺点仍然 适用。 裸字的欠考虑使用 裸字在现代化 Perl 代码中应该很少见;它们的歧义产生脆弱的代码。你可以在几乎所有 地方 避免使用它们,但是你可能会在遗留的代码中遇到若干对裸字蹩脚的使用。 在词法文件句柄之前(æ件å¥æå¼ç¨),所有文件和目录句柄使用裸字。你几乎 总是可以安全 地将这些代码用词法文件句柄重写;除了 STDIN、STDOUT、STDERR 以外。 没有启用 strict 'subs' 而编写的代码可以使用裸字函数名。你可以安全地给这些函数 的参数列 表加上括号而不会改变代码意图 (footnote: 使用 perl -MO=Deparse,-p 可以显示 Perl 如何对其进行 语法分析,接着一一加上括号。)。 类似地,旧式代码不会费神正确地对哈希对的 值 加上引号: # 不良风格;不要使用 my %parents = ( mother => Annette, father => Floyd, ); 因为名为 Floyd() 和 Annette() 的函数都不存在,Perl 将这些哈希值分析为字符串。 strict 'subs' 编译命令使语法分析器在这种情况下给出错误。 最后,内置的 sort 操作符以第二参数的形式接受一个用于排序的函数 名。作为代替, 提供一 个用于排序的函数 引用 可以避免使用裸字: # 不良用法;不要使用 my @sorted = sort compare_lengths @unsorted; # 更好的风格 my $comparison = \&compare_lengths; my @sorted = sort $comparison @unsorted; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_11.html[2011/2/21 21:22:21] 结果代码要多出一行,但是它避免了对裸字的使用。不像其他裸字示例,Perl 的语法分析 器不 需要对这种语法消歧义。它仅有一种解释 compare_lengths 的途径。然而,显式引 用带来的清晰度 为人类读者带来了好处。 Perl 5 的语法分析器并 不 理解这一单行版本: # 无法工作 my @sorted = sort \&compare_lengths @unsorted; 这起因于对 sort 的特殊语法分析;你不能使用任意表达式(诸如取具名函数的引用), 但一个 代码块或者一个标量可以行得通。 间接对象 Perl 5 中的构造器就是任何返回对象的东西;new 不是一个内置的函数。出于惯例, 构造器是一 个名为 new() 的类方法,但是你仍可以按需灵活地选择不同名称。若干陈 旧的 Perl 5 对象教程 推行 C++ 和 Java 风格的构造器调用方式: my $q = new CGI; # 不要这样写 ……而非毫无歧义的: my $q = CGI->new(); 两种语法在行为上是等价的,除了它们不等价的时候。 第一种形式就是间接对象(indirect object)形式(更精确地说,与格(dative) 形式),即动 词(方法)处于其意指的名词(对象)之前。在口语中这是可行的,但是 这样会向 Perl 5 引入 歧义。 裸字间接调用 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_11.html[2011/2/21 21:22:21] 其中一个问题就是方法名称是裸字(裸å 的解释。虽然这些启发式方法经过良好测试并且 几乎 总是正确的, 但它们出错的方式令人疑 惑。更糟糕的是,在编译和模块加载 顺序 的面前,它们 相当脆弱。 在构造器带参数时,语法分析对人类 和 计算机来说都是困难的。间接风格可能类 似于: # 不要这样写 my $obj = new Class( arg => $value ); ……这样便使得类名 Class 看上去像函数调用。Perl 5 能够 对许多这样的情况消歧, 但它的启 发式方法取决于语法分析器在该语法分析点上见到了什么包名,它已经解析了的裸 字(已经如 何解析它们),以及已在当前包内声明的函数 名称。 假设执行一个冲突的原型函数,它的名称正好和类名或者一个间接调用的方法冲突。这样的 情 况并不频繁,但是难以调试,避免这样的语法总是值得的。 间接记法的标量限制 该语法的另一危险之处就是语法分析器期望单个标量表达式是一个对象。向一个存放在集合 变 量(aggregate variable)文件句柄打印 看上去 很显然,但是事实并非如此: # 代码和行为不一致 say $config->{output} "This is a diagnostic message!"; print、close 和 say────所有操作文件句柄的关键字────都按间接方式进行操作。 当文 件句柄是包全局变量时一切正常,但是词法文件句柄(æ件å¥æå¼ç¨)使得 间接对象语法问题 显而易见。在上一个例子中,Perl 会尝试调用 $config 对象上的 say 方法。解决方法就是产出调 用者的表达式消歧: say {$config->{output}} "This is a diagnostic message!"; 间接记法的替代 直接调用记法没有这类歧义问题。要构造一个对象,直接在类名上调用构造方法即可: file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_11.html[2011/2/21 21:22:21] my $q = CGI->new(); my $obj = Class->new( arg => $value ); 对于文件句柄操作限制这一情况来说,与格用法太普遍了,你只要将目标调用者用大括号 围 起,就可以是有间接调用手法。除此之外,考虑加载 IO::Handle 核心模块,它允许 你调用文件 句柄对象(如词法文件句柄)上的方法来执行 IO 操作。 对于超级偏执狂来说,你可以通过在类名后追加 :: 来进一步对类方法调用消歧,例如 CGI::- >new()。然而,实践中很少有代码会这样做。 CPAN 模块 Perl::Critic::Policy::Dynamic::NoIndirect(一个 Perl::Critic 的插件) 可在代码审核期间 识别间接调用语法。CPAN 模块 indirect 可以在程序运行时识别它们并禁 止使用: # 对间接用法做出警告 no indirect; # 对此用法抛出异常 no indirect ':fatal'; 原型 原型(prototype) 是一小块附加在函数生命上的可选元数据。新手通常假设这些原型可 以作为 函数签名使用;但它们不是。相反它们服务于两个不同的目的:它们给予语法分析器 提示来改 变对函数及其参数的语法分析,并且它们还修改了 Perl 5 处理函数参数的方式。 声明函数原型,只要在名称后加上它: sub foo (&@); sub bar ($$) { ... } my $baz = sub (&&) { ... }; 你可以向函数前置声明(forward declaration)添加原型。你也可以在前置声明中忽 略它们。 如果你使用带原型的前置声明,该原型必须完整地在函数声明中出现;如果不 这样做,Perl 将 给出原型不匹配的警告。其逆命题不为真:你可以在前置声明中忽略 原型而在完整声明中包含 它。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_11.html[2011/2/21 21:22:21] 在前置声明中忽略原型没有什么理由,除非你有想法编写太过聪明的代码。 原型最初的目的是允许用户定义它们自己的函数,这些函数的行为和(一些)内置操作 符一 样。考虑 push 操作符的行为,它接受一个数组和列表。虽然 Perl 5 正常情况 下在调用方将数组 和列表展开为单个列表,Perl 5 语法分析器知道调用 push 必须 有效地将数组作为一个单元传 入,以使 push 能够就地操作数组。 内置操作符 prototype 接受一个函数名称并返回代表其原型的字符串。要参考内置关 键字的原 型,使用 CORE:: 形式: $ perl -E "say prototype 'CORE::push';" \@@ $ perl -E "say prototype 'CORE::keys';" \% $ perl -E "say prototype 'CORE::open';" *;$@ 一些内置操作符拥有你无法模拟的原型。在这些情况下,prototype 将返回 undef: $ perl -E "say prototype 'CORE::system' // 'undef' " undef # 你无法模拟内置函数 system 的调用惯例 $ perl -E "say prototype 'CORE::prototype' // 'undef' " undef # 内置函数 prototype 没有原型 再看看 push: $ perl -E "say prototype 'CORE::push';" \@@ @ 字符代表一个列表。反斜杠强制对对应的参数进行 引用。因此这个函数接受一个 数组引用 (因为你无法得到列表的引用)和一列值。mypush 可能为: sub mypush (\@@) { my ($array, @rest) = @_; push @$array, @rest; file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_11.html[2011/2/21 21:22:21] } 合法的原型字符包括强制标量参数的 $,对应哈希的 % (通常用作引用),以及标记 代码块的 &。完整文档参考 perldoc perlsub。 原型的问题 原型可以改变后续代码的语法分析过程而且它们会对参数进行强制类型转换。它们不是 函数参 数类型和个数的文档,它们也不对应具名参数。 原型对参数的强制类型转换以一种隐晦的方式发生,诸如在传入参数上强制标量上下文: 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; ……但请 不要 进行比一个简单表达式还复杂的操作: sub mypush(\@@); # 编译错误:原型不匹配 # (期待数组;得到标量赋值) mypush( my $elems = [], 1 .. 20 ); 这些还不算是来自原型的 最隐晦 的误导。 原型的正当使用 只要代码维护者不将其作为完整的函数签名,原型还是有一些合法的用途的。 第一,它们在用用户定义函数模拟并覆盖内置关键字时是必须的。你必须先通过确认 prototype 没有返回 undef 来检查你是否 可以 覆盖内置关键字。一旦知道了关键字的原型,你就 可以声明 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_11.html[2011/2/21 21:22:21] 和核心关键字同名的前置定义: use subs 'push'; sub push (\@@) { ... } 注意,不管词法作用域与否,subs 编译命令对 文件 余下的部分都有效。 使用原型的第二个理由就是定义编译期常数。一个由空原型(作为 无 原型的反面)声明 且求 值得单个表达式的函数将成为常数而非函数调用: sub PI () { 4 * atan2(1, 1) } 在处理该原型声明以后,Perl 5 优化器知道在余下的源代码中,它应该在碰到裸字或对 PI 带括 号的调用时用计算得到的π值进行替换(同时遵循作用域和可见性)。 相对于直接定义常量,constant 核心编译命令会为你处理这些细节并且更加易读。如果你 想将 这些常量内插入字符串中,来自 CPAN 的 Readonly 模块或许更加有用。 最后一个使用原型的理由是,它将 Perl 的语法扩展以便将匿名函数操作为代码块。CPAN 模块 Test::Exception 很好地利用这一点提供了带延迟计算(delayed computation)的良好 API。 其 中 throws_ok() 函数接受三个参数:要运行的代码块、一个匹配异常字符串的正则表达式 以及一 个对此测试可选的描述。假设你想测试 Perl 5 在意图在未定义值上调用方法时产生的异 常消 息: use Test::More tests => 1; use Test::Exception; throws_ok { my $not_an_object; $not_an_object->some_method() } qr/Can't call method "some_method" on an undefined value/, 'Calling a method on an undefined invocant should throw exception'; 此导出函数 throws_ok() 拥有原型 &$;$。它的第一个参数是代码块,Perl 会将其升级为 全功能匿 名函数。第二个要求的参数是一个标量。第三个参数则是可选的。 最最细心的读者可能发现一个语法上奇怪的缺失:在作为第一参数传递给 throws_ok() 的匿名 函 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_11.html[2011/2/21 21:22:21] 数的结尾没有逗号。这是 Perl 5 语法分析器的古怪之处。加上逗号会引发语法错误。语法分析 器期待的是空白,而非逗号操作符。 “此处没有逗号”这一规则是原型语法的缺点。 不带原型你也可以使用这一 API。这可不那么吸引人: use Test::More tests => 1; use Test::Exception; throws_ok( sub { my $not_an_object; $not_an_object->some_method() }, qr/Can't call method "some_method" on an undefined value/, 'Calling a method on an undefined invocant should throw exception'); 对函数原型的稍稍使用来替代对 sub 的需要还是合理的。原型其他用法很少能足够令 人信服地 胜过它们的缺点。 Ben Tilly 提出了第四条理由:在使用 sort 时定义自定义函数。定义原型为 ($$) 的 函数将会使 参数传入 @_ 而非包全局变量 $a 和 $b。这是一个罕见的情况,但可以 节省你的调试时间。 方法-函数等价 Perl 5 的对象系统故意很精简(ç» bless åçå¼ç¨)。因为一个类就是一个包,Perl 自身不对存 储于包中的函数和存储于包中的方法加以强制区分。相同的关键字,sub,用于 同时表达两者。 将第一个参数作 $self 对待的文档和惯例向代码的读者暗示其意图,但是 如果你试着将函数作 为方法调用,Perl 自身将把任何能找到的拥有合适名称、处于合适包中 的函数当作方法。 相似地,你可以将方法当作一个函数────完全限定、导出、或作为引用调用────如果 手动传入 你自己的调用物(invocant)的话。 两种方法都有各自的问题;两种都不要用。 调用方(Caller-side) file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_11.html[2011/2/21 21:22:21] 假设你有一个包含若干方法的类: package Order; use List::Util 'sum'; ... sub calculate_price { my $self = shift; return sum( 0, $self->get_items() ); } 如果你有一个 Order 对象 $o,下列对此方法调用 或许 看上去是等价的: my $price = $o->calculate_price(); # 不正确;不要使用 my $price = Order::calculate_price( $o ); 虽然在这一简单的例子中,它们产生相同的输出,然而后者以隐晦的方式违反了对象的封 装。 它连同对象查找一起避免了。 如果 $o 不是 一个 Order 对象,而是 Order 的子类或同质异晶体(allomorph) (è§è²)且覆盖了 calculate_price(),则“方法用作函数”调用将执行 错误的 方法。 相同地,如果 calculate_price() 的 内部实现会改变────也许从其他地方继承或通过 AUTOLOAD() 委托而来────则调用者可能 会失败。 Perl 有一种让此行为看上去显得必须的情况。如果你强制方法解析(method resolution)而 不进行分派(dispatch),你如何调用作为结果的方法引用? my $meth_ref = $o->can( 'apply_discount' ); 这里有两种可能。一是丢弃 can() 方法返回的结果: $o->apply_discount() if $o->can( 'apply_discount' ); file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_11.html[2011/2/21 21:22:21] 第二则是按方法调用语法使用此引用: if (my $meth_ref = $o->can( 'apply_discount' )) { $o->$meth_ref(); } 当 $meth_ref 包含一个函数引用,Perl 将以 $o 作为调用者(invocant)调用该引用。 这在严格 (“strict”)要求之下同样可行,就像在包含其名称的标量上调用方法一样: my $name = 'apply_discount'; $o->$name(); 通过引用调用方法有一小小的缺点;如果程序的结构在存储引用和调用引用之间有所变动, 则 此引用也许不会指向当前最合适的方法。如果 Order 类改变导致 Order::apply_discount 不再是最合 适的调用方法,$meth_ref 中的引用也不会随之更新。 如果你使用这种调用形式,请限定引用的作用域。 被调用方(Callee-side) 因为 Perl 5 并不就声明区分函数和方法,还因为作为函数或方法调用一个给定的函数 是 可能 的(然而这是不可取的),因而编写一个能用两种方式调用的函数也是可能 的。 CGI 核心模块是一个主要的冒犯者。它的函数手工检查 @_ 以决定第一参数是否 是一个可能的调 用物(invocant)。如果是,它们执行特别的修改来保证函数需要访问 的任何对象状态都是可 用的。如果第一参数不像是调用物,则函数必须参考其他地方的 全局数据。 像所有的启发式方法(heuristics)那样,存在一些边边角角的情况。对于给定的方法, 很难确 切预言什么调用物是潜在合法的,特别是考虑用户可以创建子类时。文档的负担 同时也加重了 ────对组合了过程式和面向对象接口的解释必须反映代码的二分法(dichotomy) ────就好像原本就是被误用的。当一部分代码使用过程式接口而另一部分用对象接口时会 怎么样? 为库提供隔离的过程式和对象式接口也许是正当的。一些设计使得一部分技巧比其他更 加有 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_11.html[2011/2/21 21:22:21] 用。将这两者合并为单一的 API 会加重维护的担子。请避免这样做。 捆绑(Tie) 重载(éè½½)让你就特殊类型的强制转换和访问给予类自定义行为。有一种让变量 表现如同 内置类型(标量、数组和哈希)的机制,它还带有更多特别的行为。这种机制使用 tie 关键 字;它就是 捆绑(tying)。 最初对 tie 的使用是为生成存储在磁盘上而非内存的哈希。这允许从 Perl 中使用 dbm 文件, 同时也能够访问大于内存尺寸的文件。核心模块 Tie::File 提供了一个相似的系统,通过它 便可 以处理过大的数据文件。 用于将 tie 变量的类必须遵从特定数据类型的特定的、有良好文档的接口。虽然核心模块 Tie::StdScalar、Tie::StdArray 和 Tie::StdHash 在实践中更加有用,perldoc perltie 仍是这些接口的主要 资源。从继承这些模块为开始,并仅重载你需要修改的特定方法。 这些父类的文档和实现位于 Tie::Scalar、Tie::Array 和 Tie::Hash 模块中。因此如 果你想从 Tie::StdScalar 继承,还必须同时 use Tie::Scalar。如果 tie() 没有使你 迷惑,这些代码的组织可能 就会。 捆绑变量 给出一个要捆绑的变量,可以用如下语法捆绑它: use Tie::File; tie my @file, 'Tie::File', @args; ……其中第一个参数是要捆绑的变量,第二个是用于捆绑变量的类名,@args 是由捆绑函数 要求 的可选参数列表。以 Tie::File 为例,就是要捆绑到数组上的文件名。 捆绑函数重组了构造器:TIESCALAR、TIEARRAY()、TIEHASH() 和 TIEHANDLE() 分别对应标量、数组、哈 希和文件句柄。这些函数返回 tie() 关键字返回的相同对象。大 多数人会忽略它。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_11.html[2011/2/21 21:22:21] tied() 操作符在用于捆绑变量时返回相同对象,否则返回 undef。再一次,很少有人 会使用返回 的对象。相反,他们在使用 tied() 来决定一个变量是否捆绑时只检查对象的 布尔值。 实现捆绑变量 要实现一个捆绑变量的类,从诸如 Tie::StdScalar 等核心模块继承,接着为要更改的操 作覆盖特 定方法。就捆绑标量来说,你很可能需要覆盖 FETCH 和 STORE,可能需要覆 盖 TIESCALAR(),很可能 忽略 DESTROY()。 你可以用很少的代码创建一个记录对某标量读写的类: package Tie::Scalar::Logged; use Modern::Perl; use Tie::Scalar; use parent -norequire => 'Tie::StdScalar'; sub STORE { my ($self, $value) = @_; Logger->log("Storing <$value> (was [$$self])", 1); $$self = $value; } sub FETCH { my $self = shift; Logger->log("Retrieving <$$self>", 1); return $$self; } 1; 假设 Logger 的类方法 log() 接受一个字符串和一个通过调用栈栈帧号报告位置的 数字。注意 Tie::StdScalar 并没有独立的 .pm 文件,因此你必须使用 Tie::Scalar 让其可用。 在 STORE() 和 FETCH() 方法内部,$self 如 bless 后的标量一般工作。向此标 量引用赋值改变标量 的值而从中读取则返回它的值。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_11.html[2011/2/21 21:22:21] 类似的,Tie::StdArray 和 Tie::StdHash 的方法对应地作用于 bless 后的数组和 哈希引用。perldoc perltie 文档解释了其支持的大量方法,除其他操作外,你可以从 中读取写入多个值。 -norequire 选项阻止 parent 编译命令为 Tie::StdScalar 加载文件的意图,因 为此模块是 Tie/Scalar.pm 文件的一部分。 何时使用捆绑变量 捆绑变量也许像是为编写机灵的代码提供了一个有趣的机会,但是它们在几乎所有情况下 会导 致令人迷惑的接口,很大一部分归因于它们非常罕见。除非你有充足的理由创建行为 和内置数 据类型一致的对象,否则避免创建你自己的捆绑类。 充足的理由包括方便调试(使用记录标量来帮助你理解它的值在何处改变)和使得一些不 可能 的操作变得可能(用节省内存的方式访问大文件)。捆绑变量作为对象的主要接口时 不太有 用;用它配合你的整个接口来实现 tie() 接口通常太过困难和勉强。 最后的警语既悲哀又具说服力;相当多代码并不指望和捆绑变量一起工作。违反封装的代 码也 许会妨碍对小聪明的正当且合法的使用。这是一种不幸,但是违反库代码的期待常常 引发缺 陷,并且一般你没有能力修复这样的代码。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_12.html[2011/2/21 21:22:23] 还缺少什么 Perl 5 并不完美,至少其默认行为是如此。核心部分存在一些改变这种状况的选择。更 多的则 来源于 CPAN。有经验的 Perl 开发者对理想状态下 Perl 5 应有的行为有着各自 的理解,并且 他们常常高效地使用他们各自对 Perl 5 的配置。 新手也许并不知道 Perl 如何帮助他们更好地编写程序。一手把核心模块将使你变得更加 富有 成效。 缺少的默认特性 Perl 5 的设计过程从 1993 年持续至 1994 年,尝试为语言的新方向做好准备,却无法 预言其未 来。Perl 5 增加了不少优秀的新特性,但它仍和 Perl 1 到 Perl 4 这过去的 七年保持兼容。十六 年之后,编写整洁、可维护、强大和简明的 Perl 5 代码最佳途径已 经和 Perl 5.000 时代相差甚 大。默认行为有时会对你有所妨碍,幸运的是,还有更佳的 行为可用。 strict 编译命令 strict 编译命令(ç¼è¯å½ä»¤)允许你禁用(或重新启用)诸多功能强大但可能导致 意外误用 的语言结构。 strict 执行三种功能:禁用符号引用、要求变量声明并且禁用对未声明裸字(裸å 虽然偶尔使用符号引用对于进行符号表修改和导出(除对辅助模块的使用之外, 比如 Moose)来 说是必须的,将变量用作变量名称使隐晦的超距作用(action at a distance) 错误有机可乘 ────或者更糟糕,使未经严格验证的用户输入恶意修改对内数据成为可能。 要求变量声明有助于防止变量名的打字错误并鼓励将词法变量限定于正确作用域。如果 所有词 法变量都在合适的作用域内由 my 或 our 声明,则很容易就能看出其预期 的作用域。 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_12.html[2011/2/21 21:22:23] strict 在词法范围内有效,基于其用法的编译期作用域。你可以用 no strict 禁 用某些 strict 的特 性(当然,在可能的最小范围内)。详情请参见 perldoc strict。 warnings 编译命令 warnings 编译命令(å¤çè¦å)控制着 Perl 5 中诸多警告的汇报,例 如试图字符串化 undef 值或对 值使用错误的操作符类型。它同时对使用不推荐的特性 做出警告。 最有用的警告解释了 Perl 难以理解你的意思,只能猜测正确的解释方法。即便 Perl 通 常能猜 对,由你来消除歧义总是能保证程序的正常运行。 warnings 变量命令对其用法的编译期作用域有词法效果。你可以用 no warnings 禁 用部分或全部警 告(当然,在可能的最小范围内)。详情请参见 perldoc perllexwarn 和 perldoc warnings。 结合 use warnings 和 use diagnostics,Perl 5 将对程序中出现的每一处警告显 示扩展诊断信息。这 些扩展诊断信息来自于 perldoc perldiag。此行为在 Perl 学习阶 段非常有用,但是在部署产品代 码时则非如此,因为它可能产生非常详细的错误输出。 IO::Handle Perl 5.6.0 增加了词法文件句柄。在此之前,文件句柄都是包全局变量。这样做偶尔会显 得散 乱和令人迷惑。现在你可以这样写: open my $fh, '>', $file or die "Can't write to '$file': $!\n"; ……$fh 中的词法文件句柄简单易用。词法文件句柄的实现创建了对象;$fh 是 IO::Handle 的实 例。不幸的是,即使 $fh 是一个对象,你无法在其上调用任 何方法,因为 IO::Handle 类尚未加 载。 举例来说,冲洗相关文件句柄的缓冲区偶尔也会令人痛苦。它也可以这样简单: $fh->flush(); ……但仅在你程序的某处包含 use IO::Handle 时才可以这样做。解决方法就是在你 的程序里加上 file:///C|/Users/aj/Downloads/horus-modern_perl_book-394be97/build/html/chapter_12.html[2011/2/21 21:22:23] 这一行,使得词法文件句柄────原本就是对象────在行为上也和对象一致。 autodie 编译命令 Perl 5 的默认错误检查有些吝啬。例如,如果你不仔细对每一个 open() 调用的返 回值进行检 查,你可能会尝试从一个已经关闭的文件句柄读取────或者更糟,向其写入 而导致数据丢 失。autodie 编译命令改变此默认行为。如果你编写: use autodie; open my $fh, '>', $file; ……一个不成功的 open() 调用将通过 Perl 5 的常规异常机制抛出异常。考虑到应对 失败系统调 用的最佳手段是抛出异常,该编译命令可以去掉大量刻板的代码并让你因为知 道没有忘记检查 返回值而感到轻松。 该编译命令于 Perl 5.10.1 进入 Perl 5 核心部分。详情请参见 perldoc autodie。
还剩259页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

lyconier

贡献于2013-08-04

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf