Perl进阶(第2版)

978-7-115-40206-6
作者: 【美】Randal L. Schwartz(兰德尔 L. 施瓦茨) Brian d foy(布莱恩 d. 福瓦) Tom Phoenix(汤姆 菲尼克斯)
译者: 韩雷
编辑: 傅道坤

图书目录:

详情

本书主要介绍如何更加有效地利用Perl进行开发,主要内容包括包和命名空间、引用和作用域、操作复杂数据结构、面向对象编程、编写和使用模块、测试perl代码和为cpan贡献代码。本书主要介绍如何更加有效地利用Perl进行开发,主要内容包括包和命名空间、引用和作用域、操作复杂数据结构、面向对象编程、编写和使用模块、测试perl代码和为cpan贡献代码。

图书摘要

版权信息

书名:Perl进阶(第2版)

ISBN:978-7-115-40206-6

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美] Randal L. Schwartz brian d foy Tom Phoenix

  译    韩 雷

  责任编辑 傅道坤

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Copyright © 2012 by O’Reilly Media.Inc.

Simplified Chinese Edition, jointly published by O’Reilly Media, Inc. and Posts & Telecom Press, 2015. Authorized translation of the English edition, 2012 O’Reilly Media, Inc., the owner of all rights to publish and sell the same.

All rights reserved including the rights of reproduction in whole or in part in any form.

本书中文简体字版由O’Reilly Media, Inc.授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式复制或抄袭。

版权所有,侵权必究。


Perl是一种功能强大的通用编程语言,享有“一种拥有各种语言功能的梦幻脚本语言”、“UNIX中的王牌工具”等美誉,受到了国内程序员和系统管理员的青睐。

本书作为Learning Perl一书的进阶,主要讲解了如何更加有效地利用Perl进行开发。本书总共分为21章,每章内容篇幅不大,主要内容包括Perl简介、使用模块、中级操作基础、引用简介、引用和作用域、操作复杂的数据结构、对子例程的引用、文件句柄引用、正则表达式引用、构建更大型的程序、创建自己的Perl发行版、对象简介、测试简介、带数据的对象、Exporter模块、对象析构、Moose简介、高级测试、为CPAN贡献代码等知识。

本书适合具有一定Perl基础的程序员和系统管理员阅读。对于高级Perl程序员来讲,本书也是必备的技术参考读物。


O’Reilly Media通过图书、杂志、在线服务、调查研究和会议等方式传播创新知识。自1978年开始,O’Reilly一直都是前沿发展的见证者和推动者。超级极客们正在开创着未来,而我们关注真正重要的技术趋势——通过放大那些“细微的信号”来刺激社会对新科技的应用。作为技术社区中活跃的参与者,O’Reilly的发展充满了对创新的倡导、创造和发扬光大。

O’Reilly为软件开发人员带来革命性的“动物书”;创建第一个商业网站(GNN);组织了影响深远的开放源代码峰会,以至于开源软件运动以此命名;创立了Make杂志,从而成为DIY革命的主要先锋;公司一如既往地通过多种形式缔结信息与人的纽带。O’Reilly的会议和峰会集聚了众多超级极客和高瞻远瞩的商业领袖,共同描绘出开创新产业的革命性思想。作为技术人士获取信息的选择,O’Reilly现在还将先锋专家的知识传递给普通的计算机用户。无论是通过书籍出版,在线服务或者面授课程,每一项O’Reilly的产品都反映了公司不可动摇的理念——信息是激发创新的力量。

业界评论

“O’Reilly Radar博客有口皆碑。”

——Wired

“O’Reilly凭借一系列(真希望当初我也想到了)非凡想法建立了数百万美元的业务。”

——Business 2.0

“O’Reilly Conference是聚集关键思想领袖的绝对典范。”

——CRN

“一本O’Reilly的书就代表一个有用、有前途、需要学习的主题。”

——Irish Times

“Tim是位特立独行的商人,他不光放眼于最长远、最广阔的视野并且切实地按照Yogi Berra的建议去做了:‘如果你在路上遇到岔路口,走小路(岔路)。’回顾过去Tim似乎每一次都选择了小路,而且有几次都是一闪即逝的机会,尽管大路也不错。”

——Linux Journal


Perl的面向对象机制是经典的“戏法”。它使用Perl已有的一系列非面向对象特性,例如包、引用、散列、数组、子例程和模块,然后,虽然没有暗自使用其他特性,但还是设法构造出功能完整的对象、类和方法。看起来就像是突如其来生成的一样。

这是一个非常奇妙的技巧。这意味着你能够基于你已有的Perl知识构建,并且按照你自己的方法很轻易地进入面向对象的Perl开发,而不必先去征服新语法的“大山”或者探索新技巧的“海洋”。这也意味着你能够通过从已有的Perl面向对象结构中逐步选取合适的结构来优化调整面向对象的Perl,以满足你自己的需要。

但是还有一个问题:因为Perl运用包、引用、散列、数组、子例程和模块作为它的面向对象机制的基础,也就是说,如果想要使用面向对象的Perl,你就需要理解包、引用、散列、数组、子例程和模块这些内容。

然后还有一个小问题:学习曲线不可能消除,它只是倒退几步。

也就是说,我们到底需要掌握多少关于非面向对象的Perl知识,才能够开始学习关于Perl的所有面向对象知识呢?

本书给出了以上所有问题的答案。在后续内容中,Randal通过近20年来使用Perl的经历,以及近40年观看电视剧Gilligan's IslandMr. Ed中的情节,来解释Perl语言中每一个面向对象特性的基础。而且更好的是,我们将会确切地展示如何组合这些组件,以创建更有用的类和对象。

因此,如果你在使用Perl的对象、引用和模块时感觉自己像Gilligan一样,那么本书就是剧中教授会推荐给你的。

以上消息来源可靠。

——Damian Conway,2003年5月


Perl是一门强大的通用编程语言,凭借其简单、灵活、可移植性强等特点,受到了国内程序员和系统管理员的青睐。因为Perl是解释执行,这样就极大地节省了编译的时间。Perl也内置了很多数据结构,如散列和列表,使用起来极其方便,而且配合Data::Dumper这个标准库,我们可以很方便地调试复杂的数据结构。此外,Perl可以在几乎所有的操作系统上运行,甚至有很多是你没有听过的系统。

我们假定您已经阅读过了Learning Perl这本Perl入门书,如果你不满足于对Perl的粗浅理解,想要更为深入、详细、系统地学习Perl,本书就是为您准备的!本书是Perl语言学习的进阶图书(从其英文书名Intermediate Perl上也可以看得出来),当前为第2版。需要说明的是,本书第1版的书名与第2版不同,它的名字是Learning Perl ObjectsReferencesand Modules,当时国内并没有出版社引进。

前面提到,本书是Perl的进阶图书,学习完本书之后,读者也可以朝着Mastering Perl一书迈进,从而彻底掌握Perl语言。总之,本书的目标就是让读者能够熟练地掌握Perl语言中的引用、数据结构、面向对象、编写测试、构建模块及通过CPAN安装模块等内容,以及流行的面向对象框架Moose和一些Perl的常见习语等。

感谢人民邮电出版社引进本书,这对国内广大Perl爱好者来讲是一大福音,可以帮助越来越多的人掌握这门“古老”而又不断发展的现代编程语言。感谢我的妻子Vivi,因为承接了本书的翻译工作,牺牲了很多在一起的时光,谢谢你的理解与支持。本书中文版的顺利付梓有你一份功劳。

最后,尽管本书篇幅不大,但是限于本人专业水平、翻译能力有限,对于书中内容的把握难免有不准确之处,还请读者批评指教。

韩雷     

2015年2月22日夜


我非常高兴能够有机会和诸多Perl社区讨论我的这本书、演讲和其他我所编写的内容。中国是一个世界大国,不过我还没有亲身接触过。我曾经尝试说一点中文,但是每当我发错音或者说了错误的句子后,大家都只是对我报以“嘲弄般”的微笑。

Learning Perl一书的翻译对于中国读者的帮助已经很让我感到欣慰,尽管我还有些担心我们在本书示例中使用的这个古老的美国电视节目会让人感到困惑,甚至很多美国本土的学生都不知道Gilligan’s Island的Gilligan或者Skipper。也许这并不重要,只需要知道Gilligan是那个一直并且不断地让他最好的朋友Skipper失望的“蠢货”就够了。

我一直很高兴收到来自读者的消息,以及有幸能够参与读者所完成的项目。如果你想要向brian.d.foy@gmail.com发送任何消息,我们都将会看到Google翻译的效果,或者你能够以一个Perl程序的方式发送给我,我希望在你读完本书之后,我们都能理解程序中的内容。

中国市场于我而言是全新的市场,我确信在访问Perl社区时,有各种类型的编程智慧被我错过了,如有机会也请向我分享这些编程智慧。

好运!

brian d foy


I’ve had the pleasure of talking to much of the Perl community through my books, the talks I give, and the other things I write. However, China is a huge part of the world that I don’t interact with. I’ve tried to speak a little Chinese, but only enough so that everyone smiles and laughs when I get the tones wrong and make a nonsense statement.

The translation of Learning Perl for the Chinese programmers greatly pleases me, although I worry that the ancient American television show we use in the examples won’t make any sense. Not even the American students in my classes in the United States know about Gilligan or the Skipper from Gilligan’s Island, so maybe it’s not that important. Just know that Gilligan is the fool who constantly and consistently disappoints his best friend, the Skipper.

I’m always happy to hear from readers and what they’ve done with something I’ve been a part of. If you’d like to send me a message at brian.d.foy@gmail.com, we can both see how well Google Translate works. Or maybe you can send it as a Perl program, which I hope we can both understand once you finish this book.

Since the Chinese market is new to me, I’m sure there’s all sorts of programming wisdom that’s missing from the Perl community I travel in. Please tell me about those too.

Good luck.

brian d foy


大约在20年前(几乎与互联网的历史一样长),Randal Schwartz编写了Learning Perl第1版。在接下来的这些年里,Perl本身基本上从一种很酷并且主要由UNIX系统管理员使用的脚本语言,成长为一门健壮的面向对象编程语言,而且能在所有人已知的操作系统上运行,甚至包括一些并不广为人知的系统。

Learning Perl的前后6个版本中,它保持着基本一致的片幅,大概300页,而且一直包含几乎相同的一些内容,以保持结构紧凑,很适合初级程序员学习。但是,还有很多关于Perl的内容需要学习。

