设为首页 收藏本站
查看: 1747|回复: 0

[经验分享] InterMediate Perl

[复制链接]

尚未签到

发表于 2015-12-25 14:18:16 | 显示全部楼层 |阅读模式
Chapter 1. 引言
欢迎来到认识 Perl 的下一台阶。 你来到这里的原因可能是你想比 100 行更长的 Perl 程序, 或者你的老板要你来学。
看, 我们的 Learning Perl 写得如此之好, 因为它介绍给你如何用 Perl 写那些小或中型的程序 (which is most of the programming done in Perl, we've observed). 但是, 为了避免 Learning Perl 篇幅太大以至于把初学者吓退, 我们小心谨慎地精简了许多知识。
在后面的学习中, 我们会以 Learning Perl 同样的风格来向你介绍 " 余下的故事 ". 本书将会覆盖写从 100 行到 1 万行的程序所需要的知识。
比如, 你将会学到多个程序员如何在同一项目中一起工作。 这是很重要的, 因为除非你一天工作 35 小时, 否则你很难在没有他们帮助下完成更大的任务。 同样, 在协作环境下, 你要保证你开发的所有的代码在最终的应用程序中能很好的同他人的代码配合。
这本书也展示了如何处理更大和更复杂的数据结构.例如我们可能会提到的所谓一个 散列中的散列, 或 数组中的数组中的散列中的数组 . 只要你掌握了一点关于引用的知识, 你就可以在复杂数据结构中自在遨游了。
另外, 还有一个必须知道的概念,就是什么是面向对象编程, 就是让你部分的代码(特别是可能与其它程序员共用的)能在或多或少的程度上在同一个项目中被重用。 这本书也很好的介绍了这部分内容, 尽管你可能从来没有面向对象的概念。
在团队中工作中的一个重要的方面是,在一个发布周期中进行单元测试和集成测试。 在本书中你会学到把你的代码如何打包成一个发布版本, 并对其在开发环境中进行单元测试或在最终运行环境进行代码验证。
另外, 就像我们在已经出版的 Learning Perl 中保证, 我们会在整个学习过程中用一些有趣的例子和双关语让你的学习旅程充满乐趣。 (尽管我们把 Fred , Barney , Betty 和 wilma 送回家了, 但我们让一些新的明星来担当角色。 )
1. 1. 你应该已经知晓哪些概念?
我们会假定你已经读过 Learning Perl , 或假装你已经读过, 或者你已经玩过 Perl 一段时间, 已经把基础知识拿下。 比如, 这本书里我们不会再解释如何访问一个数组里一个元素或者从一个子程序里返回一个值。
下面这些知识你应该事先掌握:
在你的平台如何运行 Perl
Perl 的三种变量类型:标量、数据和散列
控制结构的语法如: while , if , for 和 foreach
子程序
Perl 的操作符: grep , map , sort 和 print
文件操作的语法:打开、文件操作和 -X (文件检测操作)
我们可能在本书对这些话题进行深入探讨, 不过我们假定你已经掌握这些概念的基础知识。
1. 2. 那些脚注是怎么回事?
同 Learning Perl 这本书的做法一样, 我们把那些深奥的概念移掉了, 并把他们做为脚注。
  • 在你第一次读本书的时候尽可以略过不看, 在重读的时候可以把它们看看。 我们在以后提供给你的材料中不会涉及对这些脚注的理解。
    1. 3. 关于练习?
    动口又动手, 学习就是好。 培训最好的途径就是在半小时或一小时之后的讲课后有一系列的练习。 当然, 如果你阅读速度很快的话, 一章大概在半小时之内就可以看完。 那么, 停一停, 不着急, 做一下练习吧!
    每个练习都有限时。 我们打算取一个平均的解题速度, 但是, 如果你用了更长的时间, 也不必但心。 有时候这不过取决于在工作或学习中面对同样问题的次数。 只要把限时当作指导性的就可以了。
    每个练习在附录都有参考答案。 再提醒一下, 作题前别去看, 看了再作题, 学习效果就差了。
    1. 4. 如果我是讲授 Perl 课程的指导老师?
    如果你是讲授 Perl 课程的指导老师, 并且决定用此书作为课程讲义, 你应该知道, 对于大多数学生来说, 每组练习 45 分钟到一小时就足够完成了, 余下留点时间作课间休息。 一些章节可能快些, 有的章节可能要慢些。 标在方框内的限时是死的, 所以一切按实际情况决定。
    好吧, 开始喽。 翻过此页, 我们的课就马上开始. . . .
    ?
    Chapter 2. 进阶基础
    在开始深入学习这本书之前, 我们要介绍一些中阶 Perl 语言的 " 习惯用语 " ,我们会在整本书里都用到这些 " 习语 ". 对这些 " 习语 " 的掌握程度区分了一个程序员对 Perl 的运用等级是中阶还是初阶. 我们将会在贯穿整本书的例子里向您介绍这些 " 演员 ".
    2. 1. 列表操作符
    你可能已经知道 Perl 的一些列表操作符, 但并没有想过他们是怎么同列表一起工作的.最常用的列表操作符应该是 print 了。 我们给它一些参数, 然后它把他们合在一起显示出来。
    print 'Two castaways are ', 'Gilligan', ' and ', 'Skipper', "\n";
    在 Learning Perl 这本书里, 你可能知道了另外一些列表操作符。 如 sort 操作符将输入的列表按顺序列出。 在 Gilligan's Island 的主题歌中的那些求生者没有按字母次序出场, sort 可以为我们修正这一点。
    my @castaways = sort qw(Gilligan Skipper Ginger Professor Mary-Ann);
    reverse 操作符返回反向排序的列表。
    my @castaways = reverse qw(Gilligan Skipper Ginger Professor Mary-Ann);
    Perl 还有其它与列表打交道的操作符.而且一旦你使用他们, 你会发现这些语句会使你表达得更清楚, 写更少的代码。
    2. 1. 1. 用 grep 操作符来过滤列表
    grep 操作符取一个列表和一个"测试表达式 ". 它一个一个地从列表中把元素取出来放到 $_ 变量中, 并在标量环境中, 用"测试表达式"来检验这个值.如果检验出来是个"真"值, grep 会把 $_ 变量送到输出列表中。
    my @lunch_choices = grep &is_edible($_), @gilligans_posessions.
    在一个列表上下文中, grep 操作符会返回所有被选出元素的列表.而在一个标量上下文中, grep 返回被选出元素的个数。
    my @results = grep EXPR, @input_list;
    my $count = grep EXPR, @input_list;
    在下面的例子中, EXPR 代表一个返回标量的表达式, 它引用 $_ 变量(显式或隐式的).比如找出大于 10 的数, EXPR 表达式来处理 $_ 是否大于 10.
    my @input_numbers = (1, 2, 4, 8, 16, 32, 64);
    my @bigger_than_10 = grep $_ > 10, @input_numbers;
    输出结果当然是: 16 , 32 和 64. 上例中显式地引用了变量 $_. 下面有个隐式引用变量 $_ 的例子, 在正则表达式中隐式引用了变量 $_:
    my @end_in_4 = grep /4$/, @input_numbers;
    现在我们得到的输出是4和 64.
    当 grep 工作的时候, 它是从 $_ 变量中把值"借"出来用.就是说 grep 把 $_ 的值"借"过来用一下, 用完后再把原值放回 $_ 变量中.所以 $_ 变量仅仅是拷贝一下值而己.是实际数据元素的一个别名.就像 foreach loop 中的控制变量。
    如果检验表达式太复杂, 我们可以把检验代码隐藏到一个子例程里去:
    my @odd_digit_sum = grep digit_sum_is_odd($_), @input_numbers;

    sub digit_sum_is_odd {
    my $input = shift;
    my @digits = split //, $input; # Assume no nondigit characters
    my $sum;
    $sum += $_ for @digits;
    return $sum % 2;
    }
    对于这个例子, 我们得到的输出是1, 16 和 32. 程序把这些数据的数位加起来后, 因为最后一行返回的余数都是 "1" , 所以返回值为"真 ".
    所以, grep 语法有两种形式:前面秀给你们看的表达式形式和下面要展示给你们看的代码块形式.因为代码只用一次, 我们现在不把代码放到子例程中, 而是以代码块的形式直接放在 grep 语法里, 所谓的块形式:


  • 在 grep 的块形式中, 代码块和输入数组中间是没有逗号的.而在 grep 的表达式形式中, 表达式和输入数组中间必须要有一个逗号.区别如下:
    my @results = grep {
    block;
    of;
    code;
    } @input_list;

    my $count = grep {
    block;
    of;
    code;
    } @input_list;
    同 grep 的表达式形式一样, grep 临时把输入数组中每个元素放到 $_ 中去, 然后, 它用代码块来处理这个值.代码块里最后一个表达式来检验值.就像所有的测试表达式一样, 在标量上下文来检验值.因为是完整的块, 所以我们可以在其中用以块为范围的变量.我们来用块形式重写上面的例子::
    my @odd_digit_sum = grep {
    my $input = $_;
    my @digits = split //, $input; # Assume no nondigit characters
    my $sum;
    $sum += $_ for @digits;
    $sum % 2;
    } @input_numbers;
    注意与用子例程的方法有两个地方的变化:输入值是通过变量 $_ , 而不是输入参数列表, 而且在代码块形式中我们去掉了 return 关键字.实际上如果我们保留 return 的话是会出错的, 因为我们不是在用一个子例程, 仅仅是一个代码块 .[ *]当然, 这个例子我们还可以优化一下, 去掉中间变量:
  • 保留 return 的结果会导致 Perl 从包含这个代码块的子例程中退出.当然, 我们中有些人在最初编程的时候就深受其苦。
    my @odd_digit_sum = grep {
    my $sum;
    $sum += $_ for split //;
    $sum % 2;
    } @input_numbers;
    如果显式使用中间变量能使代码让你和你的同事更易理解和维护代码的话, 尽管用它.好代码才是主要的。
    2. 1. 2. 用 map 作列表的转换
    map 操作符的语法同 grep 操作符非常相像, 他们有相同的操作步骤.例如它们都是把输入列表中的元素临时地放到 $_ 变量中去, 而且他们的语法中都有表达式形式和代码块形式。
    然而, grep 中的测试表达式在 map 中变成了映射表达式 .map 操作符在列表环境中为表达式求值(而不是像 grep 那样在标量环境下求值).每次表达式求值都成为整个输出结果的一部分.为各个元素求值结果连在一起成为完整全部的输出.在标量环境下, map 返回在输入列表里多少个元素被处理.但是 map 应该总是用在列表环境下, 很少用在标量环境下。
    让我们开始一个简单的实例::
    my @input_numbers = (1, 2, 4, 8, 16, 32, 64);
    my @result = map $_ + 100, @input_numbers;
    七个项目依次映射为 $_ , 我们得到了一个结果:比 100 这个输入大的数字。所以 @result 的结果是: 101 , 102 , 104 , 108 , 116 , 132 和 164
    但是我们没有限定每个输入对应一个输出,让我们看看当一个输入对应两个输出的时候发生了什么。
    my @result = map { $_, 3 * $_ } @input_numbers;
    现在对应每个输入有两个输出了:1, 3, 2, 6, 4, 12 , 8, 24 , 16 , 48 , 32 , 96 , 64 和 192. 我们可以这些对数字保存为散列来保存一个数字对应三倍与它的数值对。
    my %hash = @result;
    或者,不使用中间数组直接使用 map 生成结果。
    my %hash = map { $_, 3 * $_ } @input_numbers;
    你可以看到 map 功能强大,我们可以将一个列表的每个元素映射为多个元素。事实上我们很少一对一的生成列表的输出。让我们看看拆分一个数字后发生了什么。
    my @result = map { split //, $_ } @input_numbers;
    大括号内的代码将数字拆分成单个的数字。对于 1, 2, 4 和 8,我们得到了一对一的结果。对于 16 , 32 和 64 , 我们得到了一对二的结果。当我们使用 map 连接这些结果,我们最后得到 1, 2, 4, 8, 1, 6, 3, 2, 6, 和 4.
    如果我们特别用一个空列表传入, map 将空列表变成更大的列表,列表依然是空的。我们利用这个特性选择并且剔除一些项目。例如,我们只想拆分后以 4 结尾的结果:
    my @result = map {
    my @digits = split //, $_;
    if ($digits[-1] = = 4) {
    @digits;
    } else {
    ( );
    }
    } @input_numbers;
    如果最后的数字是 4,我们原样返回 @digits (列表环境 ). 如果最后的数字不是 4,我们返回一个空列表,这样就剔除了指定的数值。这样,我们使用 map 达到了 grep 的效果。但反过来就不行了。
    当然,所有我们能用 map 和 grep 做的事情,都可以用 foreach 循环来做。但是话又说回来,我们也可以用汇编或者 toggling bits 来写代码。
  • 重点是正确的应用 grep 和 map 能降低程序的复杂程度。让我们可以更关注更高级的问题而不是拘泥于细节。
    2. 2. 用 eval 捕捉错误
    有一些代码看上去很平常, 但是却含有潜在的危险, 一旦某种条件不对就会使程序中断, 过早地结束程序。
    my $average = $total / $count; # divide by zero?
    print "okay\n" unless /$match/; # illegal pattern?

    open MINNOW, '>ship. txt'
    or die "Can't create 'ship. txt': $!"; # user-defined die?

    &implement($_) foreach @rescue_scheme; # die inside sub?
    可是, 不能因为代码的某一片断出错而使我们的整个程序崩溃。 Perl 用 eval 操作符来实现捕捉错误的机制。.
    eval { $average = $total / $count } ;
    如果在 eval 块里代码发生错误, 系统会退出这个块。 但是, 尽管退出块, Perl 会继续执行 eval 块之外的代码。 我们在 eval 块的后面一般做法是检查一下 $@ 变量, 这个变量要么是空(表示没有出错)或者代码出错时系统返回的"遗言", 多半是"除零错误"之类云云。.
    eval { $average = $total / $count } ;
    print "Continuing after error: $@" if $@;

    eval { &rescue_scheme_42 } ;
    print "Continuing after error: $@" if $@;
    eval 块的结束时的分号是必须的, 因为不像 if 或者 while 那样的控制结构, eval 实际上是个函数。 但是代码块是真的块, 所以可以包括词法变量( my 修饰的变量)和另外其它的断言语句。 因为是个函数, eval 有像子例程那样的返回值(最后一个表达式的求值结果, 或者由 return 语句返回的结果)。 当然, 如果代码块失败则没有值返回;如果在标量环境将返回未定义值, 在列表环境将返回一个空的列表。 因此, 比较安全的求平均值的代码的写法如下:
    my $average = eval { $total / $count } ;
    现在, 根据这个操作执行的成败, 变量 $average 要么是两数之商要么是个未定义值。
    Perl 也支持 eval 代码块的嵌套.只要代码执行, eval 块总能捕捉错误, 所以它能捕捉嵌套子例程里发生的错误.尽管如此, eval 不能捕捉到非常严重的错误, 这些错误发生时 Perl 自己会中断执行.这些错误包括无法捕捉的信号, 内存溢出或其它的灾难 .eval 同样不能捕捉到语法错误;因为 Perl 在编译 eval 及其它代码的时候检查语法错误, 而不是在运行时.还有, eval 也不能捕捉警告.(但是 Perl 的确提供一个方法来捕捉警告信息;可以查阅一下变量 $SIG{__WARN__}).
    2. 3. 用 eval 动态编译代码
    eval 有另外一种用法, 其参数是作为一个字串表达式, 而不是代码块。 在运行时, 它将字串临时编译成代码并且执行.这很易用, 但也很危险, 因为有可能会把具有危害性的代码放到字串里。 除了极少数值得一提的例外情况, 我们建议你尽量避免这种用法。 稍后我们会用这种用法, 然后我们就不用这种用法了, 我们只是展示它是怎么工作的。
    eval '$sum = 2 + 2';
    print "The sum is $sum\n";
    Perl 在词法环境中执行那段代码, 这意味着我们好像在执行的时候输入这些代码的一样。 eval 的结果就是最后一个表达式求值的值, 所以我们不必在 eval 中输入整个语句。
    #!/usr/bin/perl

    foreach my $operator ( qw(+ - * /) ) {
    my $result = eval "2 $operator 2";
    print "2 $operator 2 is $result\n";
    }
    上例中, 我们依次把 + - * / 四个操作符用到我们的 eval 代码块中。 在给 eval 的字串中, 我们内插了变量 $operator 的值。 eval 执行并返回了我们给出字串的值, 并且放到变量 $result 中。
    如果 eval 不能正确编译和执行我们传给它的 Perl 代码, 它就会像代码块形式的结果一样设置变量 $@ 一个值。 在下例中, 我们想要捕捉任何除零错误, 但我们遇到了另外一种错误 -- 分母缺失。
    print 'The quotient is ', eval '5 /', "\n";
    warn $@ if $@;
    eval 捕捉语法错误, 然后在我们调用 eval 后, 在检查 $@ 值时, 把出错信息放到变量 $@ 中。
    The quotient is
    syntax error at (eval 1) line 2, at EOF
    在稍后的第 10 、 17 和 18 章中, 我们会用这种办法来调用可选模块。 如果我们调用模块失败, Perl 会中止程序。 而我们会捕捉到这种错误, 并由我们自己以我们的办法恢复程序的运行。
    万一你还没有注意我们的警告, 我们在这里重申:要非常小心这种形式的 eval 用法。 如果你有其它的方法来替代 eval 用法, 那就试试其它方法。 我们在后来第 10 章会用到这种办法从外部文件中载入代码, 但我们同样会向你展示出更好的替代方案。
    2. 4. 习题
    答案在附录中的"第二章的答案"中;
    2. 4. 1. 习题 1 [15 分钟]
    写一个程序从命令行取一个文件清单, 然后用 grep 把那些文件大小在 1000 字节以内的文件找出来。 用 map 把这个清单里的每个字串前加四个空格并在字串后面加上换行符。 然后输出列表结果。
    2. 4. 2. 练习 2 [25 分钟]
    写一个程序要求用户输入一个正则表达式的模板。 不要以命令行参数形式输入, 要从键盘读取。 然后从一些目录中(可以是硬编码的, 如: "/etc" 或 'C:\\Windows' )中读取符合模板的文件名。 重复这个操作直到用户输入空串。 要屏蔽用户输入的正斜杠 ("/") , 因为那是 Perl 里正则表达式的分隔符;输入的模板用换行符为分隔。 要保证不会因为用户输入了一个错误的正则表达式, 如括号不匹配之类的, 而导致程序崩溃。
    Chapter 3. 使用模块
    模块是建造我们程序的代码块。 他提供了可用的子程序,变量或者是面向对象的类。 用我们的方式建造我们自己的模块,我们将向你展示一些你也许感兴趣的地方。 我们也将讲讲一些如何使用别人已经写好的模块的基本常识。
    3. 1. 标准发行版
    Perl 发行版已经自带了最受欢迎的模块。 实际上, 最近的发行版中有超过 50 兆模块。 于 1996 年十月发布的 Perl 5.003_07 有 98 个模块.现如今, 2006 年的年初发行的 Perl 5.8.8 有 359 个模块。
  • 这实在是 Perl 的优点之一:许多有用且复杂的程序用不着你动手, 发行版里已经给你带来了。
  • 在读完本书之后, 你就能用 Module::CoreList 模块来自己计算一下共有多少模块。 毕竟, 我们就是用这种方法来得到模块总数。
    在这本书里, 我们会向你标出哪些模块是 Perl 自带的(而且在大多数情况下, 会说明是从哪个版本开始收入 Perl 发行版中的)。 我们把它们称为"核心模块", 或者标注它们是在"标准发行版"中的.如果你安装了 Perl , 那你就可以用这些模块。 因为我们写这本书的时候是用的 Perl 5.8.7 版, 所以我们也假定这是 Perl 的当前版本。
    当你在开发你的程序的时候, 你可能要考虑是否你应该仅仅用核心模块.这样的话你就能保证任何用 Perl 的人都能执行你的代码, 只要他们的 Perl 版本同你相同。 [] 这里我们也不多费口舌了, 主要是因为我们太喜欢 CPAN 了, 不用它就舍不得。
    3. 2. 使用模块
    几乎所有的 Perl 模块都带有文档说明.所以尽管我们可能不知道那些模块背后的戏法是怎么变的, 如果我们知道如何使用接口, 我们就不必去担心那些细节。 这就是在这里介绍接口的原因, 毕竟:它屏蔽了复杂性。
    在我们的本机当中, 我们可以用 perldoc 命令来调出模块文档。 我们输入我们要查的模块的名字, 然后 perldoc 打印出文档内容:
    $ perldoc File::Basename

    NAME
    fileparse - split a pathname into pieces

    basename - extract just the filename from a path

    dirname - extract just the directory from a path

    SYNOPSIS
    use File::Basename;

    ($name, $path, $suffix) = fileparse($fullname, @suffixlist)
    fileparse_set_fstype($os_string);
    $basename = basename($fullname, @suffixlist);
    $dirname = dirname($fullname);
    我们在这里列出了文档的一般结构(至少是最重要的部分)。 模块文档是按 Unix 旧文档格式组织的, 以 NAME 和 SYNOPSIS 开始.
    SYNOPSIS 节给我们关于这个模块的用法的例子, 这样我们就可以稍微理解了用法就可以使用这个模块。 就是说, 这可以使你在还没有熟悉 Perl 技术和语法的情况下, 仅仅看这些例子, 就可以使程序工作起来。
    如今, 因为 Perl 成了一个过程的、函数的、面向对象和其它各种语言类型的混合体, Perl 模块文档开始有不同的接口。 我们会在不同的模块使用稍微不同风格的文档, 但是只要我们可以查文档, 我们就不会有问题。
    3. 3. 函数接口
    为了调用一个模块, 我们可以用 Perl 内置的 use 语句。 这里我们不打算更深入的了解细节问题, 我们会在第 10 章和第 15 章来说这个问题。 目前, 我们只要能调用模块就可以了。 我们就发行版的核心模块中的 File::Basename 模块开始说吧.要把它调入我们的脚本, 我们用:
    use File::Basename;
    当我们写上如上的代码后, File::Basename 向你的脚本引入了三个子例程: fileparse , basename 和 dirname. [] 自此之后, 我们就可以用如下语句了:
  • 以及实用例程, fileparse_set_fstype.
    [] 事实上他们被它引入当前的包, 只不过我们没有告诉你这些而已。
    my $basename = basename( $some_full_path );
    my $dirname  = dirname(  $some_full_path );
    就象我们曾经在我们自己代码里写过 basename 和 dirname 这两个子例程一样, 或者他们就像是 Perl 的内置函数似的。 这些例程的功能是从一个路径名中抽出文件名和目录名。 比如, 如果变量 $some_full_path 的内容是 D:\Projects\Island Rescue\plan7. rtf( 我们假定是在 Windows 环境下), 那么 $basename 的内容将会是 plan7.rtf 而 $dirname 的内容将会是 D:\Projects\Island Rescue.
    File::Basename 会自己"感知"它所处的是哪种操作系统, 并且因此针对所遇到的操作系统, 用不同的分隔符来解析字串。
    然而, 假定我们曾经在程序里写过一个同名的 dirname 函数的话, 那么 File::Basename 提供的同名函数会把你的覆盖! 如果我们打开 warnings 报警, 我们会看到一条警告信息;否则的话 Perl 不会关心这种情况。
    3. 4. 选择性地引入函数
    很幸运, 我们可以告诉 use 操作符, 通过只导入需要的子例程来限制它的行为.称为"函数导入清单", 如:
    use File::Basename ('fileparse', 'basename');
    这样的话, 模块只会将两个例程导入我们的程序, 让我们自己写的 dirname 留在程序中。 当然, 上述的写法输入起来太麻烦, 所以一般我们会看如下用引用操作符的写法:
    use File::Basename qw( fileparse basename );
    实际上, 即便只有一个参数, 我们为了维护起来的一致性, 也倾向于用 qw() 这样的形式。; 因为我们往往过后再回来找到这段代码说:"得在这里再加个参数", 如果我们一开始用 qw() 来写的话, 维护起来会更简单。
    这样我们当然保护了本地的 dirname 例程, 但是, 如果我们想用 File::Basename 模块的 dirname 提供的功能怎么办?没问题!我们只要打出这个例程的全名就可以了:
    my $dirname = File::Basename::dirname($some_path);
    use 关键字后面的名字列表并不会使模块里(在这个例子中是 File::Basename) 的子例程的定义有任何改变.我们可以忽略导入清单, 直接用全名, 像下面一样:

  • 你不必在这些调用的子例程的前面加"&"符号, 因为编译器已经知道子例程的名字了。
    my $basename = File::Basename::basename($some_path);
    在一种极端的情况(但也极端有用), 我们可能为导入列表指定一个空列表, 就像下面一样:
    use File::Basename (  );              # no import
    my $base = File::Basename::basename($some_path);
    空列表和没有列表的概念是不一样的。 空列表的意思是说"不要导入任何子例程", 而没有列表的意思是说:"请导入缺省的子例程 ".. " 如果模块的作者干得出色的话, 他缺省导出的例程正是你想要的。
    3. 5. 面向对象的接口
    相比于 File::Basename 导出的子例程, 在核心模块中有另外一个 File::Spec 模块也提供类似的功能。 File::Spec 被设计来支持对文件属性的一般操作。 (一个文件属性指文件或目录的名字, 但它可能并不是实际存在的名子, 是这样吗?)
    与 File::Basename 模块不同的是, File::Spec 模块接口是被设计成面向对象的.我们也用 use 来调入模块, 象往常一样:
    use File::Spec;
    然而, 因为这个模块有面向对象的接口,[] 它并不导入任何子例程。 取而代之的是, 接口要我们通过访问类的方法来使用其功能。 如 catfile 方法用来把一个字串列表用适当的目录分隔符连接起来:
    [] 如果我们想要专门的接口的话, 可以用 use File::Spec::Functions 的办法。
    my $filespec = File::Spec->catfile( $homedir{gilligan},  
    'web_docs', 'photos', 'USS_Minnow. gif' );
    上例就是调用了 File::Spec 类中的一个叫 catfile 的类方法.这个方法使用本地操作系统的目录分隔符建立合适的路径字患并返回单个字串。 [] 对于 File::Spec 提供的其它方法, 调用的语法都是相似的。
    [] 返回的的字段结果, 如果在 UNIX 系统, 那么多半是: /home/gilligan/web_docs/photos/USS_Minnow gif. 如果在 windows 系统里, 就会用代表目录分隔符的反斜杠。 这个模块让我们可以写出可移植的代码, 至少在文件描述上是这样的。
    File::Spec 模块还提供了许多其它的方法来用可移植的方式处理路径。 你可以通过 perlport 文档了解更多有关移植方面的专题。
    3. 6. 一个更典型的面向对象模块: Math::BigInt
    不要因为 File::Spec 模块没有任何对象, 所以看上去比较像"非面对象的"的模块而失望.让我们看一下另外一个核心模块, Math::BigInt , 它用来处理超出 Perl 内置精度的整数值 .[ *]

  • 在幕后, Perl 实际上要被其宿主的操作系统架构所限制。 这是少数硬件环境限制之一。
    use Math::BigInt;

    my $value = Math::BigInt->new(2); # start with 2

    $value->bpow(1000);               # take 2**1000

    print $value->bstr(  ), "\n";     # print it out
    如前所述, 这个模块没有导入任何东西。 其全部的接口使用对象的方法, 如用 new 跟在类名之后, 来建立实例.然后调用实例的方法, 如跟在实例名字后的 bpow 和 bstr.
    3. 7. CPAN 模块仓库
    CPAN 是众多志愿者协同工作的产物.志愿者中的许多人用他们自己的 FTP 站点来维持前台的 CPAN Web 页面。 直到 1993 年底, 他们还是用 perl-packrats 邮件列表来协调他们的工作.之后, 因为磁盘空间越来越便宜, 所以相同信息可以在所有的站点复制, 而不必放在专门的站点上。 这种想法酝酿了一年左右, 以 Jarkko Hietaniemi (芬兰人, 详见: http://users.tkk.fi/jhi/jarkko.html. 中文名:沙雅可, 日文名:奴稗谷笑)在芬兰建立的 FTP 站点为母站, 其它的镜相站点可以以此来进行及时的更新。
    这个站点的一部份工作是重新编排和组织分离的 Perl 文档.建立起放置为非 UNIX 系统的二进制文件、脚本、和 Perl 源代码的空间。 然而, CPAN 当然最关心的是占空间大部份的 Perl 模块部分。
    按模块的功能编目, CPAN 把模块用符号连接组织起来, 指向他们的作者目录--实际文件所在的地方。 模块还包含以易于 Perl 分析的格式索引, 如 Data::Dumper 这样的输出来丰富模块索引的内容。 自然啦, 这一切编目索引都是有主服务器的 Perl 程序自动从数据库生成的。 一般来说, CPAN 中从一个服务器同步到另一个服务器的工作是由 mirror.pl 这个古老的 Perl 程序完成的。
    从屈指可数的几台镜相服务器开始, CPAN 如今已经成长为超过 200 公共服务器, 至少每天(有时是每小时)刷新一次的网络。 无论你在世界的哪头, 我们总是可以找到最近的 CPAN 镜相站。
    CPAN Search (http://search.cpan.org) 的难以置信的易用性, 一定会成为你最喜欢的搜寻界面。 从那个网页, 你可以搜寻模块、看它的文档、浏览它有哪些版本、查询他们的 CPAN 测试者的报告以及许多其它事情。
    3. 8. 安装从 CPAN 下载的模块
    安装从 CPAN 获得的简单模块可以很直接:先从 CPAN 下载发布的文档, 解压到一个目录。 下例中我们用 wget 下载文档, 当然, 你可以用你习惯的工具下载。
    $ wget http://www. cpan. org/. . . /HTTP-Cookies-Safari-1. 10. tar. gz
    $ tar -xzf HTTP-Cookies-Safari-1. 10. tar. gz
    $ cd HTTP-Cookies-Safari-1. 10s
    然后我们可以以两种办法安装(我们将会在第 16 章介绍). 如果我们找到一个叫 makefile.pl 的文件, 我们可以运行如下的命令来编译, 测试和最终安装源码:
    $ perl Makefile. PL
    $ make
    $ make test
    $ make install
    如果你因为没有权限而不能在系统级的目录里建立目录,
  • 我们可以用 PREFIX 参数告诉 Perl 安装在你另外指定的路径:
  • 这些目录由管理员安装 Perl 时建立, 我们可以用 perl -V 看到是哪些目录。
    $ perl Makefile. PL PREFIX=/Users/home/Ginger
    为了让 Perl 在目录中查找到模块, 我们可以设置 PERL5LIB 环境变量。 Perl 会把这些目录加到模块搜寻清单里去。
    $ export PERL5LIB=/Users/home/Ginger
    我们也可以用 lib 编译提示符来加模块搜寻路径, 尽管这并不友好--因为这不仅要修改代码, 而且在其它的机器上不一定要相同的目录。
    #!/usr/bin/perl
    use lib qw(/Users/home/Ginger);
    不过, 等一下, 如果我们找到了 Build.PL 文件, 而不是 Makefile.PL , 那我们可以用它, 过程是一样的。 这种发布用了 Module::Build 模块来建立和安装包.因为 Module::Build 并非 Perl 的核心模块(至少现在还不是), 所以我们使用时先要安装一下。
  • 尽管它可能成为 Perl 5.10 的一部分.([的确成为 Perl 5.10 的一部分: http://perldoc.perl.org/Module/Build.html ])
    $ perl Build. PL
    $ perl Build
    $ perl Build test
    $ perl Build install
    如果要把 Module::Build 安装在你自己的目录, 我们可以加上 install_base 安装参数.就像我们以前安装 Perl 时用的参数:
    $ perl Build. PL --install_base /Users/home/Ginger
    不过有时候我们在发布的安装包里看到有 Makefile.PL 也有 Build.PL. 我们该用哪一个呢? 都可以。 请便。
    3. 9. 适时地打开路径
    Perl 会从一个专门的 Perl 数组 :@INC 中包含的目录条目中查找程序调用的模块 .use 语句会在编译时执行, 所以它会在编译时在 @INC 数组所包含的路径中查找模块.所以, 除非我们把 @INC 的因素考虑进去, 否则我们就很难理解有时我们的程序会莫明其妙地中断。
    举个例子, 假定我们有个目录 /home/gilligan/lib , 并且把模块 Navigation::SeatOfPants 放到这个目录下面的 Navigation 目录中的 SeatOfPants.pm 文件中.但 Perl 在用如下语句调用我们的模块时是不会找到它的。
    use Navigation::SeatOfPants;
    Perl 会向我们报怨在 @INC 中找不到需要的模块, 并且向我们展示数组中包含的所有目录。
    Can't locate Navigation/SeatofPants. pm in @INC (@INC contains:. . . )
    我们可能会想在调用模块之前, 把路径加进 @INC 不就成了?然而, 当我们加入如下语句:
    unshift @INC, '/home/gilligan/lib';   # broken
    use Navigation::SeatOfPants;
    这样做不对, 为什么?因为 unshift 是在运行时发生的, 远在 use 来调用模块的编译时之后.两条语句虽然在词法上紧挨着但并不表示在编辑时间上靠近.仅仅因为次序上一句挨着另一句并不意味着执行上也按相同的次序.我们得让 @INC 在 use 语句执行之前改变.一种解决办法是加上 BEGIN 块:
    BEGIN { unshift @INC, '/home/gilligan/lib'; }
    use Navigation::SeatOfPants;
    这下 BEGIN 块会在编译时被执行, 在用 use 调用模块之前设置好适当的路径。
    不过, 这样做看上去很烦琐, 不容易去解释, 特别是以后向那些维护你代码的同事去解释.这样, 我们用一个简洁的编译提示来换掉原来用的那些乱七八糟的东西。
    use lib '/home/gilligan/lib';
    use Navigation::SeatOfPants;
    这样, lib 编译提示取一个或多个参数, 并且把他们加入数组 @INC 开头, 就像前面所说的用 unshift 的效果一样。
  • 之所以有效是因为它在编译期间执行而不是在运行时执行.接下来正是时候立即用 use 了。
    因为 use lib 编译提示总是包含站点相关的路径名, 所以一般来说我们推荐你把它写在文件的开头。 这样当你需要为新系统移动文件, 或库目录名字变化时比较容易更新。 (当然, 还有一种办法, 我们压根去掉 use lib 编译提示, 如果我们可以把我们的模块直接安装在 @INC 包括的标准路径下, 但这不是总是可行的。 )
    要注意到: use lib 不是指"用这个库", 而是指"用这个路径可以找到我的库(以及模块) ." 很多情况下, 我们会看到代码被写成这样:
    use lib '/home/gilligan/lib/Navigation/SeatOfPants. pm'; # WRONG
    这样程序员会迷惑为什么没有把定义加进去.还要注意 use lib 实际上是在编译时执行的, 所以如下代码同样不能工作:
    my $LIB_DIR = '/home/gilligan/lib'; . . .  
    use lib $LIB_DIR;     # BROKEN
    use Navigation::SeatOfPants;
    当然, Perl 声明 $LIB_DIR 变量的确是在编译期(所以我们用 use strict 也不会收到出错信息, 尽管实际 use lib 时会报错), 但给变量赋上 '/home/gilligan/lib' 这个值却直到运行时才发生, 真是的, 又晚了一步!
    这个时候, 你就需要把它放在 BEGIN 块中, 或依赖另一个编译期操作:设置一个常量:
    use constant LIB_DIR => '/home/gilligan/lib'; . . .  
    use lib LIB_DIR;
    use Navigation::SeatOfPants;
    好, 又解决问题了.就是说, 直到我们需要的库取决于计算的结果 .( 要到哪里算是个头啊?停下来吧!)我们 99 %的需求可以满足了。
    3. 9. 1. 处理模块依赖
    我们刚才看到如果我们要安装一个模块, 并且这个模块要引用 Module::Build 模块的话, 我们要事先装好 Module::Build 模块.这就是个稍稍让人头痛的有关一般模块依赖性的例子.那我们的 castaways 岛的所有的椰子应该如何处理呢?我们要安装另一些模块, 而这些模块各自又依赖更多的其它不同的模块。
    幸而, 我们有工具来助一臂之力.自从 Perl 5.004 版开始, CPAN.pm 模块成为核心发布的一部份.它给我们提供了一个交互式的模块安装环境。
    $ perl -MCPAN -e shell
    cpan shell -- CPAN exploration and modules installation (v1. 7601)
    ReadLine support available (try 'install Bundle::CPAN')

    cpan>
    要装一个模块和它所依赖的模块, 我们只要发出一个带模块名字的安装命令即可。 如此, CPAN.pm 会处理所有下载、解包、编译、测试以及安装模块的工作, 并且它会递归处理所有的依赖关系。
    cpan> install CGI::Prototype
    如果觉得用上面的方法还烦琐, brian 写了个 cpan 脚本放在 Perl 的发行包里.我们只要简单的列出要安装的模块, 由脚本来处理余下的事情。
    $ cpan CGI::Prototype HTTP::Cookies::Safari Test::Pod
    还有一个工具: CPANPLUS , 是对 CPAN.pm 完全的重写.但它不是 Perl 核心包的一部份, 如下:
    $ perl -MCPANPLUS -e shell
    CPANPLUS::Shell::Default -- CPAN exploration and modules installation (v0. 03)
    *** Please report bugs to <cpanplus-bugs@lists. sourceforge. net>.  
    *** Using CPANPLUS::Backend v0. 049.  
    *** ReadLine support available (try 'i Term::ReadLine::Perl').  

    CPAN Terminal>
    我们用i命令来安装模块:
    CPAN Terminal> i CGI::Prototype
    CPANPLUS 模块同样有一个方便的脚本, 叫做 cpanp. 执行起来用i开关并列出要安装的模块列表, 像如下那样:
    $ cpanp i CGI::Prototype HTTP::Cookies::Safari Test::Pod
    3. 10. 习题
    在附录找答案。
    3. 10. 1. 练习 1 [25 分钟]
    读当前目录的文件列表并转换成全路径.不能用 shell 命令或外部程序读当前目录 .Perl 的 File::Spec 和 Cwd 两个模块对这个程序有帮助.每个路径条目前加四个空格并每个条目开个新行, 就像第二章的练习一做的那样.你可以重用原来的程序吗?
    3. 10. 2. 练习 2 [35 分钟]
    分析一下这本书的国际标准书号( 0596102062 ).从 CPAN 里安装一下 Business::ISBN 模块, 并且用它来从 ISBN 数字中抽取国家代码和发行商代码。
    Chapter 4. 介绍引用
    引用是复杂数据结构、面向对象编程和令人眩目的子例程魔术的基础。 Perl 版本4和版本5加入的一些功能使这些魔术成为可能。
    一个 Perl 标量变量保存一个单个值。 一个数组保存一个或多个标量的次序列表。 一个散列保存一个标量作为键值, 另一个标量作为值。 尽管一个标量可以是任意字串, 可以被复杂数据结构用来编入一个数组或一个散列, 但是三种数据类型中没有一个适合用来做复杂数据关系。 这就是引用的工作。 我们由一个例子来探查一下引用的重要性。
    4. 1. 用多个数组来完成一个简单任务
    在 Minnow 开始一个旅程之前(比如一个三小时的远足), 我们应该事先检查一下每个乘客和乘务人员的行李, 保证他们带了旅行所需要的东西.比如说吧, 水上安全救生装备.在 Minnow 船上的每个乘客要生命维持系统, 太阳镜和水瓶以及雨衣。 我们来写段代码来检查船长的装备。
    my @required = qw(preserver sunscreen water_bottle jacket);
    my @skipper  = qw(blue_shirt hat jacket preserver sunscreen);

    for my $item (@required) {
    unless (grep $item eq $_, @skipper) { # not found in list?
    print "skipper is missing $item. \n";
    }
    }
    grep 在标量环境下返回表达式 $item eq $_ 为真时的元素的个数, 如果在列表里就是1否则是 0.[ *]如果值是0, 则为 false , 我们打印出消息。
  • 如果列表很大, 我们有更有效率的办法.但是对于这样的小 case , 现在了了数行的办法更简便。
    当然, 如果我们想查一个 Gilligan 和教授的, 我们可能要写如下的代码:
    my @gilligan = qw(red_shirt hat lucky_socks water_bottle);
    for my $item (@required) {
    unless (grep $item eq $_, @gilligan) { # not found in list?
    print "gilligan is missing $item. \n";
    }
    }

    my @professor = qw(sunscreen water_bottle slide_rule batteries radio);
    for my $item (@required) {
    unless (grep $item eq $_, @professor) { # not found in list?
    print "professor is missing $item. \n";
    }
    }
    你可能开始注意到有些重复代码, 开始想法把它重构一下, 整合到一个通用的子例程里以便重用(你做得对!):
    sub check_required_items {
    my $who = shift;
    my @required = qw(preserver sunscreen water_bottle jacket);
    for my $item (@required) {
    unless (grep $item eq $_, @_) { # not found in list?
    print "$who is missing $item. \n";
    }
    }
    }

    my @gilligan = qw(red_shirt hat lucky_socks water_bottle);
    check_required_items('gilligan', @gilligan);
    一开始, Perl 给子例程五个元素:一个 gilligan 名字以及另外属于数组 Gilligan 的四个元素 .shift 操作之后, @_ 仅包括四个元素, 因此, grep 用每个出海必备装备来核对这个四个元素的列表。
    到目前为止进展顺利.我们可以检查船长和教授的装备, 只用如下一点代码:
    my @skipper   = qw(blue_shirt hat jacket preserver sunscreen);
    my @professor = qw(sunscreen water_bottle slide_rule batteries radio);
    check_required_items('skipper', @skipper);
    check_required_items('professor', @professor);
    对于另外两个乘客, 我们可以如法泡制.尽管以上代码符合最初的要求, 我们还是要有两个问题要解决:
    为了建立数组 @_ , Perl 会拷贝整个数据内容.对于少量数据还可以, 但如果数组庞大, 这看上去多少有些浪费时间在拷贝数组到子例程。
    假定我们要修改原始数组来强制供应单里加上些硬性规定的物品.因为我们是拷贝到子例程的(传值), 任何对数组的改变都不会自动反映到相应的供应单中。

  • 实际上, 用 shift 修改传过来的数组, 把新的标量赋值给数组 @_ 的一个元素是可以的.但这任不能改变原来的供应单。
    要解决这些问题, 我们需要传引用而不是传值给子例程.这就是医生(或教授)要求的。
    4. 2. 建立一个对数组的引用
    Among its many other meanings, the backslash (\) character is also the "take a reference to" operator. When we use it in front of an array name, e. g. , \@skipper, the result is a reference to that array. A reference to the array is like a pointer: it points at the array, but it is not the array itself.
    标量合适的操作对于引用都一样合适.它可以是数组或散列中的一个元素, 或简单就是一个标量变量, 像下面所示:
    my $reference_to_skipper = \@skipper;
    引用可以被复制:
    my $second_reference_to_skipper = $reference_to_skipper;
    甚至于:
    my $third_reference_to_skipper = \@skipper;
    我们可以互换这三个引用.我们甚至说他们是相同的, 因为, 实际上他们指的是同一地址。
    if ($reference_to_skipper =  = $second_reference_to_skipper) {
    print "They are identical references. \n";
    }
    这个等式是以数值形式来比较两个引用的.引用的数值形式就是 @skipper 这个内部数据结构在内存中的惟一地址, 且在这个变量的生命周期之内是不变的.如果我们以字串形式来看的话, 我们会得到如下调试形式的字串:
    ARRAY(0x1a2b3c)
    其内容同样是以十六进制表示的( base16 )的这个数组惟一内存地址.调试字串还标明了这个引用指向的是个数组.当然, 如果我们什么时候看到这样的输出的话, 这多半意味着我们的程序出了 bug ;我们程序的用户可对十六进制的存储地址可一点兴趣都没有!
    因为我们可以拷贝一个引用, 并且作为参数传给一个子例程, 我们可以用如下代码把对数组的引用传给子例程:
    my @skipper = qw(blue_shirt hat jacket preserver sunscreen);
    check_required_items("The Skipper", \@skipper);

    sub check_required_items {
    my $who = shift;
    my $items = shift;
    my @required = qw(preserver sunscreen water_bottle jacket); . . .  
    }
    现在子例程中的 $items 变量保存的是指向数组 @skipper 的引用.但我们如何时把一个引用变回一个原始数组呢?当然, 我们可以还原一个引用。
    4. 3. 还原一个指向数组的引用
    我们看一下 @skipper , 你会发现它包括两部份:@符号和数组名.相似地, 语法 $skipper[1] 包括当中的数组名和围绕在周围的语法符号表示取这个数组的第二个元素(索引1表示取第二个元素, 因为索引起始值是0)。
    这里有一个小戏法:我们可以用在外面套上大括号的指向数组的引用, 来替换数组的名字, 其结果就是访问原始的数组.换句话说, 就是我们写 sipper 数组名字的地方, 可以用大括号包起来的指向数组的引用来代替: {$items}. 举例来说, 下面两行都指向同一数组:
    @  skipper
    @{ $items }
    同样, 下面两行同指这个数组的第二个元素:

  • 注意, 为了对齐语法上的各部份, 我们在上面的例子中加了空格.这些空格在程序上也是合法的, 尽管许多程序不必如此。
    $  skipper [1]
    ${ $items }[1]
    运用引用的形式, 我们已经可以分离数组名字和从实际数组中访问数组的方法.我们来看看子例程的余下部分:
    sub check_required_items {
    my $who   = shift;
    my $items = shift;

    my @required = qw(preserver sunscreen water_bottle jacket);
    for my $item (@required) {
    unless (grep $item eq $_, @{$items}) { # not found in list?
    print "$who is missing $item. \n";
    }
    }
    }
    我们做的仅仅就是把 @_( 供应清单的拷贝)替换成 @{$items} , 对一个引用的还原操作来取得原始的供应清单数组.现在我们调用子例程次数相比以前少多了。
    my @skipper = qw(blue_shirt hat jacket preserver sunscreen);
    check_required_items('The Skipper', \@skipper);

    my @professor = qw(sunscreen water_bottle slide_rule batteries radio);
    check_required_items('Professor', \@professor);

    my @gilligan = qw(red_shirt hat lucky_socks water_bottle);
    check_required_items('Gilligan', \@gilligan);
    以上每个例子中, $items 指向一个不同的数组.如此, 同样的代码每次调用的时候可以应用到不同的数组.这就是引用的一个最重要的用法之一:把代码同其操作的具体数据结构分离开, 这样我们可以重用代码并使其更可读。
    用引用传数组解决了我们上面提到的两个问题中的一个.即, 相比以前我们拷贝供应清单到 @_ 数组, 现在我们传一个指向供应清单数组的单值。
    我们是否可以消除传两个值给子例程?当然可以, 不过这样牺牲了明晰性:
    sub check_required_items {
    my @required = qw(preserver sunscreen water_bottle jacket);
    for my $item (@required) {
    unless (grep $item eq $_, @{$_[1]}) { # not found in list?
    print "$_[0] is missing $item. \n";
    }
    }
    }
    我们仍有两个元素在数组 @_ 中.第一个元素是成员或乘务员的名字, 我们用它来组成出错信息.第二个元素是指向供应清单数组的引用.我们把它用在 grep 表达式中。
    4. 4. 把大括号去掉
    一般来说, 还原对数组的引用大多是一个简单的标量变量, 比如: @{$items} 或者 ${$items}[1]. 在那些情况下, 我们可以把大括号去掉, @$items 或 $$items[1] 这样的形式并不会引起歧义。
    但是, 有一点, 如果大括号里的内容不是简单的标量变量的话, 我们就不能把大括号去掉.比如, 对于前面最后一个改写过的子例程中的 @{$_[1]} , 我们不能把大括号去掉.因为那是个正访问数组的元素, 而不是一个简单的标量变量。
    这个规则也方便我们知道哪里丢了大括号.比如我们看到 $$items[1] 的时候, 知道这会有些语法上的麻烦, 我们会意识到必须在简单标量变量 $items 周围加上大括号.如此, $items 必须是一个指向数组的引用。
    因此, 看上去比较顺眼的写法应该是:
    sub check_required_items {
    my $who   = shift;
    my $items = shift;

    my @required = qw(preserver sunscreen water_bottle jacket);
    for my $item (@required) {
    unless (grep $item eq $_, @$items) { # not found in list?
    print "$who is missing $item. \n";
    }
    }
    }
    与前例惟一的区别就是去掉了大括号: @$items.
    4. 5. 修改数组
    你已经看到了如何用一个指向数组的引用来解决大量拷贝带来的问题.现在我们来看看如何修改原始数组。
    对于每个遗忘的的装备, 我们把它放到另一个数组里, 要求乘客关注这些装备:
    sub check_required_items {
    my $who   = shift;
    my $items = shift;

    my @required = qw(preserver sunscreen water_bottle jacket);
    my @missing = (  );

    for my $item (@required) {
    unless (grep $item eq $_, @$items) { # not found in list?
    print "$who is missing $item. \n";
    push @missing, $item;
    }
    }

    if (@missing) {
    print "Adding @missing to @$items for $who. \n";
    push @$items, @missing;
    }
    }
    注意我们另外增加了一个 @missing 数组.如果我们在扫描数组的时候发现有遗忘的装备, 我们就把它放到 @missing 数组里.在扫描结束后, 如果发现 @missing 里有内容, 我们就把这个数组加在供应清单后面。
    关键就在于那个子例程的最后一行.我们把指向数组的引用 $items 还原成数组, 访问还原后的数组, 并且把 @missing 数组中的元素加进去。
    同样, @$items( 或者其更一般的形式 :@{$items}) 在双引号内也可以工作.尽管我们可以在大括号里加任意空格, 但我们不能在@和后面跟着的字符间加上空格。
    4. 6. 数据结构嵌套
    在前例中, 我们的数组 @_ 有两个元素, 其中一个同样是个数组.如果一个引用所指向的数组中还包含着另外一个指向数组的引用会是什么情况?那就成了非常有用的所谓复杂数据结构。
    举个例子, 我们首先用个更大点儿的数据结构包含 skipper , Gilligan 和 Professor 供应清单的整个列表。
    my @skipper = qw(blue_shirt hat jacket preserver sunscreen);
    my @skipper_with_name = ('Skipper', \@skipper);
    my @professor = qw(sunscreen water_bottle slide_rule batteries radio);
    my @professor_with_name = ('Professor', \@professor);
    my @gilligan = qw(red_shirt hat lucky_socks water_bottle);
    my @gilligan_with_name = ('Gilligan', \@gilligan);
    现在, @skipper_with_name 有两个元素, 第二个元素就是指向数组的引用, 就是上例中我们传给子例程的那个.现在, 我们把它们组织起来:
    my @all_with_names = (
    \@skipper_with_name,  
    \@professor_with_name,  
    \@gilligan_with_name,  
    );
    注意, 现在我们的结构中有三个元素, 其中每个元素指向另外一个数组, 而那个数组又包含两个元素:名字和相名字相应的初始装备清单.具体的样子可以看图例 4-1:
    图 4-1. 数组 @all_with_name 包含一个多层的数据结构, 包括字串和指向另一数组的引用。
    这样, $all_with_names[2] 里放的是指向数组的引用, 内中存放的是 Gilligan 的数据.如果将其还原, 像这样: @{$all_with_names[2]} , 你就是得到一个有两个元素的数组: Gilligan 和另一个数组引用。
    我们如何才能访问那个数组引用呢?用我们的老规矩: ${$all_with_names[2]}[1]. 换句话说, 我们在一个表达式中像 $DUMMY[1] 形式那样把 $all_with_names[2] 还原成一个一平常的数组, 就是说用 {$all_with_names[2]} 代替 DUMMY 的位置。
    那我们如何用这个数据结构来调用现存的 check_required_items( )? 下面的代码足够简单:
    for my $person (@all_with_names) {
    my $who = $$person[0];
    my $provisions_reference = $$person[1];
    check_required_items($who, $provisions_reference);
    }
    这样对于以前写的子例程不需要做任何改变.随着循环进程, 控制变量 $person 将会是 $all_with_names[0] , $all_with_names[1] 和 $all_with_names[2]. 当我们还原 $$person[0] , 我们会得到 "Skipper , ""Professor , "和 "Gilligan , "相应的, $$persion[1] 是各个乘客所对应的装备清单数组。
    当然, 我们可以把这个过程再简化, 因为整个还原数组与参数清单精确对应:
    for my $person (@all_with_names) {
    check_required_items(@$person);
    }
    甚至于:
    check_required_items(@$_) for @all_with_names;
    正如你们看到的那样, 不同的优化层次会导致代码明晰性的困惑.所以写代码的时候要考虑一下一个月后, 当你重读这些代码的时候你会如何理解.或者, 当你离开这个岗位后, 接替你的人是否会看懂这段代码。
    4. 7. 用箭头号简化嵌套数组引用
    我们再来看一下用大括号的还原过程.像先前的例子, 我们对 Gilligan 的装备清单的数组引用是 ${$all_with_names[2]}[1]. 那么, 我们现在要访问 Gilligan 的第一个装备的话, 会怎么样呢?我们需要把这个引用再还原一下, 所以要加上另一层大括号: ${${$all_with_names[2]}[1]}[0]. 这样的语法太麻烦了!我们不能简化一下吗?当然可以!
    在任何写成 ${DUMMY}[$y] 样子的地方, 我们都可以用 DUMMY-[$y]> 这种形式代替.换句话说, 我们可以这样还原一个数组引用: 用定义一个带箭头的数组引用和一个方括号指定下标的形式表达数组里一个特定的元素。
    对于我们现在的例子来说, 如果我们要得到对 Gilligan 数组的引用的话, 我们可以简单写成: $all_with_names[2]-[1]> , 而指明 Gilligan 的第一个装备清单的写法是: $all_with_names[2]-[1]->[0].> 哇, 看上去真是好多了。
    如果你觉得还不够简洁的话?那我们还有条规则:如果箭头是在"类似于下标"中间的话, 那么箭头也可以省去 .$all_with_names[2]-[1]->[0]> 变成了 $all_with_names[2][1][0]. 现在样子看上去更简洁了。
    那为什么箭头必须在非下标符号间存在呢?好, 如果我们有一个指向数组 @all_with_names 的引用:
    my $root = \@all_with_names;
    现在我们如何时取得 Gilligan 的第一个装备呢?
    $root -> [2] -> [1] -> [0]
    很简单, 用"去箭头"规则, 我们可以写成:
    $root -> [2][1][0]
    然而, 第一个箭头不能舍去, 因为这会表示 root 数组的第三个元素, 成了一个完全无关的数据结构.让我们再与它"全包装"的形式再做一个比较:
    ${${${$root}[2]}[1]}[0]
    看来用箭头比较爽.不过, 注意, 没有快捷办法从一个数组引用中取出整个数组内容.比如, 我们要找出 Gilligan 装备清单的话, 可以这样写:
    @{$root->[2][1]}
    应该按下面的顺序来从里往外读:
    Take $root.  
    把它先还原成一个指向数组的引用, 取出那个数组中第三个元素(序号为2)
    用同样的方法取出那个数组第二个元素(序号为1)
    然后把整个数组还原出来。
    得, 最后一步不必要箭头快捷方式了。
    4. 8. 指向散列的引用
    就像我们可以取到一个指向一个数组的引用一样, 我们也可以用反斜杠取到一个指向散列的引用:
    my %gilligan_info = (
    name     => 'Gilligan',  
    hat      => 'White',  
    shirt    => 'Red',  
    position => 'First Mate',  
    );
    my $hash_ref = \%gilligan_info;
    我们也能还原一个对散列的引用, 得到它原来的数据.其方法与还原一个数组引用相同.就是当作没有引用这回事一样, 在散列引用变量的名字外面再加一对花括号.比如, 我们要取散列中一个给定键的特定值, 我们像这样写:
    my $name = $ gilligan_info { 'name' };
    my $name = $ { $hash_ref } { 'name' };
    在上例中, 同样是花括号表达了两个不同的含意.第一对花括号表示还原一个引用, 而第二个花括号限定散列键。
    对整个散列操作, 其操作也类似:
    my @keys = keys % gilligan_info;
    my @keys = keys % { $hash_ref };
    在某种环境下, 我们也可以像对数组引用那样, 用快捷方法不用复杂的花括号形式.比如说, 花括号里仅仅是简单的标量变量的话(就像到现在为止的例子展示的那样), 我们可以把花括号拿掉:
    my $name = $$hash_ref{'name'};
    my @keys = keys %$hash_ref;
    像数组引用一样, 当我们要访问一个特定的散列元素的话, 我们可以用箭头形式:
    my $name = $hash_ref->{'name'};
    因为在标量适合的地方, 散列引用也适合, 所以我们可以创建一个散列引用的数组。
    my %gilligan_info = (
    name     => 'Gilligan',  
    hat      => 'White',  
    shirt    => 'Red',  
    position => 'First Mate',  
    );
    my %skipper_info = (
    name     => 'Skipper',  
    hat      => 'Black',  
    shirt    => 'Blue',  
    position => 'Captain',  
    );
    my @crew = (\%gilligan_info, \%skipper_info);
    所以啦, $crew[0] 的内容是指向 Gilligan 信息的一个散列的引用.我们可以通过以下任一种方法取得 Gilligan 的名字。
    ${ $crew[0] } { 'name' }
    my $ref = $crew[0]; $$ref{'name'}
    $crew[0]->{'name'}
    $crew[0]{'name'}
    在最后一个例子中, 我们一样可以去掉"类似下标"间的箭头, 不管箭头是在数组方括号还是散列花括号中间。
    接下来, 我们打印一下船员的花名册:
    my %gilligan_info = (
    name     => 'Gilligan',  
    hat      => 'White',  
    shirt    => 'Red',  
    position => 'First Mate',  
    );
    my %skipper_info = (
    name     => 'Skipper',  
    hat      => 'Black',  
    shirt    => 'Blue',  
    position => 'Captain',  
    );
    my @crew = (\%gilligan_info, \%skipper_info);

    my $format = "%-15s %-7s %-7s %-15s\n";
    printf $format, qw(Name Shirt Hat Position);
    for my $crewmember (@crew) {
    printf $format,  
    $crewmember->{'name'},  
    $crewmember->{'shirt'},  
    $crewmember->{'hat'},  
    $crewmember->{'position'};
    }
    上例中最后部份看上去比较重复.我们可以用散列片断来简化写法.一样的, 如果散列的语法是这样的:
    @ gilligan_info { qw(name position) }
    那么散列引用片断的写法看上去如下:
    @ { $hash_ref } { qw(name position) }
    因为大括号里是简单的标量变量, 所以我们可以把第一个大括号去掉, 形如:
    @ $hash_ref { qw(name position) }
    因而, 我们可以把最后的循环语句替换成:
    for my $crewmember (@crew) {
    printf $format, @$crewmember{qw(name shirt hat position)};
    }
    对于数组片断或散列片断没有快捷写法, 就像对整个数组或散列也没有快捷写法一样。
    如果打印一个散列引用, 会得到一个类似于 HASH(0x1a2b3c) 一样的字串, 显示这个散列在内存中的用十六进制表示的地址.这个对终端用户来说没有多少用处.除非表示没有很好还原, 这个对程序员来说也没多大用处。
    4. 9. 习题
    在附录中"第四章的答案"中找答案
    4. 9. 1. 练习 1 [5 分钟]
    下列表达式各表示什么不同的含义:
    $ginger->[2][1]
    ${$ginger[2]}[1]
    $ginger->[2]->[1]
    ${$ginger->[2]}[1]
    4. 9. 2. 练习 2 [30 分钟]
    运用书中最后那个版本的 check_required_items , 写一个子程序 check_items_for_all , 把一个散列引用作为惟一参数.这个散列引用的键是在 Minnow 船上的所有乘客, 其对应的值是他们各自想带上船的东西。
    比如, 这个散列引用可以是这样的:
    my @gilligan  =. . . gilligan items. . . ;
    my @skipper   =. . . skipper items. . . ;
    my @professor =. . . professor items. . . ;
    my %all = (
    Gilligan  => \@gilligan,  
    Skipper   => \@skipper,  
    Professor => \@professor,  
    );
    check_items_for_all(\%all);
    我们新建的那个子程序要调用 check_required_items 来为散列中每个人更新其必需的装备清单。
    Chapter 5. 引用和范围
    我们可以像任何其它标量变量那样拷贝和传递引用.在任何给定的时间, Perl 会知道有多少引用指向一个特定的数据项。 Perl 也会为匿名数据结构(所谓没有名字的数据结构)创建引用, 以及在为了满足一定的操作自动的创建引用。 让我们来看一下引用的拷贝以及其对范围和内存使用方面的影响。
    5. 1. 更多有关对数据进行引用的故事
    在第四章里, 我们了解了如何取一个数组 @skipper 的引用, 并把它放到一个新建的标量变量中:
    my @skipper = qw(blue_shirt hat jacket preserver sunscreen);
    my $reference_to_skipper = \@skipper;
    我们现在就可以拷贝这个引用或把它传给另外一些引用, 并且, 它们全都指向同一数据, 并且是互相可替换的:
    my $second_reference_to_skipper = $reference_to_skipper;
    my $third_reference_to_skipper  = \@skipper;
    现在, 我们有四种途径来访问保存在 @skipper 中的数据:
    @skipper
    @$reference_to_skipper
    @$second_reference_to_skipper
    @$third_reference_to_skipper
    Perl 跟踪有多少途径在访问数据的机制叫做"引用计数 ". 原来的名字计数为1, 其它我们创建的每一个引用(包括对引用的拷贝)同样被计数.就目前的例子, 装备清单数组的目前引用数是 4.
    我们可以任意添加和删除引用, 并且, 只要引用计算不减到0, Perl 就会在内存中保留这个数组, 并且能通过任何其它的途径来访问这个数组.比如, 我们可能有一个临时引用:
    check_provisions_list(\@skipper)
    当这个子程序执行的时候, Perl 会创建指向这个数组的第五个引用, 并将其拷贝到这个子程序的特殊变量 @_ 中.子程序可以很自由的为那个引用创建更多的拷贝, 而 Perl 则会适时的关注.一般来说, 当子程序返回的时候, Perl 会自动扔掉所有这些子程序创建的引用, 这样你又回到了4个引用。
    我们用给引用赋任何一个与指向 @skipper 引用无关的其它的变量就可以切断引用关系了.比如, 我们可以给变量分配一个 undef 给变量:
    $reference_to_skipper = undef;
    或者, 我们可以仅仅让变量跑出范围就可以了:
    my @skipper =. . . ;

    { # naked block . . .  
    my $ref = \@skipper; . . .  . . .  
    } # $ref goes out of scope at this point
    要指出的是, 在一个子程序的私有(词法)变量保存的一个引用, 会在子程序结束时被释放。
    不管是我们改变变量的值, 或者变量跑出范围, Perl 会注意到这些, 并且适当的减少对数据的引用计数。
    只有这个数组的所有引用(包括数组名)都没有了, Perl 才会回收这个数组占用的内存. 在上例中, 只有在 @skipper 数组及我们所有对它的引用全消失后, Perl 才会回收内存.
    这些被释放的内存会被 Perl 安排用在程序中之后的其它数据的调用, 一般来说 Perl 不会把这些内存还给操作系统。
    5. 2. 假若它曾经有一个名字
    一般来说, 引用总是比其指向的变量之前被释放.但是如果引用比其指向的变量活得长会发生什么情况?比如, 考虑下面的情况:
    my $ref;

    {
    my @skipper = qw(blue_shirt hat jacket preserver sunscreen);
    $ref        = \@skipper;

    print "$ref->[2]\n"; # prints jacket\n
    }

    print "$ref->[2]\n"; # still prints jacket\n
    在我们声明 @skipper 数组之后, 我们有了一个指向这个五个元素的列表的引用。 在 $ref 被初始化后, 到块结束的地方, 我们会有两个引用。 到块结束, @skipper 名子消失了。 然而, 这不过是仅仅访问数据的一个途径而已! 这样, 这个五个元素的列表依旧在内存里, 而且 $ref 变量仍然指向这块数据。
    这个时候, 这个五个元素的列表是个匿名数组--为没有名字的数组起的一个漂亮名字。
    直到 $ref 的值改变, 或者 $ref 自己消失, 我们依旧可以使用任何我们在前面学到的还原方法, 尽管数组的名字已经不存在了.实际上, 它仍然是一个不折不扣的数组, 我们可以像对待其它任何 Perl 数组那样把它拉长和缩小:
    push @$ref, 'sextant'; # add a new provision
    print "$ref->[-1]\n"; # prints sextant\n
    此时我们甚至还可以增加引用计数:
    my $copy_of_ref = $ref;
    下面也一样:
    my $copy_of_ref = \@$ref;
    数据一直会存在, 直到我们销毀最后一个引用:
    $ref = undef; # not yet. . .  
    $copy_of_ref = undef; # poof!
    5. 3. 引用计数和数据结构嵌套
    只要最后一个引用没有被销毁, 数据就一直存在.甚至引用包含在一个大的数据结构时也是如此.有可能数组本身的一个元素就是一个引用.我们回想一下第四章见到过的例子:
    my @skipper = qw(blue_shirt hat jacket preserver sunscreen);
    my @skipper_with_name   = ('The Skipper', \@skipper);

    my @professor = qw(sunscreen water_bottle slide_rule batteries radio);
    my @professor_with_name = ('The Professor', \@professor);

    my @gilligan = qw(red_shirt hat lucky_socks water_bottle);
    my @gilligan_with_name  = ('Gilligan', \@gilligan);

    my @all_with_names = (
    \@skipper_with_name,  
    \@professor_with_name,  
    \@gilligan_with_name,  
    );
    想像一下中间变量全是子程序一部份的情况:
    my @all_with_names;

    sub initialize_provisions_list {
    my @skipper = qw(blue_shirt hat jacket preserver sunscreen);
    my @skipper_with_name = ('The Skipper', \@skipper);

    my @professor = qw(sunscreen water_bottle slide_rule batteries radio);
    my @professor_with_name = ('The Professor', \@professor);

    my @gilligan = qw(red_shirt hat lucky_socks water_bottle);
    my @gilligan_with_name = ('Gilligan', \@gilligan);

    @all_with_names = ( # set global
    \@skipper_with_name,  
    \@professor_with_name,  
    \@gilligan_with_name,  
    );
    }

    initialize_provisions_list(  );
    我们用数组 @all_with_names 的值来放三个引用。 在子程序里, 我们命名了一些数组, 而这些数组指向了事先命名的另外的数组。 最终, 这些值最后放到一个全局的数组 @all_with_names 中.然而, 当子程序返回时, 这些六个数组的名字被锁毁。 正因为每个数组都有另外一个引用指向它, 所以导致引用计数临时是2, 但当数组名字销毁后, 计数又回到 1. 因为引用计数没有回到零, 数据仍然在, 尽管它向在仅仅被当做数组 @all_with_names 的元素。
    与分配一个全局变量不同, 我们还可以重写这个程序, 去掉全局变量 @all_with_names , 直接从程序中返回列表:
    sub get_provisions_list {
    my @skipper = qw(blue_shirt hat jacket preserver sunscreen);
    my @skipper_with_name = ('The Skipper', \@skipper);

    my @professor = qw(sunscreen water_bottle slide_rule batteries radio);
    my @professor_with_name = ('The Professor', \@professor);

    my @gilligan = qw(red_shirt hat lucky_socks water_bottle);
    my @gilligan_with_name = ('Gilligan', \@gilligan);

    return (
    \@skipper_with_name,  
    \@professor_with_name,  
    \@gilligan_with_name,  
    );
    }

    my @all_with_names = get_provisions_list(  );
    上例中, 我们创建的那个最终保存在 @all_with_names 中的值是用子程序的最后一个表达式求值得到的.子程序返回一个三个元素的列表.只要子程序中的命名数组曾经至少有一个引用指向他们, 他们就仍旧是返回值的一部份, 数据仍旧存在。
  • 如果我们改变或者丢弃 @all_with_names 中的引用, Perl 会减少相应的数组的引用计数.如果那意味着引用计数降为零(在例中), Perl 一样会把数组销毁.因为 @all_with_names 数组中包含的其它数组同样包含一个引用(如对 @skipper 的引用), Perl 会把那个引用的计数降为 1. 一样, 一旦引用计数降为零, Perl 会连锁地释放内存。
  • 比较一下这与C函数中必须返回一个数组.我们要么返回一个指向静态内存区域的指针, 使子程序非重入, 或者我们必需分配新内存, 需要调用程序知道释放内存 .Perl 正好做了对的事情。
    一般来说, 如果删除了一个复杂数据结构的根数据, 会把其包含的子数据全删除.一个例外就是当我们对其嵌套数据进行了引用.比如说, 如果我们拷贝了 Gilligan 的装备:
    my $gilligan_stuff = $all_with_names[2][1];
    然后我们删除了 @all_with_names , 我们有一个活的引用指向之前的 @gilligan , 其下的数据也依然在。
    底线很清楚: Perl 总是做对的事情.如果我们依然有指向数据的引用, 我们就仍旧拿着数据。
    5. 4. 当引用计数出问题了
    用引用计数的办法来管理内存长久以来一直很受欢迎.的确使用了很长一段时间.引用计数有一个缺点, 就是在数据结构不是单向引用的时候, 它会有问题.所谓非单向引用的数据结构就是:这个数据结构中的一些部份引用与其指向的数据存在循环引用.比如:这两块数据结构中都有互相指向对方的引用(参考图 5-1):
    my @data1 = qw(one won);
    my @data2 = qw(two too to);

    push @data2, \@data1;
    push @data1, \@data2;
    图 5-1. 当一个数据结构中的引用出现循环调用的时候, Perl 的引用计数系统可能不能识别出来, 从而不能回收不再需要的内存空间
    像这种情况, 我们会有两个名字为 @data1 中的数据块命名: @data1 自身和 @{$data2[3]} , 另外有两个名字为 @data2 中的数据块命名: @data2 自身和 @{$data1[2]}. 我们创建了一个循环.实际上我们可以用可怕多的下标来访问这块数据: $data1[2][3][2][3][2][3][1].
    那这两个数组跑出范围会发生什么事? 噢, 这两个数组的引用计数会从2降为1, 但不是 0. 正因为没有归到零, Perl 会认为仍有引用会指向这些数据, 尽管已经没有了.所以, 我们已经创建了一个内存泄漏.一个程序有内存泄漏会导致消耗越来越多的内存, 噢!
    现在, 你会想例子总是有意设计出来的.当然, 我们不会在真实的程序中有意设计一个引用循环.而实际上, 程序员经常在用双向链表、循环列表或其它一些数据结构时创建这些引用循环.问题是 Perl 的程序员罕有犯这种错误的, 重要的原因是 Perl 不太会用到这些数据结构.大多数处理内存操作及连接内存碎片的操作已经被 Perl 自动处理了.如果你曾使用过其它的语言, 你可能会注意到在 Perl 中的编程相对比较容易.比如, 对列表中的元素排序或(甚至在列表中间)添加、删除元素都很方便.同样的任务在其它语言中会很困难, 而用循环数据结构是绕开这些语言限制的常用方法。
    那为什么要在这儿提这些呢? 是这样的, 因为有些 Perl 程序员有时也会从其它程序语言中把算法拷贝过来.这样做当然不会继承错误, 尽管考虑一下原来作者用"循环"数据结构是为什么, 然后用 Perl 的强项来处理这什算法.可能你要用个散列, 或用数组, 然后以后进行排序。
    另外, 经后的 Perl 可能会使用垃圾收集器来代替计数引用 .[ *] 不过到现在, 我们必须注意不要建立循环引用, 或者, 如果我们做的话, 在变量跑出范围时, 要去掉循环引用.比如, 下面的代码不会造成内存泄漏:
  • 不要问我们为什么, 在你有机会读到此书时, 相对我们写书的时间已经是很长久了.所以我能不能肯定。
    {
    my @data1 = qw(one won);
    my @data2 = qw(two too to);
    push @data2, \@data1;
    push @data1, \@data2; . . . use @data1, @data2. . .  
    # at the end:
    @data1 = (  );
    @data2 = (  );
    }
    我们在 @data1 中对 @data2 的引用, 相反也一样.现在两块数据只有一个引用, 在跑出块范围的时候, 引用计数会归零.实际上, 我们清除任何一个引用就可以了, 程序一样工作得很好.第 13 章会展示如何建立弱引用, 可以对这些问题有些帮助。
    5. 5. 直接建立匿名数组
    在早前的 get_provisions_list 子程序中(参考 5.3 节), 我们建立了半打数组, 而且创建的目的仅仅是为了后面马上要给他们建立引用.当子程序退出的时候, 所有数组名将销毁, 但引用还留着。
    临时命名的数组, 在简单环境中还可以工作, 但这些名字在数据结构开始越来越复杂的情况下会变得比较繁琐.我们应该想到, 正确地处理这些数组的名字, 这样我们可以很快就忘掉它。
    我们可以用缩小不同数组名的范围的方法来降低命名空间的繁杂性.代替在子程序范围内声明变量, 我们可以建立一个临时块:
    my @skipper_with_name;
    {
    my @skipper = qw(blue_shirt hat jacket preserver sunscreen);
    @skipper_with_name = ('The Skipper', \@skipper);
    }
    上例中, @skipper_with_name 第二个元素是个指向先前命令的数组 @skipper. 然而, 那个名字现在与程序不再有关系了。
    为了仅仅说明"第二个元素应该是个指向一个包括这些元素的引用", 而编上例中这些代码, 好像太麻烦了.我们可以创建一个直接使用匿名数组的结构, 即方括号的另一种用法:
    my $ref_to_skipper_provisions =
    [ qw(blue_shirt hat jacket preserver sunscreen) ];
    方括号把内中(相当一在一个列表环境)元素取出来;为这些元素建立一个新的, 匿名的数组;并且(这里很重要)返回一个对这个匿名数组的引用.就好像我们说过的:
    my $ref_to_skipper_provisions;
    {
    my @temporary_name =
    ( qw(blue_shirt hat jacket preserver sunscreen) );
    $ref_to_skipper_provisions = \@temporary_name;
    }
    这里, 我们不必需要提供临时变量名字, 也不要讨厌的临时代码块.用方括号返回的匿名数组结构就是一个对数组的引用, 并且用在任何标量环境中都合适。
    现在, 我们可以用它来构建一个更大的列表:
    my $ref_to_skipper_provisions =
    [ qw(blue_shirt hat jacket preserver sunscreen) ];
    my @skipper_with_name = ('The Skipper', $ref_to_skipper_provisions);
    甚至, 我们也根本不要那个临时标量.我们可以直接把临时标量替换成数组引用, 作为一个更大列表的一部份:
    my @skipper_with_name = (
    'The Skipper',  
    [ qw(blue_shirt hat jacket preserver sunscreen) ]
    );
    来, 我们回顾一下.我们已经声明了 @skipper_with_name , 它的第一个元素是 Skipper 的名字字串, 第二个元素是一个对数组的引用, 它是由把五个元素放到一个数组, 并且取其引用得到的.所以, @skipper_with_name 数组仅有两个元素长。
    别把这里的方括号和小括号搞混了.它们各有不同的目的.如果我们用小括号代替方括号, 我们结果会得到一个六个元素的列表.如果我们将外圈的(第一行和最后一行)的小括号用方括号代替, 我们就构建了一个两个元素长的匿名数组的引用, 并将其返回给 @skipper_with_name 数组 .[ *] 这样, 总之, 如果我们有以下的代码:
  • 在教学中, 我们经常会看到在使用引用时犯的直接(或不那么直接)的错误。
    my $fruits;
    {
    my @secret_variable = ('pineapple', 'papaya', 'mango');
    $fruits = \@secret_variable;
    }
    我们可以以此代替:
    my $fruits = ['pineapple', 'papaya', 'mango'];
    那么在更复杂的结构中, 这个原则还工作吗?当然!只要是需要一个指向数组的引用, 我们就可以创建指向匿名数组的引用.实际上, 我们还可以嵌套使用:
    sub get_provisions_list {
    return (
    ['The Skipper', [qw(blue_shirt hat jacket preserver sunscreen)] ],  
    ['The Professor', [qw(sunscreen water_bottle slide_rule radio)  ] ],  
    ['Gilligan', [qw(red_shirt hat lucky_socks water_bottle)   ] ],  
    );
    }

    my @all_with_names = get_provisions_list(  );
    我们由外而内地看一下上面的代码, 我们返回了三个元素.每个元素是一个数组引用, 并且是指向有两个元素的匿名数组的引用.内中每个数组第一个元素是名字字串, 而第一个元素又是一个变长的匿名数组的引用.而且变长数组包括了所有的装备清单, 并且我们不必再提供临时名字作为中间变量。
    对于调用它的子程序来说, 现在的返回值对于前一个版本是一样的.然而, 从代码维护的角度来看, 去掉那些中间名字, 这样降低了复杂性也节省了代码占用屏幕空间。
    我们也可以将引用指现空的匿名数组.比如, 如果我们加一个 Mrs.Howell 到一个遊客列表, 并且它没带多少装备, 我们可以简单的这样写:
    ['Mrs. Howell',  
    [  ]
    ],  
    这是一个只有一个元素的大列表.这是一个指向一个具有两个元素的数组的引用, 第一个元素是名字字串, 第二个元素自身是个指向空的匿名数组的引用.这个数组是空的, 因为 Howell 先生为他的旅程什么也没有带。
    5. 6. 创建匿名散列
    同创建匿名数组类似, 你也可以建立一个匿名散列.考虑一下第四章的船员注册程序:
    my %gilligan_info = (
    name     => 'Gilligan',  
    hat      => 'White',  
    shirt    => 'Red',  
    position => 'First Mate',  
    );

    my %skipper_info = (
    name     => 'Skipper',  
    hat      => 'Black',  
    shirt    => 'Blue',  
    position => 'Captain',  
    );

    my @crew = (\%gilligan_info, \%skipper_info);
    散列变量 %gilligan_info 和 %skipper_info 是为了最终数据结构而建立的临时变量.下面我们就构造一个对匿名散列的直接引用, 正像我们看到的, 花括号的另一项功能.替代代码如下:
    my $ref_to_gilligan_info;

    {
    my %gilligan_info = (
    name     => 'Gilligan',  
    hat      => 'White',  
    shirt    => 'Red',  
    position => 'First Mate',  
    );
    $ref_to_gilligan_info = \%gilligan_info;
    }
    如果用匿名散列的话:
    my $ref_to_gilligan_info = {
    name     => 'Gilligan',  
    hat      => 'White',  
    shirt    => 'Red',  
    position => 'First Mate',  
    };
    被花括号括起来的是八个元素的列表.八个元素的列表也就变成了四个元素的匿名数组(四个键/值对) .Perl 会取这个散列的引用, 并返回一个标量值, 并送给一个标量变量.所以我们可以改写程序如下:
    my $ref_to_gilligan_info = {
    name     => 'Gilligan',  
    hat      => 'White',  
    shirt    => 'Red',  
    position => 'First Mate',  
    };

    my $ref_to_skipper_info = {
    name     => 'Skipper',  
    hat      => 'Black',  
    shirt    => 'Blue',  
    position => 'Captain',  
    };

    my @crew = ($ref_to_gilligan_info, $ref_to_skipper_info);
    同前面一样, 我们还是可以避免使用临时变量, 把散列值直接放到列表里:
    my @crew = (
    {
    name     => 'Gilligan',  
    hat      => 'White',  
    shirt    => 'Red',  
    position => 'First Mate',  
    },  

    {
    name     => 'Skipper',  
    hat      => 'Black',  
    shirt    => 'Blue',  
    position => 'Captain',  
    },  
    );
    注意列表结束时的元素并非马上连着关闭的花括号, 方括号或小括号, 而是以逗号结尾.这是很好的编程风格, 因为这样做有利于代码维护.我们可以很方便的添加或重排行, 或者不破坏列表完整性的情况下注释掉代码。
    现在 @crew 里所包含的值同先前是一样的, 但不再需要为中间数据结构起名字了.就像上一个版本一样, @crew 包含两个元素, 每个元素都是一个指向一个包含以键为基础的散列, 表示了船上成员的特殊信息。
    匿名散列构造器总是把内容放在列表环境, 并把他们以键/值对的形式输出, 就像我们分配那个列表给命名散列一样 .Perl 返回一个那个散列的引用, 并将其作为标量值, 并适合任何标量合适的地方。
    现在, 下面一些话是从我们的解释器那里来: 因为代码块和匿名散列构造器都用花括号, 在语法树中大致相同的位置, 编译器不得不即时判断你到底是要用哪一个。 如果编译器没法决定, 就需要你给个暗示, 你到底是用哪个。 如果你是要用匿名散列, 那你要在花括号前加一个正号:+{… }. 如果你是用代码块, 则要在花括号后加上分号:{; … }.
    5. 7. 自生成
    我们重顾一下装备清单.假定我们从一个文件中读取这些数据, 格式如下:
    The Skipper
    blue_shirt
    hat
    jacket
    preserver
    sunscreen
    Professor
    sunscreen
    water_bottle
    slide_rule
    Gilligan
    red_shirt
    hat
    lucky_socks
    water_bottle
    我们把装备用空格缩进表示, 没有缩进的行是旅客名字.我们来建立一个装备清单.索引键是旅客名字, 其值是一个指向装备列表的引用。
    起先, 我们用一个简单的循环收集数据:
    my %provisions;
    my $person;

    while (<>) {
    if (/^(\S. *)/) { # a person's name (no leading whitespace)
    $person = $1;
    $provisions{$person} = [  ] unless exists $provisions{$person};
    } elsif (/^\s+(\S. *)/) { # a provision
    die 'No person yet!' unless defined $person;
    push @{ $provisions{$person} }, $1;
    } else {
    die "I don't understand: $_";
    }
    }
    首先, 我们定义一个变量来保存结果散列中的乘客及他们的清单.对于读出的每一行, 我们先判断它是乘客还是装备.如果是乘客, 我们记下姓名并为那个乘客创建散列元素 .unless exists 测试来保证在一个乘客的装备在文件中分两块地方放的时候, 我们不会把他的装备清单删掉。
    例如, 假定 "The skipper" 和 "sextant"( 注意前导空格) 放在文件末尾, 作为增补的项目。
    如果把乘客的名字作为键, 值是指向一个空数组的引用.如果读入的行是装备, 则使用引用数组, 把它推入当前的数组。
    这样的代码运行得很好, 但实际上它不需要写那么多.为什么? 因为我们可以把初始化指向数据的散列引用的地方去掉:
    my %provisions;
    my $person;

    while (<>) {
    if (/^(\S. *)/) { # a person's name (no leading whitespace)
    $person = $1;
    ## $provisions{$person} = [  ] unless exists $provisions{$person};
    } elsif (/^\s+(\S. *)/) { # a provision
    die 'No person yet!' unless defined $person;
    push @{ $provisions{$person} }, $1;
    } else {
    die "I don't understand: $_";
    }
    }
    那现在, 我们要把蓝衬衣放到 Skipper 的装备清单时会发生什么呢? 当查看第二项输入时, 我们结果可以看到如下的效果:
    push @{ $provisions{'The Skipper'} }, "blue_shirt";
    在这时候, $provisions{"The Skipper"} 并不存在, 但我们正在把它作为一个数组引用来处理.为了解决这种情况, Perl 自动为我们创建一个指向家的匿名数组的引用来继续操作.在此例中, 新的指向空数组的引用被创建, 并被还原, 我们再把蓝衬衫放进去, 生成了一个装备清单。
    这样的处理叫做:自生成.任何一个不存在的变量, 或个存有 undef 的变量, 当被用来查找一个变量的时候(技术上被称为左值环境), 会自动被适当的生成空项引用, Perl 然后再让处理继续。
    实际上, 我们在使用 Perl 时, 到处都会有相似的行为.只要有需要, Perl 会自动创建变量.在这之前, $provisions{"The Skipper"} 不存在, 所以 Perl 就创建它.后来 @{ $provisions{"The Skipper"} }不存在, 所以 Perl 故伎重演。
    比如, 下面在代码是工作的:
    my $not_yet;                # new undefined variable
    @$not_yet = (1, 2, 3);
    这儿, 我们还原了 $not_yet 这个值, 好像它原先是一个指向数组的引用一样.但因为它初始化是 undef , Perl 的行为好像我们说过的:
    my $not_yet;
    $not_yet = [  ]; # inserted through autovivification
    @$not_yet = (1, 2, 3);
    换句话说, 一个初始化的空的数组成为一个有三个元素的数组。
    在多层赋值时, 自生成也起作用:
    my $top;
    $top->[2]->[4] = 'lee-lou';
    初始时, $top 含有一个 undef , 但因为我们还原它, 就像它是一个数组引用一样, Perl 往里插了一个空的匿名数组的引用 .Perl 然后访问第三个元素(索引为 2) , 这导致 Perl 撑大数组到三个元素长.那个元素同样是 undef , 所以 Perl 用另外一个指向空的匿名数组的引用放进去.我们然后分拆那个新建的数组, 把第五个元素放 lee-lou 这个字串。
    5. 8. 自生成功能与散列
    在散列引用中, 自生成功能也起作用.如果我们还原一个含有 undef 的变量, 就好像它是一个散列引用一样, 那么一个指向空的匿名散列的引用就会被插入, 让操作继续下去。
    自生成功能一大用处是在那些典型的单向读取的任务(消防水带)。 比如说, 假设教授要建立一个岛间的网络.现在他要跟踪主机与主机之间的流量。 他现在开始把之间传输的字间记录到日志文件.记录源主机, 目标主机和传输的字节数:
    professor. hut gilligan. crew. hut 1250
    professor. hut lovey. howell. hut 910
    thurston. howell. hut lovey. howell. hut 1250
    professor. hut lovey. howell. hut 450
    professor. hut laser3. copyroom. hut 2924
    ginger. girl. hut professor. hut 1218
    ginger. girl. hut maryann. girl. hut 199 . . .  
    现在, 教授要生成一个有关源主机,目标主机和总传输字节数的报表。 把数据列表显示出来:
    my %total_bytes;
    while (<>) {
    my ($source, $destination, $bytes) = split;
    $total_bytes{$source}{$destination} += $bytes;
    }
    让我们看第一行如何生成的:
    $total_bytes{'professor. hut'}{'gilligan. crew. hut'} += 1250;
    因为散列 %total_bytes 初始时是空的, Perl 没有找到第一个以 professor.hut 命名的键, 但它会为了还原而建立一个未定义值作为散列引用.(记住, 这里在花括号间有隐含的箭头.) Perl 会在那个元素里放个指向空值的匿名散列, 这样它可以立即扩展数组, 将把 gilligan.crew.hut 作为一个元素放入.它的初始值是未定义值, 与零等同, 而当你把 1250 与它相加的时候, 其相加的结果 1250 也放回散列。
    任何后来的数据行, 如果有相同的源主机和目标主机, 都会重用原来的值, 并加上新读入的值, 再计算出总数.但是新的目标主机会扩展散列来包括一个新的初始未定义值, 重新计数, 每个新的源主机会用自生成功能建立目标主机散列.换句话说, Perl 总是做对的事情, 就像它一直表现的那样。
    一旦我们处理完文件, 应该是显示总计的时候了.首先, 我们取源主机:
    for my $source (keys %total_bytes) { . . .  
    好, 现在我们取所有的目标主机.这里的语法有些"搞 ". 我们用散列元素还原出来的值作为键来取目标主机, 比如:
    for my $source (keys %total_bytes) {
    for my $destination (keys %{ $total_bytes{$source} }) { . . . .  
    为了好的效率, 我们应该对列表进行排序, 以保持一致性:
    for my $source (sort keys %total_bytes) {
    for my $destination (sort keys %{ $total_bytes{$source} }) {
    print "$source => $destination:",  
    " $total_bytes{$source}{$destination} bytes\n";
    }
    print "\n";
    }
    这是一个典型的"消防水带"式的报表生成策略.[*]简单地建立一个散列引用的散列(还可以嵌套更深, 后面你就可以看到), 运用自生成功能, 根据需要填充数据结构, 然后遍历结果数据, 打印输出结果。
    5. 9. 习题
    附录部份可以找到答案
    5. 9. 1. 练习 1 [5 分钟]
    先不要运行程序, 看看你能否判断出这程序的哪部份出了问题?如果你看不出来, 就可以运行一相程序, 得到些暗示, 来看是否能修改好:
    my %passenger_1 = {
    name       => 'Ginger',  
    age        => 22,  
    occupation => 'Movie Star',  
    real_age   => 35,  
    hat        => undef,  
    };

    my %passenger_2 = {
    name          => 'Mary Ann',  
    age           => 19,  
    hat           => 'bonnet',  
    favorite_food => 'corn',  
    };

    my @passengers = (\%passenger_1, \%passenger_2);
    5. 9. 2. 练习2 [ 40 分钟]
    教授的数据文件(注意早先这章提到的)叫 coconet.dat , 保存在 O'Reilly 的网站上, 你可以下载.有些行可能被注释了一些行(用前导#号);跳过这些行.(就是说, 你的程序可以跳过他们, 不过你阅读这些行还有很有益的!)
    修改一下这章的那段程序, 使每个源主机显示所有从它那里输出的字节数.以字节数从大到小排.对于每一组中, 以所有目标主机, 也以传输到此目标主机的字节数以大到小排。
    其最终的结果就是列出传输最多的源主机, 然后以此源主机为组, 接收最多的目标主机排在最前。 教授可以以此报表来重新安排网络效率.
    Chapter 6. 处理复杂数据结构
    既然你已经知道了引用的基础知识, 那就让我们管理复杂数据结构的其它的方法。 首先, 我们会用测试工具来查看复杂数据结构的内容, 之后我们会介绍 Data::Dumper 模块, 这样我们就可以在程序中展示数据结构了。 下一步, 我们将会学到用 Storable 模块把复杂数据结构方便快捷地进行存取.最后, 我们会回顾一下 grep 和 map , 看他们如何在处理复杂数据结构上来发挥作用。
    6. 1. 使用测试工作来查看复杂数据结构
    Perl 的测试工具可以方便地显示复杂的数据结构.例如, 让我们单步执行第五章中我们说过的那个字节计数的程序:
    my %total_bytes;
    while (<>) {
    my ($source, $destination, $bytes) = split;
    $total_bytes{$source}{$destination} += $bytes;
    }
    for my $source (sort keys %total_bytes) {
    for my $destination (sort keys %{ $total_bytes{$source} }) {
    print "$source => $destination:",  
    " $total_bytes{$source}{$destination} bytes\n";
    }
    print "\n";
    }
    下面是我们要测试的数据:
    professor. hut gilligan. crew. hut 1250
    professor. hut lovey. howell. hut 910
    thurston. howell. hut lovey. howell. hut 1250
    professor. hut lovey. howell. hut 450
    ginger. girl. hut professor. hut 1218
    ginger. girl. hut maryann. girl. hut 199
    我们可以有多种方法来执行测试.其中一种最简单的是以 -d 开关在命令行执行 Perl 解释器:
    myhost% perl -d bytecounts bytecounts-in

    Loading DB routines from perl5db. pl version 1. 19
    Editor support available.  

    Enter h or 'h h' for help, or 'man perldebug' for more help.  

    main::(bytecounts:2):        my %total_bytes;
    DB<1> s
    main::(bytecounts:3):        while (<>) {
    DB<1> s
    main::(bytecounts:4):          my ($source, $destination, $bytes) = split;
    DB<1> s
    main::(bytecounts:5):          $total_bytes{$source}{$destination} += $bytes;
    DB<1> x $source, $destination, $bytes
    0  'professor. hut'
    1  'gilligan. crew. hut'
    2  1250
    如果在你这儿运行的话, 要注意因为测试工具的版本不同, 所以你的屏幕显示可能与我们的不尽相同.还有, 如果你在测试中遇到了麻烦, 可以输入h键来获得 perldoc perldebug 提供的在线帮助。
    测试工具会在程序的每一行被执行之前, 显示该语句.这个意思就是说, 在此时, 我们将会调用一个自生成, 建立我们的索引键 .'s' 表示单步执行, 而 'x' 表示以适当的格式输出值的列表.这样我们就可以看到 $source , $destination 和 $bytes 这些变量是正确的, 且现在正更新数据: update the data:
    DB<2> s
    main::(bytecounts:3):        while (<>) {
    我们已经通过自生成建立了散列条目.让我们看看我们得到了什么:
    DB<2> x \%total_bytes
    0  HASH(0x132dc)
    'professor. hut' => HASH(0x37a34)
    'gilligan. crew. hut' => 1250
    当我们给调试命令x 一个散列引用的时候, 它会把这个散列的所有内容(键/值对)打印出来.如果其中的值也是指向散列的引用的话, 它同样也会打印, 以此类推.我们可以看到散列 %total_bytes 中 professor.hut 键相对应的值是指向另一个散列的引用.就像你预期的那样, 这个散列引用内中有单个键: gilligan.crew.hut , 其对应的值为 1250.
    我们看看下一个赋值会发生什么:
    DB<3> s
    main::(bytecounts:4):          my ($source, $destination, $bytes) = split;
    DB<3> s
    main::(bytecounts:5):          $total_bytes{$source}{$destination} += $bytes;
    DB<3> x $source, $destination, $bytes
    0  'professor. hut'
    1  'lovey. howell. hut'
    2  910
    DB<4> s
    main::(bytecounts:3):        while (<>) {
    DB<4> x \%total_bytes
    0  HASH(0x132dc)
    'professor. hut' => HASH(0x37a34)
    'gilligan. crew. hut' => 1250
    'lovey. howell. hut' => 910
    现在我们已经把从 professor.hut 流向 lovey.howell.hut 主机的字节数加上了.顶层的散列没有变化, 而下一级的散列已加上了新的条目.让我们继续:
    DB<5> s
    main::(bytecounts:4):          my ($source, $destination, $bytes) = split;
    DB<6> s
    main::(bytecounts:5):          $total_bytes{$source}{$destination} += $bytes;
    DB<6> x $source, $destination, $bytes
    0  'thurston. howell. hut'
    1  'lovey. howell. hut'
    2  1250
    DB<7> s
    main::(bytecounts:3):        while (<>) {
    DB<7> x \%total_bytes
    0  HASH(0x132dc)
    'professor. hut' => HASH(0x37a34)
    'gilligan. crew. hut' => 1250
    'lovey. howell. hut' => 910
    'thurston. howell. hut' => HASH(0x2f9538)
    'lovey. howell. hut' => 1250
    哈, 有趣的事发生了.顶层散列的一个键: thurston.howell.hut 添加了一个新的条目, 于是一个新的散列引用自生成为一个空的散列。 在空散列被加好之后, 马上一个新的键/值对被加上, 标示 1250 字节从 thurston.howell.hut 传到 lovey.howell.hut. 让我们单步执行, 查看一下:
    DB<8> s
    main::(bytecounts:4):          my ($source, $destination, $bytes) = split;
    DB<8> s
    main::(bytecounts:5):          $total_bytes{$source}{$destination} += $bytes;
    DB<8> x $source, $destination, $bytes
    0  'professor. hut'
    1  'lovey. howell. hut'
    2  450
    DB<9> s
    main::(bytecounts:3):        while (<>) {
    DB<9> x \%total_bytes
    0  HASH(0x132dc)
    'professor. hut' => HASH(0x37a34)
    'gilligan. crew. hut' => 1250
    'lovey. howell. hut' => 1360
    'thurston. howell. hut' => HASH(0x2f9538)
    'lovey. howell. hut' => 1250
    现在我们添加更多的字节从 professor.hut 到 lovey.howell.hut , 我们用现存的值.这儿没有什么新鲜的, 让我们继续:
    DB<10> s
    main::(bytecounts:4):          my ($source, $destination, $bytes) = split;
    DB<10> s
    main::(bytecounts:5):          $total_bytes{$source}{$destination} += $bytes;
    DB<10> x $source, $destination, $bytes
    0  'ginger. girl. hut'
    1  'professor. hut'
    2  1218
    DB<11> s
    main::(bytecounts:3):        while (<>) {
    DB<11> x \%total_bytes
    0  HASH(0x132dc)
    'ginger. girl. hut' => HASH(0x297474)
    'professor. hut' => 1218
    'professor. hut' => HASH(0x37a34)
    'gilligan. crew. hut' => 1250
    'lovey. howell. hut' => 1360
    'thurston. howell. hut' => HASH(0x2f9538)
    'lovey. howell. hut' => 1250
    这次, 我们添加了个新的源主机, ginger.girl.hut. 注意顶层散列现在有三个元素了, 每个元素有一个不同的散列引用.好, 我们继续:
    DB<12> s
    main::(bytecounts:4):          my ($source, $destination, $bytes) = split;
    DB<12> s
    main::(bytecounts:5):          $total_bytes{$source}{$destination} += $bytes;
    DB<12> x $source, $destination, $bytes
    0  'ginger. girl. hut'
    1  'maryann. girl. hut'
    2  199
    DB<13> s
    main::(bytecounts:3):        while (<>) {
    DB<13> x \%total_bytes
    0  HASH(0x132dc)
    'ginger. girl. hut' => HASH(0x297474)
    'maryann. girl. hut' => 199
    'professor. hut' => 1218
    'professor. hut' => HASH(0x37a34)
    'gilligan. crew. hut' => 1250
    'lovey. howell. hut' => 1360
    'thurston. howell. hut' => HASH(0x2f9538)
    'lovey. howell. hut' => 1250
    现在我们已经给源发于 ginger.girl.hut. 的主机加了两个目标主机在散列中了.因为这是数据的最后一行(这次运行中), 单步执行带我们到更底的那一层:
    DB<14> s
    main::(bytecounts:8):        for my $source (sort keys %total_bytes) {
    尽这我们不能直接从括号内部验核列表值, 但我们可以显示它:
    DB<14> x sort keys %total_bytes
    0  'ginger. girl. hut'
    1  'professor. hut'
    2  'thurston. howell. hut'
    这是 foreach 语句扫描的清单.这些是所有特定日志文件中传输字节的所有源主机.下面是当我们单步执行到里层循环时发生的事情:
    DB<15> s
    main::(bytecounts:9):     for my $destination (sort keys %{ $total_bytes{$source} }) {
    当下, 我们可以由内而外精确地确定括号里的清单的值 得出的结果是什么值.我们往下看:
    DB<15> x $source
    0  'ginger. girl. hut'
    DB<16> x $total_bytes{$source}
    0  HASH(0x297474)
    'maryann. girl. hut' => 199
    'professor. hut' => 1218
    DB<18> x keys %{ $total_bytes{$source } }
    0  'maryann. girl. hut'
    1  'professor. hut'
    DB<19> x sort keys %{ $total_bytes{$source } }
    0  'maryann. girl. hut'
    1  'professor. hut'
    注意, 打印 $total_bytes{$source} 显示它是一个散列引用.这样, sort 看上去好像什么都没做, 输出的键不必以排序输出.下一步是找数据:
    DB<20> s
    main::(bytecounts:10):            print "$source => $destination:",  
    main::(bytecounts:11):              " $total_bytes{$source}{$destination} bytes\n";
    DB<20> x $source, $destination
    0  'ginger. girl. hut'
    1  'maryann. girl. hut'
    DB<21> x $total_bytes{$source}{$destination}
    0  199
    当我们用测试工具看到的, 我们可以方便地查验数据, 甚至是结构化的数据, 来帮助我们理解我们的程序。
    6. 2. 用 Data::Dumper 模块查看复杂数据
    另外一个我们可以快速查看复杂数据结构的方法是用 dump 模块打印出来.这个特别好用的 dump 模块已被收纳在 Perl 的核心发布中, 唤作: Data::Dumper. 让我们改写前面那个字节计数的程序的后半部份, 这次用 Data:Dumper 模块:
    use Data::Dumper;

    my %total_bytes;
    while (<>) {
    my ($source, $destination, $bytes) = split;
    $total_bytes{$source}{$destination} += $bytes;
    }

    print Dumper(\%total_bytes);
    Data::Dumper 模块中定义了 Dumper 子例程.这个子例子与调试工具中的x命令相似.我们可以给 Dumper 子例程一个或多个值, Dumper 会以人看得懂的格式返回这些值的内容.然而, Dumper 与调试工具中x命令的不同是 Dumper 输出的是 Perl 代码:
    myhost% perl bytecounts2 <bytecounts-in
    $VAR1 = {
    'thurston. howell. hut' => {
    'lovey. howell. hut' => 1250
    },  
    'ginger. girl. hut' => {
    'maryann. girl. hut' => 199,  
    'professor. hut' => 1218
    },  
    'professor. hut' => {
    'gilligan. crew. hut' => 1250,  
    'lovey. howell. hut' => 1360
    }
    };
    myhost%
    这段 Perl 代码很容易理解; 它显示我们有一个指向三个元素的散列的引用, 其中每个元素的值则是指向其它散列的引用的嵌套散列.我们可以求出这代码得出的散列同原来的散列值是等同的.但是, 如果你考虑通过这个将复杂数据结构保存下来, 并可以由其它的程序调用的话, 别急, 我们往下看就是了。
    Data::Dumper , 同调试工具的命令行 x 命令一样, 处理共享数据属性.比如, 我们来看看先前第五章遇到的"内存泄漏"的例子:
    use Data::Dumper;
    $Data::Dumper::Purity = 1; # declare possibly self-referencing structures
    my @data1 = qw(one won);
    my @data2 = qw(two too to);
    push @data2, \@data1;
    push @data1, \@data2;
    print Dumper(\@data1, \@data2);
    下面是程序的输出结果:
    $VAR1 = [
    'one',  
    'won',  
    [
    'two',  
    'too',  
    'to',  
    [  ]
    ]
    ];
    $VAR1->[2][3] = $VAR1;
    $VAR2 = $VAR1->[2];
    注意我们是怎么创建了两个不同的变量, 因为传给了 Dumper 两个参数.元素 $VAR1 对应对 @data1 的引用, 而 $VAR2 对应对 @data2 的引用.调试工作对值的展示类似:
    DB<1> x \@data1, \@data2
    0  ARRAY(0xf914)
    0  'one'
    1  'won'
    2  ARRAY(0x3122a8)
    0  'two'
    1  'too'
    2  'to'
    3  ARRAY(0xf914)
    -> REUSED_ADDRESS
    1  ARRAY(0x3122a8)
    -> REUSED_ADDRESS
    注意, 短语 REUSED_ADDRESS 标示数据的一些部份实际上已引用了我们已经看到的一些数据。
    6. 3. YAML
    Data::Dumper 并非在 Perl 中输出数据的惟一玩法 .Brian Ingerson 提供了 Yet Another Markup Language(YAML) 来提供一种更具可读性的(也更节省空间)的输出办法.同 Data::Dumper 的作法一样, 我们会在以后大谈特谈 YAML , 所以这里就不费话了。
    同早先的例子一样, 我们在程序中写 Data::Dumper 的地方替换成 YAML , 在用 Dumper() 函数的地方, 我们用 Dump() 函数代替。.
    use YAML;

    my %total_bytes;

    while (<>) {
    my ($source, $destination, $bytes) = split;
    $total_bytes{$source}{$destination} += $bytes;
    }

    print Dump(\%total_bytes);
    用前面提供的例子, 我们得到这样的输出:
    --- #YAML:1. 0
    ginger. girl. hut:
    maryann. girl. hut: 199
    professor. hut: 1218
    professor. hut:
    gilligan. crew. hut: 1250
    lovey. howell. hut: 1360
    thurston. howell. hut:
    lovey. howell. hut: 1250
    这比先前读起来更容易, 因为占用的屏幕比较少, 如果你的数据结构嵌套比较深的话, 就很好用。
    6. 4. 用 Storable 模块存储复杂数据结构
    我们可以取 Data::Dumper 模块中的 Dumper 子例程的输出, 将其放到一个文件中, 然后, 由另外一个程序把文件调入.在我们将这输出作为 Perl 代码来解析的时候, 我们最后可以得到两个包变量: $VAR1 和 $VAR2 , 并且这与原始的数据是一样的.这个过程就叫作调制数据:将复杂数据结构转换成一个表, 然后我们可以将其作为字节流写到文件中去, 便于以后重建这些数据。
    但是, Perl 有另外一个模块更能胜任调制数据的工作: Storable. 之所以更能胜任, 是因为相较于 Data::Dumper , Storable 产生的文件更小, 能被更快地执行 .(Storable 模块在最近版本的 Perl 的标准发布中就有, 但是, 如果你这里没有的话, 可以从 CPAN 上下载安装 .)
    与 Storable 的接口同使用 Data::Dumper 十分相似, 除了我们必须把所有东西要放到一个引用中去.比如, 让我们把存一个互相引用的数据结构:
    use Storable;
    my @data1 = qw(one won);
    my @data2 = qw(two too to);
    push @data2, \@data1;
    push @data1, \@data2;
    store [\@data1, \@data2], 'some_file';
    这步产生的内容小于 100 字节, 相比同样用 Data::Dumper 的输出, 那是非常的小.这同样也更不具可读性.你不久也会了解, 这样的话更利于 Storable 模块读这些数据 .[ *] 下一步, 我们同样用 Storable 模块读取这些数据.产生的结果是一个指向单个数组的引用.我们把读出的结果打印出来, 看看是否存得对:
  • Storable 采用的格式是缺省依赖字节顺序的结构.文档中有说明如果创建不依赖字节顺序的存储文件。
    use Storable;
    my $result = retrieve 'some_file';
    use Data::Dumper;
    $Data::Dumper::Purity = 1;
    print Dumper($result);
    下面是产生的结果:
    $VAR1 = [
    [
    'one',  
    'won',  
    [
    'two',  
    'too',  
    'to',  
    [  ]
    ]
    ],  
    [  ]
    ];
    $VAR1->[0][2][3] = $VAR1->[0];
    $VAR1->[1] = $VAR1->[0][2];
    这在功能上等同于原来的数据结构.我们现在来看看在一个数组层面上的两个数组引用.同我们先前看到的比较相近, 我们可以更显式地返回值:
    use Storable;
    my ($arr1, $arr2) = @{ retrieve 'some_file' };
    use Data::Dumper;
    $Data::Dumper::Purity = 1;
    print Dumper($arr1, $arr2);
    下面也一样:
    use Storable;
    my $result = retrieve 'some_file';
    use Data::Dumper;
    $Data::Dumper::Purity = 1;
    print Dumper(@$result);
    这样, 我们就得到:
    $VAR1 = [
    'one',  
    'won',  
    [
    'two',  
    'too',  
    'to',  
    [  ]
    ]
    ];
    $VAR1->[2][3] = $VAR1;
    $VAR2 = $VAR1->[2];
    就像我们在原来的程序里做的那样.用 Storable 模块, 我们可以先存后取.欲得更多有关 Storable 模块的信息, 可以用 perldoc Storable 来查, 老招术了:)
    6. 5. 使用 map 和 grep 操作符
    随着数据结构越来越复杂, 我们就能有更强的结构来处理那些经常性的拣选和转换的任务。 考虑这些因素, 掌握 Perl 的 grep 和 map 操作符是值得的。
    6. 6. 搞些小伎俩
    有些问题看上去好像很复杂, 可是一旦你找到解决方案后, 会发现实际上很简单.比如, 假定我们要在一个列表中把数位加起来是奇数的元素找出来, 但我们不要元素本身, 我们要它们在列表里所在的位置。
    要完成这项任务需要一些小手段 .[ *] 首先, 我们有个拣选的问题, 因此, 我们使用 grep. 我们不抓元素值本身, 找它们在列表里的位置。
  • 一条很著名的算法格言指出:"没有什么问题太复杂, 而不能被采用适当手段来解决的 ." 当然, 用些小手段会使导致程序太难懂, 所以一定有些魔术在里面。
    my @input_numbers = (1, 2, 4, 8, 16, 32, 64);
    my @indices_of_odd_digit_sums = grep { . . .  
    } 0. . $#input_numbers;
    这里, 表达式 0..$ # input_numbers 会是这个数组的索引号列表.在代码块里面, $_ 是个0~6的数字(总共7个元素).现在, 我们并不要决定是否 $_ 的数位相加为奇数.我们要决定的是在这个位置上的元素的数位相加是否为奇数.所以, 我们感兴趣的不是 $_ , 而是 $input_numbers[$_]:
    my @indices_of_odd_digit_sums = grep {
    my $number = $input_numbers[$_];
    my $sum;
    $sum += $_ for split //, $number;
    $sum % 2;
    } 0. . $#input_numbers;
    其結果是, 索引位置:0, 4, 5上的值:1, 16 , 32 符合条件.我们可以把这些索引放在数组片断里, 然后获得值:
    my @odd_digit_sums = @input_numbers[ @indices_of_odd_digit_sums ];
    这里运用 grep 或者 map 的技巧是把 $_ 值作为感兴趣的特定元素的标识来用, 比如散列的键或者数组的索引, 然后, 用这些标识, 在块中或者表达式中来访问实际的元素内容。
    这里是另一个例子:如果 @x 中的元素比 @y 中相应的元素大, 则取出来.又一次, 我们会用 $_ 来当作 @x 中的索引序号来用:
    my @bigger_indices = grep {
    if ($_ > $#y or $x[$_] > $y[$_]) {
    1; # yes, select it
    } else {
    0; # no, don't select it
    }
    } 0. . $#x;
    my @bigger = @x[@bigger_indices];
    在 grep 中, $_ 由0增长到数组 @x 的索引最大值.如果元素超出 @y 的索引边界, 则自动会选择它.否则, 我们会比较相应位置的两个元素的大小, 选择符合我们要求的。
    然而, 这样的处理还是比较繁索.我们可以简单的返回布尔表达式, 而不是1或0:
    my @bigger_indices = grep {
    $_ > $#y or $x[$_] > $y[$_];
    } 0. . $#x;
    my @bigger = @x[@bigger_indices];
    更有甚者, 我们可以不用中间数组, 简单地用 map 返回我们需要的数组:
    my @bigger = map {
    if ($_ > $#y or $x[$_] > $y[$_]) {
    $x[$_];
    } else {
    (  );
    }
    } 0. . $#x;
    如果符合条件, 则返回数组元素.如果不符合, 则返回一个空列表, 使元素蒸发。
    6. 7. 拣选和修改复杂数据
    我们可以用这些操作符在更复杂的数据结构中.我们拿第五章的装备清单来看看:
    my %provisions = (
    'The Skipper'   => [qw(blue_shirt hat jacket preserver sunscreen)],  
    'The Professor' => [qw(sunscreen water_bottle slide_rule radio)  ],  
    'Gilligan'      => [qw(red_shirt hat lucky_socks water_bottle)   ],  
    );
    在本例中, $provisions{"The Professor"} 给出一个指向数组的引用, 表示教授买的装备清单, 而 $provisions{"Gilligan"}[-1] 指向 Gilligan 要带的最后一项东西。
    我们做一个查询:谁只带了不多于五件装备在身?
    my @packed_light = grep @{ $provisions{$_} } < 5, keys %provisions;
    在此例中, $_ 变量是乘客的名字。 我们用那人名, 在放装备的数组引用中查那个人, 然后在标量环境下还原那数组, 得出装备数量, 再以此与5比较。 而且你不知道, 这个乘客就是 Gilligan.( 译者注:奇怪, 明明有两个符合条件: Gilligan 和 Professor)
    这里有个更搞的, 谁带了水瓶?
    my @all_wet = grep {
    my @items = @{ $provisions{$_} };
    grep $_ eq 'water_bottle', @items;
    } keys %provisions;
    同先前一样, 我们首先取乘客名字( keys %provisions ), 然后取那个乘客所带的所有装备列表, 之后, 在一个内部的 grep 中计数有多少元素是 water_bottle. 如果计数是0, 则表示没有, 所以返回 false 给外部 grep. 如果计数非零, 表示我们有一个水瓶, 所以返回真给外部 grep. 现在我们可以看到 Skipper 以后会是比较渴的那位, 以后没有任何缓解余地。
    我们还可以把数据转换成其它格式.比如, 将散列转换成一个对数组的引用, 其中每个数组含有两个元素。 第一个元素为乘客名字, 第二个元素则是对这个乘客的装备列表的引用:
    my @remapped_list = map {
    [ $_ => $provisions{$_} ];
    } keys %provisions;
    散列 %provisions 的键是乘客的人名。 对于每个人名, 我们构键一个两个元素的列表, 其中有名字和对应的对装备清单列表的引用。 这个列表是由匿名数组构建的, 这样我们就获得每个乘客新创建数组的引用。 三个名字进去, 三个引用出来,
  • 或者, 让我们变换一种方法。 把输入的散列转换成一系列对数组的引用.每个数组将有一个乘客的名字和一个由他身带的装备:
  • 如果我们把内层的括号去掉, 我们会最后会得出六个单品。 那不是很常用, 除非为什么建立不同的散列。
    my @person_item_pairs = map {
    my $person = $_;
    my @items = @{ $provisions{$person} };
    map [$person => $_], @items;
    } keys %provisions;
    是的, 一个 map 套一个 map. 外圈的 map 一次拣选一个乘客名字。 我们把这个名字放在变量 $person 中, 然后我们从散列中抽取装备列表。 而内层的 map 遍历装备清单, 执行一个表达式来为每个装备构建一个匿名数组引用。 每个匿名数组含有乘客的名字和他所带的装备名。
    这里, 我们已经使用 $person 来保存外圈的 $_ 临时变量。 除此以外, 我们不能同时引用外圈和内圈 map 的临时变量。
    6. 8. 习题
    在附录中找答案。
    6. 8. 1. 练习1 [ 20 分钟]
    第五章中的练习2中的程序要在每次执行的时候把整个数据文件读进内存。 然而教授每天都会有一个日志文件, 并且也不想让数据文件越来越大, 花很长的时间去执行。
    修改那个程序, 只要简单的收纳每天教授新产生的日志文件就可以计算数据文件中的即时总数。
    6. 8. 2. 练习2 [5分钟]
    要让这个程序真正有用, 还要采取其它什么功能?你只要说出来就可以了, 不必真正实现他们!
    Chapter 7. 对子程序的引用
    目前, 你已经看到了对 Perl 三种数据类型的引用:标量, 数组和散列。 同样, 我们也可以对一个子程序进行引用 (有时候我们把它叫作代码引用).
    但我们为什么要做那个呢? 与我们用指向数组的引用来实现用同一代码块在不同时间处理不同数组相同, 我们也可以使用指向子程序的引用实现在不同时间调用不同的子程序。 一样, 引用充许复杂的数据结构。 一个指向子程序的引用使子程序成为复杂数据结构的一部份成为可能。
    换一种说法, 一个变量或者一个复杂数据结构是一个程序中的数据的仓库。 一个对子程序的引用可以被想像成为一个程序动作(方法)的仓库。 本节中的例子可以向你揭示这一点。
    7. 1. 对命名子程序的引用
    Skipper 和 Gilligan 之间有一通对话:
    sub skipper_greets {
    my $person = shift;
    print "Skipper: Hey there, $person!\n";
    }

    sub gilligan_greets {
    my $person = shift;
    if ($person eq "Skipper") {
    print "Gilligan: Sir, yes, sir, $person!\n";
    } else {
    print "Gilligan: Hi, $person!\n";
    }
    }

    skipper_greets("Gilligan");
    gilligan_greets("Skipper");
    其输出结果如下:
    Skipper: Hey there, Gilligan!
    Gilligan: Sir, yes, sir, Skipper!
    到现在为止, 一切正常.然而, 你要注意, Gilligan 有两个不同的行为, 这依赖于它是否对 Skipper 说话, 还是对其它人说。
    现在, 教授到屋子里来了.这两个 Minnow 船员都向新来者问候:
    skipper_greets('Professor');
    gilligan_greets('Professor');
    其输出是:
    Skipper: Hey there, Professor!
    Gilligan: Hi, Professor!
    这下轮到教授要作出反映了:
    sub professor_greets {
    my $person = shift;
    print "Professor: By my calculations, you must be $person!\n";
    }

    professor_greets('Gilligan');
    professor_greets('Skipper');
    输出结果是:
    Professor: By my calculations, you must be Gilligan!
    Professor: By my calculations, you must be Skipper!
    咻!这程序写得真费事, 而且一点也不抽象。 如果每个乘客的行为以不同的子程序命名, 而且每个乘客都进来的话, 我们要不得不写多少程序来对付啊。 当然, 我们可以用这样难以维护的代码来处理这件事, 但是, 就像我们在数组和散列上做的那样, 我们只要加一些小技巧, 就可以简化处理过程。
    首先, 让我们用"取引用"操作符。 实际上这也不用介绍, 因为它与之前的反斜杠长得非常像:
    my $ref_to_greeter = \&skipper_greets;
    我们现在取子程序 skipper_greets() 的引用。 注意, 前导的&字符在这里是强制必须的, 而其后的小括号也不要加。 Perl 会把对这个子程序的引用放到变量 $ref_to_greeter 中, 而且, 同对其它的引用一样, 它适合于任何可以使用标量的地方。
    还原一个对子程序的引用的惟一目的就是:为了调用它。 还原对代码的引用同还原对其它数据类型的引用是相似的。 首先, 我们可以采用我们在听说引用以前写过的方法来处理(包括前导的&号)
    & skipper_greets ( 'Gilligan' )
    下一步, 我们把子程序的名字用引用变量名字外套花括号代替:
    & { $ref_to_greeter } ( 'Gilligan' )
    就是它了.这个方法调用了当前 $ref_to_greeter 变量中保存的那个对子程序的引用, 并给它传了一个字串参数: Gilligan.
    不过, 这样子是不是太丑陋了? 幸运的是同样的简化规则也能应用于对子程序的引用。 如果花括号里是简单的标量变量的话, 花括号可以去掉:
    & $ref_to_greeter ( 'Gilligan' )
    我们也可以把它转换成带箭头的格式:
    $ref_to_greeter -> ( 'Gilligan' )
    最后一种形式特别适用于在一个大数据结构中进行代码引用, 你一会儿就会看到。
    如果让 Gilligan 和 Skipper 向教授问好, 我们只需要迭代调用子程序就可以了:
    for my $greet (\&skipper_greets, \&gilligan_greets) {
    $greet->('Professor');
    }
    首先, 在小括号里面, 我们建立一个两个元素的列表, 而且这两个元素各保存一个对代码块的引用。 而每个对代码的引用都各自被还原, 调用相应的子程序并传入"教授"字串。
    我们已经看到了把代码引用作为一个列表中的元素。 那我们是否可以把代码引用放到一个大的数据结构中呢? 当然可以. 我们可以创建一个表, 来让乘客与其向他们问候动作对应, 我们可以重写之前的例子:
    sub skipper_greets {
    my $person = shift;
    print "Skipper: Hey there, $person!\n";
    }

    sub gilligan_greets {
    my $person = shift;
    if ($person eq 'Skipper') {
    print "Gilligan: Sir, yes, sir, $person!\n";
    } else {
    print "Gilligan: Hi, $person!\n";
    }
    }

    sub professor_greets {
    my $person = shift;
    print "Professor: By my calculations, you must be $person!\n";
    }

    my %greets = (
    Gilligan  => \&gilligan_greets,  
    Skipper   => \&skipper_greets,  
    Professor => \&professor_greets,  
    );

    for my $person (qw(Skipper Gilligan)) {
    $greets{$person}->('Professor');
    }
    注意, 变量 $person 是字名, 他们以前在散列中查找代码引用。 然后我们还原那个代码引用, 并传给他要问候的人名, 获得正确的问候行为, 输出结果如下:
    Skipper: Hey there, Professor!
    Gilligan: Hi, Professor!
    现在我们可以让大家互相问候了, 在一个十分友好的房间:
    sub skipper_greets {
    my $person = shift;
    print "Skipper: Hey there, $person!\n";
    }

    sub gilligan_greets {
    my $person = shift;
    if ($person eq 'Skipper') {
    print "Gilligan: Sir, yes, sir, $person!\n";
    } else {
    print "Gilligan: Hi, $person!\n";
    }
    }

    sub professor_greets {
    my $person = shift;
    print "Professor: By my calculations, you must be $person!\n";
    }

    my %greets = (
    Gilligan  => \&gilligan_greets,  
    Skipper   => \&skipper_greets,  
    Professor => \&professor_greets,  
    );

    my @everyone = sort keys %greets;
    for my $greeter (@everyone) {
    for my $greeted (@everyone) {
    $greets{$greeter}->($greeted)
    unless $greeter eq $greeted; # no talking to yourself
    }
    }
    其输出结果如下:
    Gilligan: Hi, Professor!
    Gilligan: Sir, yes, sir, Skipper!
    Professor: By my calculations, you must be Gilligan!
    Professor: By my calculations, you must be Skipper!
    Skipper: Hey there, Gilligan!
    Skipper: Hey there, Professor!
    呣.这里有些复杂.让我们使他们一个个进来。
    sub skipper_greets {
    my $person = shift;
    print "Skipper: Hey there, $person!\n";
    }

    sub gilligan_greets {
    my $person = shift;
    if ($person eq 'Skipper') {
    print "Gilligan: Sir, yes, sir, $person!\n";
    } else {
    print "Gilligan: Hi, $person!\n";
    }
    }

    sub professor_greets {
    my $person = shift;
    print "Professor: By my calculations, you must be $person!\n";
    }

    my %greets = (
    Gilligan  => \&gilligan_greets,  
    Skipper   => \&skipper_greets,  
    Professor => \&professor_greets,  
    );

    my @room; # initially empty
    for my $person (qw(Gilligan Skipper Professor)) {
    print "\n";
    print "$person walks into the room. \n";
    for my $room_person (@room) {
    $greets{$person}->($room_person); # speaks
    $greets{$room_person}->($person); # gets reply
    }
    push @room, $person; # come in, get comfy
    }
    输出结果如下, 岛上典型的一天是这样的:
    Gilligan walks into the room.  

    Skipper walks into the room.  
    Skipper: Hey there, Gilligan!
    Gilligan: Sir, yes, sir, Skipper!

    Professor walks into the room.  
    Professor: By my calculations, you must be Gilligan!
    Gilligan: Hi, Professor!
    Professor: By my calculations, you must be Skipper!
    Skipper: Hey there, Professor!
    7. 2. 匿名子程序
    在最后那个例子中, 我们并没有显式的调用子程序, 如 profressor_greets( ), 我们只是间接通过代码引用来调用它.所以, 为了初始化一个数据结构, 我们仅仅因为在其它地方使用而给子程序提供名字纯属浪费脑筋.但是, 就像我们可以建立匿名数组和匿名散列一样, 我们也可能建立一个匿名的子程序!
    让我们再添加一个岛上的居民: Ginger. 但是不同于用命名子程序来给她定义行为, 我们可能建立一个匿名子程序:
    my $ginger = sub {
    my $person = shift;
    print "Ginger: (in a sultry voice) Well hello, $person!\n";
    };
    $ginger->('Skipper');
    一个匿名子程序看上去像一个平常的子程序声明, 只是没有名字(或原型声明)在 sub 关键字和紧随的代码块之间.这同样是声明的一部份, 所以在大多数情况下, 我们需要结尾的分号, 或者其它的表达式分隔符。
    sub {. . . body of subroutine. . . };
    $ginger 的值是一个代码引用, 就像我们在其后定义了子程序一样, 然后返回引用给它.当我们到达最后一行, 我们看到:
    Ginger: (in a sultry voice) Well hello, Skipper!
    尽管我们可以把代码引用作为标量值保存, 但我们也可以直接把 sub {...} 代码块直接放在初始化的 greetings 散列中:
    my %greets = (

    Skipper => sub {
    my $person = shift;
    print "Skipper: Hey there, $person!\n";
    },  

    Gilligan => sub {
    my $person = shift;
    if ($person eq 'Skipper') {
    print "Gilligan: Sir, yes, sir, $person!\n";
    } else {
    print "Gilligan: Hi, $person!\n";
    }
    },  

    Professor => sub {
    my $person = shift;
    print "Professor: By my calculations, you must be $person!\n";
    },  

    Ginger => sub {
    my $person = shift;
    print "Ginger: (in a sultry voice) Well hello, $person!\n";
    },  

    );

    my @room; # initially empty
    for my $person (qw(Gilligan Skipper Professor Ginger)) {
    print "\n";
    print "$person walks into the room. \n";
    for my $room_person (@room) {
    $greets{$person}->($room_person); # speaks
    $greets{$room_person}->($person); # gets reply
    }
    push @room, $person; # come in, get comfy
    }
    注意我们简化了多少行代码.子程序的定义现在直接放在数据结构中.结果相当直观:
    Gilligan walks into the room.  

    Skipper walks into the room.  
    Skipper: Hey there, Gilligan!
    Gilligan: Sir, yes, sir, Skipper!

    Professor walks into the room.  
    Professor: By my calculations, you must be Gilligan!
    Gilligan: Hi, Professor!
    Professor: By my calculations, you must be Skipper!
    Skipper: Hey there, Professor!

    Ginger walks into the room.  
    Ginger: (in a sultry voice) Well hello, Gilligan!
    Gilligan: Hi, Ginger!
    Ginger: (in a sultry voice) Well hello, Skipper!
    Skipper: Hey there, Ginger!
    Ginger: (in a sultry voice) Well hello, Professor!
    Professor: By my calculations, you must be Ginger!
    添加更多的旅客就变成了简单的把问候行为放到散列中, 并把他们加入到进入房间的人名清单中.我们在效率上得到扩展, 因为我们把程序行为保存为数据, 并通过它可以查找和迭代, 这要感谢友好的子程序引用。
    7. 3. 回调
    一个对子程序的引用, 经常被用来做回调.一个回调定义了在一个算法中当子程序运行到了一个特定地点时, 程序应该做什么。
    举个例子来说, File::Find 模块导出一个 find 子程序, 它被用来以非常可移植的方式高效地遍历给定文件系统的层次结构.在这个简单的例子中, 我们传给 find 子程序两个参数:一个表示目录搜索开始点的字串, 另一个是对子程序引用.这子程序会对从给定的起始目录开始, 通过递归搜索的方法, 找到其下的每个文件或目录, 并对它们"干些什么":
    use File::Find;
    sub what_to_do {
    print "$File::Find::name found\n";
    }
    my @starting_directories = qw(. );

    find(\&what_to_do, @starting_directories);
    在例子中, find 程序开始于当前目录(.), 并且向下找到所有的目录和文件.对于找到的每个项目, 我们会调用那个子程序 what_to_do() , 把一些全局变量传进去.一般来说全局变量 :$File::Find::name 是项目的全路径名(以开始搜索的目录为起点)
    在此例中, 我们传了两项数据(开始搜索的目录)和给 find 子程序的行为子程序作为参数。
    在这里, 子程序只使用一次, 为此而起个名字的做法好像有些蠢, 所以我们还可以把子程序做成匿名子程序, 如:
    use File::Find;
    my @starting_directories = qw(. );

    find(
    sub {
    print "$File::Find::name found\n";
    },  
    @starting_directories,  
    );
    7. 4. 闭包
    我们还可以用 File::Find 来查其它一些文件属性, 比如它们的文件大小.为了回调方便, 当前目录被设为文件所在的目录, 目录中的文件名也放在缺省变量 $_ 中.:
    刚才你可能已经注意到了, 在前面的代码中, 我们用 $File::Find::name 来返回文件的名字.所以现在哪个名字是真实的呢? $_ 或者 $File::Find::name ? $File::Find::name 是给出文件自起始搜索目录的相对路径名, 而在回调程序中, 工作目录就是项目所在目录.比如, 假定我们要在当前目录找些文件, 所以我们给出它( "." )作为要搜索的目录.如果我们当前目录是 /usr , 则程序会往下找这个目录.然后程序找到 /usr/bin/perl , 此时当前目录(在回调程序中)是 /usr/bin. 变量 $_ 保存了 perl , 而 $File::Find::name 保存 ./bin/perl , 就是相对起始搜索目录的相对路径。
    这一切说明对文件的查验, 如 -s , 是自动应用在即时找到的那个文件上的.尽管这很方便, 回调程序里的当前目录还是与搜索目录不同。
    如果我们要用 File::Find 来累加所找到的所有文件的大小的话, 应该如何做呢?回调子程序不能有参数, 而调用者也忽略回调子程序返回的结果.但这没有关系.在还原后, 一个子程序引用可以看到所有指向子程序的引用的可见词法变量.例如:
    use File::Find;

    my $total_size = 0;
    find(sub { $total_size += -s if -f }, '. ');
    print $total_size, "\n";
    同以前一样, 我们调用 find 子程序时传给它两个参数:一个指向匿名子程序的引用和一个起始搜索目录.在它找到文件在目录中时(或其子目录中时), 它会调用匿名子程序。
    注意, 匿名子程序会访问 $total_size 变量.我们定义这个变量是在匿名子程序范围之外的, 但是对于匿名子程序来说还是可见的.所以, 尽管 find 调用那个回调的匿名子程序(并且不会直接访问 $total_size ), 回调的匿名子程序会访问并且更新变量。
    那种能访问所有我们声明的在运行时存在的词法变量的子程序, 我们叫它闭包(一个从数字世界借过一的词).在 Perl 术语中, 一个闭包就是指一种能引用在程序范围之外的词法变量的子程序。
    更有甚者, 在闭包内部访问变量能保证只要匿名子程序引用存在, 变量的值就能保留.比如, 让我们对输出文件计数:

  • 这里的代码好像在行尾给 $callback 赋值时多出一个分号, 不是吗? But remember, the construct sub {. . . } is an expression. 值(一段代码引用)赋给变量 $callback , 所以语句后面有分号。 在花括号定义的匿名子程序后面加上适当的标点符号是很容易被遗忘的。
    use File::Find;

    my $callback;
    {
    my $count = 0;
    $callback = sub { print ++$count, ": $File::Find::name\n" };
    }
    find($callback, '. ');
    这儿, 我们定义了一个保存有回调子程序引用的变量.我们不能在裸块中定义这个变量(其后的块并非 Perl 语法构建的一部份), 或者 Perl 在块结束时会回收它.之后, 词法变量 $count 变量会初始化为 0. 我们声明一个匿名子程序并把其引用给 $callback. 这个子程序就是个闭包, 因它指向词法变量 $count.
    在裸块的结尾, 变量 $count 跑出程序范围.然而, 因为这个变量仍旧被 $callback 所指向的匿名子程序引用, 所以此变量作为一个匿名的标量变量仍旧活着 .[ *] 当 find 子程序调用回调匿名子程序的时候, 先前被称为 $count 的这个变量的值继续从1到2到3地增加。
    7. 5. 从子程序中返回一个子程序
    尽管定义回调时用裸块的话, 会工作得很好, 但是如果让子程序返回一个对子程序的引用的做法更加有用:
    use File::Find;

    sub create_find_callback_that_counts {
    my $count = 0;
    return sub { print ++$count, ": $File::Find::name\n" };
    }

    my $callback = create_find_callback_that_counts(  );
    find($callback, '. ');
    上面那个程序与之前的程序有同样的功能, 只稍稍做了些改动.当我们调用 create_find_callback_that_counts() 的时候, 我们会把词法变量 $count 置为零.子程序返回的是一个对匿名子程序的引用, 它同样是一个闭包, 因为这个闭包访问 $count 变量.尽管 $count 在 create_find_callback_that_counts( ) 子程序结束后跑出范围, 但仍旧有一个绑定它和返回的子程序引用, 所以, 变量会一直存在, 直到子程序引用最后被丢弃。
    如果我们重用回调, 相同的变量仍会保留它最近的值.初始值是在最初调用子程序的时候创建的( create_find_callback_that_counts ), 并不是回调的匿名子程序中:
    use File::Find;

    sub create_find_callback_that_counts {
    my $count = 0;
    return sub { print ++$count, ": $File::Find::name\n" };
    }

    my $callback = create_find_callback_that_counts(  );
    print "my bin:\n";
    find($callback, 'bin');
    print "my lib:\n";
    find($callback, 'lib');
    下面这个例子从1开始为整个 bin 目录下的文件计数, 接着前面的数值, 继续为 lib 目录下所有的文件计数.在两个程序用同样一个 $count 变量的值.然而, 如果我们调用两次 create_find_callback_that_counts( ), 我们会得到两个不同的 $count 变量的值:
    use File::Find;

    sub create_find_callback_that_counts {
    my $count = 0;
    return sub { print ++$count, ": $File::Find::name\n" };
    }

    my $callback1 = create_find_callback_that_counts(  );
    my $callback2 = create_find_callback_that_counts(  );
    print "my bin:\n";
    find($callback1, 'bin');
    print "my lib:\n";
    find($callback2, 'lib');
    上面的例子中, 我们有两个分开的 $count 变量, 各自被他们自己的回调子程序访问。
    那我们怎么得到所有找到的文件的总的文件大小呢?在前一章的例子中, 我们的作法是让 $total_size 变量在范围内可见.如果我们把 $total_size 的定义放在返回回调引用的子程序里的话, 我们将无法访问这个变量.但是我们可以耍个小花招.即我们可以决定, 只要收到任何参数, 我们就不调用回调子程序, 这样的话, 如果子程序收到一个参数, 我们就让它返回总字节数:
    use File::Find;

    sub create_find_callback_that_sums_the_size {
    my $total_size = 0;
    return sub {
    if (@_) { # it's our dummy invocation
    return $total_size;
    } else { # it's a callback from File::Find:
    $total_size += -s if -f;
    }
    };
    }

    my $callback = create_find_callback_that_sums_the_size(  );
    find($callback, 'bin');
    my $total_size = $callback->('dummy'); # dummy parameter to get size
    print "total size of bin is $total_size\n";
    当然, 用区分参数存在或者不存在的来决定程序行为不是一个通用的解决方案.还好, 我们可以在 create_find_callback_that_counts( )中创建多个子程序。
    use File::Find;

    sub create_find_callbacks_that_sum_the_size {
    my $total_size = 0;
    return(sub { $total_size += -s if -f }, sub { return $total_size });
    }

    my ($count_em, $get_results) = create_find_callbacks_that_sum_the_size(  );
    find($count_em, 'bin');
    my $total_size = &$get_results(  );
    print "total size of bin is $total_size\n";
    因为创建的两个匿名子程序在同一个范围内, 所以他们都访问相同的 $total_size 变量.尽管在我们调用任一个匿名子程序之前, 这个变量已经跑出范围, 但他们仍能共享这个变量并且可以用这个变量交换计算结果。
    在返回这两个匿名子程序引用时, 并不执行他们.这时仅仅返回程序引用而已.真正调用是在他们作为回调程序被执行或还原后被执行时。
    那我们多执行几遍这个新的子程序会怎么样?
    use File::Find;

    sub create_find_callbacks_that_sum_the_size {
    my $total_size = 0;
    return(sub { $total_size += -s if -f }, sub { return $total_size });
    }

    ## set up the subroutines
    my %subs;
    foreach my $dir (qw(bin lib man)) {
    my ($callback, $getter) = create_find_callbacks_that_sum_the_size(  );
    $subs{$dir}{CALLBACK}   = $callback;
    $subs{$dir}{GETTER}     = $getter;
    }

    ## gather the data
    for (keys %subs) {
    find($subs{$_}{CALLBACK}, $_);
    }

    ## show the data
    for (sort keys %subs) {
    my $sum = $subs{$_}{GETTER}->(  );
    print "$_ has $sum bytes\n";
    }
    在创建子程序的程序片断中, 我们创建了回调/求总对的三个实例.每一个回调程序都有相应的求总程序.接下来, 在取得文件字节总数的程序片断中, 我们三次用相应的回调匿名子程序的引用调用 find 程序, 这更新了与这三个回调匿名子程序关联的三个独立的 $total_size 变量.最后, 在展示结果的程序片断中, 我们调用返回字节求总的那个匿名子程序来取得结果。
    六个匿名子程序(他们共享了三个 $total_size 变量)是引用计数的.当我们修改 %subs 或者它跑出范围时, 引用计数减少, 重用他们包含的数据.(如果这些数据同样引用其它数据, 那么那些数据的引用计数也相应减少.)
    7. 6. 作为输入参数的闭包变量
    上一章的例子展示了闭包中变量如何被修改的, 而闭包变量还可以被用来初始化变量或给匿名子程序提供参数输入(类似静态局部变量).比如, 我们来写一个子程序来创建 File::Find 回调, 打印出所有超过一定大小的文件名:
    use File::Find;

    sub print_bigger_than {
    my $minimum_size = shift;
    return sub { print "$File::Find::name\n" if -f and -s >= $minimum_size };
    }

    my $bigger_than_1024 = print_bigger_than(1024);
    find($bigger_than_1024, 'bin');
    我们把 1024 作为参数传给子程序 print_bigger_than , 这个子程序将其传给词法变量 $minimum_size. 因为我们在匿名子程序中引用这个变量, 然后再返回匿名子程序的引用, 所以这成为一个闭包变量, 只要匿名子程序引用它, 它的值就一直保持着.同样, 多次调用这个程序会为 $minimum_size 锁定不同的值, 每个都和他们各自的匿名子程序引用绑定。
    闭包是仅对词法变量跑出程序范围时"关闭 ". 因为一个包变量(因为是全局)总是在范围之内, 一个闭包不可能"关闭"一个包变量.对于所有的子程序来说, 他们都引用全局变量的同一个实例。
    7. 7. 闭包变量用作静态局部变量
    要做成一个闭包, 并不一定非要搞成匿名子程序.如果一个命名子程序访问那些跑出范围的词法变量, 其作用就如同你用匿名子程序一样.比如, 考虑一下两个为 Gilligan 计算椰子的子程序
    {
    my $count;
    sub count_one { ++$count }
    sub count_so_far { return $count }
    }
    如果我们把这短代码放到程序开始, 我们在一个裸块里声明了了变量 $count , 然后两个子程序引用这个变量, 于是就成了闭包.然而, 因为它们都有名字, 并且会保留名字直到块结束(就像所有的命名子程序一样.) 因为子程序访问声明在范围外的变量, 它们成为闭包并且因此可以在程序的生命周期内继续访问 $count.
    所以, 经过几次调用, 我们可以看到计数增长:
    count_one(  );
    count_one(  );
    count_one(  );
    print 'we have seen ', count_so_far(  ), " coconuts!\n";
    在几次调用 count_one() 或 count_so_far() , $count 会保留其原来的值, 但程序里其它部份的代码是不能访问 $count 的。
    在C语言里, 这被称作静态本地变量: 一个变量仅仅在程序子程序的一小块代码中可见, 但会在程序的生命周期内保留其值, 甚至在那些子程序的数次调用中也保留值。
    那递减会如何呢?大概应该如此:
    {
    my $countdown = 10;
    sub count_down { $countdown-- }
    sub count_remaining { $countdown }
    }

    count_down(  );
    count_down(  );
    count_down(  );
    print 'we're down to ', count_remaining(  ), " coconuts!\n";
    就是说, 只要我们把块放在程序开始, 放在任何 count_donw() 或 count_remaining() 之前就可以.为什么呢?
    如果把裸块放在那些调用之后的话就不会工作, 因为有两个功能部分牵涉到下面这行:
    my $countdown = 10;
    一个功能部份是 $countdown 的声明是作为一个词法变量.这部份是在程序被编译阶段被解释并处理的.第二个功能部分是把 10 赋值给一块分配的内存.这部份是 Perl 执行代码时处理的.除非 Perl 在运行阶段执行这些代码, 否则变量的初始是未定义值。
    一种解决方法是把代码放进 BEGIN 块:
    BEGIN {
    my $countdown = 10;
    sub count_down { $countdown-- }
    sub count_remaining { $countdown }
    }
    BEGIN 块会告诉 Perl 编译器只要这个块被成功解释了(在编译阶段), 就马上去运行这个块.假定这个块不会导致致命错误, 编译器就继续下面的块.块自身也会被丢弃, 保证其中的代码在程序中被精确地只执行一次, 甚至代码在语法上在一个循环或子程序中。
    7. 8. 练习
    答案附件找。
    7. 8. 1. 练习 [50 分钟]
    周一中午, 教授修改了一些文件, 不过现在他忘了改了哪些文件。 这种事情老是发生.他要你写个程序, 叫 "gather_mtime_between". 这个程序接受开始和结束时间作为参数, 返回一对代码引用。 第一个会被 File::Find 模块用来收集那些修改时间在两个时间点之间的文件名;第二个将返回所有文件列表。
    这里有一些代码; 它应该列出那些在最近的周一之后修改过的文件, 当然, 你可以容易的修改它来适应不同的日期。 (你不必写出所有的代码。 这个程序应该可以在 O'Reilly 网站下载, 名字叫 ex6-1.plx )
    暗示:你可以用如下代码找到一个文件的时间戳:
    my $timestamp = (stat $file_name)[9];
    因为是片断, 记住上面这段代码中那些小括号是必须要加的.别忘记回调里的工作目录不一定是 find 程序调用的起始目录。
    use File::Find;
    use Time::Local;

    my $target_dow = 1;        # Sunday is 0, Monday is 1,. . .  
    my @starting_directories = (". ");

    my $seconds_per_day = 24 * 60 * 60;
    my($sec, $min, $hour, $day, $mon, $yr, $dow) = localtime;
    my $start = timelocal(0, 0, 0, $day, $mon, $yr);        # midnight today
    while ($dow != $target_dow) {
    # Back up one day
    $start -= $seconds_per_day;        # hope no DST! :-)
    if (--$dow < 0) {
    $dow += 7;
    }
    }
    my $stop = $start + $seconds_per_day;

    my($gather, $yield)  = gather_mtime_between($start, $stop);
    find($gather, @starting_directories);
    my @files = $yield->(  );

    for my $file (@files) {
    my $mtime = (stat $file)[9];        # mtime via slice
    my $when = localtime $mtime;
    print "$when: $file\n";
    }
    注意关于 DST 的注释.在世界上的其它部份, 在夏时制的白天可能有出入, 并不一定是 86 , 400 秒.这个程序忽略了这个问题, 但是一些更"顶真"的程序员可能会把这种情况适当考虑进去。
    Chapter 8. 引用文件句柄
    我们已经看到如何通过引用传递数组, 散列和子程序, 并通过一定的手段来解决一定复杂度的问题。 同样, 我们也可以将文件句柄存到引用里。 让我们看如何用新办法来解决老问题。
    8. 1. 旧的方法
    在以往的日子里, Perl 用裸字来代表文件句柄。 文件句柄是另一种 Perl 的数据类型, 尽管人们对此讨论不多, 因为它也没有专门的符号标注。 你大概已经许多用裸字文件句柄的代码, 如:
    open LOG_FH, '>> castaways. log'
    or die "Could not open castaways. log: $!";
    如果我们要同程序的其它部份, 比如库, 共享这些文件句柄该如何做呢? 我们大概见过一些讨巧的办法, 用 typeglob 或一个对 typeglob 的引用来处理。
    log_message( *LOG_FH, 'The Globetrotters are stranded with us!' );

    log_message( *LOG_FH, 'An astronaut passes overhead' );
    在 log_message() 子程序中, 我们从参数列表中取下第一个元素, 并且存在另一个 typeglob 中.不需要详述太多的细节, 一个 typeglob 存储了包里面所有变量的名字的指针.当我们把一个 typeglob 赋值给另一个的时候, 我们就为相同的数据创建了一个别名.这样我们现在就可以用另外一个名字来访问这块数据的文件句柄了.如此, 当我们把名字当文件句柄用时, Perl 知道在 typeglob 中找到这个名字的文件句柄部份.如果文件句柄已经有符号, 那会更容易。
    sub log_message {
    local *FH = shift;

    print FH @_, "\n";
    }
    注意这里 local 的用法.一个 typeglob 同符号表一起工作, 这意味着它处理包变量.包变量不能是词法变量, 所以我们不能用 my. 因为我们不能与程序其它部份的以 FH 命名的句柄混淆, 我们必须用 local 告诉 Perl 这个 FH 是在 log_message 子程序中用作临时变量, 用完即丢, Perl 会把原来的 FH 句柄恢复, 好像没有发生过一样。
    如果这一切作法让你大为紧张, 希望没有这种事该多好, 可以。 我们不用作这种事情! 就是因为现在有更好的方法, 所以我们把这节叫作"旧的方法 ". 我们假装没有这节吧, 直接跳到下一节吧。
    8. 2. 改进的方法
    从 Perl 5.6 版本开始, 可以用一个通常的标量变量打开一个文件句柄。 相比于使用裸字来代表句柄名字, 我们可以用一个含有空值的标量变量代表。
    my $log_fh;
    open $log_fh, '>> castaways. log'
    or die "Could not open castaways. log: $!";
    如果标量已经有值的话, Perl 会报错, 因为 Perl 不会冲掉你的数据:
    my $log_fh = 5;
    open $log_fh, '>> castaways. log'
    or die "Could not open castaways. log: $!";
    print $log_fh "We need more coconuts!\n";   # doesn't work
    当然, Perl 的信条是尽量一步到位.我们可以在 open 语句中声明变量。 一开始你可能觉得好笑, 但用了两次(好吧, 多次)之后, 你会习惯并喜欢使用这种方法。 ' open my $log_fh, '>> castaways. log' or die "Could not open castaways. log: $!";
    当我们要把内容打印到文件句柄的时候, 我们就把标量变量代替以前裸字的位置.注意文件句柄后面一样没有逗号。
    print $log_fh "We have no bananas today!\n";
    尽管下面的代码可能你看上去比较搞笑, 或者, 如果你不觉得搞笑, 可能那些后来读到你的代码的人也觉得看上去比较怪异。 在《 Perl 最佳实践》这本书中, Damian Conway 建议在文件句柄部份要加上大括号以显明你的态度。 这样的语法使它看上去更新 grep 或者 map 的内嵌块形式。
    print {$log_fh} "We have no bananas today!\n";
    现在我们就可以把文件句柄引用当作标量一样到处使用了.我们不必不得不搞些怪手法来处理问题。
    log_message( $log_fh, 'My name is Mr. Ed' );

    sub log_message {
    my $fh = shift;

    print $fh @_, "\n";
    }
    我们同样可以以读的方式创建文件句柄引用.我们只要简单地在第二个参数放上适当的文件名即可:
    open my $fh, "castaways. log"
    or die "Could not open castaways. log: $!";
    现在, 我们就可以在行输入操作符中裸字的位置替换成标量。 之前, 我们会看到我们把裸字放在尖括号里:
    while( <LOG_FH> ) {. . . }
    现在我们用标量代替:
    while( <$log_fh> ) {. . . }
    一般情况下, 所有用裸这字代表文件句柄的地方, 我们都可以用含有文件句柄引用的标量变量代替。
    在以上的各种使用方式中, 只要这个标量变量跑出范围(或者我们赋另一个值给它), Perl 就会自动关闭文件。 我们不必自己显式地关闭文件。
    8. 3. 更上一层楼
    到目前为此, 我们的例子展示的 open 都是两个参数的形式, 但实际上有个隐含之处: 文件的打开方式是和文件名都放在第二个参数的位置上的。 这意味着我们在一个字串里表达了两种不同的意义, 而且我们不得不相信 Perl 一定会很好的区分。
    为了解决这个问题, 我们可以把第两个参数分开:
    open my $log_fh, '>>', 'castaways. log'
    or die "Could not open castaways. log: $!";
    这种三参数的形式有可以利用 Perl 的 IO 过滤器的优点.这里我们不涉及太多 .[ *] 在 perlfunc 中 open 函数的条目有 400 行之多, 尽管在它自己的 perldoc 教材和 perlopentut 中也有说明。
    8. 4. IO::Handle 模块
    在帷幕之后, Perl 实际上用调用 IO::Handle 模块 模块来变这个戏法的, 所以, 我们的文件句柄实际上是一个对象。
  • IO::Handle 模块 包是输入输出的基础类, 所以它处理许多不仅仅有关文件的操作。
  • 你是否曾经疑惑为什么 print 语句的文件句柄后面为什么不加逗号?这的确是对象的一种间接标记 (这是我们还没有提及的, 除非你已经在读这个脚注前先读完了本书, 就像我们在前言里说的那样!
    除非你正在创建新的 IO 模块, 一般来说你不会直接调用 IO::Handle 模块 模块.相反的, 我们可以用一些建立在 IO::Handle 模块 模块之上更加好用的模块。 我们还没有告诉你有关面向对象编程的知识(在第 11 章, 所以我们一定会说的), 但目前情况下, 你只要跟着本书的例子就可以了。
    这些模块做的工作同 Perl 自带的 open 相同(这要依赖你使用的 Perl 的版本), 但是, 当我们要尽可能晚的决定模块处理输入输出时, 用他们就很顺手。 To switch the behavior , we simply change the module name. To switch the behavior, we simply change the module name. 代替我们使用内建的 open , 我们使用模块接口。
    8. 4. 1. IO::File
    IO::File 模块是 IO::Handle 模块 模块的子类, 用来同文件打交道。 它是随标准 Perl 一起发布的, 所以你应该已经有这些模块了。 可以用多种方法来创建一个 IO::File 对象。
    我们可以用单参数构造器的形式创建文件句柄的引用。 我们可以通过检查返回值是否为空来判断文件句柄引用创建是否成功。
    use IO::File;

    my $fh = IO::File->new( '> castaways. log' )
    or die "Could not create filehandle: $!";
    如果不喜欢用这种方式(因为同样原因也不喜欢标准 open ), 你可以用另一种调用约定。 可选的第二个参数是文件句柄的打开方式。 []
    [] 这些都是 ANSI C 的 fopen 的文件打开方式的字串.你在内建的 open 中也可以使用。 实际上, IO::File 在后台也是使用内建的 open 函数的。
    my $read_fh  = IO::File->new( 'castaways. log', 'r' );
    my $write_fh = IO::File->new( 'castaways. log', 'w' );
    用打开模式掩码可以对文件进行更细致的控制 .IO::File 模块提供这些掩码的定义。
    my $append_fh = IO::File->new( 'castaways. log', O_WRONLY|O_APPEND );
    除了打开命名文件, 我们可能要打开匿名的临时文件。 对于支持这种文件的系统, 我们只要简单地以读写文件名柄建立一个新对象就可以了。
    my $temp_fh = IO::File->new_tmpfile;
    在以前, Perl 会在这些标量变量跑出范围的时候把文件关闭, 不过, 如果你还不放心, 我们可以显式关闭文件。
    $temp_fh->close;

    undef $append_fh;
    8. 4. 2. 匿名 IO:File 对象
    如果我们不是把 IO::File 返回的对象放到一个标量变量中, 那么操作起来需要一些对语法的小小变动。 比如这么一个例子, 我们现在要把所有符合* .input 的文件都拷贝到相应的* .output 文件中, 但我们要并行的进行拷贝。 首先, 我们打开所有的文件, 包括输入和输出的双方:'
    my @handlepairs;

    foreach my $file ( glob( '*. input' ) ) {
    (my $out = $file) =~ s/\. input$/. output/;
    push @handlepairs, [
    (IO::File->new('<$file') || die),  
    (IO::File->new('>$out') || die),  
    ];
    }
    好, 现在我们有了一个保存数组引用的数组, 这个数组中所每个元素都是 IO::File 对象.现在, 让我们把输入文件的数据灌入输出文件中去。
    while (@handlepairs) {
    @handlepairs = grep {
    if (defined(my $line = $_->[0]->getline)) {
    print { $_->[1] } $line;
    } else {
    0;
    }
    } @handlepairs;
    }
    只要还有文件对, 我们就会通过 grep 结构不断把列表传过来:
    @handlepairs = grep { CONDITION } @handlepairs;
    在每个传输过程中, 只有那些通过 grep 条件测试的句柄对才会留下.在条件测试中, 我们拿句柄对中第一个元素并读取其中内容.如果处理成功, 则向句柄对中第二个元素(对应的输出句柄)写行.如果打印成功, 它返回 true , 这样就让 grep 知道我们要保留那个句柄对.只要打印失败或取行返回未定义值, grep 就会认做 false 并放弃那个句柄对.放弃句柄对自动就关闭了输入输出句柄.太妙了!
    注意, 我们不能用更传统的句柄读或者写操作, 因为句柄的读写不能在简单标量变量中.我们可以通过重写那个循环, 看看拷贝句柄是否更方便:
    while (@handlepairs) {
    @handlepairs = grep {
    my ($IN, $OUT) = @$_;
    if (defined(my $line = <$IN>)) {
    print $OUT $line;
    } else {
    0;
    }
    } @handlepairs;
    }
    这样的写法理论上应该很好.大多数情况下, 简单地把复杂引用的值拷贝到一个标量看上去应该更简单.实际上, 用另一种方法写这个循环可以把讨厌的 if 结构去掉:
    while (@handlepairs) {
    @handlepairs = grep {
    my ($IN, $OUT) = @$_;
    my $line;
    defined($line = <IN>) and print $OUT $line;
    } @handlepairs;
    }
    如果你懂得 and 是个部分求值的短路操作, 只要一切 OK 就返回 true , 这就是个不错的替代.记住 Perl 的信条:"条条大路通罗马"(尽管不一定在所有的情况下都合情合理)。
    8. 4. 3. IO::Scalar
    有些时候, 我们并不想把内容直接打印到文件, 宁愿把输出送到一个字串中去.一些模块的接口不提供给我们这个选项, 所以我们不得不利用看上去像打印到文件中去的文件句柄来完成.我们可能要在把内容写到文件之前先建立好, 这样我们就可以对文件内容进行加密, 压缩或从你的程序中直接把它作为邮件发出去。
    IO::Scalar 模块在幕后是使用 tie 来实现魔法的, 它把一个文件句柄引用给一个标量.这个模块不在标准的 Perl 发行版中, 所以你可能必须自己安装它。
    use IO::Scalar;

    my $string_log = '';
    my $scalar_fh = IO::Scalar->new( \$string_log );

    print $scalar_fh "The Howells' private beach club is closed\n";
    现在我们的日志信息不是放在文件中, 而是放在标量变量 $string_log 中.那我们如何从我们的日志文件中读呢? 故伎重演而已.在这个例子中, 我们象之前一样创建变量 $scalar_fh , 然后用用输入操作符从中读行.在我们的 while 循环中, 我们从日志信息中抽出包含 Gilligan 的行(应该有很多吧, 因为他总是纠缠在许多事情中):
    use IO::Scalar;

    my $string_log = '';
    my $scalar_fh = IO::Scalar->new( \$string_log );

    while( <$scalar_fh> ) {
    next unless /Gilligan/;
    print;
    }
    在 Perl 5.8 中, 我们可以直接在 Perl 中写这样的语句, 而不必引入 IO::Scalar:
    open( my $fh, '>>', \$string_log )
    or die "Could not append to string! $!";
    8. 4. 4. IO::Tee
    如果我们要一次将结果发送到多个不同的地方, 应该如何做? 如果我们要把内容发送到一个文件的同时存入一个字串中呢? 用我们目前所知, 我们大概不得不做如下这些:
    my $string = '';

    open my $log_fh, '>>', 'castaways. log'
    or die "Could not open castaways. log";
    open my $scalar_fh, '>>', \$string;

    my $log_message = "The Minnow is taking on water!\n"
    print $log_fh    $log_message;
    print $scalar_fh $log_message;
    当然, 我们可以缩短一些, 这样我们可以只用一个打印语句.我们用 foreach 控制结构来迭代地使用句柄引用, 用 $fn 一次换一个, 打印到每个句柄。
    foreach my $fh ( $log_fh, $scalar_fh ) {
    print $fh $log_message;
    }
    不过, 这个看上去还是有些复杂.在 foreach , 我们还得决定是哪个句柄.以不能定义一组句柄来回答同样的问题呢? 哈, 这就是 IO::Tee 提供的功能.可以把它想像成舱底连接输出水管儿的T字连接口; 当水灌到T字口的时候, 它会同时向两个不同的方向流.当我们的数据灌入 IO::Tee 时, 它会流向两个(或多个)不同管道.就是说 IO::Tee 实现了多路输出.在下面 例子中, 日志同时写入 logfile 和标量变量。
    use IO::Tee;

    $tee_fh = IO::Tee->new( $log_fh, $scalar_fh );

    print $tee_fh "The radio works in the middle of the ocean!\n";
    呵呵, 还有更绝的.如果我们给 IO::Tee 一些参数(第一个为输入句柄, 其后均为输出句柄), 我们可以用同一个 Tee 过的句柄来从输入中读和从输出中写.虽然输出源和输出目的地不同, 但我们可以用同一个句柄来操作。 '
    use IO::Tee;

    $tee_fh = IO::Tee->new( $read_fh, $log_fh, $scalar_fh );

    # reads from $read_fh
    my $message = <$tee_fh>;

    # prints to $log_fh and $scalar_fh
    print $tee_fh $message;
    而且 $read_fh 并不一定非要连上文件.它可以连上一个套接字, 一个标量变量, 一个外部命令的输出,
  • 或者任何其它你想得出来的东西。
    8. 5. 目录句柄引用
    用创建对句柄的引用同样的方法, 我可以创建对目录句柄的引用。
    opendir my $dh, '. ' or die "Could not open directory: $!";

    foreach my $file  ( readdir( $dh ) ) {
    print "Skipper, I found $file!\n";
    }
    对目录句柄引用遵从我们在之前我们说过的规则.它必须在标量变量没有值的情况下才能工作, 跑出范围或变量被另外赋值时, 句柄自动关闭。
    8. 5. 1. IO::Dir
    我们也是用面向对象的接口来处理目录句柄.从 Perl 5.6 开始, IO::Dir 模块就是标准发行版的一部份了.它并没有添加什么新的功能, 无非包装了 Perl 的内建函数而已 .[ +] []'
    [] 对于每个 IO::Dir 模块名, 加上 "dir" 并用 perlfunc 查看其文档。
    use IO::Dir;

    my $dir_fh = IO::Dir->new( '. ' ) || die "Could not open dirhandle! $!\n";

    while( defined( my $file = $dir_fh->read ) ) {
    print "Skipper, I found $file!\n";
    }
    如果我们要重新查看文件列表的时候(可能在程序的后面), 我们不必重建句柄.我们可以用 rewind 来重用目录句柄:
    while( defined( my $file = $dir_fh->read ) ) {
    print "I found $file!\n";
    }

    # time passes
    $dir_fh->rewind;

    while( defined( my $file = $dir_fh->read ) ) {
    print "I can still find $file!\n";
    }
    8. 6. 习题
    答案附录找。
    8. 6. 1. 练习1 [ 20 分钟]
    写一个程序打印一周的日期, 但要允许用户选择输出到文件或标量, 或者同时输出.不论用户如何选择, 都必须用一个打印语句输出.如果用户选择打印到标量, 那么在程序结束时, 要将其打印到标准输出。
    8. 6. 2. 练习 2 [30 分钟]
    教授必须读如下的日志:
    Gilligan: 1 coconut
    Skipper: 3 coconuts
    Gilligan: 1 banana
    Ginger: 2 papayas
    Professor: 3 coconuts
    MaryAnn: 2 papayas . . .  
    他要写一系列的文件, 名字是: gilligan.info , maryann.info 等等, 以此类推.每个文件必须只有以文件名开始的行.(名字以冒号分隔.) 其结果, gilligan.info 应该是如下样子:
    Gilligan: 1 coconut
    Gilligan: 1 banana
    现在这个日志文件很大, 而计算机又不快, 所以他要一次读入, 并行输出.他该怎么办呢?
    提示: 用一个散列, 键用名字, 值是 IO::File 对象提供的每个输出文件.按照需要创建他们。
    8. 6. 3. 练习 3 [15 分钟]
    Chapter 9. 实用引用技巧
    这一章我们来看看如何优化排序并且如何处理嵌套数据结构。
    9. 1. 再来看一下排序
    Perl 内建的 sort 排序操作符缺省情况下将文本字串以他们的字符顺序进行排序 .[ *] 这在我们进行字符排序时没有任何问题:
  • 我朋友把这叫做"按 ASCII 表顺序"排序.通常来说, 当前的 Perl 已经不用 ASCII 字符表了; 它依照当前的缺省字符集的排序顺序来进行排序.具体可以查看 perllocale( 不是 perllocal !)文档页。
    my @sorted = sort qw(Gilligan Skipper Professor Ginger Mary_Ann);
    可是, 当我们对数字进行排序的时候却是一团糟:
    my @wrongly_sorted = sort 1, 2, 4, 8, 16, 32;
    排序的结果是:1, 16 , 2, 32 , 4, 8. 为什么不能按正确的顺序排序呢? 是因为是把它们按字串对待, 以字串的顺序进行排序.任何以3开头的训是排在以4开头的字串之前。
    如果我们不想按缺省的排序顺序, 我们不必重写整个排序算法, 好消息是 Perl 已经有好的方法了来处理这件事情了.因为不管我们采用什么算法, 从某种程序上说, 这本质是个A和B谁靠前的问题.这就是我们要写的那部分代码:处理两个元素的代码.然后 Perl 来处理余下的事情。
    在缺省时, 当 Perl 对元素进行排序的时候, 它采用的是字符比较.我们可以用一个放在 sort 关键字与要排序的元素列表中间的代码块来指定排序算法.[+] 在排序代码块中, $a 和 $b 代表要比较的两个元素.如果我们要对数字进行排序, 那么 $a 和 $b 会是来自于我们列表中的两个元素。
    [] 我们同样也可以用命名子程序来每个比较。
    排序代码块必须返回一个代码值来指明排序的次序.如果我们希望 $a 在 $b 前, 我们应该返回 -1 ;反之, $b 排在 $a 前, 它应该返回+1;如果次序并不重要, 则应该返回 0. 所谓次序不重要的意思是, 比如, 如果是大小写不敏感的排序, "FRED" 和 "Fred" , 或者如果数值比较的话, 42 和 42.

  • [*]实际上, 我们可以用任一个负值或正值来代替 -1 和+ 1. 新近的 Perl 版本的缺省排序引引擎很稳定, 所以, 如果返回0, 则会使用 $a 和 $b 在原来列表里的相对次序.旧版本的 Perl 不能保证如此的稳定, 而未来的版本可能不用这种稳定的排序, 所以不能依赖于此。
    举个例子, 以正常的次序对数字进行排序, 我们可以用个排序代码块比较 $a 和 $b , 像这样:
    my @numerically_sorted = sort {
    if ($a < $b)    { -1 }
    elsif ($a > $b) { +1 }
    else            {  0 }
    } 1, 2, 4, 8, 16, 32;
    目前, 我们对数字进行了合适的比较方法, 所以有了正常的数字排序.当然, 如此的排序方法还是繁琐, 所以我们可以用一个飞船操作符来代替:
    my @numerically_sorted = sort { $a <=> $b } 1, 2, 4, 8, 16, 32;
    飞船操作符按我们前面的算法返回 -1 , 0或+ 1. 如果是降序, 在 Perl 中也很简单 :[+][]
    [] [+]在 5.8.6 版本中, Perl 识别反向排序, 而且并不产生临时, 中间列表。
    my @numerically_descending =
    reverse sort { $a <=> $b } 1, 2, 4, 8, 16, 32;
    但是, 所谓殊途同归.飞船是近视的; 它不能看到参数中哪个来自 $a , 哪个来自 $b ;它只看哪个值在它的左边, 哪个在右边.如果 $a 和 $b 换了位置, 则飞船操作符则会将其反序排列:
    my @numerically_descending =
    sort { $b <=> $a } 1, 2, 4, 8, 16, 32;
    在前例中, 表达式原来返回-1的, 现在返回+1, 相反也是一样.所以, 排序出来的结果是反向排序, 所以也不要 reverse 关键字了.这样也容易记得, 因为如果 $a 是在 $b 左边, 我们得到从小到大的排序, 就像a和b在结果列表中一样。
    哪种方法更好? 什么时间我们用 reverse 来反向排序, 什么时候用交换 $a 和 $b 位置来排序呢? 好, 大多数情况下, 他们对性能上没有什么差异.所以可能为了表达清楚, 我们用 reverse. 然而, 为了更复杂的的比较, 单个 reverse 又可能不是最好。
    类似于飞船操作符, 我们可以为字串排序用 cmp , 尽管其很少单独使用, 因为它是排序方法.我们以后马上就会讲到, cmp 操作符在复杂比较中非常常用。
    9. 2. 用索引排序
    在之前的第二章, 我们用 grep 和 map 结合索引来解决了一些问题.我们也可以用排序结合索引来得到一些有趣的结果.比如说, 让我们对前面的名字列表排序:
    my @sorted = sort qw(Gilligan Skipper Professor Ginger Mary_Ann);
    print "@sorted\n";
    我们所需的输出是:
    Gilligan Ginger Mary_Ann Professor Skipper
    但是, 如果我们要看这排序后的列表中各元素在排序前的位置应该如何做呢? 比如, Ginger 排序后是在第二位, 而在原始列表中它是第四位元素.我们如何确它排序后的第二位元素是排序前的第四位元素呢?
    好, 我们可以稍微间接的来做.我们来为名字的索引排序, 而不是为实际的名字排序。
    my @input = qw(Gilligan Skipper Professor Ginger Mary_Ann);
    my @sorted_positions = sort { $input[$a] cmp $input[$b] } 0. . $#input;
    print "@sorted_positions\n";
    这次, $a 和 $b 并非列表中的元素, 而是索引.所以, 不是对 $a 和 $b 比较, 我们用 cmp 对 $input[$a] 和 input[$b] 所含的字串进行比较.而排序的结果则是索引, 这索引是按数组 @input 中的相应的次序进行排列的.输出是0 3 4 2 1, 这意思是:排序后的首位元素是原来列表的首位元素, Gilligan. 排序后的第二位元素是原始列表的第4个元素, 即 Ginger , 以此类推.现在我们可以不仅仅是把名字移来移去, 而可以做个分级排名的东西了。
    事实上, 我们也有了倒过来的排名.即, 给定原始的列表, 在排序后他们所占的位置是什么.当然, 戏法也得上一层楼, 我们可以这样做:
    my @input = qw(Gilligan Skipper Professor Ginger Mary_Ann);
    my @sorted_positions = sort { $input[$a] cmp $input[$b] } 0. . $#input;
    my @ranks;
    @ranks[@sorted_positions] = (0. . $#sorted_positions);
    print "@ranks\n";
    这段代码输出是这样的:0 4 3 1 2. 这就是说 Gilligan 在输出列表中还是老大, Skipper 是在输出列表中倒数第一, 教授是3, 以此类推.这里的序数是以0为基数的, 所以我们可以加1, 让他看起来像人话.一种骗术是用 1..@sorted_positions 代替 0..$ # sorted_positions , 所以写出来是这样的:
    my @input = qw(Gilligan Skipper Professor Ginger Mary_Ann);
    my @sorted_positions = sort { $input[$a] cmp $input[$b] } 0. . $#input;
    my @ranks;
    @ranks[@sorted_positions] = (1. . @sorted_positions);
    for (0. . $#ranks) {
    print "$input[$_] sorts into position $ranks[$_]\n";
    }
    其输出结果如下:
    Gilligan sorts into position 1
    Skipper sorts into position 5
    Professor sorts into position 4
    Ginger sorts into position 2
    Mary_Ann sorts into position 3
    这些一般的方法可以使方便地我们以不同的角度来看我们的数据.可以我们以效率的原因使我们数据以数字顺序排, 但有时我们又要他们以字母顺序排.或者, 可能这些数据项目自己本身顺序并没有多大意义, 比如一个月的服务器日志的价值。
    9. 3. 更有效率的排序
    因为教授要维护社区的计算设备(全都由竹子, 椰子, 菠萝, 并由一个经过 Perl 黑客级别认证的猴子来提供支援), 然后他发现有些人把太多数据给猴子来处理, 所以决定打印出一份罪犯名单。
    教授写了一个子程序 ask_monkey_about() , 这个程序接受一个 castaway 成员的名字, 然后返回他们用了菠萝总储量中的多少.我们问猴子是因为他管这事儿.最初的找出罪犯的程序可以如下的样子:
    my @castaways =
    qw(Gilligan Skipper Professor Ginger Mary_Ann Thurston Lovey);
    my @wasters = sort {
    ask_monkey_about($b) <=> ask_monkey_about($a)
    } @castaways;
    按理, 这个程序不错.对于第一对名字( Gilligan 和 Skipper) , 我们问下猴子: "Gilligan 有多少菠萝?" 和 "Skipper 手里有多少菠萝?" 我们从猴子那里得到这个值后, 并以此来将 Gilligan 和 Skipper 在最终的列表中排座次。
    然而, 我们还得把 Gilligan 手中的持有的菠萝数量与其它 castaway 成员手中持有的菠萝数目相比较。 比如, 假定我们对比的是 Ginger 和 Gilligan. We ask the monkey about Ginger, get a number back, and then ask the monkey about Gilligan. . . again. 这可能会让猴烦不胜烦, 因为我们早前已经问过它了。 但我们必须再二再三, 再三再四地为每个值去问, 直到把七个值排顺。
    这可能会成为一个问题, 这太刺激猴子了。
    那我们怎么能将询问猴子的次数降为最低呢? 这样, 我们先建一张表.我们用一个 map 和七个输入输出项, 将每个 castaway 元素做成一个数组引用, 每个数组引用包含两个元素, 一个是成员名字, 一个猴子报告的其所持菠萝的数目:
    my @names_and_pineapples = map {
    [ $_, ask_monkey_about($_) ]
    } @castaways;
    这次, 我们在一次把把七个问题向猴子问完了, 但这也是最后一次! 我们现在已经有了要完成任务所有的东西了。
    为了下一步, 我们把数组引用排序, 以猴子报告的数值为序:
    my @sorted_names_and_pineapples = sort {
    $b->[1] <=> $a->[1];
    } @names_and_pineapples;
    在这个子程序中, $a 和 $b 列表中要排序的两个元素.当我们对数字进行排序的时候, $a 和 $b 是数字.当我们对引用进行排序时, $a 和 $b 就是引用.我们将他们还原成相应的数组, 并且将他们的第二个元素取出来(猴子报告的菠萝数目).因为 $b 排在 $a 之前, 所以, 它是一个由大到小的降序排列.(我们需要降底是因为教授要有菠萝持有最多的那个人)
    我们差不多要完成了, 但如果我们仅仅要名字, 而不要名字和菠萝数呢? 我们只要做一下另一个 map , 把引用变换成原来的数据就可以了:
    my @names = map $_->[0], @sorted_names_and_pineapples;
    列表中每个元素都是 $_ , 所以, 我们还原它, 并取出第一个元素, 就是名字。
    这样我们就有一个名字的列表, 并以他们所持菠萝的数目由大到小排列, 仅仅用三步, 也可以把猴子轻松放下。
    9. 4. 施瓦茨变换
    每一步当中的中间变量, 除了作为下一步的入, 实际上并不需要他们.我们可以把这些步骤全都堆在一块儿, 这也节省点力气。
    my @names =
    map $_->[0],  
    sort { $b->[1] <=> $a->[1] }
    map [ $_, ask_monkey_about($_) ],  
    @castaways;
    因为 map 和 sort 操作是从右到左分析的, 我们读这些结构时应该由下而上的读.所以顺序是这样的:先取数组 @castaways , 问下小猴一些问题后, 创建一个数组引用, 将数组引用列表排序, 并抽出数组引用中的名字.这样我们就将名字列表以希望的顺序排序。
    这个结构一般叫做施瓦茨变换, 它以兰德命名(并不是他本人起的), 感谢新闻组张贴他的程序使他成名多年。 施瓦茨变换已经被证明是我们的排序技巧的武器库中非常有效的利器。
    如果你觉得这个技巧太复杂而难以记忆或提供一种简明的办法, 下面这种简化成常量的表达可能更灵活一点:
    my @output_data =
    map $_->[0],  
    sort { SORT COMPARISON USING $a->[1] AND $b->[1] }
    map [ $_, EXPENSIVE FUNCTION OF $_ ],  
    @input_data;
    基本的结构将原始的列表变成一个数组引用的列表, 为每个成员只计算一次昂贵的计算; 将数组引用排序以缓存中通过前面昂贵的计算得到的结果进行排序[*], 然后抽出原始的值, 以前的次序排.我们所要做的全部工作就是将两个操作合适发安排, 然后事儿就这样成了.比如, 按照施瓦茨变换来实现一个不区分大小写的排序, 我们可以这样编码:[+]
  • 一个昂贵的操作是花相对时间长的操作, 或者相对使用大量内存的操作。
    [] 这只是在当转换大写是非常昂贵时才是有效的, 或当我们的字串很长或要排的字串很多时。 对于小数量的或不长的字串, 简单的一句: my @output_data = sort { "\U$a" cmp "\U$b"} @input_data 就能解决问题, 足够有效率了。 如果不信, 作基准测试吧。
    my @output_data =
    map $_->[0],  
    sort { $a->[1] cmp $b->[1] }
    map [ $_, "\U$_" ],  
    @input_data;
    9. 5. 用施瓦茨变换作多层排序
    如果我们需要用多个测试条件进行排序, 施瓦茨变换照样可以处理这样的任务。
    my @output_data = map $_->[0], sort { SORT COMPARISON USING $a->[1] AND $b->[1] or ANOTHER USING $a->[2] AND $b->[2] or YET ANOTHER USING $a->[3] AND $b->[3] } map [ $_, SOME FUNCTION OF $_, ANOTHER, YET ANOTHER ], @input_data;
    这个代码结构是三层的排序条件, 把三个计算过的值放到一个匿名数组里(还有把原始值放在排过序的列表中第一个位置.)
    9. 6. 数据的嵌套定义
    我们到现在为止处理的引用都是固定结构的, 可有时候我们要处理一般同递归来定义的层次结构的数据。
    举个例子来说, 考虑一下一个含有表行表列的 HTML 表, 而表里的单位格可能还有其它的表.例二是个虚拟的文件系统的例子, 一个文件系统中有一些目录, 而在目录中有文件或其它目录.例子三是公司的组织结构图, 各部经理向他们的上司报告, 而其中有的经理向自己报告.例子四是更加复杂的组织结构图, 可以包括上述例一的 HTML 表、例二的文件系统, 或者整个公司的组织结构图表……
    我们可以用引用的办法来获得, 存储以及处理这些层次结构的信息.一般来说, 处理这些层次结构的子程序最终都是递归程序。
    递归算法用处理起始的一个基础例子并由此建立的程序来处理无限复杂的数据 .[ *] 所谓基础例子是指在一个最最简单的情况下如何处理:没有分支的叶子节点, 当数组还是空的情况, 当计数器是零时.实际上, 在递归算法的不同分支中一般有多个基础例子.如果递归算法没有基础例子, 程序就会导致无限循环。
  • 递归程序应该都有一个基础的, 或最简的例子, 这种例子无须再用递归处理了, 而且其它递归可以在此结束.就是说, 除非我们手上有的是时间让它永无止境地运行下去。
    递归子程序有一个分支来调用自己来处理部份任务, 有一个分支处理基础例子.在上面第一个例子中, 基础例子就是当表格单元空的时候.同样空行或空表也是基础例子.在第二人例子中, 基础例子是文件或者空的目录。
    比如, 下面的一个处理阶乘的递归子程序, 是最简单的递归应用:
    sub factorial { my $n = shift; if ($n <= 1) { return 1; } else { return $n * factorial($n - 1); } }
    Here we have a base case where $n is less than or equal to 1, which does not invoke the recursive instance, along with a recursive case for $n greater than 1, which calls the routine to handle a portion of the problem (i. e. , compute the factorial of the next lower number).
    这个任务可能用迭代来做比用递归更好, 即使阶乘的经典定义是常常被作为一个递归操作。
    9. 7. 构建嵌套定义的数据
    我们可能要收集一个文件系统的信息, 包括文件名和目录名, 以及他们的内容。 用一个散列代表目录, 在其中, 键代表条目名字, 其值如果是未定义则代表是一般的文件。 以 /bin 目录为例:
    my $bin_directory = {
    cat  => undef,  
    cp   => undef,  
    date => undef,  . . . and so on. . .  
    };
    类似的, Skipper 的主目录同样包括一个属于他自己的 bin 目录(多少象~ /skipper/bin ), 其中有些他个人的工具:
    my $skipper_bin = {
    navigate            => undef,  
    discipline_gilligan => undef,  
    eat                 => undef,  
    };
    上面两个例子没有说目录是否是在一个层次结构里面.它仅仅表示了一个目录里的一些内容。
    我们往上跑一级, 到 Skipper 的主目录, 里面有些文件, 并且有他自己的一个 bin 目录:
    my $skipper_home = {
    '. cshrc'                        => undef,  
    'Please_rescue_us. pdf'        => undef,  
    'Things_I_should_have_packed' => undef,  
    bin                             => $skipper_bin,  
    };
    哈, 注意, 我们现在有三个文件, 但是第四个条目 bin 没有含有未定义值, 而是一个散列引用, 这个引用是先前建立的指向 Skipper 的个人的 bin 目录。 这就是我们标识子目录的方法.如果值不是未定义, 则它是一个文件; 如果是个散列引用, 我们就是指向一个子目录, 其拥有自己的文件和其它子目录。 当然, 我们可以把两者合在一起:
    my $skipper_home = {
    '. cshrc'                    => undef,  
    Please_rescue_us. pdf        => undef,  
    Things_I_should_have_packed => undef,  

    bin => {
    navigate            => undef,  
    discipline_gilligan => undef,  
    eat                 => undef,  
    },  
    };
    现在分层性质的数据开始起作用了.
    显然, 我们不必在程序里用硬编码的形式来构建和维护这个结构。 我们可以用子程序来获取这些数据。 写个子程序, 如果找到的是文件, 则返回未定义值, 如果是目录的话, 则返回一个散列引用。 最基本的查看文件的例子是最简单的, 所以我们可以这样写:
    sub data_for_path {
    my $path = shift;
    if (-f $path) {
    return undef;
    }
    if (-d $path) { . . .  
    }
    warn "$path is neither a file nor a directory\n";
    return undef;
    }
    如果 Skipper 调用这个时找到 .cshrc , 我们返回未定义值, 表示看到一个文件。
    现在要对付目录部份了.我们需要一个散列引用, 我们声明一个命名散列放在子程序中。 为散列中每个元素, 我们调用自己来发布元素值.程序如下:
    sub data_for_path {
    my $path = shift;
    if (-f $path or -l $path) {        # files or symbolic links
    return undef;
    }
    if (-d $path) {
    my %directory;
    opendir PATH, $path or die "Cannot opendir $path: $!";
    my @names = readdir PATH;
    closedir PATH;
    for my $name (@names) {
    next if $name eq '. ' or $name eq '. . ';
    $directory{$name} = data_for_path("$path/$name");
    }
    return \%directory;
    }
    warn "$path is neither a file nor a directory\n";
    return undef;
    }
    这个递归算法中的基础情况是文件和符号链接。 如果文件系统中的符号链接指向目录, 好像是真的(硬)连接, 这个算法不能正确遍历文件系统.因为如果符号链接指向一个包含着符号链接的目录的话, 它会最终走向一个循环。
  • 在遍历一个错误格式的文件系统时也会出错.所谓错误格式的文件系统是指, 目录形成一个循环结构, 而不是树形结构。 尽管错误格式的文件不一定成为问题, 递归算法一般来说在遇到循环数据结构时会有麻烦。
  • 这并不是说我们任何人都没有碰到过, 并奇怪为什么程序一直运行.第二次确实不是我们的错, 第三次只是运气不好.这就是我们的故事而且挥之不去。
    对于目录中的每个文件都会查一下, 从递归调用 data_for_path 得到的结果就是未定义值。 这就生成了散列中大部份的值。 当一个命名引用返回时, 引用立即跑出程序范围, 所以成为对一个匿名散列的引用。 (数据本身并没有改变, 但是我们可以有多种方法来访问数据变化。 )
    如果这其中是个子目录, 则嵌套子程序调用使用 readdir 抽出目录内容, 并返回一个散列引用, 并由调用者放到散列结构中。
    一开始, 这看上去好像很搞, 但是, 只要我们慢慢读完这个程序, 我们会发现它总能完成任务。 调用它一下, 看看结果如何.(在当前目录中)检查一下结果:
    use Data::Dumper;
    print Dumper(data_for_path('. '));
    显然, 如果我们自己的目录里有子目录的话, 那看上去就有趣多了。
    9. 8. 显示嵌套数据g
    用 Data::Dumper 模块的 Dumper 程序显示输出是不错, 但如果我们不喜欢它使用的格式怎么办呢? 我们可以写一个程序来显示数据.同样, 对于嵌套定义的数据, 我们用递归子程序是关键。
    为了打印出数据, 我们必须知道顶层目录的名字, 因为它不会存储在嵌套结构中:
    sub dump_data_for_path {
    my $path = shift;
    my $data = shift;

    if (not defined $data) { # plain file
    print "$path\n";
    return;
    } . . .  
    }
    对于文件, 我们打印出路径名; 对于目录, 变量 $data 是一个散列引用.我们则遍历所有的键, 输出对应的值:
    sub dump_data_for_path {
    my $path = shift;
    my $data = shift;

    if (not defined $data) { # plain file
    print "$path\n";
    return;
    }

    my %directory = %$data;

    for (sort keys %directory) {
    dump_data_for_path("$path/$_", $directory{$_});
    }
    }
    对于目录中的每个元素, 我们传一个包含下一项元素的当前路径, 以及一个散列值, 这个值要么是未定义, 表示是文件, 或下一个子目录的散列引用.我们运行一下, 看下结果:
    dump_data_for_path('. ', data_for_path('. '));
    同样, 如果散列数据有子目录的话, 效果更精彩.不过输出结果同如下的脚本类似:
    find. -print
    取自于 UNIX shell 的提示。
    9. 9. 习题
    答案附录找。
    9. 9. 1. 习题 1 [15 分钟]
    用 glob 操作符, 把 /bin 目录中所有的文件, 以他们的文件大小为序排序, 可能的代码如下:
    my @sorted = sort { -s $a <=> -s $b } glob "/bin/*";
    用施瓦茨变换重写这个程序。
    如果你发现在 /bin 中没有文件, 可能是因为你用的不是 UNIX 系统, 所以可以按需改一下 glob 的参数。
    9. 9. 2. 练习2 [ 15 分钟]
    读一下 Perl 里的 Benchmark 模块.写个程序解决一个问题:"用了施瓦茨变换使练习1的任务快了多少?"
    9. 9. 3. 练习3 [ 10 分钟]
    用施瓦茨变换, 读一列表词, 以"字典顺序"对他们进行排序.所谓字典顺序忽略大小写和和音节符.暗示:下列转换可能有用:
    my $string = 'Mary-Ann';
    $string =~ tr/A-Z/a-z/;       # force all lowercase
    $string =~ tr/a-z//cd;        # strip all but a-z from the string
    print $string;                # prints "maryann"
    注意, 不要把数据搞乱了! 如果输入是 Professor 和 skipper , 那么输出也应该是这个次序, 同样的大小写。
    9. 9. 4. 练习4 [20 分钟]
    修改一下递归目录打印程序, 让它以缩进的形式显示嵌套子目录.一个空的目录应该如下显示:
    sandbar, an empty directory
    非空的子目录应该用缩进两人空格的方法显示嵌套内容:
    uss_minnow, with contents:
    anchor
    broken_radio
    galley, with contents:
    captain_crunch_cereal
    gallon_of_milk
    tuna_fish_sandwich
    life_preservers

    sub navigation_turn_toward_port { . . code here. .  
    }

    1;
    是的, 每个标量, 数组名, 散列, 文件句柄或者子程序现在都必须加上一个 navigation_ 前缀, 这样才能保证不与其它的库里潜在的用户发生冲突。 显然, 对于老水手来说, 他是不会干这种事的.我们能用什么替代方案呢?
    Chapter 10. 构建更大的程序
    这一章我们来看看如何把程序分成一些小的部份, 并且包括那些把小程序组合成一个整体时会发生的问题, 以及多个人协同完成一个项目时分发生的问题。
    10. 1. 修改通用代码
    Skipper 写了许多 Perl 程序应 Minnow 的要求为一般的港口提供导航服务。 他发现自己不停的在各个程序之间复制和粘贴一个通用子例程:
    sub turn_toward_heading {
    my $new_heading = shift;
    my $current_heading = current_heading(  );
    print "Current heading is ", $current_heading, ". \n";
    print "Come about to $new_heading ";
    my $direction = 'right';
    my $turn = ($new_heading - $current_heading) % 360;
    if ($turn > 180) { # long way around
    $turn = 360 - $turn;
    $direction = 'left';
    }
    print "by turning $direction $turn degrees. \n";
    }
    这个通用子例程提供从当前航向的最短的转向(从子程序 current_heading() 返回)到一个新的航向(由第一个参数输入)。
    子程序的第一行可以用如下行代替:
    my ($new_heading) = @_;
    这是另一特色的调用:两种情况, 第一个参数都结束于 $new_heading. 然而, 正像他们指出的, 从 @_ 提取元素比较方便。 所以, 我们大多数情况下用 "shift" 风格的参数解析.现在回到我们手头的程序……
    用这个例程写了一打程序之后, Skipper 发现这样用下来, 当他花时间调到正确的航向时, 已经有非常多的输出 (或者作简单的在正确的航向漂流). 毕竟, 如果当前航向是 234 度, 而他要转 234 度, 我们会看到:
    Current heading is 234.  
    Come about to 234 by turning right 0 degrees.  
    真烦人! Skipper 决定修正这个0航向的问题:
    sub turn_toward_heading {
    my $new_heading = shift;
    my $current_heading = current_heading(  );
    print "Current heading is ", $current_heading, ". \n";
    my $direction = 'right';
    my $turn = ($new_heading - $current_heading) % 360;
    unless ($turn) {
    print "On course (good job!). \n";
    return;
    }
    print "Come about to $new_heading ";
    if ($turn > 180) { # long way around
    $turn = 360 - $turn;
    $direction = 'left';
    }
    print "by turning $direction $turn degrees. \n";
    }
    不错.新的子程序工作得很好.然而, 因为前期他已经用拷贝粘贴的办法把这个程序贴在导航程序里很多地方, 其它程序仍旧出现令 Skipper 不胜其烦的超量输出信息。
    Skipper 需要一种方法, 只写一篇程序, 然后把它共享给其它程序.而且, 正像 Perl 的大多数事物一样, 条条大路通罗马。
    10. 2. 用 eval 插入代码
    Skipper 可以把程序 turn_toward_heading 的定义独立出为另一个文件以节省磁盘空间(也是脑力空间)。 比如, 如果 Skipper 发现与导航相关的半打通用子程序, 他可能用在大多数或所有的程序中。 他可以把它们放在一个分开的文件叫做 navigation.pm 中, 只包含有需要的子程序。
    到目前为止, 我们如何告诉 Perl 从另外一个程序中拉出一块程序代码呢? 我们可以用硬编码, 第二章所讨论过的用 eval 的形式来求一个字串的值。
    sub load_common_subroutines {
    open MORE_CODE, 'navigation. pm' or die "navigation. pm: $!";
    undef $/; # enable slurp mode
    my $more_code = <MORE_CODE>;
    close MORE_CODE;
    eval $more_code;
    die $@ if $@;
    }
    Perl 把 navigation.pm 程序的代码写入变量 $more_code. 我们用 eval 来让 Perl 把这段文本以代码来处理.任何 $more_code 变量中的词法变量被当作本地变量来求值。
  • 如果其中有语法错误, Perl 会设置 $@ 变量, 并且导致程序以适当的出错信息退出。
  • 奇怪的是, 变量 $morecode 同样对于求值代码可见, 不像其它 eval 求值时会改变变量。
    现在, 不必在每个文件里的打上几十行通用代码, 我们方便地把一个子程序放到每个文件中。
    不过这不是最漂亮的, 特别是当我们需要重复这些工作的时候。 好在, Perl 有多种途径来帮助我们。
    10. 3. 使用 do
    Skipper 把一些导航通用子程序放入 navigation.pm 后, 如果 Skipper 只将如下语句:
    do 'navigation. pm';
    die $@ if $@;
    放到导航程序里的话, 它几乎同我们用 eval 把代码插在同一地点的执行结果是相同的。 []
    [] 这排除了考虑 @INC 、 %INC 的情况, 以及丢失文件定位处理, 这个我们在后面的章节会遇到。
    那就是说, do 操作符的功能就像把 navigation.pm 中的代码直接引入现当前的程序一样, 尽管在它自己的块范围内, 所以词法变量( my 声明的变量) 和大多数指示字(如 use strict )不会流到主程序里去。
    这样, Skipper 可能安全的修改一处拷贝, 而不必把这些修改和扩展拷贝到所有其它他创建和使用的导航程序中.图 10-1 展示了 Skipper 如何使用他的通用程序库。
    图 10-1.Skipper 在他其它所有的导航程序中使用 navigation.pm 文件中的程序
    当然, 这样做需要一些约束, 因为如果在给出的子程序中破坏了一个预期的接口, 会影响到许多其它的程序而不只自己一个。 [] Skipper 需要对组件的重用性和模块化的设计给予专门的考虑.我们先假定 Skipper 有这方面的经验, 但是我们会在以后的章节中展示更多这方面的知识。
    [] 在后面的章节中, 我们会展示如何建立测试程序来维护可重用的代码。
    通过将一些代码放到文件中, 其它的程序员可以重用 Skipper 写的程序, 反过来也一样.如果 Gilligan 写了一个程序 :drop_dnchor() , 并且将其放到文件 drop_anchor.pm 中, 这样 Skipper 就可以通过引入库的办法使用 Gilligan 的代码:
    do 'drop_anchor. pm';
    die $@ if $@; . . .  
    drop_anchor(  ) if at_dock(  ) or in_port(  );
    所以, 从分开的文件引入代码可以使我们可以更方便地维护和协同编程。
    当代码从一个 .pm 文件导入的时候可以有直接可执行的语句, 这比用 do 简单定义子程序更为常用。
    我们再回到 drop_anchor.pm 库, 如果 Skipper 要写个程序需要"抛锚"和导航呢?
    do 'drop_anchor. pm';
    die $@ if $@;
    do 'navigation. pm';
    die $@ if $@; . . .  
    turn_toward_heading(90); . . .  
    drop_anchor(  ) if at_dock(  );
    这一工作很好很顺利.子程序在两个库中定义, 使用起来就像在这个程序里一样。
    10. 4. 使用 require
    假定 navigation.pm 自己, 因为一些导航任务而把 drop_anchor.pm 引进自己的模块里 .Perl 在处理导航程序包的时候将文件一次直接读入.在这次重新定义 drop_anchor() 是不必要的.更糟的是, 如果我们把警告打开, 我们会从 Perl 得到一个警告信息, 说我们已经重新定义子程序, 尽管是一模一样的定义。
  • 你开启警告是吗?你可以用 -w 开关, 以及 use warnings 来开始警告;
    我们需要一种机制来跟踪哪些文件我们已经调入了, 而且应该只把它们调入一次 .Perl 提供了这个功能, 叫作 require. 把前面的代码改成如下就可以了:
    require 'drop_anchor. pm';
    require 'navigation. pm';
    require 操作符会在 Perl 每次读入文件的时候进行跟踪[+] 一旦 Perl 成功加载了一个文件, 它就会忽略之后 require 引入相同的文件.这意味着就算 navigation.pm 包括 require "drop_anchor.pm" , Perl 只会把 drop_anchor.pm 引入一次, 我们就不会收到烦人的警告重复定义子程序的消息了(见图 10 -2).更重要的是, 我们同样节省时间, 不必多次加载文件了。
    [] 在 %INC 散列中, 正如 perlfunc 文档中 require 条目所描述的一样。
    图 10-2. 一旦 Perl 调入 drop_anchor.pm 文件, 它会忽略其它相同文件的导入
    require 操作符同样有以下两个功能:
    任何语法错误会导致程序终止; 所以许多 die $@ if $@ 语句在这里是不需要的。
    文件最后一个求值必须返回真值
    正因为第二点, 所以大多数给 require 导入的文件最后求值总有个神秘的1; 这保证了最后的表达式总是 1. 努力保持这个传统吧。
    一开始, 强制性的返回真值是给导入文件的一个方法, 给调用者一个信号--代码被成功处理且没有错误。 However, nearly everyone has adopted the die if. . . strategy instead, deeming the "last expression evaluated is false" strategy a mere historic annoyance.
    10. 5. require 和 @INC
    到目前为止, 这些例子都忽略了我们如何建立目录结构来安排主要代码和要导入的代码文件应该安放的位置。 那是因为"能工作就行", 用最简单的情形, 我们把程序和它的库放在同一目录, 并就在这个目录里运行程序。
    当库文件不在当前目录的时候, 事件就有些复杂了。 实际上, Perl 按库一个库搜索路径来查找库, 有些像 UNIX shell 用 PATH 环境变量一样。 当前目录(在 Unix 里以句点表示)是查寻目录之一。 所以只要我们的库在我们的当前工作目录, 就没问题。
    查寻目录是在一个特别的数组 @INC 中的列表中一系列的元素, 就象我们在第三章讲过的一样。 缺省时, 数组包括当前目录和一些由编译 Perl 的用户指定的目录。 在命令行输入 Perl -V 可以在最后几行显示出这些目录。 用如下的命令也可以显示 @INC 所包括的目录:

  • 在 Windows 操作系统的电脑上, 用双引号代替单引号。
    perl -le 'print for @INC'
    在输出列表中, 除了句点., 除非我们是负责在这台电脑上维护 Perl 的人, 我们大概不能写任何其它的目录进去.在这种情况下, 我们应该能把它们写进去。 象我们后面要看到的, 余下的目录是 Perl 系统搜索系统库和模块的目录, 象我们后面所看到的。
    10. 5. 1. 括展 @INC
    我们可能碰到这些情况, 即不能(想)在预先配置在 @INC 中的目录中安装模块。 但我们可以在 require 之前先改变数组 @INC 自己, 这样 Perl 也会去查找我们的目录。 数组 @INC 就是平常的数组, 所以让 Skipper 把它主目录加进去:
    unshift @INC, '/home/skipper/perl-lib';
    现在, 除了搜索标准目录和当前目录, Perl 还搜索 Skipper 的个人 Perl 模块库。 实际上, Perl 首先就会查这个目录, 因为这条会排在数组 @INC 最前面。 因为使用的是 unshift 而不是 push , Perl 会将取 Skipper 的文件放在优先位置, 以此解决 Skipper 的私有文件与系统安装的文件之间可能的冲突。
    一般来说, 我们要保证添加搜索目录一定要放在其它事之前, 我们可以把它放进 BEGIN 块 .Perl 会在编译阶段执行在 BEGIN 块中的语句, 而在运行时执行 require 语句。 除此之外, Perl 会以文件中的自然顺序执行语句, 所以我们要保证我们的 unshift 要出现在我们的 require 之前。
    BEGIN {
    unshift @INC, '/home/skipper/perl-lib';
    };
    因为这个操作太常见了, 所以 Perl 有一个编译指示字来处理。 编译指示字发生在任何运行时之前, 所以我们可以得到预期的效果。 它会把我们指示的目录放到 @INC 数组的最前面, 正象我们前面做的那样。
    use lib qw(/home/skipper/perl-lib);
    我们不一定总是能事先预知目录路径。 在前面的例子中, 我们对路径是硬编码的。 如果我们事先不知道路径是什么, 这有可能是我们在几个机器之前传送代码, Perl 自带的 FindBin 模块可以帮助你。 它会找到脚本所在的目录的全路径, 这样我们可以依此来建立自己的路径。
    use FindBin qw($Bin);
    现在, 变量 $Bin 中的路径是我们的脚本所在的路径.如果我们把库放在同一路径, 我们下一行可以是:
    use lib $Bin;
    如果我们把库放在脚本目录中的一个目录, 我们只要把正确的路径加上去就可以了, 这样就能工作了。
    use lib "$Bin/lib";    # in a subdirectory

    use lib "$Bin/. . /lib"; # up one, then down into lib
    这样, 如果我们知道脚本目录的相对路径, 我们就不必硬编码全路径了.这使我们的脚本更加易于移植。
    10. 5. 2. 用 PERL5LIB 环境变量扩展 @INC
    Skipper 必须编辑每个程序里以使用他的私有库来导入前面的那些代码。 如果这样编辑太麻烦了, 他可以设置 PERL5LIB 环境变量, 加上库文件目录.比如, 在C shell 中, 他可以用下面这行:
    setenv PERL5LIB /home/skipper/perl-lib
    在 Bourne 类型的 shell 中, 他可以用类似于如下的行:
    PERL5LIB=/home/skipper/perl-lib; export PERL5LIB
    Skipper 可以在一次设置 PERL5LIB 后就把它忘了。 然而, 除非 Gilligan 使用同样的 PERL5LIB 环境变量, 它的程序就会失败! 因为 PERL5LIB 对个人使用非常有用, 当我们与其它人共享程序时, 我们不能依靠它。 (而且我们也不能让我们整个程序员团队的人用一个共同的 PERL5LIB 变量.相信我们, 我们试过。 )
    PERL5LIB 环境变量可以加多个目录, 中间用分号分开 .Perl 会把所有这些目录加到 @INC 中。
    当一个系统管理员把 PERL5LIB 作为系统范围的设置, 大多数人可能为此而不悦。 PERL5LIB 的目录是使非管理员来扩展 Perl 搜索目录.如果一个系统管理员要添加目录, 它仅仅需要重编译和重装 Perl.
    10. 5. 3. 用 -I 扩展 @INC
    如果 Gilligan 注意到 Skipper 的程序丢失了合适的指示字, Gilligan 可以通过设置正确的 PERL5LIB 环境变量, 也可以用 Perl 的 -I 选项。 比如, 要调用 Skipper 的 get_us_home 程序, 在命令行可能是:
    perl -I/home/skipper/perl-lib /home/skipper/bin/get_us_home
    显然, 如果程序自己定义额外的库, 对 Gilligan 来说更方便。 但有时加一个 -I 仅是修复一些东西。
  • 这在 Gilligan 无需编辑 Skipper 的程序的情况下也能工作。 当然, 它要有读的权限, 但是, 举个例子来说, Gilligan 可以用这个技术试一个使用 Skipper 程序的新版本。
  • 括展 @INC with either PERL5LIB orI also automatically adds the versionand architecture-specific subdirectories of the specified directories. 自动引入这些目录也简化了安装 Perl 模块的任务, 如果这些代码是结构化的或对版本敏事情的, 就像编译过的C代码之类。
    10. 6. 名字空间冲突的问题
    有时候 Sipper 要把船开进一个小岛, 但有时程序里会发生一对名字发生冲突的情况。 假定 Skipper 把他的所有的有用和酷的子程序加到 navigation.pm 中, 而 Gilligan 已经导入他自己的导航程序包, head_toward_island:
    #!/usr/bin/perl

    require 'navigation. pm';

    sub turn_toward_port {
    turn_toward_heading(compute_heading_to_island(  ));
    }

    sub compute_heading_to_island {
    #. . code here. .  
    }

    #. . more program here. .  
    Gilligan 开始调试自己的程序(可能有一个有个聪明的人在帮他, 我们叫他"教授"), 一切顺利。
    然而, 现在 Skipper 决定修改他的 navigation.pm 库, 加一个子程序: turn_toward_port , 使船可以以 45 度角向左舷转向(航海术语, 左舷是靠向岸的舷)。
    Gilligan 的程序总是在他准备转向岸时就出现灾难:他会使船总是转圈! 问题在于 Perl 编译器开始编译 Gilligan 主程序中的 turn_toward_port , 然后当 Perl 在运行时解析 require 时, 它以 Skipper 的定义的 turn_toward_port 重新定义了这个程序。 当然, 录果 Gilligan 打开了警告, 他会注意有出错信息, 但他为什要仰赖这个呢?
    问题在于 Gilligan 定义的 turn_toward_port 的作用是"把左舷靠向岛", 而 Skipper 的定义是"向左转 ". 如何来解决这个问题呢?
    一种方法是需要 Skipper 加个显式的前缀在每个他定义的库中的程序名上, 比如, navigation_. 这样, Gilligan 的程序看上去是这样的:
    #!/usr/bin/perl

    require 'navigation. pm';

    sub turn_toward_port {
    navigation_turn_toward_heading(compute_heading_to_island(  ));
    }

    sub compute_heading_to_island {
    #. . code here. .  
    }

    #. . more program here. .  
    这样就明白了, navigation_turn_toward_heading 来自于 navigation.pm 文件。 这对 Gilligan 是不错, 不过让 Skipper 很窘, 因为他的程序现在有很长的程序名:
    sub navigation_turn_toward_heading {
    #. . code here. .  
    }
    10. 7. 包名字作为名字空间分隔符
    如果上例中的名字前缀不必在每个使用的地方都打印, 那工作该多好.我们可以用程序包来增强可读性。
    package Navigation;

    sub turn_toward_heading {
    #. . code here. .  
    }

    sub turn_toward_port {
    #. . code here. .  
    }

    1;
    在文件的开始的程序包声明, 显式地告诉 Perl 将 Navigation:: 插入到文件中大多数名字的前面.这样, 上面的代码实际上在说:
    sub Navigation::turn_toward_heading {
    # code here. .  
    }

    sub Navigation::turn_toward_port {
    # code here. .  
    }

    1;
    现在 Gilligan 导入这个文件, 他只要简单的把在库里引用的子例程前加上 Navigation:: 前缀即可, 而在他自己的同名程序前面不加前缀。
    #!/usr/bin/perl

    require 'navigation. pm';

    sub turn_toward_port {
    Navigation::turn_toward_heading(compute_heading_to_island(  ));
    }

    sub compute_heading_to_island {
    #. . code here. .  
    }

    #. . more program here. .  
    程序包名字与变量名字相同:他们包括字母和数字及下划线, 但是不能以数字开头.同样, 因为在 Perl 的 perlmodlib 文档中说明的理由, 一个程序包名应该以大写开头, 并且不与现存的 CPAN 或核心的模块名重名.包名可以以双冒号分隔定义多个名字, 如: Minnow::Navigation 及 Minnow::Food::Storage.
    几乎所有的标量、数组、散列、子程序及文件句柄名字实际上已经加上了当前的程序包的名字, 除非原来的名字已经包括一个或多个双冒号标记。
  • 除了词法变量, 我们会在后面说到。
    所以, 在 navigation.pm 中, 我们可以用下面的变量名:[]
    [] 小注: 21.283 度以北, 157.842 度以西是现实生活中一个地点, 上过一个著名的电视节目.如果你不信, 可以查查 Google Maps.
    package Navigation;
    @homeport = (21. 283, -157. 842);

    sub turn_toward_port {
    #. . code. .  
    }
    我们可以在主程序中用全名引用 @homeport 变量:
    @destination = @Navigation::homeport;
    如果每个名字前面都有包名字, 那么主程序的名字会是什么?是的, 他们同样有包名字, 称作 main. 就好象在 main 这个程序包里面;在每个文件开始的地方.所以, 要 Gilligan 避免说 Navigation::turn_toward_heading , navigation.pm 文件中可以用:
    sub main::turn_toward_heading {
    #. . code here. .  
    }
    现在, 子程序被定义在 main 程序包中, 不在 navigation 包中.这不是一个好的解决方法(我们会在第 15 章, 讨论 Exporter 的时候来说更好的做法), 但是, 至少目前也没有其它利害的独特方案可以匹分 main 与其它包。
    这就是在第三章中的那些模块在导入符号到脚本时要做的事情, 但是当时我们没有告诉你整个故事的细节.这些模块把子例程和变量导入到当前的包(通常, 这是你这个脚本的 main 包).换句话说, 这些符号仅存在于那些包中, 除非你全名引用.我们会在后面来详述这套机制是如何工作的。
    10. 8. 包指示字的范围
    所有的文件都好像以 main 程序包开始;
  • 所有的包指示字的范围在其声明处开始到下一个包指示字声明的地方结束, 除非那个包指示字在一个大括号范围之内.在那种情况下, Perl 会记住前面的包, 并在其范围结束时恢复它.这里是个例子。
  • Perl 不会让我们像C那样创建一个显式的 main( )循环 .Perl 清楚每个脚本都需要它, 所以它自动为我们做了这项工作。
    package Navigation;

    {  # start scope block
    package main;  # now in package main

    sub turn_toward_heading {  # main::turn_toward_heading
    #. . code here. .  
    }

    }  # end scope block

    # back to package Navigation

    sub turn_toward_port { # Navigation::turn_toward_port
    #. . code here. .  
    }
    当前的包是词法范围的, 像 my 声明的变量一样, 范围限制在大括号的最里层或我们引入包的文件范围之内。
    大多数的库只在一个文件里放一个包, 并且在文件开始的地方声明包名.大多数的程序就把缺省的 main 作为包名.不过知道我们可以临时有个不同的当前包名是不错的 .[ +] []
    [] Some names are always in package main regardless of the current package: ARGV, ARGVOUT, ENV, INC, SIG, STDERR, STDIN, and STDOUT. We can always refer to @INC and be assured of getting @main::INC. The punctuation mark variables, such as $_, $2, and $!, are either all lexicals or forced into package main, so when we write $. , we never get $Navigation::. by mistake.
    10. 9. 包和词法变量
    一个词法变量(以 my 声明的变量) 不会有当前包名做前缀, 因为包变量已经是全局的了: 我们总是可以引用一个包变量, 如果我们知道它的全名。 一个词法变量通常是临时的或只在程序的局部可访问。 如果我们声明一个词法变量, 然后不带包前缀地使用它, 就获得一个词法变量。 一个包前缀保证我们可以访问一个包变量, 而不是词法变量。
    举例来说, 假定 navigationpm 中的一个子例程声明了一个词法变量 @homeport. 那么, 任何使用 @homeport 时, 都是指新引入的词法变量, 但是如果用全名来引用 @havigation::homeport 将访问的是包变量。
    package Navigation;
    @homeport = (21. 283, -157. 842);

    sub get_me_home {
    my @homeport;

    #. . @homeport. . # refers to the lexical variable
    #. . @Navigation::homeport. . # refers to the package variable

    }

    #. . @homeport. . # refers to the package variable
    显然, 这段代码会导致混淆, 所以我们不应该搞这种没必要的双重声明。 尽管结果早就会料到。
    10. 10. 习题
    答案见附录。
    10. 10. 1. 练习 1 [25 分钟]
    岛上的土著 Oogaboogoo 对于日期和月份着不同寻常的名字.这里有个从 Gilligan 来的简单但写得不是很好的代码.修改一下, 给月份名加一个对话函数, 并把这些放到一个库里.为了更好的可靠性, 添加一个错误检查程序以及文档。
    @day = qw(ark dip wap sen pop sep kir);
    sub number_to_day_name { my $num = shift @_; $day[$num]; }
    @month = qw(diz pod bod rod sip wax lin sen kun fiz nap dep);
    10. 10. 2. 练习2 [ 15 分钟]
    写个程序使用你的库并用如下代码打印一些信息, 如今天是 dip , sen 15 , 2011 , 表示今天是八月的周一.(暗示: localtime 返回的年月数字可能并不一定是你想要的, 所以你要查一下文档.)
    my($sec, $min, $hour, $mday, $mon, $year, $wday) = localtime;
    Chapter 11. 介绍对象
    面向对象编程( OOP )帮助程序员把代码到他们可以命名的对象里的办法使代码运行起来更快和维护起来更方便。 我们需要一点功夫来找出对象, 但对于长远来说是值得的。
    当程序超过N行的时候, 面向对象的好处就显露出来了。 不过, 对于这个N到底是多少, 各有各的持法, 但对于 Perl 程序来说, 1000 行左右代码是比较能被接受的。 如果我们的程序就区区几百行, 用面向对象编程可能就太过了。
    如同引用, Perl 的对象架构也是从一些现存的前 Perl 5已经用的代码移植过来的.所以我们必须保证不对现有的语法有影响。 令人惊奇的是, 实现向面向对象重生的只用了一个附加的语法, 就是简单引入了方法调用。 但其语不意义需要我们一些研究, 所以让我们开始吧。
    Perl 的对象结构严重依赖于包, 子例程和引用, 所以如果你已经跳过书中的这些章节, 请回到开始的部份好好看完.准备好了吗?我们开始。
    11. 1. If We Could Talk to the Animals. . .
    显然, castaways 船员不可能仅仅靠椰子和菠萝活下来.幸运的是, 一船满载农场牲畜的驳船在他们来此不久也在小岛搁浅了.于是船员们开始牧养这些牲畜。
    让我们听一下那些动物的叫声:
    sub Cow::speak {
    print "a Cow goes moooo!\n";
    }
    sub Horse::speak {
    print "a Horse goes neigh!\n";
    }
    sub Sheep::speak {
    print "a Sheep goes baaaah!\n";
    }

    Cow::speak;
    Horse::speak;
    Sheep::speak;
    其输出结果如下:
    a Cow goes moooo!
    a Horse goes neigh!
    a Sheep goes baaaah!
    这里没有什么特别的:简单的子程序, 虽然来自不同的包, 但用完整的包名.让我们来建立整个牧场:
    sub Cow::speak {
    print "a Cow goes moooo!\n";
    }
    sub Horse::speak {
    print "a Horse goes neigh!\n";
    }
    sub Sheep::speak {
    print "a Sheep goes baaaah!\n";
    }

    my @pasture = qw(Cow Cow Horse Sheep Sheep);
    foreach my $beast (@pasture) {
    &{$beast. "::speak"};                # Symbolic coderef
    }
    其输出结果如下:
    a Cow goes moooo!
    a Cow goes moooo!
    a Horse goes neigh!
    a Sheep goes baaaah!
    a Sheep goes baaaah!
    哇, 在 loop 中有代码的符号引用还原操作是够糟的.我们需要指望没有严格的 'refs' 模式.当然, 在大型程序中不建议这样.为什么要这样做?因为包名与我们包里我们要调用的子程序名字是不能分开的。
  • 尽管书中所有的例子都是有效的 Perl 程序, 但是有些章节的例子会打破由 strict 定义的规则, 来使其看上去易懂.在章节的结尾, 我们会展示如用 strict 兼容的例子。
    或者, 如何才好呢?
    11. 2. 介绍方法调用符
    一个类是一组具有相同很行为性状的事物的集合.对于 Perl , 我们就认为类 -> 方法就是 Class 包调用 method 子程序.一个方法就是面向对象版本的子程序, 所以从现在开始, 我们会说"方法 "[ *] 这不是特别准确, 但是第一步.让我们像下面那样使用:
  • 在 Perl , 实际上子程序和方法没什么差别.他们都以 @_ 作参数, 我们来决定什么是对的。
    sub Cow::speak {
    print "a Cow goes moooo!\n";
    }
    sub Horse::speak {
    print "a Horse goes neigh!\n";
    }
    sub Sheep::speak {
    print "a Sheep goes baaaah!\n";
    }

    Cow->speak;
    Horse->speak;
    Sheep->speak;
    同样, 其输出是:
    a Cow goes moooo!
    a Horse goes neigh!
    a Sheep goes baaaah!
    不好玩是不是? 我们得到了相同的输出结果, 都是常量, 没有变量.然而, 现在被拆开了:
    my $beast = 'Cow';
    $beast->speak;                # invokes Cow->speak
    哈!既然包名和子程序的名字分开, 我们就可用一个变量包名.这次, 我们就可以得到我们用 use strict 'refs' 时也可以工作的东西。
    我们用箭头调用来看前面农场的例子:
    sub Cow::speak {
    print "a Cow goes moooo!\n";
    }
    sub Horse::speak {
    print "a Horse goes neigh!\n";
    }
    sub Sheep::speak {
    print "a Sheep goes baaaah!\n";
    }

    my @pasture = qw(Cow Cow Horse Sheep Sheep);
    foreach my $beast (@pasture) {
    $beast->speak;
    }
    如此! 现在所有的动物都能安全地交谈, 不必使用符号代码引用。
    但查看一般代码.每个 speak 方法都相似结构:一个打印操作符和一个包含一般文本的字串, 除了两个字不同 .OOP 的一个核心原则就是把通用的代码最小化:如果我们仅写一次, 我们就节约了时间.如果我们调试一次就可以了, 我们就节省了更多时间。
    即然我们已经知道了箭头调用符是如何工作的, 我们就得到了入门的捷径。
    11. 3. 方法调用的额外参数
    调用:
    Class->method(@args)
    会以如下方式调用子程序 Class::method:
    Class::method('Class', @args);
    (如果它找不到方法, 那么继承者会介入, 我们会在以后的章节中展示.)这意味着我们以类名作为第一个参数, 或者说在没有参数的情况下是仅有的一个参数.我们可以将其重写为:
    sub Sheep::speak {
    my $class = shift;
    print "a $class goes baaaah!\n";
    }
    另外两只动物的代码也一样写:
    sub Cow::speak {
    my $class = shift;
    print "a $class goes moooo!\n";
    }
    sub Horse::speak {
    my $class = shift;
    print "a $class goes neigh!\n";
    }
    在以上例子中, $class 类为那个方法取到适当的值。 可是又一次, 我们又看到许多相似的结构。 我们能不能更进一步, 求出最大公约数呢? Yesby calling another method in the same class.
    11. 4. 调用第二个方法来简化操作
    我们可以从 speak 调一个方法叫 sound. 这个方法提供一个常量做为声音的内容:
    { package Cow;
    sub sound { 'moooo' }
    sub speak {
    my $class = shift;
    print "a $class goes ", $class->sound, "!\n";
    }
    }
    现在, 当我们调用 Cow-speak> , 我们会在 speak 中获得一个 Cow 的 $class. 这样, 选择 Cow-sound> 方法, 返回 moooo. 马会怎么样呢?
    { package Horse;
    sub sound { 'neigh' }
    sub speak {
    my $class = shift;
    print "a $class goes ", $class->sound, "!\n";
    }
    }
    只有包名和声音变了.那么我们可以把牛和马的 speak 定义共享吗? 是的, 用继承!
    现在, 让我们定义一个共享的方法的包, 叫 Animal , 其 speak 定义如下:
    { package Animal;
    sub speak {
    my $class = shift;
    print "a $class goes ", $class->sound, "!\n";
    }
    }
    这样, 对于每个动物, 我们说它继承自 Animal , 并有此动物专有的叫声:
    { package Cow;
    @ISA = qw(Animal);
    sub sound { "moooo" }
    }
    注意我们加了 @ISA 数组.我们后面会介绍的。
    现在我们调用 Cow-speak> 会发生什么?
    首先, Perl 创建参数列表.在这个例子中, 就是 Cow. 然后 Perl 找 Cow::speak. 当前包没有, 所以 Perl 就在其祖先数组里找 @Cow::ISA. 这里面有 Animal 包。
    然后 Perl 就用 Animal 里的 speak 来代替了, 就是 Animal::speak. 找到后, Perl 用已经不变的参数列表来调用, 就像我们这样写的一样:
    Animal::speak('Cow');
    在 Animal::speak 方法里, $class 变成 Cow , 作为第一个参数传入.当我们打印时就调用 $class-sound> , 它会找到 Cow-sound:>
    print "a $class goes ", $class->sound, "!\n";
    # but $class is Cow, so. . .  
    print 'a Cow goes ', Cow->sound, "!\n";
    # which invokes Cow->sound, returning 'moooo', so
    print 'a Cow goes ', 'moooo', "!\n";
    这样就得到我们需要的输出结果。
    11. 5. 有关 @ISA 的一些说明
    这个神奇的 @ISA 变量(发音是 "is a" 不是 "ice-uh" )声明了 Cow" 是一个"动物 .[ *] 注意它是一个数组, 不是一个简单的标量值, 因为在罕有的情况下它可能有多重祖先, 我们将会在下面讨论这个问题。
  • ISA 实际上是一个语言学上的术语.再提醒一次, Larry Wall 的语言学家的背景又在反过来影响了 Perl.
    如果 Animal 同样也有一个 @ISA 数组, Perl 也会再去查找 .[ +] 一般来说, 每个 @ISA 只有一个元素(多个元素意味着多重继承及多重"头痛"), 这样我们就可以得到一个清楚的继承树 .[ +] []
    [] 查找是递归的, 在每个 @ISA 数组中是从上到下, 从左到右。
    [] 同样可以从 UNIVERSAL 和 AUTOLOAD 继承;可以查看 perlobj 手册页来得到它的身世。
    当我们打开 strict 后, 我们会有关于 @ISA 的警告信息, 因为它既不是一个有明确包名的变量, 也不是一个词法( my 声明)的变量.我们不能把它定义为词法变量, 因为它属于它所继承的那个包。
    有两个简单方法来处理对 @ISA 的声明和设置.最简单的方法是指出包名:
    @Cow::ISA = qw(Animal);
    我们也允许它作为一个隐含命名的包变量:
    package Cow;
    use vars qw(@ISA);
    @ISA = qw(Animal);
    如果你用的是比较新版本的 Perl , 你可以用如下的简短形式:
    package Cow;
    our @ISA = qw(Animal);
    当然, 如果你需要你的代码让那些执着于 Perl 5.005 或更早的版本的人, 最好避免使用 our.
    如果我们要用从外面(通过一个面对对象的模块)带进来的类, 我们可以改成:
    package Cow;
    use Animal;
    use vars qw(@ISA);
    @ISA = qw(Animal);
    或者:
    package Cow;
    use base qw(Animal);
    这是非常简短的形式了.此外, use base 有个优点是它是在编译时执行的, 这样消除了在运行时设置 @ISA 可能发生的潜在错误, 就像先前的一些解决方案那样。
    11. 6. 重载方法
    让我们来加一种少有耳闻的老鼠:
    { package Animal;
    sub speak {
    my $class = shift;
    print "a $class goes ", $class->sound, "!\n";
    }
    }
    { package Mouse;
    @ISA = qw(Animal);
    sub sound { 'squeak' }
    sub speak {
    my $class = shift;
    print "a $class goes ", $class->sound, "!\n";
    print "[but you can barely hear it!]\n";
    }
    }

    Mouse->speak;
    其输出是:
    a Mouse goes squeak!
    [but you can barely hear it!]
    这里, 老鼠有自己的发声程序, 所以 Mouse-speak> 不会立即调用 Animal-speak.> 这被称为重载.我们用重载来在继承类( Mouse )中重写方法, 因为我们有个特别版本的子程序来代替原来基础类里更一般化的类方法(在 Animal 类中).实际上, 我们甚至根本不需要初始化 @Mouse::ISA 来声明 Mouse 是动物, 因为所有有关发声的方法都已经在 Mouse 中定义了。
    我们现在已经在 Animal-speak> 重复写了些代码了; 这会引起维护上的问题.比如, 某人认为 Animal 类的输出的词不对, 是个错误.现在代码维护者改变了程序.而我们的老鼠仍旧说原来的话, 意味着错误仍旧存在.问题是我们采用剪切和粘贴来复制代码, 在面向对象编程中, 这是不可饶恕的罪过.我们应该通过继承来重用代码, 而不是靠剪切和粘贴。
    我们能避免吗? 我们可以做到一只老鼠能做其它动物能做的, 然后有其自己的特殊的情况吗?当然可以!
    像我们首先尝试的, 当我们直接调用 Animal::speak:
    { package Animal;
    sub speak {
    my $class = shift;
    print "a $class goes ", $class->sound, "!\n";
    }
    }
    { package Mouse;
    @ISA = qw(Animal);
    sub sound { 'squeak' }
    sub speak {
    my $class = shift;
    Animal::speak($class);
    print "[but you can barely hear it!]\n";
    }
    }
    注意, 因为我们已经不用方法箭头, 我们必须用 $class 参数(当然其值是 Mouse )作为 Animal::speak 的第一个参数。
    为什么我们不用方法箭头?哦, 如果我们在那里用 Animal-speak> , 那给方法的第一个参数就是 Animal 而不是 Mouse , 而且当程序调用 sound 的时候, 它不会选用正确的类。
    然而, 调用直接调用 Animal::speak 是一个错误.如果 Animal::speak 事先不存在呢? 它会从 @Animal::ISA 继承?如:
    { package LivingCreature;
    sub speak {. . . } . . .  
    }
    { package Animal;
    @ISA = qw(LivingCreature);
    # no definition for speak(  ) . . .  
    }
    { package Mouse;
    @ISA = qw(Animal);
    sub speak { . . .  
    Animal::speak(. . . );
    } . . .  
    }
    因为我们不用方法箭头, 我们有一个且只有一个机会命中正确的方法, 因为我们对待它就像是一般的, 没有继承特色的子程序.我们会在 Animal 类中找, 没找到它, 程序就停止。
    现在 Animal 类名被硬编码用于方法选择.这对维护代码的人很不方便, 为 Mouse 改 @ISA , 并不会注意到 Speak 中的 Animal 类.所以, 这并非最好的解决方法。
    11. 7. 从不同的地方查找
    一个更好的解决方案是告诉 Perl 在继承链中从不同的地方去查找。
    { package Animal;
    sub speak {
    my $class = shift;
    print "a $class goes ", $class->sound, "!\n";
    }
    }
    { package Mouse;
    @ISA = qw(Animal);
    sub sound { 'squeak' }
    sub speak {
    my $class = shift;
    $class->Animal::speak(@_);
    print "[but you can barely hear it!]\n";
    }
    }
    啊.虽然丑陋, 但是能用啊.用这个语法, 如果没有立即找到方法, 就会从继承链中去找方法。 第一个参数是 $class (因为我们再次使用了箭头), 所以找到的 speak 方法象是 Mouse 的第一个条目, 回到 Mouse::sound.
    然而, 这也并非最终的解决方法。 我们还是要使 @ISA 和初始包同步(改了一个句就必须考虑另一个)。 更糟的是, 如果在 @ISA 中 Mouse 类有多个条目, 我们不知道哪个实际上定义了 speak.
    那么, 还有更好的方法吗?
    11. 8. 用 SUPER 方法来处理问题
    在调用时, 把 Animal 类改成 SUPER 类, 我们可以自动查到我们的所有的超级类(在 @ISA 列表中的类):
    { package Animal;
    sub speak {
    my $class = shift;
    print "a $class goes ", $class->sound, "!\n";
    }
    }
    { package Mouse;
    @ISA = qw(Animal);
    sub sound { 'squeak' }
    sub speak {
    my $class = shift;
    $class->SUPER::speak;
    print "[but you can barely hear it!]\n";
    }
    }
    所以, SUPER::speak 表示在当前包的 @ISA 查找 speak , 如果找到多个, 则调用第一个被找到的。 在这个例子中, 我们找到仅有的一个基础类: Aniaml , 找到方法: Animal::speak , 并将其作为参数传给 Mouse 类。
    11. 9. @_ 的作用
    在上一个例子中, 没有任何额外的参数给 speak 方法(如多少次, 或者唱什么调), 参数将会被 Mouse::speak 方法忽略。 如果我们要把他们未经解释的传给父类, 我们可以把它作为参数加进去:
    $class->SUPER::speak(@_);
    这句调用父因的方法, 包括所有我们还没有传入的参数列表。
    哪个方法是对的? 这要按情况看。 如果我们写一个类, 只加到父类的行为, 那么最好就是把我们没能处理的参数传给他。 然而, 如果我们要精确控制父类的行为, 我们应该明确决定参数列表, 并传给它。
    11. 10. 我们已经到哪了. . .
    至此, 我们已经用了方法箭头这个语法:
    Class->method(@args);
    或者等价的:
    my $beast = 'Class';
    $beast->method(@args);
    创建一个参数列表:
    ('Class', @args)
    尝试调用:
    Class::method('Class', @args);
    然而, 如果 Perl 没有找到 Class::method , 它会去查 @Class::ISA( 以递归的方式)来定位实际包含执行方法的包, 并调用。
    第 12 章展示如何给出相关的属性来区分不同的动物, 所谓实例变量。
    11. 11. 习题
    答案附录找。
    11. 11. 1. 练习1 [ 20 分钟]
    输入 Animal , Cow , Horse , Sheep , 和 Mouse 类的定义.在 use strict 下可以工作。 如果你的 Perl 版本很新, 那么你可以用 our 关键字。 你的程序要问用户输入一个或多个农场动物的名字。 然后以这些动物创建整个农场, 每个动物都有自己的叫声。
    11. 11. 2. 练习2 [ 40 分钟]
    在 Aniaml 同一层加上 Person 类, 而且他们两个都继承自一个新类: LivingCreature. 写一个 speak 方法, 它取一个参数作为说话内容, 如果没有给参数, 则使用 sound (对于 Person 类来说是 humming )。 因为这不是怪医杜立德, 所以要保证动物们不能对话。 (就是说 speak 对于动物来说没有任何参数 ) 不要写重复代码, 但是要保证捕获到相似的错误, 如忘了为某个动物定义叫声。
    用调用 Person 类, 然后再调用 Person 类, 并让他说些话。
    Chapter 12. 带数据的对象
    应用第 11 章介绍的简单的语法, 我们就可以创建类方法, (多重)继承、重载和扩展。 我们也可以把代码中共同的部份找出来并用变量代之以重用。 这是面向对象编程的核心概念, 而且对象也提供实例数据, 这点我们还没有开始了解。
    12. 1. 一匹马属于马类, 各从其类是吗?
    让我们看一下我们在第 11 章用于表示动物类和马类的代码:
    { package Animal;
    sub speak {
    my $class = shift;
    print "a $class goes ", $class->sound, "!\n"
    }
    }
    { package Horse;
    @ISA = qw(Animal);
    sub sound { 'neigh' }
    }
    这样, 让我们调用 Horse-speak> 从而会找到 Animal::speak , 然后会回调 Horse::sound 来取得特定的声音, 其输出是:
    a Horse goes neigh!
    但是所有的 Horse 对象都必须完全相同。 如果我们加了一个方法, 则所有的 horses 对象都自动共享它。 这对于保持对象都有的共性这点很好, 但是如何捕捉到每个 horse 个体的属性呢? 比如说, 假定我们要给我们的马取个名字。 应该有个方法来区分它的名字与其它马的名字。
    我们可以建立一个实例来实现这个功能。 一个实例一般来说根据一个类创建, 就是汽车由汽车工厂生产一样。 一个实例有其关联的属性, 称为实例的变量 (或者叫成员变量, 如果你有C++或 Java 背景的话). 每个实例有个惟一标识(有些像注册过的赛马的序列号一样), 共享属性(赛马的毛色与出色的体力), 和一般的行为 (e. g. , pulling the reins back tells the horse to stop).
    在 Perl 中, 一个实例必须是一个对内建类的引用.通过最简单的引用可以保存马的名字, 一个标量引用:

  • 这是最简单的, 但鲜有在实际的代码中使用, 原因我们后面会说到。
    my $name = 'Mr. Ed';
    my $tv_horse = \$name;
    现在 $tv_horse 是对一个实例数据(马的名字)的引用.最后一步就是把它变成一个真实的实例, 这要么一个专门的操作符叫 bless:
    bless $tv_horse, 'Horse';
    bless 操作符根据引用找到其指的变量, 在这个例子中, 是标量 $name. Then it "blesses" that variable, turning $tv_horse into an objecta Horse object, in fact. (想象一个小的标签说那匹马现在与 $name 关联在一起了。 )
    这下, $tv_horse 现在是 Horse 的一个实例 .[ +] 那就是, 它现在是一个不同的马了.而引用并没有改变什么, 它仍然可以以传统的还原操作符还原 .[ +] []
    [] 实际上, $tv_horse 指向一个对象, 但是, 在常用的术语来说, 我们几乎总是用引用与对象打交道来处理对象的.之后, 我们就可以简单地说 $tv_horse 就是马, 而不是 "$tv_horse 引用的那个东西"
    [] 尽管在一个类之外做这事是个糟糕的想法, 这个我们会在后面解说。
    12. 2. 调用一个实例方法
    方法箭头可以用在实例上, 就像用在包名上(类)一样。 让我们用 $tv_horse 发声:
    my $noise = $tv_horse->sound;
    为了调用 sound 方法, Perl 首先注意到 $tvHorse 是一个"被祝福"的引用, 所以是一个对象实例。 于是 Perl 创建一个参数列表, 有些像当我们用类名加方法箭头那种形式。 在这个例子中, 它就是 ($tv_horse). (之后我们会展示参数将跟在实例变量后面, 就像跟着类一样。 )
    现在有趣的地方来了: Perl 被祝福的实例中的类, 在这个例子中是 Horse , 并用它来定位并调用方法, 就好像我们用的是 Horse-sound> 而不是 $tv_horse-sound> 一样。 最初所谓的"祝福"的目的就是把一个类和它的引用关联起来, 让 Perl 能找到适当的方法。
    在此例中, Perl 直接找到 Horse::sound( 没有用到继承), 也就是最终的子程序调用:
    Horse::sound($tv_horse)
    注意这里第一个参数仍旧是实例, 不是像以前一样的类名。 "neigh" 是输出值, 像以往 $noise 变量一样。
    如果 Perl 没有找到 Horse::sound , 那么它会根据 @Horse::ISA 列表回溯在父类中查找方法, 就象对类方法的操作一样。 类方法和实例方法的区别是第一个参数是否为实例(一个被祝福过的引用)或一个类名(一个字串)。

  • 这同你可能熟悉的其它面向对象语言有所不同。
    12. 3. 访问实例数据
    因为我们把实例作为第一个参数, 我们现在可以访问实例自己的数据.在这个例子中, 让我们添加一个方法来获取一个名字:
    { package Horse;
    @ISA = qw(Animal);
    sub sound { 'neigh' }
    sub name {
    my $self = shift;
    $$self;
    }
    }
    现在调用名字:
    print $tv_horse->name, " says ", $tv_horse->sound, "\n";
    在 Horse::name 中, @_ 数组只包含了 $tv_horse , 并保存到 $self 中。 一般来说它把第一个参数传到实例方法的 $self 变量中, 所以保持这个风格, 除非你有十足的理由用其它的风格 (然而, Perl 对 $self 并没有特殊的意义). [] 如果你把 $self 作为一个标量引用还原, 那么输出 Mr.Ed 是:
    [] 如果你有其它面向对象语言的背景, 你可能会用 $this 或 $me 为变量起名, 不过你可能与其它 Perl 面向对象的黑客混淆。
    Mr. Ed says neigh.  
    12. 4. 如何创建 Horse类
    如果我们手工创建所有的马, 那我们很可能一次次犯错。 而且暴露 Horse 内部的信息也违反了面向对象编程的基本原则。 我们不是兽医, 我们只想拥有一匹马而已。 我们用 Horse 类创建一个新的马:
    { package Horse;
    @ISA = qw(Animal);
    sub sound { 'neigh' }
    sub name {
    my $self = shift;
    $$self;
    }
    sub named {
    my $class = shift;
    my $name = shift;
    bless \$name, $class;
    }
    }
    现在, 用新的 named 方法, 我们来创建一个 Horse:
    my $tv_horse = Horse->named('Mr. Ed');
    参考类方法, 所以有两个参数传给 Horse::named , "Horse" 和 "Mr.Ed". Bless 操作符不仅"祝福"了 $name , 也返回了 $name 的引用, 所以返回值是对的。 我们就是这样创建一个 horse 对象。
    我们调用构建函数 named 来快速地把参数作为马的名字。 我们可以用不同名字的构建函数来给对象加上生日(如谱系记录或生日)。 然而, 大多数人喜欢用 new 来命名构建函数, 并对不同的参数以不同的解释.只要能太到目的, 两种方法都可以。 CPAN 上大多数模块用 new , 也有些是例外, 比如 DBI 模块的 DBI-connect().> 这根据开发者的习俗。
    12. 5. 继承构建函数
    对于那个方法中 Horse 类有其它特殊的吗? 没有。 因此, 它同样可以继承自 Animal 类, 所以我们可以把它放到这儿:
    { package Animal;
    sub speak {
    my $class = shift;
    print "a $class goes ", $class->sound, "!\n"
    }
    sub name {
    my $self = shift;
    $$self;
    }
    sub named {
    my $class = shift;
    my $name = shift;
    bless \$name, $class;
    }
    }
    { package Horse;
    @ISA = qw(Animal);
    sub sound { 'neigh' }
    }
    嗯, 但如果我们在实例上调用 speak 会发生什么呢?
    my $tv_horse = Horse->named('Mr. Ed');
    $tv_horse->speak;
    我们会得到一个测试值:
    a Horse=SCALAR(0xaca42ac) goes neigh!
    为什么呢?因为 Animal::speak 方法期一个类名作为它的第一个参数, 而不是一个实例。 当我们传一个实例时, 我们会把"祝福"过的标量引用当作一个字串, 就像我们直接打印一个引用一样, 不过是前面有个类名提示。
    12. 6. 让一个方法在类和实例都可以使用
    解决这个问题, 我们所要做的就是查这个方法的调用者是个类还是实例。 最直接的方法是用 ref 操作符。 这个操作符在用在一个 blessed 引用上会返回一个字串(类名), 而用在一个字串(如类名)时返回的是空值。 我们改下看看:
    sub name {
    my $either = shift;
    ref $either
    ? $$either                # it's an instance, return name
    : "an unnamed $either";   # it's a class, return generic
    }
    这里  ? : 操作符选择是否是还原或是类字串。 现在不管是实例或者是类我们都可以使用他们。 注意, 我们把第一个参数位改成 $either 来显示这是有意的:
    print Horse->name, "\n";      # prints "an unnamed Horse\n"

    my $tv_horse = Horse->named('Mr. Ed');
    print $tv_horse->name, "\n";   # prints "Mr. Ed. \n"
    我们将使用这个设置叫声:
    sub speak {
    my $either = shift;
    print $either->name, ' goes ', $either->sound, "\n";
    }
    因此 sound 方法既可以在类上也可以在实例上运行, 完成了!
    12. 7. 给方法导入参数
    现在让我们训练我们的动物会吃:
    { package Animal;
    sub named {
    my $class = shift;
    my $name = shift;
    bless \$name, $class;
    }
    sub name {
    my $either = shift;
    ref $either
    ? $$either # it's an instance, return name
    : "an unnamed $either"; # it's a class, return generic
    }
    sub speak {
    my $either = shift;
    print $either->name, ' goes ', $either->sound, "\n";
    }
    sub eat {
    my $either = shift;
    my $food = shift;
    print $either->name, " eats $food. \n";
    }
    }
    { package Horse;
    @ISA = qw(Animal);
    sub sound { 'neigh' }
    }
    { package Sheep;
    @ISA = qw(Animal);
    sub sound { 'baaaah' }
    }
    然后试试:
    my $tv_horse = Horse->named('Mr. Ed');
    $tv_horse->eat('hay');
    Sheep->eat('grass');
    输出如下:
    Mr. Ed eats hay.  
    an unnamed Sheep eats grass.  
    一个带实例方法把实际作为参数, 然后是参数列表.调用的样子如下:
    Animal::eat($tv_horse, 'hay');
    一个实际的方法就是一个对象的应用程序接口( API )。 一个好的面向对象的设计很大程度上取决于 API 设计, 因为 API 决定了对象如何被使用和被维护, 以及其子类应该是什么样子的。 不要在还没有考虑好你(或其他人)如何用这个对象前急急忙忙的确定 API 设计。
    12. 8. 更有趣的实例
    如果一个实例需要更多的数据如何?大多数有用的实例是由许多成员组成, 而其中每个成员可以是一个引用或另一个对象。 保存这些成员最简单的办法就是把它们放在一个散列中。 这个散列的键是对象的名字(也被叫作实例或成员变量), 而且相应的值就是, 值。
    我们把 horse 变成一个散列如何?
  • 回想一下, 所谓对象实际上是被"祝福"的引用。 我们可以像"祝福"一个标量引用一样容易的"祝福"一个散列引用, 只要把它们看作引用就可以了。
  • 就是说不要叫屠夫来就可以了。
    让我们做一个有名有色的绵羊:
    my $lost = bless { Name => 'Bo', Color => 'white' }, Sheep;
    $lost-{Name}> 里存的是 "Bo" , $lost-{Color}> 里放的是 white. 但我们要用 $lost-name> 访问 name 变量, 但是因为要用标量引用, 所以会造成混乱.别急, 这很容易解决:
    ## in Animal
    sub name {
    my $either = shift;
    ref $either
    ? $either->{Name}
    : "an unnamed $either";
    }
    named 方法创建标量的 sheep 对象, 所以让我们来修改一下:
    ## in Animal
    sub named {
    my $class = shift;
    my $name = shift;
    my $self = { Name => $name, Color => $class->default_color };
    bless $self, $class;
    }
    那么缺省毛色呢?
    ## in Sheep
    sub default_color { 'white' }
    然后, 为了不用在每个类都作定义, 我们直接在 Animal 中定义一个缺省的方法。
    ## in Animal
    sub default_color { 'brown' }
    这样, 所有的动物都是棕色(土色, 也许吧), 除非个别动物特别指定了毛色, 对这个方法进行了重载。
    现在, 因为 name 和 named 是引用这个对象仅有的方法, 所以其它方法可以不变, 这样 speak 仍然可以像以前那样使用。 这就支持了面向对象编程的一条基本法则:如果要访问对象内部数据, 那修改结构的时候应该用最少的代码修改。
    12. 9. 一匹有不同色彩的马
    我们来加一两个方法来设置颜色, 这样让所有出生的马都是棕色。
    ## in Animal
    sub color {
    my $self = shift;
    $self->{Color};
    }
    sub set_color {
    my $self = shift;
    $self->{Color} = shift;
    }
    我们可以为 Mr.Ed 修改一下颜色:
    my $tv_horse = Horse->named('Mr. Ed');
    $tv_horse->set_color('black-and-white');
    print $tv_horse->name, ' is colored ', $tv_horse->color, "\n";
    其输出是:
    Mr. Ed is colored black-and-white
    12. 10. 取回存储
    根据代码的写法, 设置方法也可以返回更新的值.当我们写设值代码时我们要考虑, 或者要写下来。 设置代码应该返回什么?下面是一般的答案:
    更新过的值 (类似于传入的是什么)
    以前的值 (类似于用掩码的方法或选择工作时单值形式的参数)
    对象自身
    成功/失败码
    几种方法各有优缺点.比如, 如果我们返回更新的值, 我们可以将其用于其它的对象:
    $tv_horse->set_color( $eating->set_color( color_from_user(  ) ));
    上面的例子返回新更新的值.一般来说, 这样写代码是容易, 执行起来也最快。
    如果我们返回更改前的值, 我们可以容易地写临时处理程序:
    {
    my $old_color = $tv_horse->set_color('orange'); . . . do things with $tv_horse. . .  
    $tv_horse->set_color($old_color);
    }
    实现结果是:
    sub set_color {
    my $self = shift;
    my $old = $self->{Color};
    $self->{Color} = shift;
    $old;
    }
    为了效率, 我们可以用 wantarray 函数在没有返回值的情况下不存以前的值:
    sub set_color {
    my $self = shift;
    if (defined wantarray) {
    # this method call is not in void context, so
    # the return value matters
    my $old = $self->{Color};
    $self->{Color} = shift;
    $old;
    } else {
    # this method call is in void context
    $self->{Color} = shift;
    }
    }
    如果想返回对象自身, 我们可以链式设置:
    my $tv_horse =
    Horse->named('Mr. Ed')
    ->set_color('grey')
    ->set_age(4)
    ->set_height('17 hands');
    这样的代码是可行的, 因为每个设置方法都是原始对象, 成为下个方法调用对象.还可以这样:
    sub set_color {
    my $self = shift;
    $self->{Color} = shift;
    $self;
    }
    避免无返回值的方法这里也可以使用, 尽管我们已经建立了 $self 变量。
    最后, 如果程序明显出错, 返回一个返回状态要比一个意外报错要好.其它的变化会发一个例外并终止程序, 以示程序错误。
    总结:经过考虑后, 按需要应用, 但无论如何要写下来(而且在发布后不要再改)
    12. 11. 不要往盒子里看
    我们可以通过下面的散列引用 $tv_horse-{Color}> 通过类的外部来获取或设置颜色。 然而, 这样就因为暴露了内部结构而违反了对象的封装性。 对象应该是一个黑盒, 而我们已经撬开了绞链, 看到了里边。
    面向对象设计的目标之一就是要让 Animal 或 Horse 的代码维护者在进行合理独立的改动方法的实现的时候, 使接口仍然可以工作。 要看为什么直接访问散列就破坏了封装, 让我们打个比方, 如果我们不用简单的颜色名字来代表颜色, 而是用 RGB 三色数字来代表颜色(用一个数组引用来代表)。 在这个例子中, 我们用一个假想的(写这本书的时候) Color::Conversions 模块来改幕后的色彩格式:
    use Color::Conversions qw(color_name_to_rgb rgb_to_color_name); . . .  
    sub set_color {
    my $self = shift;
    my $new_color = shift;
    $self->{Color} = color_name_to_rgb($new_color);  # arrayref
    }
    sub color {
    my $self = shift;
    rgb_to_color_name($self->{Color});               # takes arrayref
    }
    我们可以在维护旧的接口时一样可以用设置和取值程序, 因为在用户不知道具体细节的情况下他们可以自动转换.我们也可以添加新的方法来直接改 RGB 三色数字:
    sub set_color_rgb {
    my $self = shift;
    $self->{Color} = [@_];                # set colors to remaining parameters
    }
    sub get_color_rgb {
    my $self = shift;
    @{ $self->{Color} };                  # return RGB list
    }
    如果我们在类的外面直接看 $tv_horse-{Color}> , 这样的改变是不可能的。 它不能在存数组引用( [0 , 0, 255] )的地方存字串( 'blue' )或把数组引用当作字串。 这就是为什么面向对象的编程鼓励你用设置器或取值器, 尽管他们可能费些写代码的时间。
    12. 12. 更快的取值器和设置器
    因为我们打算总以比较良好的方式调用取值器和设置器, 而不直接改变数据结构, 设置器和取值器会被调用得很频繁。 为了节约调用时间, 我们可以看到他们被写成这样:
    ## in Animal
    sub color     { $_[0]->{Color} }
    sub set_color { $_[0]->{Color} = $_[1] }
    写这些代码的时候我们省了点儿时间, 代码执行也快了点儿, 尽管对于这些代码在我们的程序里具体发挥什么作用可能也不太了解。 变量 $_[0] 是访问 @_ 数组的第一个元素。 相比于把数组的变量放到另一个变量中的作法, 我们简单的直接引用它。
    12. 13. 既是设置器也是取值器
    另一种建立设置器和取值器替代方案是用一个方法, 以参数作区分是取值还是设置值。 如果参数缺失, 那么就作取值操作; 如果有值, 那就设置值。 简例如下:
    sub color {
    my $self = shift;
    if (@_) {              # are there any more parameters?
    # yes, it's a setter:
    $self->{Color} = shift;
    } else {
    # no, it's a getter:
    $self->{Color};
    }
    }
    这样我们就可以这样写:
    my $tv_horse = Horse->named('Mr. Ed');
    $tv_horse->color('black-and-white');
    print $tv_horse->name, ' is colored ', $tv_horse->color, "\n";
    第二行出现的参数表明我们正设置颜色, 如果它缺失, 表示我们调用取值器。
    这种方案具有吸引力是因为其简洁, 但这也有其缺点.它混淆了频繁的取值的动作.它也使通过我们的代码来找用特殊参数的设置器变得困难, 而这往往比取值重要.在以往, 因为一个设置器因为在升级后另一个函数返回多个值而变成了取值器的情况造成的麻烦就很多 .< 似乎应该反过来--译者>
    12. 14. 将方法限制成类的或对象实例的
    给一个无法命名的抽象的"马"起名字多半不是个好主意; 对实例也是一样。 在 Perl 语言中没有一种方法定义"这是一个类的方法" 或"这是一个对象实例的方法 ". 好在 ref 操作符让我们可以在调用出错的时候抛出一个异常。 考虑下面一个仅仅是"实例"或"类"方法的例子, 我们用参数来决定下面一步是什么:
    use Carp qw(croak);

    sub instance_only {
    ref(my $self = shift) or croak "instance variable needed"; . . . use $self as the instance. . .  
    }

    sub class_only {
    ref(my $class = shift) and croak "class name needed"; . . . use $class as the class. . .  
    }
    ref 函数对于实例会返回 true , 对于被"祝福"的引用也是一样, 如果是类就返回 false , 就是个字串。 如果它返回一个我们不要的值, 我们可以用 Carp 模块(在标准发行版)中的 croak 函数。 croak 函数把出错信息看上去好像是调用者发出的一样, 而不是被调用的函数发出。 这样调用者会得到如下的出错信息, 并显示调用者的行号:
    instance variable needed at their_code line 1234
    croak 提供了 die 函数的替代方案, Carp 模块同样提供 carp 来替代 warn 的方案。 它们都指出出问题的调用函数所在的行号。 我们可以在代码中用 die 和 warn 一样用 Carp 模块中的函数。 你的用户会因此感谢你的。
    12. 15. 练习
    答案索引找
    12. 15. 1. 练习 [ 45 分钟]
    给 Animal 类添加设置和取得名字和颜色的能力。 要保证在 use strict 下能运行.而且要保证 get 方法在类和实例情况下都能工作.并以以下代码测试:
    my $tv_horse = Horse->named('Mr. Ed');
    $tv_horse->set_name('Mister Ed');
    $tv_horse->set_color('grey');
    print $tv_horse->name, ' is ', $tv_horse->color, "\n";
    print Sheep->name, ' colored ', Sheep->color, ' goes ', Sheep->sound, "\n";
    要在类层面设置名字或颜色, 你该如何做?

      

  • 运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
    2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
    3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
    4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
    5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
    6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
    7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
    8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

    所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-156235-1-1.html 上篇帖子: [转移]安装 BugZilla 时 Perl 模块的安装 下篇帖子: perl正则表达式
    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    扫码加入运维网微信交流群X

    扫码加入运维网微信交流群

    扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

    扫描微信二维码查看详情

    客服E-mail:kefu@iyunv.com 客服QQ:1061981298


    QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


    提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


    本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



    合作伙伴: 青云cloud

    快速回复 返回顶部 返回列表