Perl高效编程(第2版)

978-7-115-39438-5
作者: 【美】Joseph N. Hall(约瑟夫 N.霍尔) Joshua A. McAdams(约书亚 A.麦克亚当斯) brian d foy (布莱恩 D.福瓦)
译者: 盛春王晖张东亮 蒋永清
编辑: 杨海玲

图书目录:

详情

本书是Perl编程领域的“圣经级”著作。它提供了一百多个详实的应用案例,足以涵盖编程过程中经常遇到的方方面面,由此详细阐释出各种高效且简洁的写法。本书第1 版曾畅销十年之久,而在第2 版中不仅修正了前版存在的一些问题,更与时俱进地引入了许多Perl 领域的新主题,使内容更加完善丰富,也更具实用性。

图书摘要

PEARSON

Effective Perl Programming Ways to Write Better,More Idiomatic Perl Second Edition

Perl高效编程(第2版)

[美]Joseph N.Hall Joshua A.McAdams brian d foy 著

盛春 王晖 张东亮 蒋永清 译

人民邮电出版社

北京

图书在版编目(CIP)数据

Perl高效编程/(美)霍尔(Hall,J.N.),(美)麦克亚当斯(McAdams,J.),(美)福瓦(Foy,B.D.)著;盛春译.--2版.--北京:人民邮电出版社,2015.9

ISBN 978-7-115-39438-5

Ⅰ.①P… Ⅱ.①霍…②麦…③福…④盛… Ⅲ.①Perl语言—程序设计 Ⅳ.①TP312

中国版本图书馆CIP数据核字(2015)第186380号

内容提要

本书是Perl编程领域的“圣经级”著作。它提供了100多个详实的应用案例,足以涵盖编程过程中经常遇到的方方面面,由此详细阐释出各种高效且简洁的写法。本书第1版曾畅销10年之久,而在第2版中不仅修正了前版存在的一些问题,更与时俱进地引入了许多Perl领域的新主题,使内容更加完善丰富,也更具实用性。

本书为初级Perl程序员铺就了一条通往高阶之路,而对高级Perl程序员来说,本书也是必备的技术参考。

◆著 [美]Joseph N.Hall [美]Joshua A.McAdams [美]brian d foy

译 盛春 王晖 张东亮 蒋永清

责任编辑 杨海玲

责任印制 张佳莹 焦志炜

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

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

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

三河市海波印务有限公司印刷

◆开本:800×1000 1/16

印张:20.75

字数:459千字  2015年9月第2版

印数:1-3000册  2015年9月河北第1次印刷

著作权合同登记号 图字:01-2010-5083号

定价:65.00元

读者服务热线:(010)81055410 印装质量热线:(010)81055316

反盗版热线:(010)81055315

广告经营许可证:京崇工商广字第0021号

版权声明

Authorized translation from the English language edition,entitled Effective Perl Programming:Ways to Write Better,More Idiomatic Perl,Second Edition,9780321496942 by Joseph N.Hall,Joshua A.McAdams,brian d foy,published by Pearson Education,Inc.,publishing as Addison Wesley,Copyright © 2010 by Pearson Education,Inc.

All rights reserved.No part of this book may be reproduced or transmitted in any form or by any means,electronic or mechanical,including photocopying,recording or by any information storage retrieval system,without permission from Pearson Education,Inc.

CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD.and POSTS & TELECOM PRESS Copyright © 2015.

本书中文简体字版由Pearson Education Asia Ltd.授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签,无标签者不得销售。

版权所有,侵权必究。

译者简介

盛春

大二开始自学 Perl编程,在通读完《Perl 5详解》后利用暑期打工的机会,专职于Perl语言的CGI开发。毕业后的工作也一直以Perl语言作为主要开发工具,目前就职于思科 IronPort的邮件及 Web安全部门,负责中文反垃圾邮件规则的开发和维护以及内部工具和报告的开发。早年曾为PerlChina筹建社区站点翻译过数篇技术文章。2007年和蒋永清合作翻译《Perl Testing程序高手秘笈》,2009年与蒋永清、王晖合作翻译《Perl 语言入门(第 5版)》,2010年再度合作,翻译了《使用Perl实现系统管理自动化》以及这本《Perl高效编程》。此外,在2009年还主持翻译了开源图书《Pro Git》。

王晖

《Perl 语言入门(第5版)》及《使用Perl实现系统管理自动化》的合译者。接触到Perl是在2000年,很快喜欢上了这门语言和它的社区,熟悉Linux/ Unix,在盛春的影响下成为Mac用户,目前和盛春一样服务于IronPort 的邮件及Web安全部门,大部分工作都是在FreeBSD下使用Perl来完成的,包括数据分析、采集、系统管理及Web应用等。希望国内能有更多的Perl爱好者及基于Perl的应用出现。

张东亮

2004年接触并喜欢上了Perl,对正则表达式相逢恨晚。建有个人博客“我爱正则表达式”,专用来记录Perl等脚本语言中正则表达式的学习心得和应用技巧。目前服务于IronPort的邮件及Web安全部门,负责维护反垃圾邮件/反病毒系统和内部网络的建设。

蒋永清

1997年在一台收银机上开始了Linux的自学,2002年夏开始接触 Perl编程,2003年开始参与 PerlChina技术论坛。2004年至 2009年完成了数十篇技术文章和两本 Perl书籍的翻译。2010年起在北京从事互联网维护工作,随后翻译完成了另外两本Perl书籍。目前和家人、孩子、朋友一起在北京生活。

10年前,当我开始学习 Perl的时候,我认为自己对这门语言已经了解得很多了——没错,对这门语言本身,我确实知道得很多。而我所不知道的,则是那些真正赋予Perl力量的惯用方法和其他灵活的语法结构。尽管不用它们也能写出绝大多数程序,但不掌握这些,则意味着自己的知识结构还不够完善,自己的工作效率也远远达不到理想状态。

我是幸运的,因为我得到了本书的第1版。不过,那本书从来没有机会停留在我的书架上,它一直都在我的包里,一有空我就会打开来读一段。