Randal将英文原书第一版命名为Learning Perl Object, References, and Modules,后来我们将英文原书重命名为Intermediate Perl,但是我们认为也可以叫做Learning More Perl。这是一本介绍Learning Perl中所没有涵盖的内容的书。我们将向你展示使用Perl语言编写更大型程序的方法。

Learning Perl中,我们将每一章都设计得非常短,以至于你只需要花费大概1小时的时间来阅读。每一章都以一系列练习结束,这些练习可以帮助读者巩固所学的知识,而且这些练习的答案可以参考书末的附录。与Learning Perl一样,我们也为本书开发了很多素材,可以将这些素材用于教学环境。

除非我们特别进行了标记,否则本书介绍的全部内容能够适用于任何平台,无论是UNIX、Linux还是Windows,无论是来自ActiveState的ActivePerl,还是 Strawberry Perl,或者是Perl的其他现代实现。要想更好地使用本书,你只需要熟悉Learning Perl中的内容,而且有志于进一步地学习Perl。

在你读完本书之后,你会了解你所需的Perl语言的大多数核心概念。这个系列的下一本书是Mastering Perl,该书将专注于应用你所了解的内容,来编写更高效、更健壮的Perl应用,以及管理Perl软件开发的生命周期。

在你Perl职业生涯的任何时候,你都应当拥有Programming Perl这本书,该书是(几乎是)Perl语言最权威的“圣经”。

本书分为3个部分。第一部分介绍如何处理引用,这也是复杂数据结构以及Perl面向对象编程的关键所在;第二部分介绍对象以及Perl如何实现面向对象编程;第三部分将介绍如何处理Perl的模块结构、测试,以及用于发布代码的社区基础架构。

你最好从头开始阅读本书,适时停下完成每一章的练习。每一章都基于之前的章节,而且我们也将假定在介绍新主题时,你已经了解了本书之前章节中的内容。

第1章是关于全部内容的简单介绍。

第2章介绍如何使用Perl的核心模块和其他人编写的第三方模块。后续章节将展示如何创建我们自己的模块,但在此之前,你仍然可以使用已有的模块。

第3章选择性地介绍一些中级Perl技巧,你会在本书后续部分用到这些技巧。

第4章介绍一定程度的重定向,允许相同的代码操作不同的数据集。

第5章讲述Perl如何跟踪数据指针,并且引入匿名数据结构和自动带入。

第6章讨论创建、访问以及输出任意深度和嵌套的数据结构,包括数组的数组和散列的散列。

第7章介绍如何以动态创建并且后续执行的匿名子例程的方式捕获行为。

第8章讲述如何以标量形式存储文件句柄,可以很容易地在程序间传递或者在数据结构中存储。

第9章介绍如何编译正则表达式而不必立即使用它们,随后将它们作为构建块用于更大的模式之中。

第10章介绍关于排序的复杂操作、施瓦茨变换和处理递归定义的数据。

第11章讨论如何通过将代码分散在不同的文件和命名空间中来构建更大的程序。

第12章讲解如何创建一个Perl发行版作为你面向对象编程的第一步。

第13章介绍类、方法调用、继承和重载。

第14章讲述如何开始测试你的模块,这样就会发现你所创建的发行版中的代码问题。

第15章讨论如何为每个实例添加数据,包括构造函数、getter和setter。

第16章使用多重继承、自动方法以及文件句柄的引用。

第17章讲述use的工作方式,我们如何决定从模块中导出的内容,以及如何创建自己的导入例程。

第18章介绍如何向已销毁的对象中添加行为,包括对象持久性。

第19章介绍Moose,Moose是CPAN上的一个对象框架。

第20章介绍高级测试,测试代码以及元代码复杂的一面,诸如文档以及测试覆盖率。

第21章介绍如何通过将你的代码上传到CPAN上来与全世界分享你的代码。

附录提供所有练习的答案。

提示 

这个图标用来强调一个提示、建议或一般说明。

警告 

这个图标用来说明一个警告或注意事项。

本书的目的是为了帮助读者完成工作。一般而言,你可以在你的程序和文档中使用本书中的代码,而且也没有必要取得我们的许可。但是,如果你要复制的是核心代码,则需要和我们取得联系。例如,你可以在无须获取我们许可的情况下,在程序中使用本书中的多个代码块。但是,销售或分发O’Reilly图书中的代码光盘则需要取得我们的许可。通过引用本书中的示例代码来回答问题时,不需要事先获得我们的许可。但是,如果你的产品文档中融合了本书中的大量示例代码,则需要取得我们的许可。

在引用本书中的代码示例时,如果能列出本书的出处信息最好不过。出处信息通常包括书名、作者、出版社和ISBN。例如:“Intermediate Perl by Randal L. Schwartz, brian d foy, and Tom Phoenix. Copyright 2012 Randal L. Schwartz, brain d foy, and Tom Phoenix, 978-1-449-39309-0.”

在使用书中的代码时,如果不确定是否属于正常使用,或是否超出了我们的许可,请通过permissions@oreilly.com与我们联系。

如果你想就本书发表评论或有任何疑问,敬请联系出版社。

美国:

O’Reilly Media Inc.

1005 Gravenstein Highway North

Sebastopol, CA 95472

中国:

北京市西城区西直门南大街2号成铭大厦C座807室(100035)

奥莱利技术咨询(北京)有限公司

我们还为本书建立了一个网页,其中包含了勘误表、示例和其他额外的信息。你可以通过如下地址访问该网页:

http://www.oreilly.com/catalog/9781449393090

关于本书的技术性问题或建议,请发邮件到:

bookquestions@oreilly.com