Joseph N.Hall这本书的内容编排简单得让人爱不释手,每一段内容虽短,但都饱含智慧,而且讲得十分明白透彻。不瞒您说,我们免费的 Perl Tips电子报(http://perltraining.com.au/tips/)正是受了本书的启发才创刊的,这份电子报一直致力于探讨Perl及其社区的发展。

对于一门语言来说,10年意味着很大的变化,而社区对语言的认知则有更大的变化。因此,让我非常高兴的不仅是听到这本书的第2版即将出版的消息,更重要的是这个新版本出自Perl社区最杰出的两位成员之手。

不用说,brian对Perl的全心投入是有目共睹的。他不仅写了很多Perl语言方面的书,还负责出版一份杂志(The Perl Review),并且维护着Perl官方网站中的FAQ(常见问题解答),另外他在众多Perl及编程语言社区一直享有盛誉。

而Josh则以他运营的著名播客网站Perlcast闻名,他从2005年就开始在这个网站中以音频形式播放Perl新闻了。Josh总能找到那些著名的、有趣的人,对他们进行采访,这使他自己快速积累了大量知识,也让我对他羡慕不已。

总之,能向亲爱的读者朋友推荐这本书的第2版,我感到荣幸之至。希望它能让你真正掌握这门语言的精髓,就像当年第1版对我的启蒙那样。

Paul Fenwick

Perl Training Australia总裁

前言

很多Perl程序员都是通过本书的第1版启蒙的。在1998年Addison-Wesley出版第1版的时候,整个世界似乎都在使用Perl。当时.com大潮正在兴起,所有懂点HTML的人都能找到程序员的工作。而这些人一旦开始编程,就需要迅速提升自己的技能。本书和其他两本“圣经级”著作Programming Perl[1]、Learning Perl[2]基本上是这些新程序员的必读书。

当时市面上还有不少其他的 Perl 书籍。如今的编程学习者应该很难想象当时美国书店的情况,那时候的书店中有数十米的书架摆放的都是编程书,而大多数都是关于 Java和 Perl的。如今的书店则只在一个小角落里摆放编程书,每种语言往往只会有几本书,而且大多数的书在上架后的半年内就会被其他书取代。

尽管如此,本书还是畅销了10年之久。这要归功于Joseph Hall对Perl编程哲学的深刻理解和他本人的过人智慧。毕竟这本书主要讨论的是Perl编程思想,他在第1版中给出的建议直到现在都还非常实用。

不过,如今Perl的世界和1998年相比已经有了很大的变化,值得提倡的理念也更多了。CPAN (Comprehensive Perl Archive Network,Perl综合典藏网)仅仅经过几年的发展,如今已经成了Perl最吸引人的特性。人们已经发现了许多更新更好的编程方式,而且这10年来业界积累了更多使用Perl的经验,也催生了很多新的最佳实践和惯用技法。

自从本书第1版面世以来,Perl本身也有了很大的变化。第1版存在于从Perl 4到Perl 5的过渡时期,当时大家仍然在广泛使用Perl 4的一些古老特性。在这个新版本中,我们基本上消除了这些差异。现在只有一个Perl,那就是Perl 5(本书不讨论Perl 6,那应该另写一本书)。

现代Perl已经能够支持Unicode(而不仅仅是ASCII),因此你也应该适应这一点,我们为这个主题专门设置了一章。几年来,在 Michael Schwern的推动之下,Perl已经成为被测试最多的语言,几乎每一个模块都非常稳定。Perl粉丝们怀念的“蛮荒时代”已经成为历史。今天,即使是快速原型的开发也可以同时考虑测试。如果你开发的是企业应用,那么你应该好好看一看我们针对测试给出的建议。如果你是一位正则表达式高手,那么你一定想了解最新的Perl正则特性,本书将介绍其中那些最常用的。

Perl仍然在成长中,新的主题还在不断涌现。有些主题本身就值得用一本书的篇幅来介绍,比如 Moose这个“后现代”的 Perl面向对象框架,因而本书也就不勉为其难了。另一些主题,比如POE(Perl Object Environment,Perl对象环境)、对象关系映射器,还有GUI工具包等也都因为同样的原因而没有办法在本书中详细介绍。不过,我们已经计划再写一本More Effective Perl,到时候可能会涵盖更多的内容。

最后,Perl的各种文档和专著较以前也丰富多了。虽然本书会尽可能多地讲到我们认为你应该知道的内容,但如果市面上已经有了详细讨论某些内容的书,我们自然也就不必置喙了,附录会推荐其他一些Perl图书,这样做无疑也给更深入地讨论当前本书中的这些主题留出了余地。

Joseph N.Hall、Joshua A.McAdams和brian d foy

[1].Larry Wall、Tom Christiansen及Jon Orwant合著的Programming Perl,Third Edition(O’Reilly Media,2000)。

[2].Randal L.Schwartz、Tom Phoenix及brian d foy合著的Learning Perl,Fifth Edition(O’Reilly Media,2008)。

第1版前言

我曾经写过大量C和C++代码。在专注于Perl之前,我参与的一个重点项目是实现一门脚本语言,该语言能用来画图、计算概率和生成FrameMaker格式的书。这个语言用了大约5万行平台无关的C++代码,其中有不少相当有趣的特性。应该说,这个项目还是非常有意思的。

它耗费了我两年的时间。

对我来说,大多数有趣的 C或者 C++项目都需要数月乃至数年的时间才能完成。这是因为那些有趣的功能往往比较复杂,所以需要耗费很多时间开发。不过在换为一门高级语言之后,只要3个月的时间,我也可以把原先一大堆平淡无奇的想法变成有趣的项目。

这是我最初对Perl感兴趣的原因之一。吸引我的是Perl这个脚本语言中强大的字符串处理、正则表达式和流程控制能力。这正是那些C和C++程序员(面对时间紧凑的项目时)最需要的特性。于是我学了 Perl,而且实实在在地喜欢上了它。这要归功于一个相关项目,我在该项目中负责实现处理文本文件的功能:获取一个程序的输出,格式化之后再发给另一个程序处理。我用 Perl只花了不到一天就实现了这个功能,而如果使用其他语言,则恐怕需要几天甚至几周时间。

为何要写这本书

我一直都想成为一个作家。小时候我就迷上了科幻小说。我一直热衷于此,有时候一天能读三本,甚至还试着自己写(但写得不好)。之后的1985年,我参加了在密歇根州东兰辛市(East Lansing)举办的Clarion科幻小说作家培训班。随后一年左右的时间里,我偶尔会写些短篇小说,不过从来没有发表过。后来,上课和工作占用的时间越来越多,我最终也就打消了写科幻小说的念头。不过,我还是坚持写作,只不过写的都是技术文档、教程、方案和文件。当然,我这些年来还陆续接触了好几个技术作者。

其中一个就是Randal Schwartz。我在一个工程项目中聘用他给我帮忙达一年之久(这是我第一次做技术主管,也是我第一次管理软件开发类的项目,相信大多数认识 Randal 的人能猜到这点)。后来他选择离职去教Perl,过了一段时间我也去了。

在这段时间中,我对写作的兴趣更浓厚了。在C++、Perl、Internet和World Wide Web等这些热门领域打拼了很多年,我觉得应该把其中一些有趣的东西写下来。应用和教授Perl的经验也让我的这个想法越来越强烈。我盼望着能写一本书,把自己日积月累的各种Perl技巧和反反复复遇到的陷阱汇集起来。

1996年5月,在圣何塞的一次开发者大会上,我和Keith Wollman有了一次交谈。当时并没有谈到我想写书,我们只是讨论了哪些好的题材可以写成书。当谈到Perl的时候,他问我:“你觉得一本名叫 Effective Perl的书会不会受欢迎呢?”这个书名打动了我。要知道,Scott Meyers的Effective C++是我最喜欢的一本C++著作,而给该系列写一本Perl的书显然是个好主意。

Keith的话始终在我耳边回响。过了一段时间,我在Randal的帮助下写了一个选题报告,而Addison-Wesley公司批准了这个选题。

接下来,好戏开场了。我开始没日没夜地写作,常常在电脑跟前一坐就是 12小时,除了用FrameMaker写作,还在Perl 5 Porters邮件列表中不厌其烦地问了不少的问题,查阅了几十本书和手册,编写了很多很多段Perl代码,也喝了很多很多罐健怡可乐和百事可乐。在查阅资料时,偶尔还会发现一些曾被自己忽略的最基础的Perl知识。就这样过了一段时间,本书的第一稿诞生了。

这本书是我的一个尝试,希望借此与大家分享我在学习Perl的过程中收获的经验和乐趣。最后,非常感谢你花时间阅读,希望这本书对你有价值,也能让你感到乐在其中。

Joseph N.Hall

1998年于亚利桑那州钱德勒市

致谢

第2版致谢

许多人帮我审阅第2版,指出了我们忽略的一些问题。对此,我们要感谢Abigail、Patrick Abi Salloum、Sean Blanton、Kent Cowgill、Bruce Files、Mike Fraggasi、Jarkko Hietaniemi、Slaven Rezic、Andrew Rodland、Michael Stemle和Sinan Ünür。书中的某些地方也会直接指出他们的贡献。

另外有些人则为我们做了更多,他们几乎针对书中的每一章都提出了问题。正是因为他们的努力,书中许多错误才会在付梓前得以消灭。他们是 Elliot Shank、Paul Fenwick 以及 Jacinta Richardson。若还有错误,或许就只能归咎于我们没有管好自己的猫,这个调皮的小家伙一定是在我们不曾注意的时候到键盘上蹓过弯儿。

Joseph N.Hall、Joshua A.McAdams和brian d foy

第1版致谢

这本书写来不易。我自认已经倾尽全力了,但如果不是得到了许多程序员、作者、编辑以及其他专业人员的帮助肯定会更加艰难。我衷心感谢所有为本书面市而贡献了宝贵时间和精力的人。

Chip Salzenberg和Andreas“MakeMaker”König帮我修正了不少程序漏洞,使得本书更加精练。对Chip的感激光用言语是不足以表达的。我对程序的关注实在太少了,向Chip致敬!

Perl 5 Porters邮件列表工作组也起到了很大的作用,他们为我解答了不少问题。其中尤其要感谢的是Jeffrey Friedl、Chaim Frenkel、Tom Phoenix、Jon Orwant(以The Perl Journal杂志[1]闻名)和Charlie Stross。

Randal Schwartz是畅销书作者、教师和Just Another Perl Hacker的发起人,他也是我最主要的技术审稿人。所以如果你发现书中有任何问题,可以直接给他写邮件(开玩笑的)。非常感谢Randal,因为他在这本书上付出了许多时间和精力。

感谢Larry Wall创造了Perl,而他本人也解答了我很多的疑问,并且在一些地方提出了建议。

能和Addison-Wesley在这个项目上合作,我觉得非常荣幸。这里遇到的每个人都善良而乐于助人,特别要感谢Kim Fryer、Ben Ryan、Carol Nelson和Keith Wollman。

还有许多朋友在其他方面提供了帮助,Nick Orlans、Chris Ice和Alan Piszcz都耗费了大量时间用于阅读初稿。我的几位现任及前任老板Charlie Horton、Patrick Reilly和Larry Zimmerman则提供了许多灵感,也给了我很大鼓励。

另外,我在写作过程中尽可能坚持原创,但仍不可避免地受到Perl在线手册和Programming Perl等资料的影响。殊途同归,我已尽力用最有创意的方法来阐述,但很多情形下那些有关Perl的经典诠释是难以超越的。

非常感谢Jeff Gong,他总是帮我“骚扰”电话公司,从而让我的T-1线路保持畅通。Jeff总是懂得如何让客户开心。

非常感谢高尔夫这项运动,击球的简单动作能够让我保持清醒,并帮我排解压力。同样地,还要感谢《猎户座之王》和《文明II》两款游戏。

最后,我必须感谢Donna,我的未婚妻和终生伴侣,她也是专业的程序员。没有她的支持、鼓励和爱,这本书就无法写成。

Joseph N.Hall

1998年

[1].The Perl Journal由Jon Orwant创立,该杂志已成为联系Perl社区的纽带和讨论Perl发展的前沿阵地。——编者注

引言

“学习某种语言的基础是一回事,但是知道如何有效使用这个语言进行程序设计则完全是另一回事”,这是Scott Meyers在Effective C++的简介中所说的,这句话对Perl来说也同样适用。

Perl是一门非常高级的语言,可以称为VHLL(Very High Level Language)。它集成了许多高级语言的功能,比如正则表达式、网络管理和进程管理,而且它的语法也是非常“人性化”的。对于文本处理来说,Perl比其他常见的计算机语言要好用得多,可以说它是这个领域最好的语言。Perl对于Unix系统管理来说也是极为高效的脚本工具,同时也是UNIX CGI脚本开发的最佳选择。Perl还支持面向对象编程、模块化设计、跨平台开发、嵌入式开发,具有高度的可扩展性。

这本书适合你吗

本书假设你已经有了一些Perl开发经验。如果你正准备开始学习Perl,那么这本书对你来说可能有些早。我们的目标是让你成为一个好的Perl程序员,而不只是一个普通的Perl程序员。

这本书算不上参考手册,虽然我们确实希望你常常把它放在案头。书中涉及的许多主题比较繁杂,因而我们难以一一深入介绍。不过我们会尝试告诉你最基本的理念,而它们在大多数情况下会很有用。不过这些都还只是敲门砖,你如果真的对此感兴趣的话,还需要更深入地研究一番才行。所以,你仍然需要不时地翻阅一下Perl文档,当然也包括附录中提到的那些书。

Perl有许多值得学的地方

你若读过 Perl 的入门教材,或者学完了相关的课程,那就可以开始写点程序了。不过 Perl的创造者Larry Wall喜欢把这时候写出来的程序称为“咿呀学语”。很多的Perl程序都可以归到这一类,它们简单、直白,而且冗长。这不是什么坏事,你可以用适合自己的任何风格来编写Perl程序。

但到了某个阶段,你可能会不满于这样的咿呀学语,而希望学习更简洁、更独特的写法。这本书就是为这样的你准备的。它的目标就是让你能写出流畅且表达能力强的Perl程序。为此我们会从以下几方面给你一些建议。

知识,或者称为“小技巧”。许多很复杂的Perl任务其实都有更简单的实现方法,甚至完全有可能只用短短几条语句来实现。Perl高效编程的秘诀在于积累足够的经验,学会那些做事情的“正确”方法。首先,在看到你认为好的解决方案后,可以用它来解决自己的问题。其次,当你对什么方案才能算好有了自己的认识后,你也可以自己创造新的方案,然后激扬文字,指出它到底好在哪里。

如何使用CPAN。CPAN如今已经成了吸引人们学习Perl的主要原因。使用其中超过5G的源代码、许多重要的编程框架以及各式常用库程序接口,能让你利用他人的成果快速完成许多任务。CPAN也由此使得常规Perl编程任务得以简化。就像学习其他语言那样,你的技能就体现在充分利用已有的成果上。

如何解决问题。你可能已从其他语言学会了许多分析问题和调试错误的技巧。这本书通过展示多个问题及其 Perl解决方案,教你如何使用 Perl解决问题,同时也通过展示如何高效开发和改善程序,教你如何解决使用Perl时遇到的问题。

风格。本书主要通过示例来展现Perl语言的惯用风格,并教你写出更简洁优雅的Perl程序。如果简洁优雅不是你的目标,你至少能了解到如何避免写出某些笨拙的架构。另外,你也能学着品评自己或他人的程序。

如何进一步成长。这本书不可能涵盖你想知道的所有东西。虽然我们力图写一本高阶的Perl 编程资料,但是要想真的涉及所有的高阶主题实在不切实际,因为哪怕只是罗列个大概也可能需要上千页的篇幅。所以本书的主要目标是让你具备成为高级 Perl程序员的潜质,也就是说使你具备自我提升的能力,这包括如何找到需要的资源,如何不断通过实践来学习,以及如何评断自己的水平。

我们力图把这本书写成一本鼓励思考的书。大多数示例都有不少精微之处,我们会着重阐释那些比较复杂的,而简单的就不再赘言了,我们希望读者自己去领会。有时候我们会特别介绍某段示例代码的一个方面,而对其他部分则略过不表,但是大体上会使其尽可能简单,而又不影响你的理解。如果一时读不懂你也不必紧张,Perl是一门独特的语言,和你学过的大多数语言都不一样,所以有时候必须得反复练习才能掌握某些窍门。学习的过程固然辛苦,但其间也蕴含着各种各样的乐趣和意想不到的收获。

Perl的世界

Perl是一门卓越的语言。在我们看来,它是最成功的模块化编程语言。

事实上,Perl 模块具有“软件芯片”的美名,因为它们常见于各类软件中。(在这里,软件等价于集成电路,是一种可以用于各类应用,而不必理解其内在工作原理的部件。)原因有很多,其中最重要的是因为存在集中协作的模块库CPAN,它降低了我们在竞争、不兼容功能实现上所消耗的精力。参阅附录可以了解更多资源。

Perl内建了最基础但足够充分的模块化和面向对象编程框架。由于其中缺乏严格的对象访问限制,Perl可以写出非常自然简洁的代码。软件行业似乎有个自然法则,那就是最有用的功能却往往来自使用框架和模板最少的开发环境。而Perl正是以某种颠覆的方式实现了对这一“规则和惯例”的支持。

Perl还提供了卓越的跨平台支持。而它之所以适合作为UNIX平台的系统管理工具,也正是因为它把UNIX各个版本之间的差别尽可能地隐藏起来了。你能编写跨平台的Shell脚本吗?能,但实在太复杂了。大多数人应该不会进行这样的尝试。你能编写跨平台的Perl脚本吗?能,很简单。Perl程序也可以很好地在UNIX和Windows、VMS等诸多其他平台间移植。

作为一名Perl程序员,你能得到一些世界上最好的资源支持。因为你可以获得所有可用模块的源代码,同时还有语言本身的源代码。你如果觉得自己找出代码缺陷太慢,还可以在网上获得24/7的技术支持。如果你不满意免费的技术支持,那么还可以考虑购买商业技术支持。

最终,你有了一门特立独行的语言。Perl十分流畅,至少在目前几种脚本语言里,Perl最具表达能力,甚至能达到随心所欲的境界(Do What I Mean,可简称为DWIM)。这个思路或许有些唬人,却揭示了计算机技术的真正发展进程,它超越了时钟频率、硬盘空间和内存大小等物理性能指标的提升。

术语

通常来说,Perl语言用到的术语和其他语言中的大致相同,当然一些术语也会有独特的含义。随着Perl的发展,其中一些术语已经渐渐不用了,同时也有一些新的术语生成。

通常我们说到语言本身的时候用的是Perl(P大写),而说到解释器和源程序的时候用的是perl。大多数时候我们不会特别讨论解释器,所以通常你看到的是Perl。

操作符(operator)在Perl中是一个不需要圆括号的动词(当然它的参数要放在括号中)。

列表操作符(list operator)则是一个标识符,后跟一个以逗号分隔的元素列表:

print "Hello", chr(44), " world!\n";

Perl中的函数(function)是一个标识符,后跟一对圆括号,其中可以列出所有参数:

print("Hello", chr(44), " world!\n");

现在你可能看出来了:在Perl中列表操作符和函数很类似。实际上,两种语法有相同的功能。一般来说,我们会尽可能使用操作符这个词来描述Perl的内置函数,如print和open,只会偶尔使用函数这个称呼,这两者之间没有本质区别。

在 Perl 中,子程序就叫子程序(subroutine),当然称它为“函数”“操作符”甚至“过程”都是可以接受的。但是请注意,Perl中“函数”的用法与数学中定义的不同,一些计算机科学家对这个词的滥用颇有微词。

所有的Perl方法(method)都是遵从某种约定的子程序。这些约定既不是Perl要求的,也不被正式承认。不过,把这种调用称为“方法调用”还是比较合适的,因为Perl为了支持面向对象编程而实现了这种特殊的语法。方法和普通子程序的区别在于:方法的作者期望你按方法调用的语法来呼叫它。

Perl的标识符(identifier)类似于C语言,是以字母或者下划线开始,后面跟着一个以上字母、数字或下划线的词。标识符用来命名Perl变量。另外,Perl变量就是组合了某些特定符号的标识符,比如$a或&func。

虽然我们并不是刻意要使用Perl内部的称呼,不过确实可以把Perl里面那些具有特别句法意义的标识符称为关键词(keyword),比如if和while。相比之下,其他具有“函数”或“操作符”语法的标识符,比如print和oct,可以称为内置函数(built-in)。

左值(lvalue)是指可以出现在赋值符号左边的值。这是这个词的一般意思,不过对 Perl来说,某些特别的结构也可以作为左值,比如substr操作符。

使某个变量本地化(localizing)意味着使它的作用域局限在某个代码块或文件的“范围”内。特殊变量必须用local操作符来实现本地化。而普通变量则可以用my或者local来本地化(参考第4章的条款43)。事实上,这是Perl的一个设计失误,Larry Wall当年其实想用另一个名字来代替local的,但目前只能这样沿用下去了。有时候为了有所区别,我们会明确地说“用my来本地化”。

图解

在本书中,我们会使用Joseph的PEGS(Perl Graphical Structure,Perl图形化结构)来阐明数据结构。这种图示方法非常简单易懂,这里大致介绍一下。

变量是带有名字的值。变量名出现在值的上方,用一侧尖端的方框表示。标量值使用简单的方框来表示:

数组和列表采用相似的图形。其中数值用最上方有粗条的栈表示:

散列则在栈名旁列出相应的值:

引用的画法和LISP语言曾使用的图形化方法相似,使用点号加上箭头的方式表示:

以上就是数据结构的所有基本图示方法。

Perl风格

从这本书中你应该还能领会到一些好的Perl编程风格。

当然了,所谓风格也是个人的喜好和考虑。我们不认为自己掌握的是最佳风格,不过还是希望读者会看到一种鲜活、实用而高效的编程风格。不过,有时候为了增加数据的可读性,我们也会暂时牺牲在风格方面的追求。基本上,本书的风格是遵从perlstyle文档的。

此外,考虑到书中篇幅对代码特有的限制,我们会特别注意行的长度,避免列出那种长篇大段(需要好几页)的代码,并尽可能避免采用那些太过冗长、枯燥乏味的代码作为例子。我们要求每个例子都能突出一两个重点,以避免使读者在代码中迷失。因此,你可能会发现它们不一定遵从最佳实践的要求。

在某些例子中,我们可能会为了突出重点而略过一些细节。在某些代码中,我们使用“...”来代表被省略的代码。(不过,到了本书面世的时候,你可能会发现这个“...”也成了一个合法的语句。因为Perl 5.12引入了yadda yadda操作符,这使得该语句能成功通过编译,仅在执行时会产生运行时错误。但这是编写桩代码的一种好方法。)

有些例子需要特殊版本的Perl才能运行。这里我们如果不特别声明的话,就是需要Perl 5.8,这个版本稍微有点老,但却非常成熟。如果用到了Perl 5.10的特性,那么我们会在例子的第一行注明(例如第1章的条款2):

use 5.010;

另外,我们不会介绍Perl的开发版本,它们的小版本号一般为奇数,比如Perl 5.009或者Perl 5.011。我们介绍某个特性的时候,会列出引入它的首个稳定版本。

并非每段代码都能通过 warnings 或者 strict 的约束(见条款 3)。我们建议所有的 Perl程序员都遵从这两条最基本的约束。不过如果举例时总是提前声明这些约束,也可能会让读者抓不住我们所要论述的重点,因而我们舍弃了这种做法。如果合适,我们会尽可能遵守这些约束,但有时候从大段程序中抽取出来的代码并不一定和原来的程序一样严谨。

我们一般也会尽量减少标点符号的使用(见条款18)。当然我们不是要鼓励大家尝试“Perl高尔夫”[1],这个游戏关心的是程序最短能写成什么样。我们只是试图去除那些不必要的字符,而更多使用空白,这样能突出真正重要的部分,而不是那些死板的模式。

最后,我们尽可能选取有实用价值的代码。虽然并非所有例子都特别有用,但我们已尽可能采用某些实际应用程序中的代码。

组织结构

前两章主要是为了之后的章节做铺垫,使得读者能够逐渐适应较复杂的编程,而后的内容就会比较有挑战性。阅读过程中,请随时查看本书的目录和 Perl 文档(可以访问 http://perldoc .perl.org/)。

在第2版中,我们重新组织了书的结构。我们把第1版中某些条款分解成了多个新的条款,同时也对某些进行了合并或删除,因为其他的书中可能已经涉及了这些主题。附录则列出了一些你可能会用到的资源。

这本书的内容并非仅限于这些纸张之上,我们同时还架设了一个网站(http://effectiveperl-programming.com/)。那里你会发现更多与本书有关的信息,有些是我们遗漏的,有些是还未来得及写入本书的,还有些是其他Perl相关的有用话题。

[1].Perl高尔夫(Perl Golf)是一种不定时举行的Perl程序设计游戏,以程序编码最短者获胜,就像高尔夫球中最少杆者获胜一样,详细可参考:http://perlgolf.sourceforge.net/。——编者注

第2章 Perl的地道用法

Perl语言的设计者是一位语言学家,所以和任何一种人类语言相同,Perl也有很多习语。

这里所说的习语,一部分是指那些非常精炼的Perl写法,另一部分是指约定俗成的写法。经验老道的Perl程序员常常会这么写,新手也往往乐此不疲。

具体来说,到底哪些是习语哪些是惯例,并没有明确的界限,判断标准也因人而异。不管是简单的还是复杂的算法,Perl都有多种表达处理方式,而其中有些明显要比其他方法更“正确”些。尽管Perl有句口号是“条条大路通罗马”,但其实往往“大多数路都是错的”,或者说,“总有某些路要更好一些”。

习语和惯例对Perl来说更为重要,相比之下,C、Bourne shell或者C shell就不那么依赖它。这就是为什么我们说C语言编程其实并不算复杂的原因之一。你要是觉得这么说太疯狂,请去任何一个计算机书店看看就会明白。书架上不少的书会告诉你如何灵活使用C语言,不过大都没有C++的书厚。同样的,尽管有很多关于shell编程的书,但在有关Perl的书对比下都会显得薄些。

Perl是一门富于表达能力的语言,所以也往往非常简练。Larry Wall在设计这个语言时,特别注重代码的精炼。这就是我们常常听到的赫夫曼编码(Huffman coding)原则:最常用的那些操作应该只需要最少次数的键盘敲击。这在Perl的高级特性中非常常见,比如<>、正则表达式以及grep的用法中都能看到:

# 对换 $a 和 $b

( $a, $b ) = ( $b, $a );

# 从文件或标准输入读取各行,排序后打印

print sort <>;

# 打印所有带有joebloe这个词的行

print grep /\bjoebloe\b/, <>;

# 把@n中可以被5整除的数筛选出来,存入@div5

my @div5 = grep { not $_ % 5 } @n;

# 把"123.234.0.1"转化成二进制整数0xb7ae0010的方法之一

$bin_addr = pack 'C4', split /\./, $str_addr;

你可以用其他方法完成同样的任务,不过要是用其他语言编写,多半写出来的是更长更低效的代码。比如像<>的功能,虽然也能用你自己的方式表达出来,但结果一定繁杂而冗长,最终导致程序中真正有用的代码变得很“模糊”。而调试和维护也会变得更为麻烦,因为程序的长度和复杂度都已经大大增加。

从某种意义上来说,习语和编程风格会有交集。虽然像 print sort <>这样的代码无可挑剔,但总有处于灰色地带的情况:

# 逐行打印%h中的所有键值对

foreach my $key ( sort keys %h ) {

print "$key: $h{$key}\n";

}

# 另一种打印键值对的方法

print map "$_: $h{$_}\n", sort keys %h;

第一段是非常直白的写法,不但高效,而且可读性强。因为它只使用了语言的基础特性。而第二段代码更短,而且对某些人来说,它显得“酷毙”了,因为这里用了map操作符和列表上下文来代替普通的 foreach 循环。无论如何,在写出这样的代码之前,请先考虑自己或其他人将来读到这段代码时的感受。这种写法不但有些难懂(但也不能算非常难懂),执行效率也有可能变得低下。

每个Perl程序员都应该掌握基础习语,同时也该对更高级的习语有所了解。作为程序员,应该熟练并习惯于使用那些高效、简洁并且可读性好的习语。而那些高度复杂的习语并不那么重要,具体使用要看场合,主要取决于程序作者、读者身份以及程序本身等各方面因素。

本章我们会展示许多Perl习语,其实有些你肯定已经看到过。相信你会立刻喜欢并着手使用那些简单的习语。而对于那些稍显复杂的,请自行决定哪些合用,哪些不合用。

Perl代码反应了作者的品性。只要你喜欢,平铺直叙地写代码也毫无问题。编程就像盖房子,是一门堆砌砖块的艺术。虽然简单,但只要能顺利运行,那就很好。只不过循规蹈矩出来的作品,大抵不会有什么特色。

而与此相反,你可能一心想着要把所有干净利落的语法特性都用起来。这就好像成天蹲在工具房浪费时间琢磨该用哪种新式强悍工具的建筑师,忘了完成任务才是工作的根本。能以各式最新最酷的编码技术当然很好,但往往你最终会发现,最朴素最经得住考验的,还是那些普通的锤子。

或许经过一段时间后,你能在上述两者之间取得某种平衡。

有时需要的是s/\G0/ /g,而有时需要的,仅仅是一句$a = $b。

条款15 为优雅、简洁而使用$_

Dollar underscore或者写为$_——你可能喜欢它,也可能厌恶它,但不管怎样,如果想要成为一名熟练的Perl程序员,就必须要深刻理解它。

$_是许多操作符的默认参数,有时也是一些控制结构语句的默认参数。请看下面示例:

# $_作为默认参数

print $_; #写明print的默认参数是$_,实际可以省去

print;   # 省略后效果完全相同

print "found it"

if $_ =~ /Rosebud/; # 用于匹配和替换操作

print "found it"

if /Rosebud/;     # 效果同上

$mod_time = -M $_;  # 大多用于文件句柄测试

$mod_time = -M;    # 效果同上

foreach $_ (@list) { do_something($_) } # 用于foreach循环

foreach (@list)   { do_something($_) } # 效果同上

while ( defined( $_ = <STDIN> ) ) {

# while 是一种特殊情况

print $_;

}

while (<STDIN>) { print } # 效果同上

最后一个例子在条款7中也曾涉及,展现的是它的特殊用法,在while循环中使用<file-handle>操作符是一种简写,实际的操作过程是:逐行读入文件到$_,直到文件末尾。它会自动检查文件读取的返回值,一旦到达文件末尾,会返回undef宣告结束。

看上去有些凌乱,不过碰到困惑,最好的办法还是查阅Perl文档指南。不必记住整个文档,因为它是用来查阅的。比如说,split函数会对$_作何特殊处理?查查文档就好。

其实$_也可以算是一个普通的标量变量,可以用来存放数据、打印内容、修改内容等。不过它还有些独特之处需要你特别注意。

1.$_和main包

在Perl 5.10之前,$_只属于main包,哪怕当时正在执行的代码位于其他包内也是如此:

package foo;

$_ = "OK\n"; # 这仍然等同于$main::_

package main;

print;     # 这会打印出"OK"

其实,所有的特殊变量([$@%]中的任一个加上标点符号,以及某些其他变量)都是这样的。你确实可以用$foo::_这样的形式,但它只是一个普通变量,不会具有和$_一样或类似的特殊功能。

2.$_的本地化

在Perl 5.10之前,你只能用local来对$_进行本地化。使用特殊变量时,多半都需要限制它的生命期和作用域。有了local之后,你可以在当前范围内使用自己的$_,直到所在作用域结束:

{

local $_;

# ...使用自己的$_版本

some_sub(); # 也会使用自己的$_

}

有了local,你可以在限定的作用域内随意修改特殊变量,包括在所调用的子程序中仍然可见和有效。如此一来,就可以在整个程序中使用自己的$_版本。

Perl 5.10版本以后,$_开始支持词法域,对它所作的修改,仅限于当下代码的作用域可见,从外部是无法访问的。即便是同一个作用域内所调用的子程序 some_sub,也不能访问到词域类型的$_变量:

{

my $_;

#...使用自己的$_版本

some_sub(); # 它看不到你的$_

}

3.编程风格与$_

在使用$_的地方,往往$_都隐匿于无形。像下面这段代码,你能计算出总共隐形地用到了几次$_吗?

while (<>) { # 第一次

foreach (split) { # 第二、三次

$w5++ if /^\w{5}$/ # 第四次

}

}

# 查找以.txt结尾,并小于5000字节的文件

@small_txt =

grep { /\.txt$/ and (-s) < 5000 } @files; # 第五、六次

正因为如此,有些Perl程序员会觉得$_好像更容易让人迷惑,而不是化繁为简。甚至有一本书[1]中直白地指出,“很多Perl程序员在他们的程序中随意使用$_,相比自己命名的变量,这样的代码更难读懂、更容易出问题。”真的是这样吗?看看下面哪种写法更容易读懂呢?

while ( defined( $line = <STDIN> ) ) {

print $line if $line =~ /Perl/;

}

while (<STDIN>) { print if /Perl/ }

其实这里使用$_并不会带来什么理解上的困惑。只要学一点Perl基础并善于查阅文档,那么所有的困惑都是暂时的。

4.要点

多数操作符都使用$_变量作为默认参数。

链接多项操作时,应避免使用临时变量,而改用默认变量。

使用$_前可通过本地化避免影响其他代码。

条款16 了解其他默认参数

$_并非 Perl 里面唯一的用作默认参数的特殊变量,除此以外还有其他几个类似的。但和$_一样,不管哪个默认参数,有关的详细信息都在官方文档中,阅读也好,参考也罢,核心文档总归是最好的学习指南。

1.@_作为默认参数

在子程序中,shift 操作会默认用@_作为参数。所以如果看到没有任何参数的这类调用,其实是在获取@_最左边的元素:

sub foo {

my $x = shift;

...;

}

这里要介绍一个有趣的语法,它能直接对被引用的数组进行传递,而且只有一行代码:

bar( \@bletch );

sub bar {

my @a = @{shift};

...;

}

在这个例子中,Perl会把@{shift}中的shift理解为变量名,而非内建函数。只要打开strict编译指令(见条款3),你就会看到Perl实际上是这样理解的:

Global symbol @shift requires explicit package name ...

所以,你必须在花括号中加点什么,好让Perl知道这个标识符其实并非变量名,而是一个函数。一般最常见的做法就是添加圆括号:

my @a = @{ shift() };

也可以使用单目操作符规避歧义,比如用+,实际的作用相当于占位符,让Perl知道shift并不是字符串:

my @a = @{ +shift };

要是不太适应这种将参数引用解开来的写法,当然也可以拆解成平常的几步来做,这是你的自由。

2.@ARGV作为默认参数

在子程序之外,shift会把@ARGV作为默认参数。知道了这点,我们就可以对命令行参数作自定义处理。比如把所有以连字符开头的参数当作开关选项,其他参数当作文件名:

foreach (shift) {

if (/^-(.*)/) {

process_option($1);

}

else {

process_file($_);

}

}

当然,这项工作没必要自己来,Perl 有很多现成的模块用于处理程序参数,比如Getopt::Long模块。

另外请注意,shift操作符总是会使用@ARGV或main::@_,就算当前运行在其他包的名字空间中也一样。

3.其他使用$_的函数

另外还有一些内建函数会默认使用$_:-X文件测试操作符(但-t除外)、abs、alarm、chomp、chop、chr、chroot、cos、defined、eval、exp、glob、hex、int、lc、lcfirst、log、lstat、oct、ord、pos、print、quotemeta、readlink、ref、require、reverse(仅限于标量上下文)、rmdir、say、sin、split、sqrt、stat、study、uc、ucfirst,以及unlink。

4.默认使用STDIN

多数文件测试操作符都以$_作为默认参数,但-t却以 STDIN文件句柄作为默认参数。-t使用UNIX的isatty()函数检查文件句柄是否可以交互。交互的意思,就是说终端前是否有一个人在敲键盘输入,或者其他自动回馈信息的机制。下面两条-t调用是完全等效的:

print "You're alive!" if -t STDIN;

print "You're alive!" if -t;

使用-t操作符可以根据环境的交互能力决定程序行为。比如通过-t测试决定是否让命令行启动的CGI程序进入调试模式。类似的任务也可以用IO::Interactive模块实现,而该模块还能应付许多其他特殊情况。

从特定文件句柄读入单个字符的getc函数,也默认使用STDIN作为参数:

my $char = getc STDIN;

my $char = getc;

5.其他默认参数

无需赘言,请参考表2-1。该表总结了Perl内置函数所使用的其他默认参数。

6.要点

并非所有操作符都将$_作为默认参数。

有些函数对默认参数的使用是随上下文而变的,比如shift。

如果不想使用默认参数,当然也可以自己指定要使用的参数。

条款17 常见简写和双关语

Perl这个语言的语法是上下文相关的,这有点类似人类语言。你可以充分利用这一点,在某些可以省略的位置上,让解释器自己找到默认要处理的东西,例如默认参数、$_、可写可不写的标点符号等。Perl通常都能根据上下文充分领会你的意图。

另外Perl也属于那种极富表达力的语言,但有时多变的语法并不能完美兼顾。所以某些情况下,你得亲自给它一点暗示,才能帮助它作出正确选择。接下来我们会给出一些建议,并指出需要小心处理的情况。

1.使用列表赋值来进行变量对调

Perl并没有专门用于变量值对换的操作符,但你可以用列表赋值来完成这个任务。Perl会先计算等号右边的表达式,然后按对应位置赋值:

( $b, $a ) = ( $a, $b ); # 对换$a和$b的值

( $c, $a, $b ) = ( $a, $b, $c ); # 轮转$a、$b和$c的值

数组切片能让你用简单的语法随意置换数组内容:

@a[ 1, 3, 5 ] = @a[ 5, 3, 1 ]; # 对换个别数组元素

也可以进一步使用数组切片来完成数组奇偶元素的对调:

@a[ map { $_ * 2 + 1, $_ * 2 } 0 ..( $#a / 2 ) ] = @a;

2.用[]或()[]转为列表上下文

有时需要强制Perl在列表上下文计算某个表达式。比如用正则表达式切分字符串,开始的时候你可能会这么写:

# 按+号切分$_中:之前的部分

my ($str) = /([^:]*)/;

my @words = split /\+/, $str;

为了避免使用临时变量$str,可以把上面两条语句合并为一条。不过这得需要一些技巧,因为第一个模式匹配在标量上下文(也就是split期望处理的目标字串)的地方不会返回匹配到的字串。我们可以利用切片,将它转为列表上下文,然后取第一个位置上的值:

my @words = split /\+/, (/([^:]*)/)[0];

如果想要一步完成列表计算结果到引用的转换,可以用匿名数组构造符[]。引用操作符\施加在列表上,只会返回一个新的列表,包含的是原来列表元素的引用,而非数组引用(见条款61):

my $wordlist_ref = \( split /\++/, $str ); # 错误

my $wordlist_ref = [ split /\++/, $str ]; # 正确

3.用=>构造键值对

大箭头操作符=>的功能其实与逗号操作符差不多,唯一的差别在于:如果=>左边的标识符能识别为一个单词,那么Perl就会自动将它当成一个字符串,而不是函数调用。这样一来你就能在=>的左边使用time这样的字串:

my @a = ( time => 'flies' ); # 这里time 只是一个单词

print "@a\n";          # 打印"time flies"

my @b = ( time, 'flies' );  # 这里的time 是内建函数

print "@b\n";          # 打印"862891055 flies"

使用=>还能让数据的初始设定式更加美观,特别是对于散列结构的初始设定式来说更是如此。哪一列是键,哪一列是值,一目了然:

my %elements = (

'Ag' => 47,

'Au' => 79,

'Pt' => 78,

);

在表示散列键的地方可以省略引号:

my %elements = (

Ag => 47,

Au => 79,

Pt => 78,

);

有时连=>都可以省略。如果键和值都是没有空格的字串,就可以用qw操作符(见条款21)直接构造:

my %elements = qw(

Ag 47

Au 79

Pt 78

);

4.用=>操作符来模拟命名参数

你可以为函数调用模拟命名参数。只要在子程序内作好设置,就可以让它以散列的形式接受参数。而要快速创建散列,只需把默认值放在前面,后面加上函数得到的参数即可。在后面的例子中,%params预先包含了参数align的默认取值:

sub img {

my %params = ( align => 'middle', @_ );

# 把散列的键和值输出成HTML标签的格式

print "<img ",

(

join ' ', map { "$_=\"$param{$_}\"" }

keys %param

),

">";

}

在调用img子程序时,可以用自己的键值对作为参数:

img( src => 'icon.gif', align => 'top' );

这里设置的align => 'top'覆盖了%params里的默认取值,所以最终你会得到正确的img标签:

<img src="icon.gif" align="top" />

请不要在实际应用中照搬上面的做法,这里只是演示概念。处理 HTML 文件还请使用专门的HTML模块。

5.用=>指明操作方向

最后还有一个关于=>操作符的有趣用法,就是作为“语法糖”来指明操作方向。比如在rename函数中,用它来表示从旧文件名改为新文件名:

rename "$file.c" => "$file.c.old";

请不要混淆=>和->,它们的意义不同,后者是用来对引用取数据的(见条款58),或用于面向对象的方法调用。

6.小心使用{}

圆括号、方括号、尖括号和花括号在Perl里面都是多面手。Perl在处理花括号(或者其他括号)时会先看它所处的上下文,然后再决定该如何解释。一般得到的结果都符合常理,但也有让人意外的情况。

使用花括号时请特别小心,它大概算是Perl里面功能最多的标点符号了。花括号可用来圈定程序块,用作变量名分隔符,创建匿名散列,或者进行散列元素的存取、解引用,等等。面对这么多用途,如果太过纠结的话肯定会晕头转向。不过好在解释器能清楚地区分匿名散列构造和程序块定义。

如果看到花括号中有个孤零零的加号,又貌似毫无必要,那它很可能是被有意放在那里的。Perl 的单目加号对跟在后面的参数没有实际影响,但可借此对语法解析进行修正。比如对某个返回数组引用的函数进行解引用操作时,将函数名放在{}内,会被当成是软引用(可以用strict捕获,见条款3):

my @a = @{func_returning_aryref}; # 错误!

而以下三种写法都能正常工作,因为你给了Perl提示:func_returning_aryref是一个函数名:

my @a = @{ func_returning_aryref() }; # 正确,因为有圆括号

my @a = @{ &func_returning_aryref }; # 正确,因为有&符号

my @a = @{ +func_returning_aryref }; # 正确,因为有加号

若是不巧,可能会遇上难以分辨匿名散列还是程序块定义的窘境。像后面这段代码,假设我们想要函数返回的键值对列表,以匿名散列的形式从eval代码块中返回。花括号是Perl里面被重载定义得最多的一个操作符,所以出现歧义的可能性比较大。碰到实在没有可供分辨的线索,我们可以手工提供一个,好让Perl排除歧义。下面的写法是将匿名散列作为返回表达式的一部分,这样Perl就知道函数key_value_pairs()返回的内容,被用于构造匿名散列表:

my $hashref = eval {

return { key_value_pairs() } # 正确

}

或者也可以用单目加号来暗示Perl,后面的花括号并非块定义,那就只可能是匿名散列构造:

my $hashref = eval {

+{ key_value_pairs() } # 正确

}

但若没有以上提示的话,Perl会猜测花括号是用来为 key_value_pairs()限定作用域的,而这个猜测显然不是我们的本意:

my $hashref = eval {

{ key_value_pairs() } # 可能会导致问题

}

最后还要提一点,Perl会把所有在花括号中的单个标识符(包括两边的空白)都理解成字符串。所以如果此处是一个函数名字的话,实际并不会调用函数,而仅仅是将此名字当作普通字串处理。为排除歧义,只需破坏“单词”的形式,比如借助加号:

${shift} = 10;  # 完全等同于$shift = 10

sub soft { ${ +shift } = 10; } # 是软引用!

soft 'a';             # 相当于$a = 10

鉴于这种写法很容易让人困惑,请不要这样设置变量。如果看到别人这么用,最好劝他改掉。

7.用@{[ ]}或eval {}的形式来复制列表

有时候我们希望操作的是某个列表的副本,以免原始数据遭到破坏。比如查找缺失的.h头文件,可以先读取所有的.c文件,然后将文件名换成.h结尾,再按此列表核对哪些文件不存在:

my @cfiles_copy = @cfiles;

my @missing_h =

grep { s/\.c$/\.h/ and not -e } @cfiles_copy;

Perl里面没有提供复制数据结构的函数,如果需要一份列表的匿名副本,可以把列表放在匿名列表构造操作符[]中,然后对它进行解引用,以符合grep函数对列表上下文的预期:

my @missing_h =

grep { s/\.c$/\.h/ and !-e } @{ [@cfiles] };

另一种产生临时副本的方法是把它放在eval块中,它能返回块内部最后一个表达式的计算结果:

my @missing_h =

grep { s/\.c$/\.h/ and !-e } eval { @cfiles };

遇到这种情形,请尽量使用eval方式,比起前一种写法它更高效。

不过,你还是应该先考虑一下是否真的需要临时副本。在前面这个例子中,完全可以在grep里面作一些改进,引入一个$_变量的临时副本就可以了:

my @missing_h = grep {

my $h = $_;

$h =~ s/\.c$/\.h/ and !( -e $h )

} @cfiles;

另外请注意,以上手法创建的都是“影子副本”即完整独立的副本。如果原始列表中的元素是引用的话,复制的结果就会和原始数据共享。如果确实需要一份完全独立的深层副本,请使用Storable模块的dclone函数:

use Storable qw(dclone);

my $copy_ref = dclone( \@array );

现在的$copy_ref 就和原来的@array完全不相干了。

究竟使用哪种技术,取决于实际要解决的问题。但不管怎样,对列表的复制往往会给人造成困扰,使用的时候要注意区分。

8.要点

Perl语法渊源广博,因此颇多双关。

用=>能更好地展示数据间的关系。

可以用dclone对数组进行深层次地复制。

条款18 避免过分依赖标点符号

Perl程序容易变成满是标点符号的样子,有时甚至会达到令人畏惧的地步。标点符号的滥用会造成程序可读性下降,聪明的程序员会想尽办法,充分利用Perl本身的特性避免使用不必要的标点符号。

1.无括号方式调用子程序

调用子程序时,前置的&符号、函数名后面的圆括号都是可写可不写的,组合起来便有好几种形式。下面这些写法,只有最后一种需要在运行之前先作函数声明或函数定义:

&myfunc( 1, 2, 3 );

myfunc( 1, 2, 3 );

myfunc 1, 2, 3;

传统的&语法也有它的用处:如果子程序的名字恰好是保留的关键字,那么“&子程序名”就是唯一的调用方法,例如&for。而类似列表操作(也就是不带&符号和圆括号)的语法则要求事先明确函数定义或声明。这种写法是最简洁的,但有个细节需要注意。

如果Perl的解释器还没处理到有关子程序的定义,就不能用列表操作语法调用:

myfunc 1, 2, 3; # 错误

sub myfunc { }

此外,这里按照的是源代码中的先后顺序,所以放在 BEGIN 块里面的函数定义实际上是在函数调用后面:

myfunc 1, 2, 3; # 错误

BEGIN {

sub myfunc { }

}

另外,子程序必须在编译前就已完成定义,所以通过eval来动态构造函数的方法也不能用列表语法调用:

eval "sub myfunc {}";

myfunc 1, 2, 3; # 错误

BEGIN块可以写在子程序调用前,但看上去会比较怪异:

BEGIN { eval "sub myfunc {}" } # 可以工作,但很怪异

myfunc 1, 2, 3;

如果能提前声明子程序,就算推后定义,也一样可以使用列表语法。因为 Perl 已经知道myfunc是一个子程序,那它在解析源代码时就不会困惑:

use subs qw(myfunc);

myfunc 1, 2, 3;

sub myfunc { }

2.用and和or替代&&和||

另一个可以借助的功能是优先级较低的逻辑短路操作符and和or。在操作符的结合优先级表中,它们位于最低一级(另外同一级别的还有not和xor操作符,但都没这两个常用)。通过逻辑短路,可以免去对表达式的分组,有效减少圆括号的使用。

最低优先级的and和or操作符允许我们省略列表操作、赋值操作和匹配操作中使用的圆括号。对照各组示例前后,哪一种写法看起来更好?

print("hello, ") && print "goodbye.";

print "hello, " and print "goodbye.";

( my $size = -s $file )|| die "$file has zero size.\n";

my $size = -s $file or die "$file has zero size.\n";

( my $word =~ /magic/ )|| $mode = 'peon';

my $word =~ /magic/ or $mode = 'peon';

open( my ($fh), '>', $file )

|| die "Could not open $file: $!";

open my ($fh), '>', $file

or die "Could not open $file: $!";

请记住,花括号中的最后一个分号总可以省略。这一点对那些单语句块来说非常有用,尤其是map、grep、do、eval这样的操作符:

my @caps = map { uc $_; } @words; # 这里的分号意义不大

my @caps = map { uc $_ } @words; # 看起来更利索些

最后一种避免使用圆括号和花括号的方法是使用表达式修辞,即“后向条件式”语法。一旦习惯,你就会爱上这种简练的写法:

if (/^__END__$/) { last } # 实在平淡无奇

last if /^__END__$/;    # 这样更自然,不是吗?

3.要点

避免滥用圆括号和花括号。

尽可能采用列表语法调用子程序。

借助低优先级的and和or减少无谓的括号。

条款19 调整列表格式以便于维护

尽管之前我们说要避免标点符号的无谓使用(见条款18),但凡事无绝对,有些情况下额外添加标点符号反而利大于弊。作为Perl的设计者,Larry Wall认识到包容其他语言中看似错误的语法也很必要。如果有人一而再,再而三地犯同一个错误,那么最好的解决办法是把错误包容起来,不去理会。

所以,Perl允许你在列表最后一个元素末尾加上一个逗号:

my @cats = ( 'Buster Bean', 'Mimi', ); # 不会出错

那么以后再加入新条目时,就不必照顾前面的代码补上逗号:

my @cats = ( 'Buster Bean', 'Mimi', 'Roscoe' );

如果按每行一个元素的格式排布,看起来会更清楚:

my @cats = (

'Buster Bean',

'Mimi',

'Roscoe',

);

这样,新增或删除某个元素时,都不必担心末尾的逗号影响列表结构。临时注释掉某个元素也很方便:

my @cats = (

'Buster Bean',

# 'Mimi',

'Roscoe',

);

对散列表来说,这种排布格式的优势更为明显,每行写一个键值对,按列对齐:

my %spacers = (

husband => "George",

wife   => "Jane",

daughter => "Judy",

son    => "Elroy",

);

如果列表元素都不含空格,可以直接使用qw操作符初始化,这样逗号和引号可以一并省略:

my %spacers = qw(

husband  George

wife    Jane

daughter Judy

son    Elroy

};

要点

列表元素独立成行,条理清晰,便于编辑。

习惯在列表末尾附加逗号,添补新元素时就不会忘记补漏。

将散列键值对按列对齐,以便于维护。

条款20 善用foreach、map和grep

Perl提供了好几种遍历列表元素的方法。

Perl程序员普遍趋向于避免使用C语言风格的for循环和按下标遍历列表的方式。使用下标的循环通常比其他循环方式慢,因为下标检索需要耗费 Perl一定的计算时间,而 C语言风格的代码结构复杂,叫人望而却步:

for ( my $i = 0 ; $i <= @array ; $i++ ) {

print "I saw $array[$i]\n";

}

大多数程序员都爱用foreach、map和grep形式的循环。这三种方式有相似之处,不过各自的应用场合不同。如果要较真,任何一种形式的循环都可以用其他形式等价表示,但不合时宜的使用往往会使人困惑,困惑的不仅是那些将来会阅读你的代码的其他人,还包括你自己。所以,知才善用,才是最恰当的做法。

1.通过foreach来进行列表元素的只读遍历

如果仅仅是要遍历列表中的所有元素,那么foreach循环就已足够:

foreach my $cost (@cost) {

$total += $cost;

}

foreach my $file ( glob '*' ) {

print "$file\n" if -T $file;

}

请记住,如果没有特别设置的话,foreach默认使用$_作为控制变量:

foreach ( 1 ..10 ) { # 打印一到十的平方

print "$_: ", $_ * $_, "\n";

}

另外,所有用到foreach的地方都可以改用等价的简写名for——不必担心,Perl能明白你的意思:

for (@lines) { # 打印第一个以From:开头的行

print, last if /^From:/;

}

2.用map函数从现有列表延展出新列表

如果是从现有列表推导出新列表,请使用map函数。后面两行是从文件名列表推演到文件大小列表的代码。请记住,大多数文件测试操作符的默认参数都是$_(见条款51):

my @sizes = map { -s } @files;

my @sizes = map -s, @files;

map接受列表上下文参数,第一个参数是用于数据转换操作的表达式或代码块,第二个参数是要遍历的数组或列表。我们常常用它返回空列表或包含多个元素的列表。而在做数据转换时,匹配操作可以写得非常简约直观。

比如接下来的两个例子,map在做数据转换时,返回模式匹配操作符m//内部圆括号捕获的数据。因为这是一个列表上下文,所以m//返回的要么是没有匹配时的空列表,要么是匹配时找到的结果集合而成的列表。

下面这个map返回以.txt结尾的文件名的主名,不包括后缀。由于匹配操作位于列表上下文,所以按列表返回圆括号中匹配的内容:

my @stem = map { /(.*)\.txt$/ } @files;

下面的 map 操作基本类似,从邮件头中找出以 From:开头的行,并取出其中的邮件地址。只有当成功匹配时,它才会返回相应结果:

my ($from) = map /^From:\s+(.*)$/, @message_lines;

出于性能考虑,一般我们会直接对默认的$_变量操作。它其实相当于当前列表元素的别名,所以一旦在map表达式中修改了$_的内容的话,原始数据也会随之改变。一般认为这种行为不太好,——可谁知道——连你自己都有可能会被这种用法弄昏了头。所以如果你怕搞不清楚,那就记住一个原则,确实要修改原始列表内容的话,就改用foreach循环。(稍后就会谈到。)

另外在使用map时,得明确它的意义在于返回相应的列表数据,而不是用作控制结构做一堆杂事。下面的例子违反了正确使用map的三条原则:第一,tr///修改了$_的值,从而改变了原始数据@elems;第二,map 的返回值没有意义,tr///返回的是它在每个元素中删除的数字个数;第三,最后得到的输出结果无用武之地,这和map的设计初衷背道而驰:

map { tr/0-9//d } @elems; # 可能有问题

虽然下面的例子返回了一个有意义的列表,但tr///还是在修改@elems(见条款114):

my @digitless = map { tr/0-9//d; $_ } @elems; # 坏习惯

如果确实需要在map中使用tr///、s///之类的操作,可以复制$_到词域变量再做改动处理。像下面这段代码,就是将$_的值复制到$x后以便再行替换:

my @digitless = map {

( my $x = $_ ) =~ tr/0-9//d;

$x

} @elems;

3.用foreach修订列表元素内容

如果目的是要修订列表元素内容,请使用foreach循环。和map、grep一样,循环体中的控制变量其实是列表当前元素的别名。所以修改控制变量,实际上就是修改原始数据:

# 将列表@nums中所有元素的值更新为原来的两倍

foreach my $num (@nums) {

$num *= 2;

}

# 移除数组@ary各个元素中的数字

foreach (@ary) { tr/0-9//d }

# 使用s///的版本,更慢一些

foreach (@elems) { s/\d//g }

# 依次将$str1、$str2和$str3改成大写

foreach ( $str1, $str2, $str3 ) {

$_ = uc $_;

}

4.用grep筛选列表元素

grep比较特别,一般用于筛选列表元素或对元素计数。但有时会被滥用,那些觉得foreach不如grep酷的程序员,喜欢什么循环都用grep实现,虽说没什么不可以,但作为头脑清醒的开发人员,以最简单直接的方式使用grep才是正道。

下面是grep在列表上下文中的常规用法:

print grep /joseph/i, @lines;

顺便提一下,grep 块的第一个参数,不管是表达式还是代码块,都是在标量上下文中的,这和map不同。这个一般不会对结果有什么影响,但知道总比不知道要好。

在标量上下文中,grep返回符合条件的元素个数,而不是元素列表:

my $has_false = grep !$_, @array;

my $has_undef = grep !defined($_), @array;

5.要点

在需要从一个列表推演到另一个列表时,应使用map函数。

在筛选列表元素时,应使用grep函数。

如果在遍历时需要修改变量,应使用foreach函数。

条款21 了解各种字符串引用方法

Perl有着多种多样的字符串引用方法。

最简单的是单引号方式,除了用于转义的反斜杠和表示字串引用范围的单引号以外,其他字符完全按字面上原本的意义解释:

'Isn\'t she "lovely"?' # Isn't she "lovely"?

而相比之下,双引号则支持多种表示特殊意义的转义序列。比如常见的有\t、\n、\r等,这些都源自C语言:

"Testing\none\n\two\n\three" # Testing

# one   wo

#    hree

此外还有八进制和十六进制的ASCII值转义,比如\101和\x41:

"\x50\x65\x72\x6c\x21" # Perl!

还可以通过Unicode代码点的方式以\x{}的写法定义Unicode字符(见条款74):

"There be pirates! \x{2620}";

用charnames模块的话,还可以按Unicode的字符名称表示Unicode字符:

use charnames;

my $str = "There be pirates! \N{SKULL AND CROSSBONES}";

双引号内出现的变量,包括$和@打头的,都会用变量的值替换,我们称为变量内插:

foreach $key ( sort keys %hash ) {

print "$key: $hash{$key}\n";

}

数组或列表在做变量内插时,会将所有元素串联起来,以特殊变量$"的值作为元素间的分隔符——通常默认为空格字符:

my @n = 1 ..3;

print "testing @n\n"; # testing 1 2 3

如果变量名后紧跟其他字符,为有所区别,我们得在变量名标识符前后添上{}:

print "testing @{n}sies\n"; # testing 1 2 3sies

而像\u、\U、\l、\L、\E这类特殊转义操作符,会在双引号内改变后续字串的大小写:

my $v = "very";

print "I am \u$v \U$v\E tired!\n"; # I am Very VERY tired!

以上介绍的并非全部,变量内插还有许多其他用法,具体细节请查阅perlop文档中的相关内容。

1.其他可选引用方式:q、qq以及qw

当单引号或双引号频繁出现在字串中时,若能使用其他字符圈引字串,就能免除逐个转义的麻烦,并且看起来也更干净。从原则上来讲,Perl允许使用任意标点符号作为字串两边的圈引边界,只要不产生歧义就没问题。随便拿一个你喜欢的,放在q或qq的后面,然后在字串末尾用同样的标点标注结束就可以了。唯一的差别是,qq相当于双引号,允许变量内插,而q相当于单引号,没有变量内插:

q*A 'starring' role* # A 'starring' role

qq|Don't "quote" me!| # Don't "quote" me!

如果字串碰巧用到了我们自定义的边界符号,那就必须转义了,否则就无从分辨。不过一般我们可以改用其他符号:

q*Make this \*bold\** # Make this *bold*

如果用的是可以配成对的字符(无非就是(、[、<或{之一),那在结束时必须改用对应的结束字符。如下所示,这四种写法完全等效:

qq<Don't "quote" me!>

qq[Don't "quote" me!]

qq{Don't "quote" me!}

qq(Don't "quote" me!)

Perl能准确识别嵌套的配对分界符:

qq<Don't << quote >> me!> # Don't << quote >> me!

2.用q{}或qq{}圈引源代码

边界字符的选择,多少会起到体现数据意义的作用,所以对源代码,最好能用花括号来圈引,既符合习惯,又干净明了。比如用Benchmark测试代码性能时,目标代码的引用:

use Benchmark;

our $b = 1.234;

timethese(

1_000_000,

{

control => q{ my $a =   $b },

sin   => q{ my $a = sin $b },

log   => q{ my $a = log $b },

}

};

3.用qw()构造列表可免除逗号和引号的使用

对于快速创建字符串列表,我们可以用 qw(quote words)这样的形式一次取得所有单词字串列表。Perl会用空格符分割qw内字串:

@ISA = qw( Foo Bar Bletch );

前面的写法,比起后面这种传统写法来,要简捷明快得多,因为用于区分单词的逗号和引号也都省掉了:

@ISA = ('Foo', 'Bar', 'Bletch');

不要在qw操作符内误用逗号,它会被当成是字串的一部分。以下面代码为例,最终得到的是“Foo,”、“Bar,”和“Bletch”这三个字串列表。除了最后一个“Bletch”没改变外,其余字串末尾都多了一个逗号:

@ISA = qw( Foo, Bar, Bletch );

如果启用warnings的话(见条款99),就会看到这样的警告:

Possible attempt to separate words with commas ...

4.另一种引用方式:here doc字符串

Perl的here doc(或全称here document)字串提供了圈引大段文本的另一种途径。可能许多人已经对此非常熟悉了——Perl的这个功能源自于UNIX中shell的here doc特性。一般对于大段文本或源代码,我们都会选用here doc的方式圈引。

here doc字符串从<<加一个标识符名称之后的一行开始,一直到出现该标识符开头的独立行之前结束。如果标识符两边使用引号(包括单引号、双引号或反引号),那么引号的类型决定了here doc引起的字符串是否允许变量内插等。如果标识符两边没加引号,则默认使用双引号:

print <<EOT; # ;表示该语句结束,所以这里的注释不包含于该字符串中

Dear $j $h,

You may have just won $m!

EOT

如果不希望变量内插,可以在标识符两边使用单引号:

print <<'XYZZY'; # 这次用单引号……

Dear $j $h,

You may have just won $m!

XYZZY

由于一次允许使用多个here doc,所以下面这样的写法看起来有些奇怪。实际上它分别定义了两段字串:

print <<"HERE", <<"THERE";

This is in the HERE here doc

HERE

This is in the THERE here doc

THERE

要是把here doc用作子程序参数,看起来也会很奇怪,但本质上都是一样的,只不过把大段文本提取到子程序调用语句之外而已:

some_sub( <<"HERE", <<"THERE" );

This is in the HERE here doc

HERE

This is in the THERE here doc

THERE

你当然可以这么写,但考虑到让其他人更容易理解,或许避免这种写法更妥当些。

5.要点

用q()或qq()圈引普通字串。

用qw()自动圈引单词列表。

用here doc圈引多行文本。

条款22 掌握多种排序方式

在Perl最基本的排序操作中,所用的代码相当简洁。仅仅用一个sort操作符,就能对列表进行排序。它会返回排序后得到的新列表:

my @elements = sort qw(

hydrogen

helium

lithium

);

Perl默认按UTF-8排序规则进行排序(除非指定用use locale或use bytes),也就是说,排序时依照UTF-8设定的各字节的码值(编码后的字节数值),从第一个元素的第一个字节起比较,然后第二个,第三个,再到后一个元素的各个字节,只要还未区分出大小前后,就会一直逐个比较下去。不过这种规则要求我们对于字符的定义,得先有个准确理解(见条款77)。

因此在对数字或大小写混杂的字串排序时,会看到意外结果,可以用后面的例子试一试:

print join ' ', sort 1 ..10;

print join ' ', sort qw( my Dog has Fleas );

如果不想用默认的UTF-8排序,那你就需要自己编写用于比较的子程序。

1.比较(sort)子程序

所谓的Perl排序子程序,实际上并非完整的排序算法,比较恰当的说法是“比较子程序”。

和一般的子程序不同,排序子程序得到的参数是经过硬编码的两个特殊包变量$a和$b,而非@_ 。$a和$b在排序子程序内部是本地化的,这就好比是在子程序内部开头的地方,总有一句无形的local($a, $b)语句一样(但不用麻烦我们自己写)。

$a和$b能得到use strict vars的特别照顾——使用之前无需特别声明(见条款3)。它们属于当前包,而不像特殊变量那样只属于main包。

Perl在排序过程中会不断调用这个sort子程序。它的任务是比较$a和$b的大小,然后根据$a小于$b、$a等于$b、$a大于$b这三种情况分别返回-1、0、1。

Perl内建的排序方式相当于用cmp操作符比较。下面是通过具名子程序的方式,在utf8ly内指定排序规则:

sub utf8ly { $a cmp $b }

my @list = sort utf8ly @list;

而更恰当的方式,是在sort后面使用代码块。直接将原来子程序中的内容移过来就好了:

my @list = sort { $a cmp $b } ( 16, 1, 8, 2, 4, 32 );

$a和$b的比较方法决定了最终得到的排序顺序。比如把cmp操作符换成<=>,就变成了按数字大小排序:

my @list = sort { $a <=> $b } ( 16, 1, 8, 2, 4, 32 );

还可以进行大小写无关的排序,只要先把字符串全部转换成小写再作比较就可以了:

my @list = sort { lc($a) cmp lc($b) } qw(This is a test);

# ('a', 'is', 'test', 'This')

而对换$a和$b的位置就能得到倒序结果:

my @list = sort { $b cmp $a } @list;

当然也可以根据$a和$b的值计算后再作比较,比如接下来的这条代码是按文件最后修改时间排序:

my @list = sort { -M $a <=> -M $b } @files;

我们时常需要根据散列值的大小对散列键进行排序。可以借助$a和$b取得散列值做比较:

my %elems = ( B => 5, Be => 4, H => 1, He => 2, Li => 3 );

sort { $elems{$a} <=> $elems{$b} } keys %elems;

甚至还可以按照多个键先后排序。还记得逻辑短路操作符or的妙用吗?正如接下来这段代码所展示的,若是姓相同,则再按名字排序:

my @first = qw(John Jane Bill Sue Carol);

my @last = qw(Smith Smith Jones Jones Smith);

my @index = sort {

$last[$a] cmp $last[$b] # 先按姓排序,然后

or

$first[$a] cmp $first[$b] # 按名排序

} 0 ..$#first;

for (@index) {

print "$last[$_], $first[$_]\n"; # Jones, Bill

} # Jones, Sue

# Smith, Carol, etc.

前例中,实际是根据数组下标取得的数据进行排序,这是很常见的手法。请注意其中or的使用,每次Perl调用比较子程序时,会先计算or左边的表达式,如果结果是非零真值(按此例来讲,就是当$a和$b不相同时),or直接结束,返回左边的求值结果。否则,它会继续计算右边的表达式,并返回它的结果。

请注意,若是Perl的or操作符(或优先级更高的||操作符)只能返回数字1或0两种结果的话就不可能这么用了。由于or返回的,实际上是左边表达式或右边表达式的计算结果,这种用法才成立。

2.高级排序:一般做法

有时在比较两个值的时候需要进行大量计算,比如按照密码文件的第三个字段(也就是uid字段)来排序:

open my ($passwd), '<', '/etc/passwd' or die;

my @by_uid =

sort { ( split /:/, $a )[2] <=> ( split /:/, $b )[2] }

<$passwd>;

乍看之下,这段程序没什么问题,它确实能按指定字段排序。但每次比较都要执行两次复杂的split计算,而拆分方式又毫无分别,显然可以进一步优化性能。

对于每个要比较的元素,sort都会运行一次或多次子程序。假设需要排序的元素个数为n,那么排序时子程序的调用次数就是 n*log(n)。所以为了提升排序的整体效率,就要想办法提高每次比较的速度。如果比较之前需要进行复杂计算,应该考虑使用缓存机制,在比较前就先计算好要比较的数据,比较时直接使用缓存,以提高排序速度。

针对前例,在作具体比较前先创建一个散列表,以原始数据行作为关键字,而以拆分出来的用户标识号uid作为值,然后对散列排序:

open my ($passwd), '<', '/etc/passwd' or die;

my @passwd = <$passwd>;

# 整个一行成为键,而值是uid

my %lines = map { $_, ( split /:/ )[2] } @ passwd;

my @lines_sorted_by_uid =

sort { $lines{$a} <=> $lines{$b} } keys %lines;

3.高级排序:更酷的做法

不管是经过深思熟虑,还因为是随意尝试或灵光乍现,久而久之Perl程序员在持续的实践中逐渐积累起一套实现复杂排序变形的惯用习语。

上面那个实现方式的主要缺点在于,它需要额外准备一条语句,建立辅助排序的数组或散列。我们有一个回避这种用法的技巧,称为Orcish Maneuver(或者|| cache),它充分利用了不为人熟知的||=操作符的特性。让我们从之前按文件最后修改日期排序的例子开始,看看如何改进。

用Orcish Maneuver技巧排序

下面是之前实现的方式,由于多次重复计算-M,所以性能不佳:

my @sorted = sort { -M $a <=> -M $b } @files;

而改用Orcish Maneuver技巧后的写法如下:

my @sorted =

sort { ( $m{$b} ||= -M $b ) <=> ( $m{$b} ||= -M $b ) }

@files;

这段代码究竟表示什么?请别急,先了解一下||=:

$m{$a} ||= -M $a

这是一种简写,它实际上等同于下面这条语句:

$m{$a} = $m{$a} || -M $a

在排序子程序第一次遇到某个文件$a 时,对应的$m{$a}的值是 undef,也就是假,于是||=右边的表达式-M $a求值后存入$m{$a}。请注意,Perl中的||和C语言的不同,它返回的是右边表达式的计算结果,而非简单的数字0或1(真或假)。所以,此刻计算结果得到缓存。

之后再对该文件取最后修改时间作比较时,就会直接使用第一次比较时保存到$m{$a}中的值。

这个%m 散列是临时变量,在排序开始前应该是空的,或处于未定义状态。为确保没有干扰,我们可以把该变量的声明放在排序之前,并用花括号限定作用域,类似这样:

{

my %m;

@sorted = sort ...

}

施瓦茨变换

目前为止最精简的全能排序技术还得算是施瓦茨变换(Schwartzian Transform),名字来自发明人Randal Schwartz。简而言之,施瓦茨变换就是接续若干map实现的sort排序。

为说明施瓦茨变换,让我们一步步来看数据的辗转变化过程。还是借之前按文件最后修改时间排序的例子来说,但这次改造后的性能会更高。第一件要做的事情,是取得文件名列表:

my @names = glob('*');

然后,将文件名列表转化为相同长度的数组列表:

my @names_and_ages = map { [ $_, -M ] } @names;

列表中每个元素都是一个匿名数组,每个数组包含两个子元素——可以称这样的数组为二元组(见条款13)。第一个子元素是原始文件名(来自于$_),第二个子元素是最后修改时间,单位为天(针对隐含的默认参数$_,通过-M计算的结果)。

下一步,在sort块内根据列表中匿名数组的第二个元素按数字大小排序:

my @sorted_names_and_ages =

sort { $a->[1] <=> $b->[1] } @names_and_ages;

在 sort 内的$a 和$b,实际代表的是@names_and_ages 的元素,也就是数组引用。所以$a->[1]表示二元组的第二个元素,即文件最后修改日期到今天的天数。而最终的结果是对二元组的天数按升序进行数字排序(因为这里用的是<=>操作符)。

基本步骤就是这些,最后要做的无非是从二元组中提取文件名。这个步骤非常简单,一次map即可:

my @sorted_names =

map { $_->[0] } @sorted_names_and_ages;

好了,所需步骤就这么多。但上面逐行递推的方式太过繁琐,可以组装起来并为一条语句,成为真正的施瓦茨变换:

my @sorted_names =

map { $_->[0] }   # 4.提取原始文件名

sort { $a->[1]

<=>

$b->[1] }  # 3.对[name, key]二元组排序

map { [ $_, -M ] } # 2.创建[name, key]二元组

@files;       # 1.原始数据

从下往上阅读,就能看出这段代码对应于之前分开写的每条步骤——只不过现在都堆叠起来省掉了临时变量。

单个元素的排序也可以借鉴这个思路,只需要在匿名数组中修改右边的元素值,并调整所需要的比较操作符。下面是密码文件按第三个字段提取uid后按大小排序的代码,也是用施瓦兹变换实现的:

open my ($passwd), '<', '/etc/passwd' or die;

my @by_uid =

map { $_->[0] }

sort { $a->[1] <=> $b->[1] }

map { [ $_, ( split /:/ )[2] ] } <$passwd>;

显而易见,这比先前的要简明得多。借助施瓦兹变换直接从文件句柄读取数据,不但可以省去对缓存数据的临时变量%key的使用,还可以省去@passwd数组。

4.要点

Perl默认按UTF-8规则排序。

可以让sort使用自定义的排序子程序。

为提高运算复杂的排序程序的性能,可以采取先缓存键值后排序的方式。

条款23 通过智能匹配简化工作

Perl 5.10 引入了智能匹配操作符~~。它能根据两端算子的类型决定实际的匹配方式。使用智能匹配可以用最少的指令写出功能强大的条件判断式。完整的使用细则可见perlsyn文档,这里会列出一些有趣的案例。智能匹配操作符通常和given-when一起联合使用(见条款24)。

启用智能匹配,需确保Perl版本在5.10.1以上。智能操作符在设计之初是符合交换律的,也就是说两边的算子可以对调,但后来做了修订,交换左右算子会改变匹配行为。所以为了避免误解,本条款中所有例子都预先启用Perl 5.10.1版本:

use 5.010001;

先来看一看有关检查散列键或数组元素是否存在的例子。任务很简单,写起来也不复杂:

if ( exists $hash{$key} ) { ...}

if ( grep { $_ eq $name } @cats ) { ...}

用智能匹配改写,代码看起来更漂亮,而且尽管任务不同,形式上也能保持一致:

use 5.010001; # ~~的行为自Perl 5.10.1起被修订了

if ( $key ~~ %hash ) { ...}

if ( $name ~~ @cats ) { ...}

接下来,考虑用正则表达式检查散列键,但好像只能手工逐个遍历:

my $matched = 0;

foreach my $key ( keys %hash ) {

do { $matched = 1; last } if $key =~ /$regex/;

}

if ($matched) {

print "One of the keys matched!\n";

}

这样写很啰嗦。其实可以把这些逻辑封装到某个子程序内部实现,实际上,在Perl 5.10.1中,这些工作早已内建完毕,直接拿来测试即可:

use 5.010001;

if ( %hash ~~ /$regex/ ) {

say "One of the keys matched!";

}

就算检查的是数组,写法也大体相同:

use 5.010001;

if ( @array ~~ /$regex/ ) {

say "One of the elements matched!";

}

其他通过智能匹配可以大大简化代码的操作有:

%hash1 ~~ %hash2    # 键相同的两个散列

@array1 ~~ @array2   # 完全相同的两个数组

%hash ~~ @keys     # 数组中是否存在%hash 中的某个键

$scalar ~~ $code_ref  # $code_ref->( $scalar ) 是否返回真

有关智能匹配操作符的完整功能列表,请参考perlsyn文档。

要点

用智能匹配封装常见的复杂匹配模式。

根据算子类型,智能匹配操作符会自动匹配相应的行为。

请限定Perl 5.10.1以上版本,以确保稳定的智能匹配行为。

条款24 用given-when构造switch语句

大概从Perl初生的那天起,Perl程序员就没停止过抱怨:其他都有了,为什么不把C语言的switch语句也借来用用?这个愿望一直到Perl 5.10才得以实现,Perl不仅引入了相同语法,还把它改造得和Perl里面其他东西一样——酷酷的非比寻常。

1.更少的输入

Perl给 switch 起了一个新名字,叫作 given-when。这个名字符合人们谈到这一操作时的普遍说法;“我给出(given)某个条件,当(when)实际情况吻合,操作如下。”你也许已经会用一组if-elsif-else语句表达:

my $dog = 'Spot';

if   ( $dog eq 'Fido' ) { ...}

elsif ( $dog eq 'Rover' ) { ...}

elsif ( $dog eq 'Spot' ) { ...}

else             { ...}

根据$dog种类,需要运行不同代码。这种老式写法要求每个条件分支都重复一遍相近的判断语句。对于Perl这样的高级语言来说,这么写实在有点大材小用。事实上,可以使用given-when来代替这种写法,而改写后的代码如下:

use 5.010;

given ($dog) {

when ('Fido') { ...}

when ('Rover') { ...}

when ('Spot') { ...}

default     { ...};

};

这能帮你省掉不少敲打键盘的功夫!因为 Perl在幕后悄悄完成了几项任务:首先,将$dog设置为当前话题,也就是把$dog 这个变量赋给$_,这样就不必反复输入$dog;然后,Perl会自动完成$_和给定数据间的比较。下面是实际展开后的代码,如果你不怕麻烦的话,也可以直接这样写:

use 5.010;

given ($dog) {

my $_ = $dog;

when ( $_ eq 'Fido' ) { ...}

when ( $_ eq 'Rover' ) { ...}

when ( $_ eq 'Spot' ) { ...}

default { ...};

};

2.智能匹配

given-when结构不只是简单的串联条件判断式。之前的例子用的是显式的字符串eq比较。而在更靠前的例子中,Perl又是如何知道该用字符比较操作?实际上,除非你明确指定比较操作符,默认情况下,Perl总是自动使用智能匹配操作符~~(见条款23):

use 5.010;

given ($dog) {

when ( $_ ~~ 'Fido' ) { ...}

when ( $_ ~~ 'Rover' ) { ...}

when ( $_ ~~ 'Spot' ) { ...}

default { ...};

};

智能匹配能根据算子数据类型自动选择合适的比较方法。在这段代码中 Perl 看到的是标量$_和字符串'Fido',所以决定使用字符串比较。任何地方都可以使用智能匹配,而如果在when语句中不加指定,默认用的就是智能匹配。

如果智能匹配时你给定的数据类型不同,得到会是与数据类型相对应的智能匹配行为。perlsyn文档列出了所有类型的比较,以下列选部分比较有趣的:

$dog ~~ /$regex/  # $dog 能被正则匹配

$dog ~~ %Dogs    # $dog 是%Dogs 中的键

$dog ~~ @Dogs    # $dog 是@Dogs 中的元素

@Dogs ~~ /$regex/  # @Dogs 中至少有一个元素能与正则匹配

%Dogs ~~ /$regex/  # %Dogs 中至少有一个键能与正则匹配

在 when 中简单给出测试条件的写法,实际是将被比较的目标(即当前主题$_)放在智能匹配的左侧,把测试条件放在右侧,完成智能匹配。所以,下面两条语句实际是等效的:

when (RHS) { ...}

when ( $_ ~~ RHS ) { ...}

3.多分支处理

默认情况下,只要某个when块得到匹配,这段程序的运算就算结束了。Perl不会再计算其他when块,这点和if-elsif-else结构相似。这就好比是在每个when块末尾都有一个隐形的break语句,用以直接跳出整个循环:

use 5.010;

given ($dog) {

when ('Fido') { ...; break }

when ('Rover') { ...; break }

when ('Spot') { ...; break }

default     { ...};

};

当然,利用continue语句,就可以使程序在当前when块运算结束后进入下一个when块继续比较。在后面这个例子中,能完成针对$dog名称而进行的所有when测试:

use 5.010;

my $dog = 'Spot';

given ($dog) {

when (/o/) { say 'The name has an "o"'; continue }

when (/t/) { say 'The name has a "t"'; continue }

when (/d/) { say 'The name has a "d"'; continue }

};

4.代码组合

if-elsif-else结构还有一个缺陷是,它不能在两个条件中组合其他代码。任何代码,都必须在找到匹配条件后才能执行。而在given-when结构中,你可以在when块之间自由插入任意代码,哪怕是中途修改主题变量的也没问题:

use 5.010;

my $dog = 'Spot';

given ($dog) {

say "I'm working with [$_]";

when (/o/) { say 'The name has an "o"'; continue }

say "Continuing to look for a t";

when (/t/) { say 'The name has an "t"'; continue }

$_ =~ tr/p/d/;

when (/d/) { say 'The name has an "d"'; continue }

};

5.对列表进行分支判断

在foreach循环中我们也能用when,这和在given中相似,只不过它是依次从列表取测试目标:

use 5.010;

my $count = 0;

foreach (@array) {

when (/[aeiou]$/) { $vowels_count++ }

when (/[^aeiou]$/) { $count++ }

}

say "\@array contains $count words ending in consonants and

$vowel_count words ending in vowels";

6.要点

如果需要写分支语句,请使用given-when结构。

when语句中的智能匹配操作,默认用的是$_变量。

除given外,其他循环结构中也能使用when语句。

条款25 用do {}创建内联子程序

do {}这种语法能把几条语句组合成单条表达式,这有点类似于内联子程序。比如下面这段代码,在读取某个文件所有内容之前,先将表示输入数据分隔符的$/变量本地化(见条款43),然后通过词法文件句柄打开文件(见条款52),读取数据返回后,存入标量变量。所有这一切,都在同一个语句块中完成:

my $file = do {

local $/;

open my ($fh), '<', $filename or die;

<$fh>;

};

因为有了do语句限定代码作用域,其中local和my声明的变量也都随之被限定在内,和外部代码完全不相干。最后一条表达式<$fh>会作为代码块的返回值返回,这和传统的子程序类似,最后将返回值保存到外部变量$file。

下面看看若是不用 do {}写出来的代码会是什么样子。由于代码块中的数据生命期有限,所以为了让计算结果得以传递到外部,我们不得不在最后以完整的赋值语句作为结束。这样加上开始时的变量声明,就得敲上两遍$file,既麻烦又累赘:

my $file;

{

local $/;

open my ($fh), '<', $filename or die;

$file = join '', <$fh>;

}

这种借助if-elsif-else结构返回不同数据的写法,也可以归纳为do {}的形式,不但能省略了大量重复代码,语义也更为清晰。举个例子,根据地区语言的不同,你也许需要选择适合的千位分隔符和小数点表示方式。一般总是先声明这两个分隔符变量,然后根据不同条件分别赋值:

my ( $thousands_sep, $decimal_sep );

if ( $locale eq 'European' ) {

( $thousands_sep, $decimal_sep ) = qw( ., );

}

elsif ( $locale eq 'English' ) {

( $thousands_sep, $decimal_sep ) = qw( , .);

}

这样的代码显得非常笨拙。重复输入的变量,相类似的赋值结构,无形之中模糊了原本的意图,厚重且拖沓。而借助do语句,利用最后求值的表达式即返回值这一特性,合并目标变量,代码的意义就会变得直接明确:

my ( $thousands_sep, $decimal_sep ) = do {

if   ( $locale eq 'European' ) { qw( ., ) }

elsif ( $locale eq 'English' ) { qw( , .) }

};

有时候我们也会借助do来弱化错误处理的相关代码。比如打开一组文件,但不希望在某次打开失败时就立即中止,我们可以在or右端接续一个do语句,在其内部处理发出警告的任务,并随后指示它继续运行:

foreach my $file (@files) {

open my ($fh), '<', $file or do { warn ...; next };

...do stuff ...;

}

要是 do {}块里的内容太长,最好还是把它写成一个单独的子程序。原本就是为了方便而使用的,但一味滥用的话就违背这个习语的初衷了。

do还有一个用途,但比较少见,该特定情形就是在第一次while条件判断之前,需要先运行一次循环内容才能得到数据以作后续条件判断。比如不断提示用户输入,直到特定信息到来时再终止的程序。这种情况下,必须先取得第一次输入,然后才能作条件测试,决定继续提示还是终止,显然,这么写又出现了代码重复:

print "Type 'hello': ";

chomp( my $typed = <STDIN> );

while ( $typed ne 'hello' ) {

print "Type 'hello': ";

chomp( $typed = <STDIN> );

}

通过使用条件后置的while循环,先执行do {},再判断是否需要继续提示用户输入:

my $typed;

do {

print "Type 'hello': ";

chomp( $typed = <STDIN> );

} while ( $typed ne 'hello' );

这里的$typed变量必须声明,虽然还是不够美观,但至少比起之前的写法还是要简洁得多。

要点

do语句块返回最后一个表达式求值的结果。

任何能使用表达式的地方都可以使用do语句块。

可以利用do语句块来限制变量的作用域。

条款26 用List::Util和List::MoreUtils简化列表处理

掌握列表的各项操作,是深入理解Perl的必经之路。事实上,在Higher Order Perl这本书的序言中,Mark Jason Dominus就曾说过,Perl其实更接近Lisp语言,而不是C。此话不假,Perl有内置的map、grep和foreach语句,组合起来能完成许多复杂的列表处理。而有些列表操作极其常见,与其重复发明轮子,还不如直接用List::Util或List::MoreUtils这两个C语言实现的模块提供的函数来得更快。

1.快速查找最大值

查找列表中的最大值,你自己写的话也不算太麻烦:

my @numbers = 0 ..1000;

my $max   = $numbers[0];

foreach (@numbers) {

$max = $_ if $_ > $max;

}

不过纯粹用Perl实现,相对来说性能要差些。而Perl自带的List::Util模块提供了C语言实现的max子程序,可以直接返回列表中的最大值:

use List::Util qw(max);

my $max_number = max( 0 ..1000 );

此外还有一个maxstr子程序,能返回列表中最大的字符串:

use List::Util qw(maxstr);

my $max_string = maxstr( qw( Fido Spot Rover ) );

类似的,对列表中所有数字求和,若由你自己写,就可能写成这样:

my $sum = 0;

foreach ( 1 ..1000 ) {

$sum += $_;

}

而用List::Util提供的sum子程序则要简单快捷得多:

use List::Util qw(sum);

my $sum = sum( 1 ..1000 );

2.列表归并

对一系列数字求和的办法还有一个,就是利用List::Util提供的reduce函数逐项迭代。它也是用C语言实现的,所以执行起来很快,语法也更明晰:

use List::Util qw(reduce);

my $sum = reduce { $a + $b } 1 ..1000;

与 sort 类似,reduce 也以代码块作为参数,不过运行机制稍有不同。每次迭代,它会先从参数列表中取出前两个元素,分别设置别名$a和$b,这样参数列表的长度就会缩短两个元素。然后reduce把语句块返回的计算结果再压回到参数列表的头部。如此往复,直到最后列表里只剩下一个元素,也就是迭代的计算结果$sum。

了解工作原理后,就能灵活实现某些尚未定义成标准函数的计算方式,比如对列表元素逐项累乘:

my $product = reduce { $a * $b } 1 ..1000;

3.判断是否有元素匹配

纯粹用Perl实现的话,找出列表中第一个符合某项条件的元素,比找出所有符合条件的要麻烦一些。比如下面这条语句用于判断是否有大于1000的元素,它写起来倒是很容易:

my $found_a_match = grep { $_ > 1000 } @list;

但若是@list有一亿个元素,而第一个元素就是1001呢?上面的代码照旧会逐项检查每一个元素,而答案其实早就知道了。当然,我们可以自行控制退出循环,但有谁会愿意每次都写上这么一大段代码?

my $found_a_match = 0;

foreach my $elem (@list) {

$found_a_match = $elem if $elem > 1000;

last if $found_a_match;

}

List::Util的first子程序就是为了解决这样的问题而设计的,并且它会告诉你第一个找到的元素是什么。它不会浪费时间做无用功,一旦找到答案,就会立即停止扫描,而不会为了找出那一个大于1000的元素去扫描整个列表:

use List::Util qw(first);

my $found_a_match = first { $_ > 1000 } @list;

此外,在CPAN的List::MoreUtils模块中,也提供了许多额外的实用函数下载:

use List::MoreUtils qw(any all none notall);

my $found_a_match = any   { $_ > 1000 } @list;

my $all_greater  = all   { $_ > 1000 } @list;

my $none_greater = none  { $_ > 1000 } @list;

my $all_greater  = notall { $_ % 2   } @list;

4.一次遍历多个列表

有时候我们手上会有几个相互关联的列表需要同时遍历。最普通的做法是利用数组下标,同步提取对应元素,计算后存入另一个列表。比如下面这段代码,它依次取出数组@a和@b中的元素,求和后存入数组@c相同的位置。每次迭代都要通过数组下标定位,代码比较复杂:

my @a = (...);

my @b = (...);

my @c;

foreach my $i ( 0 ..$#list ) {

my ( $a, $b ) = ( $a[$i], $b[$i] );

push @c, $a + $b;

}

或者,用List::MoreUtils的pairwise子程序,以更漂亮的方式实现同步叠加:

use List::MoreUtils qw(pairwise);

my @c = pairwise { $a + $b } @a, @b;

pairwise 子程序只适用于两个列表的同步计算,对于三个及以上的列表,可选用相近的each_array子程序。虽然写起来好像有些复杂,不过比起纯粹自己实现,已经相当简单了:

use List::MoreUtils qw(each_array);

my $ea = each_array( @a, @b, @c );

my @d;

while ( my ( $a, $b, $c ) = $ea->() ) {

push @d, $a + $b + $c;

}

5.数组合并

合并多个数组的操作虽然也可以自己写,但终究不如用List::MoreUtils 的mesh子程序方便:

use List::MoreUtils qw(mesh);

my @odds = qw/1 3 5 7 9/;

my @evens = qw/2 4 6 8 10/;

my @numbers = mesh @odds, @even; # 返回1 2 3 4 ...

6.其他更多函数

以上只是介绍了List::Util和List::MoreUtils模块提供的几个常用函数,除此之外还有许多函数可以简化列表操作,不光是输入的代码变少了,程序要表达的逻辑也更清晰。至于这些函数的具体用途,还请自行参考相关模块文档。

7.要点

使用List::Utils和List::MoreUtils模块简化日常列表处理。

用List::MoreUtils的all、any、none或者notall函数筛选列表元素。

用pairwise或each_array完成多个列表的同步处理。

条款27 用autodie简化错误处理

Perl有许多内置函数都是系统调用,可能会因为不可控制的原因而失败,所以有必要对最终运行结果作检查。比如尝试打开某个文件,在使用文件句柄前,得先确认打开文件的操作是成功的:

open my ($fh), '<', $file

or die "Could not open $file: $!";

不管怎么说,尽管加上错误处理的代码会让整个程序看起来比较乱,但这些代码是必要的。有一种办法可以避免手工输入这类代码,即用autodie编译指令(从Perl 5.10.1起开始自带,也可以直接从CPAN安装):

use autodie;

open my ($fh), '<', $file;   # 会自动加入die 相关的检查

默认情况下,autodie会对它能起作用的所有函数生效,这包括绝大多数内建的同系统底层交互的函数。如果只是希望对某些特定函数起作用,可以将各个函数的名字或一组函数的组名列出来告诉autodie:

use autodie qw(open close);   # 只对特定函数生效

use autodie qw(:filesys);    # 只对某组函数生效

如果只需要小范围内启用 autodie,也可以把它用在代码块内作为词法编译指令。这和strict编译指令用法类似:

{

use autodie;

open my ($fh), '<', $file; # 会自动加入die相关的检查

}

当然,相对的,也可以全局设置为启用autodie,到个别代码块中再临时关闭:

use autodie;

{

no autodie; # 在这个块中回到普通方式

chdir('/usr/local') or die "Could not change to $dir";

}

在autodie捕获到错误时,它会把$@设置为autodie::exception对象,而$@就是表示eval错误的变量:

use autodie;

eval { open my ($fh), '<', $file };

my $error = $@; # 必须立刻提取$@,以防其他错误覆盖

接下来我们可以通过查询错误对象的信息,探求问题出现的根源。autodie非常聪明,它能把错误类型事先进行归类,以便在后续操作中灵活处理。autodie::exception能感知智能匹配操作符(见条款23)的运算,并以相协调的方式工作,所以我们可以借此写出条理清晰、层次分明的错误处理代码。下面是典型的按照特定顺序检测错误消息或错误类型的代码(具体类型请参考autodie文档):

use 5.010;

use autodie;

eval { open my ($fh), '<', $file };

my $error = $@; # 必须立刻提取$@,以防其他错误覆盖

given ($error) {

when (undef) { say "No error"; }

when ('open') { say "Error from open"; }

when (':io') { say "Non-open, IO error."; }

when (':all') { say "All other autodie errors." }

default    { say "Not an autodie error at all." };

}

如果手头没有 Perl 5.10以上的版本,也就是说无法使用智能匹配操作,那么处理 autodie的错误就难免复杂一些:

# 以下代码来自autodie文档

if ( $error and $error->isa('autodie::exception') ) {

if ( $error->matches('open') ) {

print "Error from open\n";

}

if ( $error->matches(':io') ) {

print "Non-open, IO error.\n";

}

}

elsif ($error) { # 不是autodie抛出的异常

...;

}

要点

可以用autodie自动处理内建函数报出的错误。

autodie起作用的代码范围和函数范围是可以限定的。

用eval语句捕获autodie抛出的异常。

[1].David Till所著的Teach Yourself Perl 5 in 21 Days(Berkeley,CA:Sams Publishing,1996)。

相关图书

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

相关文章

相关课程