欢迎登录我们的网站(http://www.oreilly.com),查看我们的更多书籍、课程、会议和最新动态等信息。

Facebook: http://facebook.com/oreilly

Twitter: http://twitter.com/oreillymedia

YouTube: http://www.youtube.com/oreillymedia

Safari在线图书(www.safaribooksonline.com)是一个按需订阅的数字图书馆。它通过图书与视频形式分发来自技术领域和商务领域的世界级作者的专家级内容。

技术专家、软件开发人员、Web设计师以及商业和创意人士都使用Safari在线图书作为研究、解决问题、学习和认证培训的首选资源。

Safari在线图书还为组织、政府机构和个人提供了一系列产品组合和计费项目。用户可以通过完全可搜索的数据库来访问数千本图书、培训视频以及出版前的手稿,这些内容都来自诸如O’Reilly、Prentice Hall Professional、Addison-Wesley Professional、Microsoft Press、Sams、Que、Peachpit Press、Focal Press、Cisco Press、John Wiley & Sons、Syngress、Morgan Kaufmann、IBM Redbooks、Packt、Adobe Press、FT Press、Apress、Manning、New Riders、McGraw-Hill、Jones & Bartlett、Course Technology等出版社。

来自Randal

Learning Perl第1版的前言中,我感谢了Beaverton McMenamin的Cedar Hills酒吧[1],在这个距离我家不远的“免费办公室隔间”里面,我在我的Powerbook 140上编写了该书的大部分初稿。就像我最喜欢的球队会在季后赛里面每天都穿上 “幸运袜”一样,我在同一个小酒馆里面编写了本书的全部内容(包括这段文字),希望第一本书的幸运之光将照耀我两次(当我在更新本书第2版的前言时,我发现我的幸运袜确实有效果)。

这家McM酒吧也有同样棒的精酿啤酒和油腻的三明治,但是没有我喜欢的披萨和面包,取而代之的是黑莓脆皮馅饼(当地一种特产)和辛辣的什锦饭(酒吧加了两个摊位,放了一些合伙经营的餐桌)。此时我没有再使用Powerbook 140,而是使用Titanium Powerbook,相对前者来说,后者有1000倍的磁盘容量、500倍的内存容量和快200倍的CPU,运行在真正基于UNIX的操作系统(OS X)上而不是限制版的MacOS上。我也通过我的144kbit/s的手机调制解调器更新了全部章节的初稿(包括这部分),然后直接发送电子邮件给审校人员,而不必等到回家用我自己的9600波特的外置调制解调器和电话线。时代真是变了啊。

然后,再次感谢McMenamin的Cedar Hills酒吧的工作人员提供的摊位和热情服务。

Learning Perl之前的版本一样,我也感激在这里所说的一切,以及向Stonehenge咨询服务公司的同学表示感谢,当我过度使用“huh?”时,他们给我及时并且准确的反馈(通过眨眼睛和尴尬地组织问题的方式)。通过数十场演讲的反馈,我能够持续精炼和重构本书中的内容。

说到这里,本书中的内容来自于Silicon Graphics公司Margie Levine的一个为时半天的“What’s new in Perl 5?”的演讲摘要,此外,作为我经常当场讲授的一个4天的“大羊驼”课程(当时的主要目标是第4版的Perl)的补充。最终,我有一个想法就是增加这些记录,使其变为一门完整的课程,并招募Stonehenge公司的讲师Joseph Hall完成这个工作(他从绘制的示例中选择完整形式)。Joseph为Stonehenge公司开发了一个为期两天的课程,与他自己卓越的Effective Perl Programming图书(Addison-Wesley Professional)配套,我们也使用这本书作为我们的课程教材(直到今天)。

Stonehenge公司的其他讲师在过去一些年中也涉猎了“包、引用、对象和模块”的课程,包括Chip Salzenberg和Tad McClellan。但是,最近的大量修改是由我的高级讲师Tom Phoenix完成的,他经常荣获“Stonehenge公司月度最佳员工”,可能我要为此放弃我最喜欢的停车位。

在我编写本书的过程中,Tom Phoenix贡献了本书中的大部分练习和一套及时的复习笔记,包括为我提供完整的段落,以替换我已经写好的“糊涂话”。不论在课堂上,还是在编写过程中,我们合作得都非常好,因为他的这些贡献,我们都认为Tom是作为一个合著者参与的,但是,本书任何部分的错误和疏漏之处都归因于我,因为那些都不是Tom的错。

最后一点也很重要,要特别感谢brian d foy,他带领本书进入了第2版,而且编写了上一版和这一版之间大部分的修改内容。

一本书如果没有主题和发行渠道,就什么都不是,为此我必须感谢与Larry Wall和Tim O’Reilly的长期合作。谢谢你们,创造了一个行业,在近20年的时间里面,我可以用从中的收益支付我所有的生活必需品、所有想要购买的东西和梦想。

一如既往,特别感谢Lyle和Jack教会我所需的写作知识,说服我不仅要做一个程序员,还应该学习如何写作;我也是个作家。只是碰巧知道如何编程而已。谢谢!

对于你,本书的读者,为此我辛苦了无数个小时准备,啜饮着冰镇啤酒,狼吞虎咽地吃着美味的芝士蛋糕,小心翼翼地不让蛋糕碎屑掉落在笔记本电脑的键盘上:感谢你们阅读本书。衷心希望本书能够对于你精通Perl有所帮助(哪怕只是少许帮助)。如果你在街上遇见我,请告诉我:Hi,我喜欢你的这本书[2]。我很喜欢这样。非常感谢!

首先,我必须感谢Randal,因为我从Learning Perl第1版开始学习Perl,然后通过为Stonehenge咨询服务公司讲授“美洲驼”和“羊驼”课程,学习Perl的其余部分内容。讲课通常就是最好的学习方式。

最需要感谢Perl社区,人们组织了丰富多彩和多种多样的小组,使我们引以快乐地使用这门语言来编写工具、网站和使Perl更有用的模块。很多人通过我在其他工作过程中与他们的讨论,也为本书提供了间接的贡献。有太多人了,但是如果你也曾经与我一起做过一些与Perl相关的东西,可能你也为本书做过一些贡献。

首先感谢O’Reilly的整个团队帮助我们完成这本书。

感谢我在Stonehenge咨询服务公司共事多年的同事和同学,以及在Usenet上帮助过我的人。你们的想法和建议极大地改进了本书中的内容。

真诚地感谢合著者Randal,给我很大的自由,让我以不同的方式开发教学素材。

感谢我的妻子Jenna Padbury,感谢你为我处理了很多善后工作。

感谢审校人员为本书的初稿提供建议。Tom Christansen做了很多令人惊叹的工作,不但修正了他所发现的每一个技术问题,而且润色了本书的内容。本书在他的帮助之下变得更完善。David Golden,PAUSE管理团队的一位成员和CPAN工具链的黑客,在模块发布流程的部分帮助我们校正了很多细节问题。Moose的一些开发者,包括Stevan Little、Curtis“Ovid”Poe和Jesse Luehrs,也对于相关章节提供了帮助。Sawyer X, Module::Starter模块当前的维护人员,在我们编写本书的相关章节时,也极大地帮助了我们。

也感谢我们的学生,是你们多年来的反馈使我们知道哪些部分的课程内容需要改进。也正是因为你们的帮助,我们所有人才能为本书感到自豪。

感谢诸多的Perl Monger,当我们访问你们所在的城市时,让我们感到很轻松自在。希望有机会我们再次一起相聚。

最后,我们最由衷地感谢我们的朋友Larry Wall,正是因为你的智慧,向全世界分享你的这个真正酷并且强大的工具,使我们的工作变得更快、更简单而且更有趣。

[1] http://www.mcmenamins.com/.

[2] 同时可以询问一个Perl问题,我不介意。


欢迎你进一步了解Perl。你打开这本书的原因可能是你想编写超过100行的Perl程序,或者你的老板要求你来学。

Learning Perl是一本经典著作,因为该书将介绍运用Perl编写小型和中型程序的方法(据观察,这也是Perl编程所完成的主要工作)。但是,为了避免“本书”的篇幅太长以至于吓退初学者,我们特意慎重地对知识进行梳理和选择。

后面的篇幅会以同样的风格介绍余下的故事。本书将会覆盖编写100行甚至1万行(甚至更长)的程序所需要的知识。

例如,你将会学到与多名程序员在同一项目中共同工作的方式,通过编写可复用的Perl模块,你可以将这些模块包含进发行版中由普通的Perl工具使用。这很重要,因为除非你每天工作35小时,否则你很难在没有帮助的情况下完成更庞大的任务。同样,在协作环境下,你要确保你开发的所有代码在最终的应用程序中能够很好地同他人的代码配合。

本书也展示如何处理更大和更复杂的数据结构。例如我们可能会提到的一个所谓的“散列的散列”或“数组散列的数组的数组”。只要你掌握了一些关于引用的知识,你就在通往理解任意复杂的数据结构的道路上,这将使你的工作更轻松。

其次,你还必须理解面向对象编程这一时髦概念,就是允许你的部分代码(由其他程序员编写的代码)能在同一个项目中或多或少地被重用。本书也很好地介绍该部分内容,尽管你可能从来没有面向对象的概念。

在团队中工作的一个重要的方面是:有一个发布周期以及一个进行单元测试和集成测试的流程。在本书中你会学到如何把你的代码打包成一个发行版并且对该发行版提供单元测试,在目标环境中对你的代码进行开发和验证。

另外,就像我们在已经出版的Learning Perl中所保证和传递的那样,我们将会在整个学习过程中用一些有趣的例子和双关语让你的学习过程充满乐趣。尽管我们把Fred、Barney、Betty和Wilma送回家了,但我们让一些新的明星来担当角色。

我们假定你已经读过Learning Perl,至少是读过第5版,或者至少你假装已经读过,并且已经使用Perl有一段时间,已经完全理解它的基础知识。例如,我们将不会在本书中解释访问数组元素或者从子例程返回值的方法。

确保你已经理解下面的内容,所有这些都在Learning Perl中介绍过。

本书对于这些主题进行深入探讨,不过假定你已经掌握这些基础知识。

本书最后的部分讲述发行版和对CPAN做贡献。要做到这些,你现在需要申请一个PAUSE账户,当你读到对应部分的时候,就可以使用它。可以通过以下链接申请该账户:https://pause.perl.org/pause/authenquery?ACTION=request_id.

Learning Perl中介绍了strict和warnings杂项,并且希望你在所有代码中使用它们。然而对于你将在本书中见到的大多数代码,假定我们已经打开strict和warnings,因此我们不必因为重复的样本代码而分散精力,正如我们不使用shebang行和常见的文本块。当我们呈现完整的示例时,我们也将包含这些杂项。

你可能希望做我们做的事情,而不是从零开始编写程序,我们打开一个模板,里面有我们常用的代码。直到你开发出自己的模板,完成标准的文档并且用你喜欢的做事方式,你可以从一个简单的示例开始,假定周围都是如下所示的代码示例:

本书使用当前最新的Perl v5.14版本,该版本于2011年发布。通常,该语言的详细信息在此版本下是稳定的。特别是自从一些双重生命的模块在CPAN上分别独立出现时,我们使用的一些模块可能已经更新。因为我们通常提出Perl的基本理念并且通常扼要概述模块,所以对于每一个模块的更新,你总是需要查看该模块的文档。

注意 

当我们于2012年年中完成本书的编写时,Perl v5.16版已经准备在本书递交出版商一周后发布,我们可能很巧合地在本书中会描述到它的某些特性。

一些更新的特性需要我们明确地声明,我们希望使用它们而不必影响针对之前Perl版本的程序。启用这些特性最简单的办法是告诉Perl我们需要的版本。数字5.014必须有3个数字放置于小数点后(假使有一天出现Perl 5.140):

可以加上符号v和它的多个部分:

使用两个小数点的形式,可以省略字母v:

但是,这将诱惑我们在所有情况下省略字母v。

每当我们编写一些需要Perl特定版本中特性的代码时,我们将会插入use v5.14行(或者任何合适的版本信息),使用能够使特性生效的第一个版本。如果可以,我们也会展示一些能够在Perl之前版本上工作的代码。我们考虑使用Perl v5.8版,该版本于2002年第一次发布,是任何人都能使用的最早版本,因此当没有指定一个版本时,该代码示例就假定为Perl v5.8。通常情况下,我们致力于使编写的代码能够用于尽可能多的人和尽可能多的Perl版本上,但是我们也希望你使用尽可能新的Perl版本。

要掌握关于Perl v5.14的更多基础知识,你可能需要查阅Learning Perl第6版。

在本书中,我们通过前面的字母,以v5.M.N的形式表示Perl的版本。目前为止,我们也在版本前加一个前缀“Perl”,但是当我们谈及版本间的差异时,将会变得很沉闷。相反,我们将不再谈论Perl的这一点。当我们说“v5.14.2”时,我们就是在谈论Perl 5.14.2版。这是在我们编写本书时的维护版本,尽管v5.16版的发布指日可待。

在v5之后的数字要么是奇数要么是偶数,并且这就是实验版本和维护版本的差别。维护版本(如v5.14)用于普通用户和生产场景。实验版本(如v5.15)是用于Perl 5核心维护小组(Porter)添加新特性、重新实现或者优化代码和改变不稳定的代码的地方。当他们准备完毕时,将实验版本升级到维护版本,是通过将第二个数字加一,变为偶数来表示的。

对于第三个数字(如v5.14.2中的2)是一个修正发布。当我们说v5.14时,我们所指的是该版本下的全部修正版本。有时候,我们需要表示一个特定版本,在Learning Perl中,你可能记得在v5.10.0版和v5.10.1版之间,智能匹配修正了一个严重的设计bug,并且改变了它的行为。

本书仅限于v5,还有另外一件事,有时候叫做Perl v6,但是这与v5毫无关系。Perl v6被设计成一个全新的语言规范,并且也是由Larry wall设计的,但它不是v5的升级(即使在2000年,我们认为可能是这样)。我们知道这很让人迷惑,并且对于v6的用户而言也是一样,这就是v6规范的实现使用了不同名字的原因,如Rakudo和Niecza。

类似于Learning Perl,本书加入了一些更深奥的主题,并且对于这些主题添加了脚注。第一次阅读本书的读者可以选择先不读,再到下一次阅读时再阅读该部分主题。脚注中的内容与本书后续内容关系不大。

完成每一章后面的练习很重要。亲自动手训练效果会更好。提供该训练的最佳方式是在每看书半个小时至一个小时后,提供一系列练习。如果你的阅读速度很快,阅读完一章花费的时间可能小于半小时。慢一点,深呼吸,然后做练习!

每个练习有一个“完成分钟数”的评级。我们打算使该评级能够达到钟形曲线的中点,但如果你花费了更多或者更少的时间,也别灰心。有时候这仅仅取决于你在学习或工作中,面对相同编程任务的次数。这个数字只是一个参考。

附录中列出了每个练习的答案。再说一次,别偷看答案;否则你将毁掉练习的价值。

作为本书的作者,我们总是很高兴尽我们所能地帮助别人,但是我们已经被爆满的E-mail所淹没。有一些在线资源,你可以从那里获取帮助,你可以直接从我们这边获取这些资源,也可以从其他很多在Perl社区中有过贡献的人那里获得。

Stack Overflow(http://www.stackoverflow.com/

Stack Overflow是一个对于所有类型的编程问题进行免费问答的网站,并且有很多知识丰富的Perler经常回答你的问题。你很可能在一个小时内得到免费的完美解答。你甚至可能从本书作者那里得到答案。

Perlmonks(http://www.perlmonks.org/

Perlmonks是一个在线Perl社区,你可以在这里提问,发表你关于Perl的想法,并且和其他Perler互动。如果你有一个关于Perl的问题,人们可能愿意在Perlmonks上讨论。你可以搜索归档文件或者开创一个新话题。

learn@perl.org和http://learn.perl.org/

learn@perl.org邮件列表是一个专门为Perl新手提问题而设计的安全地方,不必担心你会打扰任何人。该列表在等待你的问题,无论你问的是多基础的问题。

module-authors@perl.org

如果你的问题是专门关于编写和发布模块的内容的,有一个特别的邮件列表:module-authors@perl.org。

comp.lang.perl.misc

如果你更多的时间在使用Usenet,你可以在comp.lang.perl.misc上提问题。一些长期的Perl用户关注这个组,并且有时候他们也很有帮助。

如果你是一个Perl讲师,决定使用本书作为你的教材,你需要知道,对于大多学员而言,本书中的练习都足够短小,只需45分钟到一个小时就可以完成,留下的一点时间可以休息一下。一些章节的练习可能会更快一些,而另一些章节可能需要稍长的时间。这是因为一旦在方括号中写入所有的小数字,我们就发现我们不知道添加的方式。

现在,让我们翻开下一页,正式进入学习阶段。

在每一章结尾,我们按照如下方式列出了练习。在每个练习之前,我们加入了我们认为大多数人完成该练习所需花费的时间。如果你需要更长时间,这也没什么。

可以在附录A中找到本练习的答案。

1.[5分钟] 在http://pause.perl.org/创建一个PAUSE账号。你将在本书的最后一章用到该账号,并且我们希望你能提前创建好。

2.[5分钟] 查阅本书的网站:http://www.intermediateperl.com/。你可能对于下载的部分感兴趣,该部分有一些对于练习有用的文件。下载归档文件,即使你后续没有办法访问互联网,你也能离线使用这些文件。


Perl的一个杀手级特性是Perl综合典藏网(Comprehensive Perl Archive Network),我们称其为CPAN。Perl安装时已经自带许多模块,但是CPAN拥有更多的第三方模块。如果我们需要用Perl解决某些问题,或者完成某个任务,可能在CPAN上就有现成的模块能够帮助我们完成。高效的Perl程序员是能够聪明地使用CPAN的人。Learning Perl中对CPAN有一个简单的介绍,因为它太重要了,我们将在此再次叙述。

注意 

我们可以浏览CPAN的主页(http://www.cpan.org/),或者它的搜索界面:CPAN Search(http://search.cpan.org/)和MetaCPAN(https://www.metacpan.org/)。

模块是用于程序的构建块,模块能够提供可复用的子例程、变量和甚至是面向对象的类。在通往构建模块的道路上,我们将展示你可能感兴趣的一些东西。我们也将介绍一些使用模块的基础知识,这些知识已经由其他人编写完成。

正如我们在Learning Perl中所注明的,我们不必理解模块的全部内容,以及当使用模块时它们内部的运作方式(学习本书后你将会有更深入的理解)。通过模块文档中的示例,我们能够做很多事情。为了启动Perl,我们将立即开始使用Perl模块,等以后再解释它们的结构和特殊语法。

Perl已经自带很多流行的模块,在v5.14发行版中,超过66MB的内容是模块。在1996年10月,v5.3.7发行版拥有98个模块。在2012年年初,v5.14.2发行版拥有652个模块。这确实是Perl的一个优势:它已经自带了我们所需要的很多东西,让你不需要做太多额外的工作,就可以完成有用并且复杂的程序。

注意 

使用Module::CoreList模块查看不同Perl版本中自带模块的信息,这就是我们最终得到这些数字的方法。

贯穿本书,我们将试图确定Perl安装时自带的模块是哪些(并且通常情况下,是Perl第一次收录这些模块时的版本)。我们称它们为“核心模块”,或者将它们标注为 “标准发行版”。如果我们安装了Perl,我们将拥有这些模块。因为我们使用v5.14编写本书,当考虑Perl的核心内容是哪些时,我们将假定为Perl当前版本的模块内容。

当开发我们自己的代码时,我们可能希望考虑是否仅使用核心模块,以便我们可以确保每一个安装相同Perl版本的人,都将拥有这些模块。我们将避免在此争论,主要是因为我们太热爱CPAN了,以至于不能没有它。同时我们也将立刻展示如何确定哪个模块来自于哪个Perl版本的方法。

CPAN无疑是Perl最吸引人的特性,它由一群努力工作的志愿者提供工具和服务,使人们发布高质量的软件并且评估和安装模块变得轻而易举。尽管这不是有用的CPAN工具综合列表,但是它包括我们最常使用的服务。从这个列表开始,我们也将很快地找到有用的服务。

CPAN Search(http://search.cpan.org/)

最久负盛名且最为大众所知的CPAN搜索服务是Graham Barr的CPAN搜索。我们能够浏览或者搜索模块,并且每个发行版的页面拥有关于该发行版重要事件和信息的链接,包括来源于第三方的信息,如测试结果、bug报告等。

MetaCPAN(https://www.metacpan.org/

MetaCPAN是CPAN的下一代搜索界面。它几乎完成CPAN Search的所有事情,但是加入了一个API,因此我们能够基于他们的数据编写我们自己的应用。

CPAN Testers(http://cpantesters.org/)

由作者上传至CPAN的每一个模块都被自动测试。一大群测试人员下载当前发行版,并且在他们各自的平台上测试。他们将结果发送至CPAN Testers的中央数据库,该数据库整理所有的测试报告。作为模块的作者,我们拥有免费的测试服务。作为模块的用户,我们能够检查测试报告,判断发行版的质量或者查看它是否能够在我们的计算机配置上工作。

CPANdeps(http://deps.cpantesters.org/)

David Cantrell做了比CPAN Tester更进一步的工作:合并了测试报告中所有关于模块依赖性的信息。不是单独依赖于模块本身的测试。通过记录整个依赖性链中的测试结果,我们能够查阅安装问题的可能性。对于任何软件的安装,最让人感到失落的就是在安装过程中出现错误,但是CPANdeps能够帮助我们杜绝这类问题。作为该服务的一部分,David也在维护C5.6PAN和C5.8PAN,这些是CPAN的专业化版本,分别拥有仅能够在v5.6和v5.8这两个发行版上运行的每个模块的最终版本。

CPAN RT(http://rt.cpan.org/)

RT是来源于Best Practical[1]的问题跟踪系统,并且为CPAN的模块作者提供服务。CPAN上的每个模块在RT上自动获取问题队列,并且对于很多模块,RT是主要的问题队列。一些模块的作者可能偏好使用其他的bug跟踪系统,但使用RT可以作为良好的开端。

几乎每个Perl模块都具备文档,即使我们可能不知道所有幕后的工作方式,但如果知道接口的使用方式,我们就真的不需要担心这些事情。毕竟,这就是有接口的原因:隐藏细节。

注意 

我们也可以使用http://perldoc.perl.org/网站,以HTML格式或者PDF格式读取Perl一些版本的文档。

在本地机器上,可以使用perldoc命令读取模块文档[2]。输入感兴趣的模块名称,然后得到它的输出文档:

上面的文档只显示包含在文档的顶部最重要的部分(至少当我们开始时是最重要的)。模块文档通常遵循传统UNIX帮助手册的格式,以命名(NAME)部分和概要(SYNOPSIS)部分作为开始。

概要提供模块的使用示例,如果我们能够暂时抛开模块并且遵循示例中的方法,我们就能够使用该模块。这就是说,我们可能尚未熟悉一些Perl技巧和概要中的语法,但我们能够仅遵循示例中的方法进行并且完成期望的工作。

现在,因为Perl是一个过程、函数、面向对象和其他一些语言类型的混合体,Perl模块引入大量不同的接口。我们将以略微不同的方式使用这些模块,但只要我们检查文档,就不会犯错误。

要加载一个模块,可以使用Perl内置的use函数。我们不会在此深入讨论所有细节,稍后会在第11~17章展开。此时此刻,我们只是想要使用模块。我们从File::Basename模块开始,该模块来自于核心模块。将该模块加载到脚本中,可以按照如下所示方法:

当按照上面的方法编写程序时,File::Basename模块把三个子例程(fileparse、basename和dirname)引入脚本(第17章将展示使用它们的方法)。从此以后,我们能够使用这些子例程,如同已经直接在同一文件中定义了它们一样。

这些例程从一个完整的路径名中提取文件名和目录部分。例如,如果我们在Windows上运行,并且$some_full_path为D:\Projects\Island Rescue\plan7.rtf,然后$basename将得到plan7.trf,并且$dirname将得到D:\Projects\Island Rescue。如果我们运行在类UNIX系统上,并且$some_full_path为/home/Gilligan/Projects/Island Rescue/plan7.rtf,然后$basename将得到plan7.trf,并且$dirname将得到/home/Gilligan/Projects/Island Rescue。

File::Basename模块能够识别当前环境下的操作系统类型,因此它的函数能够理解对于我们可能遇到的不同种类的分隔符,正确解析字符串的方法。

然而,假如我们已经拥有一个dirname子例程。我们现在使用File::Basename模块提供的函数定义覆盖它!如果我们打开警告,就将看到一条由此产生的消息,但除此以外,Perl真的不在乎。

很幸运,我们能够通过在模块名之后指定一个子例程列表,使use操作限定它的操作,该列表叫做导入列表:

现在,该模块仅仅提供这两个导入的子例程,并且不干涉我们自己的dirname例程。但这样输入很笨拙,所以更常见的是按照如下方式使用qw操作符编写:

即使导入列表中仅有一项,我们仍然倾向于将该项写入qw()列表,使程序一致性更好并且易于维护;通常情况下,我们将返回到要求“给我该模块的另一种编写方法”,如果使用qw()操作符编写导入列表,事情将变得很简单。

我们保护了自己拥有的dirname例程,但如果我们还是想要使用File::Basename的dirname例程提供的功能该怎么做?没问题,我们只需要把它以完整包的规范拼写出就可以使用。

use后的列表名称不会改变在模块的包中定义的子例程(在该示例中,File::Basename)。我们能够始终使用方法完整的路径名,不必考虑导入列表,如下所示:

注意 

在调用子例程时,我们不需要在前面添加“&”符号,这是因为编译器已经通过use语句知道子例程的名称。

举一个极端的(但极端有用)例子,我们能够将导入列表指定为一个空列表,如下所示:

空列表和没有列表的概念是不一样的。空列表的意思是说:“请不要导入任何子例程”,而没有列表的意思是说:“请导入默认的子例程”。如果模块的作者工作做得比较好,默认值可能就是我们想要的子例程。

与File::Basename模块导入的子例程相比,另一个核心模块File::Spec也提供类似的功能。File::Spec模块用于支持文件规范的一般操作。(文件规范通常是文件或者路径的名称,但它可能是一个不存在的文件名称,也就是说,它不是一个真的文件名,是这样吗?)

注意 

如果我们想要一个函数接口,我们就可以使用File::Spec::Functions模块。

与File::Basename模块不同,File::Spec模块有一个主要面向对象的接口。与之前一样,我们使用如下use语句加载该模块:

然而,因为该模块拥有一个面向对象的接口,所以它不导入任何子例程。相反,接口告诉我们使用它的类方法访问该模块的功能。catfile方法使用合适的目录分隔符连接一个字符串列表:

以上语句调用File::Spec模块的catfile类方法,该方法为本地操作系统创建一个合适的路径,并且返回单个字符串[3]。这与File::Spec模块提供的其他大概二十多个操作在语法上都是相似的。

File::Spec模块提供一些以恰当的方式处理文件路径的其他方法。我们能够在perlport文档中获得更多关于可移植性问题的信息。

不要因为File::Spec模块没有创建对象,而且看上去比较像是“非面向对象”模块而感到失望。让我们查看另一个核心模块:Math::BigInt,该模块能够处理超出Perl本身范围的整数。

注意 

Perl被当前运行的硬件架构所限制,这是少数几个硬件限制之一。

不必像使用字面量一样使用数字,Math::BigInt模块将其变为数字:

如前所述,该模块没有任何导入内容。它的整个接口使用类方法,如new关键字,放置于类名之后用于创建实例,然后调用实例方法,如bpow方法和bstr方法,这些方法放置于实例名称后。

Perl的强项之一是它的报表功能。我们可能认为它仅限于文本,但是通过使用合适的模块,我们能够创建任何格式。例如,使用Spreadsheet::WriteExcel模块,当我们能够创建不仅有用而且格式漂亮的Excel文档时,我们就成为办公室的明星。

正如我们直接使用Excel应用程序所知道的,我们打开一个工作表并且在工作表中放入数据。如下所示,直接使用文档中的代码,我们很容易创建第一个工作表:

此时可以插入数据。与Excel类似,该模块对于以字母命名的行号和以数字命名的列号能够跟踪行和列。将内容放入第一个单元格,我们遵循文档中的示例代码,按照如下方式,使用write方法写入数据。

然而,在程序内部,可以很容易使用数字跟踪行和列,因此Spreadsheet::WriteExcel模块也按照该方法实现。写方法已经足够智能,能够识别我们正在使用的单元格描述,如下所示,由于我们不得不记住模块从零开始计数,因此第一行和第一列都是零。

这已经使我们可以做很多事情,但我们还能够做更多,使工作表看起来更漂亮。首先,必须创建一个表单格式:

如下所示,一旦我们拥有了一个格式,我们就能够调用该格式作为最后一个参数写入数据:

除了write之外,还有一些其他方法用于处理特定类型的数据。如果我们想要插入如同“01234”的字符串,我们不希望Excel忽略前导的数字“0”。然而,如果没有对于Excel进行特别指定,Excel就尽可能猜测输入的数据内容。如下所示,我们将向Excel说明,可以使用write_string方法写入字符串:

还有一些其他关于write方法的特殊用法,因此检查模块文档,以查看可以写入单元格的其他内容。

除数据以外,还能够创建公式。如下所示,可以使用write_formula方法写入公式,但公式字符串以“=”符号开始(与GUI中的操作方式一致):

关于该模块还有很多其他内容,并且我们应当通过检查模块文档,快速理解该模块的其他特性。后续关于引用的章节将展示更多示例。

核心模块、标准库、发行版或者版本,是标准发行版的一系列模块和插件(我们通过CPAN下载的版本)。当人们谈论“核心模块”时,通常是指一系列模块,我们能够期望任何特定的Perl版本都拥有这些模块,因此通常可以确保任何人都使用我们的程序而不必安装额外的模块。

可是,这样的描述使定义变得不明确。某些发行版,如Strawberry Perl(http://strawberryperl. com/)和ActivePerl(http://www.activestate.com/activeperl),它们在其各自的发行版中添加了额外的模块。一些供应商的版本,如OS X,向通过他们的操作系统发布的Perl包中添加了额外的模块,或者甚至改变了一些标准模块。这些情况并不是那么令人厌烦。真正令人厌倦的是供应商从标准发行版删除了部分模块,或者把标准发行版分成不同供应商的安装包,因此我们不得不安装原本应该拥有的这些模块安装包[4]

Module::CoreList模块只是一个数据结构和接口,该模块把很多关于Perl 5版本的模块历史信息聚合在一起,并且提供一个可编程的方式以访问它们。该模块是一个关于变量和“类”对象接口的混合体。

如下所示,我们可以查阅一个特定的Perl版本中自带模块的版本号,只需要在小数位之后指定5位数字(3位数字为最小版本号,两位数字为补丁版本号):

有时候我们希望知道相关的其他信息:第一个将该模块放入标准库的Perl版本是哪个?Module::Build模块是Perl的编译系统,第12章将描述该模块。Module::CoreList模块自v5.9.4版本起成为Perl标准库的一部分。

如果只需要检查模块在第一次发布时的Perl版本号,就不必为加载Module::CoreList模块编写一个程序。只须运行corelist程序:

如果我们拥有Perl的最新版本,就同样也包含Module::CoreList模块,我们可以使用该模块查找自身的信息:

CPAN是众多志愿者共同工作的产物,在开始流行使用Web之前,很多志愿者使用它们自己的小型(或者大型)FTP站点。直到1993年年底,他们还在perl-packrats邮件列表上协调各自的工作量,之后,因为磁盘空间变得越来越便宜,所以他们决定将同样的信息复制到所有站点,而不是放在各自专门的站点上。该想法酝酿了一年左右,以Jarkko Hietaniemi建立的芬兰FTP站点作为CPAN的母站,所有其他镜像站点每天或者每小时从母站获取更新。

该站点的一部分工作是重新整理和组织分散在各处的Perl归档文件。为非UNIX架构操作系统的Perl二进制文件、脚本和Perl的源代码本身建立存放空间。可是,模块部分成为CPAN中最大并且最令人关心的部分。

在层次化功能目录中,用符号链接树组织CPAN中的模块,指向作者目录中实际文件的位置。模块部分还包含格式一般易于被Perl解析的索引,例如,Data::Dumper模块的输出用于模块索引的具体细节。这些索引都由主服务器使用其他的Perl程序自动派生。通常情况下,CPAN从一个服务器同步到另一个服务器的镜像工作由一个非常古老的Perl程序完成,该程序叫作mirror.pl。

从它屈指可数的几台镜像服务器开始,CPAN如今已经成长为超过200台遍布于互联网各个角落的公共服务器,至少每天,甚至频繁到每小时,都在不停地更新。无论在世界的哪个角落,我们总能找到用于下载最新模块的临近CPAN镜像站点

众多CPAN搜索和聚合站点中的一个,如https://www.metacpan.org/或者http://search. cpan.org/,将可能成为我们与模块仓库交互最喜欢的站点。通过这些站点,我们能够搜索模块、查看它们的文档、浏览它们的不同发行版、查询它们的CPAN测试者报告以及完成其他许多事情。

通过CPAN可以直接安装一个简单的模块。可以使用Perl自带的cpan程序,只要告诉cpan需要安装的模块名称。如果我们想要安装Perl::Critic模块(可以自动审阅代码),按照如下所示的方法向cpan传递该模块名称:

当初次运行CPAN时,我们可能必须仔细检查初始化CPAN.pm模块的每一步配置,但在此之后,该模块将直接工作。CPAM.pm程序下载模块并且开始编译它。如果需要安装的模块依赖于其他模块,cpan将自动获取依赖的模块并且编译它们。

如果不带参数运行cpan,就将启动CPAN.pm中的交互shell模式。通过shell提示符,能够执行各种命令。可以按照如下方式安装Perl::Tidy模块,该模块能够用于清除Perl代码的格式。

要查阅关于cpan的其他特性,可以按照如下方式,通过perldoc命令查阅它的文档:

CPANPLUS从v5.10版开始成为Perl的核心模块,它提供连接至CPAN的另一个可编程接口。CPANPLUS的工作方式与CPAN.pm类似,但也有我们没有在此展示的一些其他特性。如下所示,CPANPLUS拥有cpanp命令,并且我们使用-i开关安装模块。

与cpan类似,可以打开一个交互式的shell,然后安装所需要的模块。如下所示,安装该模块将允许我们以编程的方式创建一个Excel电子表格。

要查阅关于cpanp的其他特性,可以通过perldoc查阅它的文档:

还有另一个便捷的工具,cpanm(cpanminus的简写),尽管目前它还不是Perl的核心模块。cpanm被设计为零配置、轻量级的CPAN客户端,用来处理大多数人想要做的事。我们能够从http://xrl.us/cpanm站点下载单独的文件,并且遵循它简单的说明启动。

一旦拥有cpanm,我们可以按照如下方式安装需要的模块:

我们也可以手动完成这些cpan为我们完成的工作,如果我们之前从没有尝试过,这至少还有教育意义。如果我们理解工具所做的事情,就更易于追踪所遭遇的问题。

我们下载模块发行版的归档文件、解压缩,然后进入它所在目录。如下所示,我们在此使用wget,但并不在乎使用何种下载工具。我们必须找到需要使用的确切URL,该URL可以从CPAN站点获得。

在此处,我们使用两种方法中的一种(该方法将在第12章中详细解释)。如果我们找到一个名为Makefile.PL的文件,就运行如下这一系列编译、测试和最终安装源代码的命令。

如果我们没有在系统目录中安装模块的权限,就可以通过配置INSTALL_BASE参数将该模块安装到其他路径。

注意 

Perl的默认库目录由任何配置和安装Perl的人设定(即使这意味着我们接受默认设定)。这可以通过perl -V查看。

为了使Perl能够在以上目录中查找所安装的模块,可以设置PERL5LIB环境变量。Perl把这些目录添加到它的模块目录搜索列表中。下面是在Bourne shell中的设置方法:

也可以使用lib编译指令将模块的安装目录添加到模块的搜索路径中(尽管这样使用看起来并不友好),因为我们不但要修改代码,而且当我们想要在其他机器上运行代码时,可能在使用不同的目录。

倒退一分钟,如果我们在模块的安装目录中找到的是Build.PL文件而不是Makefile.PL文件,如下所示,安装模块的操作流程是一致的,只是这些发行版使用Module::Build模块编译和安装代码。

如果使用Module::Build模块在私有目录下安装模块,我们只需要添加--install_base参数,告诉Perl以之前同样的方式查找模块:

有时候我们会发现在安装包中有Makefile.PL和Build.PL,那么该使用哪一个呢?答案是两个都可以。

Perl会浏览一个特殊的Perl数组@INC中的目录元素,以查找需要调用的模块。当编译perl时,选择一个默认的目录列表作为模块的搜索路径。可以通过运行perl并且使用-V命令行开关,在输出中查阅这些信息。

也可以通过编写单行Perl命令输出它们:

use语句在编译时执行,因此它在编译时通过@INC数组查询模块的搜索路径。除非将@INC数组列入考虑范围,否则这使程序变得难以理解。我们需要在加载模块之前修改@INC数组。

例如,假定我们拥有自己的目录(位于/home/gilligan/lib),并且安装我们自己的Navigation::SeatOfPants模块(位于/home/gilligan/lib/Navigation/SeatOfPants.pm)。当我们加载该模块时,Perl却找不到它:

Perl将报错,指出它不能在@INC数组中找到所需加载的模块,并且显示它在该数组中的全部目录。

我们可能想要在调用use语句之前,将需要的模块目录添加到@INC数组中。然而,如果按照如下方式添加:

程序还不能运行。为什么?因为unshift语句在运行时执行,远远落后于编译时执行的use语句。两条语句在词法上接近,但不是执行时间上接近。仅仅因为我们在编写代码时,使两条语句相邻,并不意味着语句执行顺序与编写顺序一致。我们希望在use执行前修改@INC数组的内容。其中一个方法是在unshift两侧添加BEGIN块:

现在,BEGIN块在编译时编译和执行,为后续use语句的调用设定合适的路径。

然而,这样编写很繁琐,并且需要为此做很多令人讨厌的解释,特别是向后期维护和修改这些代码的程序员。可以按照如下方式用之前使用过的一条简洁的编译指令替换这堆乱七八糟的东西:

现在,lib编译指令获取一个或者多个参数,并且将它们添加到@INC数组的起始部分,这与之前的unshift命令实现同样的功能。该方法可行的原因是它在编译时执行,而不是运行时。因此,及时为use后面紧跟的语句做准备。

注意 

use lib语句也能够从所需要的库中unshift一个独立架构的库,这显得比之前相应的显式声明更有价值。

因为use lib编译指令几乎总是有一个独立位置的路径名,所以这是传统的方式并且我们建议你将它放置于文件的顶部。当我们需要把该文件移植到一个新的操作系统中或者当lib路径名变更时,这将易于查明和更新。(如果能够将模块安装于一个标准@INC数组的路径下,就能够完全消除use lib语句,但这样并不总是实用。)

可以认为use lib语句不是指“使用这个库”,而是“使用这个路径查找库(和模块)”。很多情况下,我们看到代码按如下方式编写:

然后,程序员会对它在定义中没有获取后续内容的原因感到疑惑。use lib语句确实在编译时运行,因此以如下方式编写的代码同样不能运行:

Perl在编译时创建$LIB_DIR变量的声明(因此我们使用use strict语句也不能够得到错误,尽管实际使用use lib语句将会报错),但是直到运行时才会执行对于/Users/gilligan /lib/实际的赋值操作。又一次晚了!

此时此刻,我们需要将该声明语句放入BEGIN块中,或者依赖于另一个编译时操作:使用use constant语句设定一个常量:

这样,问题再次解决。也就是说,我们需要的库最终取决于运算结果。这将处理我们需要解决的99%的问题。

我们不必始终理解路径是在运行时还是编译时执行。在之前的示例中,我们对路径进行硬编码。如果我们不知道确切的路径将是什么,因为我们需要在很多机器之间复制这些代码,FindBin模块,Perl自带的核心模块将帮助我们完成该任务。该模块查找脚本目录的完整路径,我们能够使用它创建需要的新路径:

现在,$Bin变量是对应脚本所在目录的路径。如果在同一目录中有库文件,下一行可以按照如下方式编写:

如果库文件放置于与脚本目录相邻的目录中,我们就按照如下方式,将正确的路径部分组合起来,使代码能够正确运行:

因此,如果通过脚本所在目录知道了相对路径,我们就不必对整个路径进行硬编码,这将使脚本更具可移植性。

当我们在第12章开始编写自己的模块时,将介绍更多相关的技巧。

use lib语句有一个很大的弊端:我们不得不在源代码中放入库文件的路径。我们可能在某个位置拥有我们自己安装的本地模块,但我们的同事在另一个位置拥有该模块。我们不希望在从团队其他成员中获取源代码时,每次都修改源代码,并且我们不希望在源代码中列出所有模块的位置。Perl提供一系列方法,使我们能够扩展模块的搜索路径而不必修改源代码。

Skipper必须编辑使用他的私有库文件的每个程序,包括之前部分的代码行。如果有太多代码需要修改,就能够设定PERL5LIB环境变量为目录名。例如:在C shell中,可以使用如下方式:

在Bourne风格的shell中,可以使用如下方式:

Skipper能够设定一次PERL5LIB,然后忽略它。然而,除非Gilligan能够拥有同样的PERL5LIB环境变量,否则它的程序将会运行失败!当PERL5LIB环境变量适用于个人使用时,我们不能依赖它与其他人共享程序。(并且我们不能为整个团队的程序员添加一个通用的PERL5LIB环境变量。相信我们,我们尝试过。)

PERL5LIB变量能够在类UNIX系统中包含由冒号分割的多个目录,并且在Windows系统中包含由分号分割的多个目录(除了这些之外,我们靠自己)。Perl在@INC的起始部分部插入所有指定的目录。在UNIX系统中使用类bash的shell,将按照如下方式设定:

在Windows系统中,按照以下所示设定:

当系统管理员可能需要在系统级启动脚本上添加一个PERL5LIB设定时,大多数人对此感到不悦。设定PERL5LIB环境变量的目的是使非管理员用户能够扩展Perl,以识别额外的模块安装目录。如果系统管理员希望添加额外的模块安装目录,他只需要重新编译并且重新安装Perl。

如果Gilligan意识到Skipper的一个程序丢失合适的指令,Gilligan要么添加合适的PERL5LIB变量,要么使用一个或者多个-I选项直接调用perl。例如,调用Skipper的get_us_home程序,可能需要按照如下方式在命令行执行:

显然,如果程序本身定义额外的库文件,对于Gilligan就会很容易。但有时需要一直等到添加一个-I开关才解决。即使Gilligan不能编辑Skipper的程序也能工作。他仍然有权限阅读代码,但是比如,Gilligan能够和Skipper的程序一样使用该技术尝试新版本库文件。

注意 

使用PERL5LIB或者-I选项扩展@INC,同样自动添加指定目录下的特定于版本和架构的子目录。自动添加这些目录,将简化包含架构或者版本敏感组件的Perl模块的安装任务,例如,编译的C代码。

默认情况下,CPAN工具将新模块安装到与perl相同的目录,但是我们可能没有在该目录创建文件的权限,或者我们可能不得不调用某种管理员权限来实现。这是Perl新手的一个常见问题,因为他们不能理解在他们喜欢的任何地方安装Perl模块的难度。一旦我们知道如何做,我们就能够安装和使用任何模块,而不必请求系统管理员为我们安装。

local::lib模块,我们将不得不从CPAN获取,因为该模块目前还不是Perl的核心模块[5]。该模块设定多种环境变量,它们将影响CPAN客户端安装模块的位置和Perl程序查找所安装模块的位置。我们能够看到它们在命令行中使用-M开关设定加载模块的方法,但是没有任何其他参数。假若那样,local::lib模块使用Bourne shell命令输出它的设定,可以放置如下所示的任意一个登录文件:

注意 

即使使用不同的shell环境变量,local::lib也能输出Bourne shell命令。我们不得不将这些命令转换成我们自己的。

诀窍在于安装local::lib模块,这样我们就可以使用该模块,可以按照如下方式手动下载和安装local::lib模块:

注意 

我们需要一个最新版本的CPAN.pm模块或者APP::Cpan模块,以使用cpan的-I开关,我们已经在Perl v5.14中添加local::lib模块特性。

一旦使用local::lib模块,我们就能够通过CPAN工具使用它。如果我们使用-I开关安装模块,cpan客户端就将支持local::lib模块。

cpanm工具是智能化的。如果我们已经设定的环境变量与local::lib模块为我们设定的环境变量一致,就直接使用它。如果不一致,就检查默认模块目录的写入权限。如果我们没有写权限,它就自动为我们启用local::lib模块。如果我们想要明确地指定使用local::lib模块,就需要按照如下方式:

如果我们使用local::lib模块,当在程序中加载模块时,程序就知道在何处查找我们安装的模块:

我们展示了local::lib模块使用的默认设定,但是有一种方法可以处理我们想要使用的任何路径。在命令行条件下,可以通过-M命令向加载的模块提供一个导入列表:

可以在附录中的“第2章答案”部分找到这些练习的答案。

1.[25分钟] 读取当前目录中的文件列表,并且将名字变为完整的路径规范。不要使用shell或者外部程序来获取当前路径。对于File::Spec模块和Cwd模块,两者都是Perl自带的模块,能够帮助你完成任务。在每条路径的输出之前加上4个空格,之后加上换行符。

2.[20分钟] 安装local::lib模块,并且当你安装Module::CoreList模块(或者你喜欢的其他模块)时,使用它。写一个程序,该程序报告Perl v5.14.2版本下所有模块的名称和第一次发布的时间。查阅local::lib的文档,查看是否有特别的安装说明。

3.[35分钟] 解析在原书封底的国际标准图书编号(International Standard Book Number,ISBN)(9781449393090)。从CPAN安装Business::ISBN模块,并使用该模块从数字中提取组编码和出版商编码。

[1] 更多信息可以查询http://www.bestpractical.com/

[2] 在UNIX系统上,man命令也可以。

[3] 在UNIX系统中,该字符串可能类似于/home/gilligan/web_docs/photos/USS_Minnow.gif。在Windows系统中,通常使用反斜线作为目录分隔符。该模块使我们很容易编写可移植的代码,至少顾虑到文件规范。

[4] 根据Perl的许可证,这些供应商不允许使用这种修改过的Perl版本,但他们这样做了。

[5] 从v5.14开始成为核心模块。


目前,你已经看到了对Perl三种数据类型的引用:标量、数组和散列。同样,也可以对一个子例程进行引用(有时称其为代码引用)。

这样做的原因是什么?在对数组取引用时,可以使同一代码在不同时间处理不同的数组。与之相同,对子例程取引用可以让同一代码在不同时间调用不同的子例程。同样,引用允许构造复杂的数据结构。一个指向子例程的引用,使子例程实际上成为复杂数据结构的一部分成为可能。

换一种说法,一个变量或者一个复杂数据结构是整个程序的数值仓库。一个子例程的引用可以被想象成一个程序中的行为仓库。本节的例子将揭示这一点。

Skipper和Gilligan之间有一段对话:

其输出结果如下:

到现在为止,一切正常。然而,注意,Gilligan有两个不同的行为,这取决于它是对Skipper说话,还是对其他人说话。

现在,Skipper和Gilligan准备与Professor打招呼:

其输出是:

这下轮到Professor要作出回应。

输出结果是:

该程序真麻烦,而且一点也不通用。如果每个乘客的行为以不同的子例程命名,并且当有新乘客到来时,我们不得不指定需要调用的子例程。当然,可以用大量难以维护的代码来处理这件事,但是,就像对于数组和散列所做的处理那样,只要加一些小技巧,就可以简化处理过程。

首先,使用“取引用”操作符。 实际上这也不用介绍,因为它与之前的反斜杠的形式相同:

我们现在对子例程skipper_greets()取引用。注意,前导字符“&”在这里是必需的,但有意删除了其后的小括号。Perl会把这个子例程的引用(coderef)存储于标量$ref_to_greeter中,并且同其他引用一样,它适用于可以使用标量的任何地方。

通过解引用还原一个子例程引用的唯一目的就是调用它。对代码引用的解引用和对其他数据类型引用的解引用是相似的。首先,可以采用在知道引用以前的方法来处理(包括可选的前缀&号):

下一步,把子例程的名字用大括号外加大括号内引用的名字替换:

这样就有了它。这条语句调用当前被 $ref_to_greeter引用的子例程,并把它传递给单个字符串参数:Gilligan。

不过,这样是不是太丑陋了?幸运的是,同样的简化规则也适用于此。如果大括号里的值是简单的标量变量,大括号可以删除:

也可以把它转换成带箭头的形式:

最后一种形式特别适用于在一个大数据结构中进行代码引用,你一会儿就会看到。

如果让Gilligan和Skipper向Professor问好,我们只需要迭代调用所有的子例程:

首先,在小括号里面,创建一个包含两个元素的列表,而且每个元素是一个代码引用,每个代码引用都各自被解引用,即调用相应的子例程并且传递"Professor"字符串。

我们已经看到在标量变量中的代码引用,并且将代码引用作为列表中的一个元素。是否可以把这些代码引用放入一个更大的数据结构中呢? 当然可以。可以创建一个表,映射乘客与他们相互问候的行为,然后使用表重写之前的例子:

注意,变量 $person是一个名字,在散列中查找它,以调用相应的代码引用。然后对那个代码引用进行解引用操作,传递将要问候的人名,并且获得正确的行为,其输出结果如下:

现在可以在一个气氛十分友好的房间里让大家互相问候了:

其输出结果如下所示:

这有些复杂。我们让他们一个个走进房间。

输出结果是在热带岛屿上典型的一天:

在上一个例子中,我们并没有显式地调用子例程,例如profressor_greets;我们只是通过代码引用间接来调用它们。所以,为了初始化一个仅在其他地方使用一次的数据结构,而给子例程提供名字纯属浪费脑细胞。但是,就像我们可以创建匿名数组和匿名散列一样,我们也能够创建匿名子例程!

让我们再添加岛上的一个居民:Ginger。但是不同于用一个命名子例程来定义她的行为,我们创建一个匿名子例程:

匿名子例程看上去像是普通子例程,只是在sub关键字和紧随的代码块之间没有名字(或原型声明)。因为这同样是语句的一部分,所以通常需要拖着一个分号,或者其他表达式的分隔符在结尾。

$ginger的值是一个代码引用,正如我们在其后定义了后面的块作为子例程,然后对该子例程取引用。当我们到达最后一行语句时,我们看到:

尽管我们可以把值作为标量变量存储,但我们还可以直接把sub {...}结构的代码块放入greetings散列的初始化中:

注意这简化了多少行代码。子例程的定义在右边直接引用的数据结构之中。结果相当直观:

添加更多的旅客就变成了简单地把问候行为放入散列中,并把他们添加到进入房间的人名清单中。我们在效率上得到提升,因为我们把行为保存为数据,并通过它可以查找和迭代,这要感谢友好的子例程引用。

一个子例程引用经常被用于回调。回调定义在一个算法中当子例程运行到一个特定位置时所做的事情。它给我们一个机会来提供自己的子例程。回调在如下情况下使用。

例如:File::Find模块导出一个find子例程,它用来以可移植的方式非常高效地遍历给定文件系统的层次结构。在其最简单的形式中,传给find子例程两个参数:一个表示目录开始点的字符串,另一个是对子例程的引用。该子例程会从给定的起始目录开始,通过递归搜索的方法,找到其下的每个文件或目录,并对它们“干些什么”。“干些什么”指定为子例程引用。

find例程开始于当前目录(.),并且定位所有目录或文件。对于找到的每个条目,我们会调用子例程what_to_do(),通过全局变量给它传递一些记录值。特别是,$File::Find::name的值是项目的完整路径名(以开始搜索的目录为起点)。

在此例中,作为find例程参数传递数据(开始目录的列表)和行为。

为只使用一次的子例程起个名字好像有些愚蠢,所以用匿名子例程编写之前的代码,例如:

我们还可以用File::Find来查找文件的一些其他属性,比如文件大小。为了回调方便,当前工作目录被设为文件所在的目录,目录中的文件名也放在变量$_中。

在前面的代码中,使用$File::Find::name作为返回的文件名。哪个名字是真的,$_还是$File::Find::name? $File::Find::name给出文件自起始搜索目录的相对路径名,而在回调程序中,工作目录就是条目所在目录。例如,假定我们想要用find()在当前工作目录找一些文件,所以我们给出( "." )作为要搜索的目录列表。如果调用find(),当前工作目录是 /usr时,则find例程会往下找这个目录。当find定位到/usr/bin/perl时,当前工作目录(在回调程序中)是 /usr/bin。变量$_保存perl,并且$File::Find::name保存./bin/perl,这就是相对起始搜索目录的相对路径。

所有这一切都意味着进行文件测试,例如-s,对于即时查询的文件自动报表。尽管这很方便,但在回调中的当前目录与搜索的起始目录是不同的。

如果我们想要使用File::Find模块累加查询所得到的所有文件大小,该如何实现?callback子例程不能使用参数,而且caller子例程忽略返回的结果。但这没关系,当解引用时,如果子例程引用被调用,子例程引用就能够查看所有可见的词法变量。例如:

与之前一致,当调用find子例程时,使用两个参数:匿名子例程的引用和起始目录。当在目录(或者它的子目录中)中找到需要查询的文件名时,调用匿名子例程处理。

该匿名子例程访问$total_size变量。在匿名子例程作用域的外部声明该变量,但该变量对于子例程仍然是可见的。因此,尽管find子例程调用回调子例程(这将不会直接访问$total_size),回调子例程仍然访问和更新变量。

这种能够访问声明时就存在的所有词法变量的子例程叫做闭包(从数学领域借用的专有名词)。在Perl术语中,闭包是一个包含已经超出作用域的词法变量的子例程。

更进一步,从闭包内部访问变量能够确保只要子例程引用存在,访问的变量就存在。例如,对输出的文件进行计数:

声明一个保存回调匿名子例程引用的变量。但不能在裸块中声明该变量(后续的语句块不是大型Perl语法结构的一部分),否则perl将在语句块末尾回收变量。下一步,词法变量$count初始化为0。然后声明一个匿名子例程,并且将它的引用放入$callback。该匿名子例程是一个闭包,因为它指向的是在块末尾超出作用域的词法变量$count。记住匿名子例程之后的分号;这是一条语句,而不是通常的子例程定义。

在裸块末尾,$count变量超出作用域。然而,因为该变量仍由$callback变量指向的匿名子例程所引用,所以它仍然以匿名标量变量的形式存在。当$callback变量被find子例程调用时,之前叫做$count变量的值开始从1到2到3,持续增长。

注意 

闭包声明增加了相应对象的引用计数,就像是另一个引用被显式调用一样。在裸块的末尾,$count的引用计数为2,但在块语句运行结束后,引用计数的值仍然为1。尽管没有其他代码访问$count,但是它仍将保存在内存中,直到子例程的引用可以在$callback或者其他地方使用。

尽管裸语句块能够完美地定义回调,但是使子例程返回值为该子例程的引用可能更佳:

此处的操作过程是相同的,只是编写方法略有不同。当调用create_find_callback_that_counts子例程时,将词法变量$count初始化为0。来自该子例程的返回值是一个匿名子例程的引用,因为它访问$count变量,所以该匿名子例程引用同样也是一个闭包。即使$count变量在create_find_callback_that_counts子例程末尾超出作用域,在该变量和返回的子例程引用间也仍有绑定,因此变量仍然存在,直到子例程引用最终被销毁。

如果重用回调,同样的变量也将拥有它最近使用的值。如下所示,在初始子例程(create_find_callback_that_counts)处发生初始化,而不是callback(匿名)子例程:

以上示例对于bin目录下的所有文件输出从1开始的连续计数,当进入lib目录时,继续对所有文件计数。这两种情形下使用相同的$count变量。然而,如下所示,如果调用create_find_callback_that_counts子例程两次,将得到不同的$count变量:

如上例,现在拥有两个独立的$count变量,每一个都能被它们自身的回调子例程访问。

如何通过回调得到所有文件大小的总数呢?在前一章的示例中,做法是使$total_size变量全局可见。如果把将$total_size变量的定义插入返回值为回调引用的子例程中,我们将不能访问该变量。但是我们能够做点手脚。首先,我们能够确定将绝不会使用任何参数调用该回调子例程,如下所示,因此如果该子例程收到任意一个参数,就返回总字节数:

通过参数的存在或者不存在来识别程序行为并不是一个通用的解决方案。如下所示,很幸运,我们能够在create_find_callback_that_counts子例程中创建多个子例程引用:

因为我们通过同样的作用域创建了两个子例程引用,并且它们都能够访问同一个$total_size变量。尽管在调用这两个子例程中的任意一个子例程之前,变量已经超出作用域,但它们仍然能够分享这个相同的继承得到的变量,并且能够使用该变量与计算的结果交互。

通过创建子例程来返回两个子例程引用将不会调用它们。此时的引用仅仅是数据。直到作为回调程序或者显式的子例程解引用调用它们,它们才会真正地执行。

如果多次调用这个新的子例程将会发生什么?

在建立子例程部分,成对创建了三个callback-and-getter实例。每个回调程序都有一个相应的子例程用于获取结果。下一步,在收集数据部分,通过每个相应的回调子例程引用调用find例程三次。这将更新与每个回调程序相关联的独立的$total_size变量。最后,在显示数据部分,通过调用getter子例程获取结果。

6个子例程(和它们共享的三个$total_size变量)都是引用计数。当修改%subs或者它运行超出作用域时,拥有引用计数的值将减少,并且回收所包含的数据(如果数据同样引用其他数据,其他数据的这些引用计数也将相应地减少。)

上一节的示例展示了闭包变量是如何修改的,闭包变量也同样用于为子例程提供初始变量或者持续输入。例如,我们编写一个子例程用于创建File::Find回调,该回调程序输出容量超过规定大小的文件名称:

将数字1024作为参数传入print_bigger_than子例程,然后子例程通过shift语句将参数传入词法变量$minimum_size中。因为通过print_bigger_than变量的返回值,访问在子例程之内引用的变量,所以该变量为闭包变量,值在子例程引用存在的时间内都将保持。再一次,多次调用该子例程,为$minimum_size变量创建独特的“锁定”值,每个值都与它们各自相应的子例程引用绑定。

因为词法变量最终将超出作用域,所以闭包仅仅是在词法变量上“封闭的”。又因为包变量(该包变量为全局变量)不会超出作用域,所以闭包绝不会关闭一个包变量。对于所有的子例程而言,它们都引用全局变量的同一个实例。

为了在本书中展示这些内容,创建了File::Find::Closures模块,每个都能返回两个闭包的一批生成器子例程。如下所示,其中一个闭包用于find,另一个用于获取匹配文件的列表:

我们没有打算要所有人真正使用这个模块甚至于从中窃取信息。如下所示的findby min_size子例程创建一个闭包,用于查找任何大于或者等于输入字节数的文件:

我们能够轻易地根据需要修改这些代码,然后将其放入我们的程序中。

子例程并不一定要是匿名子例程才能成为闭包。如果一个命名子例程访问词法变量并且这些变量超出作用域,命名子例程保留一个指向词法变量的引用,正如用匿名子例程展示的一样。例如,考虑如下两个为Gilligan统计椰子的例程:

如果在程序开始部分放置这些代码,就在裸块作用域内部声明变量$count。引用该变量的两个子例程就变成闭包。然而,因为它们都有一个名称,所以它们将会像其他命名子例程一样,直到块作用域结束仍将保留该名称。因为子例程在作用域外仍将保持,并且在该作用域中访问声明的变量,所以它们就成为闭包,因此我们可以在本程序的生命周期内持续访问$count变量。

所以,经过几次调用,我们可以看到计数的增长:

$count变量在调用count_one或者count_so_far之间保留它的值,但是根本没有其他部分的代码能够访问这个$count变量。

如果要递减计数该如何操作?如下所示的代码将会实现递减计数:

也就是说,只要将它放入程序的起始部分,在count_down子例程或者count_remaining子例程的任何调用之前,程序就能工作。为什么?

因为在第一行程序有两个功能,所以当将如下语句放在调用语句之后时,该语句块将不会工作:

$countdown声明的一部分功能是作为词法变量,在编译阶段,该部分作为已经解析的程序被识别和处理。第二部分是将10赋值到相应的存储空间,在运行阶段,该部分作为perl解释器执行的代码。除非perl在运行阶段执行这些代码,否则该变量的初始值始终为undef。

该问题的一个实际解决方案是把静态局部变量所在的语句块转换成BEGIN语句块:

BEGIN关键字告诉Perl编译器:一旦成功解析到该语句块的位置(在编译阶段),就立即跳转到运行阶段,并且也运行该语句块。如果执行该BEGIN语句块中的语句没有产生致命错误,就会编译然后继续执行后续语句块中的语句。该语句块本身也被丢弃,保证其中的代码在程序中精确地执行一次,即使它依照语法规则在一个循环语句或者子例程中出现。

Perl v5.10版开始为子例程引入了另一种方法生成私有的、持续的变量。我们在Learning Perl一书中介绍过这些内容,但是在此将做一个扼要的回顾。如下所示,可以使用state语句在子例程内部声明变量,而不必使用为词法变量创建BEGIN语句块的方法构造作用域:

我们能够在某些人一般不认为是“子例程”的某些地方使用state语句,比如sort语句块。如下所示,如果我们想要查看这些数字比较的结果,就能够按照下列方式跟踪这些比较数字,而不必在sort语句块外部额外创建一个变量:

也能够在map语句块中使用state语句。如果我们想要对行排序,但仍然想记住它们的初始位置,就能够使用state变量跟踪行号:

然而,state变量有一个限制:到目前为止,我们仅能使用state语句初始化标量变量。我们能够使用state语句声明任意类型的变量,但是不能初始化它们:

如果我们只希望使用一次,而不是每次运行子例程,初始化这些变量将变得很麻烦。为了避免这些麻烦,我们将坚持使用标量。但是,等一下!引用是标量,因此我们能够初始化数组引用或者散列引用:

匿名子例程有一个身份问题;它们不知道它们是谁!虽然我们不在乎它们是否有名称,但是当需要告诉我们它们的名称是什么时,有一个名称就显得很便捷。假定我们想要使用匿名子例程编写一个递归子例程。当它还没有完成创建时,我们将使用什么名称再次调用相同的子例程?

这能够按照以下两个步骤完成,因此保存引用的变量已经存在:

输出为递减计数:

以上程序能够工作,因为Perl解释器不在乎$countdown变量的内容,直到该变量实际上需要使用它。如下所示,为了避开这一点,Perl v5.16版引入__SUB__标记(token),返回一个指向当前子例程的引用:

以下代码也能与命名子例程一起使用:

我们将如何调试匿名子例程?当我们开始自由地传递它们时,如何知道正在传递的是哪一个?对于标量的值、数组的值和散列的值,我们能够按照如下方式输出它们[1]

除了匿名子例程以外,还可以通过输出得到究竟是什么在里面,对于匿名子例程,我们必须实际运行它才能得到它内部的值,而不能够仅仅解引用:

在此做一些有用的事情并不难,但是我们不得不拿出很多在此前没有完整介绍过的“黑魔法”。这些可是高级功夫,因此,如果你对此感到不适,请不必担心。反正这不是你需要每天完成的编程任务。

注意 

对于更详细信息,查阅overload模块。

首先,我们在这里将匿名子例程作为对象使用,尽管我们没有告诉你如何实现(这将在第15章中详细介绍)。在这个对象内部,重载了字符串化。我们将告诉Perl解释器如何字符串化该引用,然后也将对它添加一些有用的信息。这些有用的信息将来自于B模块,该模块能够观察Perl的解析树以完成多种多样的工作。我们将使用B模块获取文件和子例程定义的行号。这样我们才能知道去哪儿进一步解决这类问题。下一步,我们将使用B::Deparse模块中的coderef2txt子例程将Perl的内部代码调整回更可读的Perl代码。最后,我们将所有这些放到一起,构成子例程引用的字符串版本:

当创建匿名子例程时,就通过新的子类传递它,然后在我们想要对它字符串化时,对它做检查:

现在,输出显示了子例程定义的位置,以及子例程内部的内容(包括在创建子例程时,所有有效的编译指令的设定):

这里给出另一个妙计:通过将它变为一个闭包,因此名称的值可以不在子例程中:

现在,因为我们不知道$name的值是什么,所以输出就变得不那么有用:

有一个叫做PadWalker的模块,能够回溯Perl的解析,以查找这些闭包变量。如下所示,使用该模块的close_over子例程获取这些变量的散列,然后用Data::Dumper模块输出它们:

现在可以查看$name的值:

还在继续看吗?放轻松些,这些确实是棘手的内容。现在你已经理解它,我们希望你从来不必使用它。我们不希望你的同事陷入一个孤立无援的境地。然而,下一节将揭示幕后所发生的事。

既然已经描述了转储闭包的“笨办法”,下面将展示更灵巧的方法。这就像是一个谋杀之谜一样,第一嫌犯绝对不是真正的凶手。

Data::Dump::Streamer模块是增强版的Data::Dumper模块,如下所示,而且它甚至能够处理代码引用和闭包:

转储散列的值,就得到如下所示的输出(忽略一些令人厌烦的数据)。注意,该语句也转储@luxuries数组的内容,因为它知道Gringer子例程也需要它:

可以在附录中的“第7章答案”部分找到这些练习的答案。

注意 

你不必输入所有的代码。本书中的代码可以通过http://www.intermediateperl. com/网站下载,此时只需从下载的文件中找到一个名叫ex7-1.pl的文件。

1.[50分钟] Professor在周一下午修改了一些文件,但是现在他忘记修改了哪一些。这类事情总是会发生。他希望你编写一个叫做gather_mtime_between的子例程,该子例程提供一个开始和结束的时间戳,返回一对代码引用。其中第一项将使用File::Find模块收集在这个期间内修改过的文件名称;第二项可用于收集所查找到的文件列表。

下面的代码需要尝试;它仅会列出在上星期一最后修改的文件,尽管你可以轻易地修改它为在另一天使用。

提示:可以通过以下代码查找文件的时间戳(mtime):

因为这是一个数组切片,所以记住这里的圆括号是必需的。不要忘记,在回调内部的工作目录不必是调用find方法的起始目录:

注意关于DST的注释。在世界上的很多地方,在白天缩短或者使用夏令时的日子中,一天就不再是86 400秒。该程序掩盖住这些问题,但是更仔细的程序员会采用某种适当的方式处理这类问题。

[1] 一些这类实例在作者的博客“Enchant closures for better clebugging output”中首次出现,http://www. effectiveper lprogramming.com/blog/1345


相关图书

Rust游戏开发实战
Rust游戏开发实战
JUnit实战(第3版)
JUnit实战(第3版)
Kafka实战
Kafka实战
Rust实战
Rust实战
PyQt编程快速上手
PyQt编程快速上手
Elasticsearch数据搜索与分析实战
Elasticsearch数据搜索与分析实战

相关文章

相关课程