你需要知道的Perl - 第2部分

简介

在本文中,我们将继续讨论在开始为mod_perl编程之前你应该知道的Perl基本知识。

跟踪警告报告

有时很难理解警告在抱怨什么。你看到了源代码,但无法理解为什么某个特定的代码片段会产生那个警告。这种神秘往往源于代码可以来自不同的地方,如果它位于子例程内部。

以下是一个例子

  warnings.pl
  -----------
  #!/usr/bin/perl -w

  use strict;

  correct();
  incorrect();

  sub correct{
    print_value("Perl");
  }

  sub incorrect{
    print_value();
  }

  sub print_value{
    my $var = shift;
    print "My value is $var\n";
  }

在上面的代码中,print_value()打印传入的值。子例程correct()将值传递给打印,但在子例程incorrect()中我们忘记了传递它。当我们运行脚本时

  % ./warnings.pl

我们得到警告

  Use of uninitialized value at ./warnings.pl line 16.

Perl在尝试打印其值的行上抱怨未定义的变量$var

  print "My value is $var\n";

但我们怎么知道为什么它是未定义的呢?这里的明显原因是调用函数没有传递参数。但我们怎么知道调用者是谁?在我们的例子中,有两个可能的调用者,在一般情况下可能有更多,也许位于其他文件中。

我们可以使用caller()函数,它告诉我们谁在调用我们,但这可能还不够:可能有更长的调用子例程序列,而不仅仅是两个。例如,这里是有错误的子例程third(),在子例程second()中放入caller()并不能解决问题

  sub third{
    second();
  }
  sub second{
    my $var = shift;
    first($var);
  }
  sub first{
    my $var = shift;
   print "Var = $var\n"
  }

解决方案很简单。我们需要的是完整的调用栈跟踪到触发警告的调用。

Carp模块通过其cluck()函数来帮助我们。让我们修改脚本,添加几行。脚本的其他部分保持不变。

  warnings2.pl
  -----------
  #!/usr/bin/perl -w

  use strict;
  use Carp ();
  local $SIG{__WARN__} = \&Carp::cluck;

  correct();
  incorrect();

  sub correct{
    print_value("Perl");
  }

  sub incorrect{
    print_value();
  }

  sub print_value{
    my $var = shift;
    print "My value is $var\n";
  }

现在当我们执行它时,我们看到

  Use of uninitialized value at ./warnings2.pl line 19.
    main::print_value() called at ./warnings2.pl line 14
    main::incorrect() called at ./warnings2.pl line 7

花点时间理解调用栈跟踪。最深的调用首先打印。所以第二行告诉我们警告是在print_value()中触发的;第三行告诉我们print_value()是由子例程incorrect()调用的。

  script => incorrect() => print_value()
O'Reilly开源大会 -- 7月22-26日,加州圣地亚哥。

从研究前沿到企业核心

mod_perl 2.0,下一代 Stas Bekman将在即将于7月22-26日在圣地亚哥举行的O'Reilly开源大会上概述mod_perl 2.0的新功能和未来计划。

我们进入incorrect()确实看到我们忘记了传递变量。当然,当你编写像print_value这样的子例程时,在开始执行之前检查传入的参数是一个好主意。我们省略了这个步骤,以构建一个易于调试的例子。

当然,你说,通过简单地检查代码我就能找到那个问题!

好吧,你说得对。但我要告诉你,如果你的代码有几千行,你的任务将非常复杂且耗时。此外,在mod_perl中,某些对eval运算符和“here documents”的使用会导致Perl的行编号出错,因此报告警告和错误的消息可能会出现错误的行号。这可以通过帮助编译器使用#line指令来轻松解决。如果你在你的脚本开始时放入以下内容

 #line 125

那么它将告诉编译器,对于报告需要,下一行是125号。当然,其余的行也会相应地调整。

获取跟踪非常有帮助。

嵌套子例程中的my()作用域变量

在继续之前,让我们假设我们想要在strict编译指令下开发代码。只要可能,我们将使用词法作用域变量(借助my()运算符)。

毒药

让我们看看这段代码

  nested.pl
  -----------
  #!/usr/bin/perl

  use strict;

  sub print_power_of_2 {
    my $x = shift;

    sub power_of_2 {
      return $x ** 2;
    }

    my $result = power_of_2();
    print "$x^2 = $result\n";
  }

  print_power_of_2(5);
  print_power_of_2(6);

不要被奇怪的子程序名称欺骗,子程序print_power_of_2()应该打印传入它的数字的平方。让我们运行代码,看看它是否工作

  % ./nested.pl

  5^2 = 25
  6^2 = 25

哎呀,出问题了。也许是Perl存在一个错误,它不能正确处理数字6?让我们再试一次,用5和7

  print_power_of_2(5);
  print_power_of_2(7);

然后运行它

  % ./nested.pl

  5^2 = 25
  7^2 = 25

哇,它只对5工作吗?用3和5试试

  print_power_of_2(3);
  print_power_of_2(5);

结果是

  % ./nested.pl

  3^2 = 9
  5^2 = 9

现在我们开始理解了——只有第一次调用print_power_of_2()函数是正确的。这使得我们怀疑我们的代码在第一次执行的结果中存在某种形式的记忆,或者在后续执行中忽略了参数。

诊断

让我们遵循指南,使用-w标志

  #!/usr/bin/perl -w

在Perl版本5.6.0+中,我们使用warnings编译指令

  #!/usr/bin/perl
  use warnings;

现在执行代码

  % ./nested.pl

  Variable "$x" will not stay shared at ./nested.pl line 9.
  5^2 = 25
  6^2 = 25

我们从未见过这样的警告信息,我们也不太清楚它的意思。diagnostics编译指令肯定能帮我们。让我们在我们的代码中将这个编译指令放在strict编译指令之前

  #!/usr/bin/perl -w

  use diagnostics;
  use strict;

然后执行它

  % ./nested.pl

变量“$x”在./nested.pl第10行(#1)不会被共享

(W)内部(嵌套)命名的子程序引用了在外部子程序中定义的词法变量。

当内部子程序被调用时,它可能看到的值是外部子程序变量在第一次调用之前的值;在这种情况下,在外部子程序第一次调用完成后,内部和外部子程序将不再共享变量的公共值。换句话说,变量不再共享。

此外,如果外部子程序是匿名的并且引用它自身之外的字面量变量,那么外部和内部子程序永远不会共享给定的变量。

通常可以通过将内部子程序匿名化来解决这个问题,使用sub {}语法。当内部匿名子程序引用外部子程序中的变量时,它们在调用或引用时会自动绑定到当前变量的值。

  5^2 = 25
  6^2 = 25

好了,现在一切都清楚了。在我们的代码中,我们有内嵌的子程序power_of_2()和外部的子程序print_power_of_2()

当内部power_of_2()子程序第一次被调用时,它看到外部print_power_of_2()子程序的$x变量值。在随后的调用中,内部子程序的$x变量不会更新,无论在外部子程序中给$x赋予什么新值。现在有两个$x变量的副本,不再是两个程序共享的一个。

解决办法

diagnostics编译指令建议,可以通过使内部子程序匿名来解决这个问题。

匿名子程序可以相对于词法作用域变量作为闭包。基本上,这意味着如果你在某个特定的词法上下文中在某个时刻定义了一个子程序,那么它将在稍后以相同的上下文运行,即使是从外部上下文调用。结果是,当子程序运行时,你得到与子程序定义时可见的相同的词法作用域变量副本。因此,你可以在定义函数时传递参数,也可以在调用它时传递参数。

让我们重写代码以使用这种技术

  anonymous.pl
  --------------
  #!/usr/bin/perl

  use strict;

  sub print_power_of_2 {
    my $x = shift;

    my $func_ref = sub {
      return $x ** 2;
    };

    my $result = &$func_ref();
    print "$x^2 = $result\n";
  }

  print_power_of_2(5);
  print_power_of_2(6);

现在 $func_ref 包含了一个匿名函数的引用,稍后我们需要获取2的幂时我们会使用它。(在Perl中,函数与子程序是同一事物。)由于它是匿名的,这个函数将自动重新绑定到外部作用域变量 $x 的新值,现在的结果应该是预期的。

让我们验证一下

  % ./anonymous.pl

  5^2 = 25
  6^2 = 36

因此,我们可以看到问题已解决。

当您无法摆脱内部子程序时

首先,您可能会想知道,为什么有人需要定义一个内部子程序?例如,为了减少Perl脚本启动开销,您可能会决定编写一个守护进程,该进程只编译一次脚本和模块,并将预编译的代码缓存到内存中。当某个脚本要执行时,您只需告诉守护进程要运行的脚本名称,它就会完成剩下的工作,由于编译已经完成,所以会运行得更快。

这似乎是一个简单的任务;确实如此。唯一的问题是,一旦脚本被编译,如何执行它?或者让我们换个说法:在它第一次被执行并保持在守护进程的内存中后,如何再次调用它?如果您能让所有开发者编写脚本,每个脚本都有一个名为 run() 的子程序来实际执行脚本中的代码,那么我们就解决了问题的一半。

但是,如果所有这些都在 main:: 命名空间中运行,守护进程如何知道要引用特定的脚本?一个解决方案可能是要求开发者在每个脚本中声明一个包,并且包名应从脚本名称派生。然而,由于有可能存在多个具有相同名称但位于不同目录中的脚本,因此为了防止命名空间冲突,目录也必须是包名称的一部分。别忘了,脚本可能被移动到另一个目录,所以您必须确保每次脚本被移动时包名称都得到纠正。

但是,为什么要在开发者身上强加这些奇怪的规则,当我们可以安排我们的守护进程来完成这项工作?对于守护进程即将第一次执行的每个脚本,脚本应被包装在一个由脚本的混乱路径和名为 run() 的子程序构建的包中。例如,如果守护进程即将执行脚本 /tmp/hello.pl

  hello.pl
  --------
  #!/usr/bin/perl
  print "Hello\n";

那么在运行之前,守护进程将代码更改为

  wrapped_hello.pl
  ----------------
  package cache::tmp::hello_2epl;

  sub run{
    #!/usr/bin/perl
    print "Hello\n";
  }

包名称由前缀 cache:: 构建,每个目录分隔符反斜杠被替换为 ::,并且非字母数字字符被编码,例如,点 .(点)变为 _2e(一个下划线后跟点在十六进制表示中的ASCII代码)。

 % perl -e 'printf "%x",ord(".")'

打印:2e。下划线与您在URL编码中看到的是相同的,除了使用 % 字符代替(%2E),但是,由于 % 在Perl中有特殊含义(散列变量的前缀),因此不能使用。

现在当守护进程被要求执行脚本 /tmp/hello.pl 时,它只需根据脚本的位置构建包名称,并调用其 run() 子程序

  use cache::tmp::hello_2epl;
  cache::tmp::hello_2epl::run();

我们刚刚编写了我们想要的守护进程的部分原型。唯一悬而未决的问题是,如何将脚本路径传递给守护进程。这个细节留给读者作为练习。

如果您熟悉 Apache::Registry 模块,那么您知道它几乎以相同的方式工作。它使用不同的包前缀,通用函数被称为 handler() 而不是 run()。要运行的脚本通过HTTP协议的头部传递。

现在你明白了一些情况下你的常规子程序可以变成内部的,因为如果你的脚本是一个简单的

  simple.pl
  ---------
  #!/usr/bin/perl
  sub hello { print "Hello" }
  hello();

被包装成 run() 子程序后,它变成

  simple.pl
  ---------
  package cache::simple_2epl;

  sub run{
    #!/usr/bin/perl
    sub hello { print "Hello" }
    hello();
  }

因此,hello() 是一个内部子程序,如果你在 hello() 内部定义并修改了 my() 范围的变量,从第二次调用开始它就不会按预期工作,如前节所述。

内部子程序的处理方法

首先,不必担心,只要别忘了打开警告。如果你确实遇到了“嵌套子程序中的 my() 范围变量”问题,Perl 会始终提醒你。

既然你有这个问题的脚本,有哪些方法可以解决这个问题?有很多种方法,我们在这里将讨论其中一些。

我们将使用以下代码来展示不同的解决方案。

  multirun.pl
  -----------
  #!/usr/bin/perl -w

  use strict;

  for (1..3){
    print "run: [time $_]\n";
    run();
  }

  sub run{
    my $counter = 0;

    increment_counter();
    increment_counter();

    sub increment_counter{
      $counter++;
      print "Counter is equal to $counter !\n";
    }

  } # end of sub run

此代码执行 run() 子程序三次,每次执行时都会将 $counter 变量初始化为 0,然后调用内部子程序 increment_counter() 两次。子程序 increment_counter() 在增加后打印 $counter 的值。人们可能会看到以下输出

  run: [time 1]
  Counter is equal to 1 !
  Counter is equal to 2 !
  run: [time 2]
  Counter is equal to 1 !
  Counter is equal to 2 !
  run: [time 3]
  Counter is equal to 1 !
  Counter is equal to 2 !

但是,正如我们从前面的章节中学到的,这并不是我们将要看到的。事实上,当我们运行脚本时,我们看到

  % ./multirun.pl
  Variable "$counter" will not stay shared at ./nested.pl line 18.
  run: [time 1]
  Counter is equal to 1 !
  Counter is equal to 2 !
  run: [time 2]
  Counter is equal to 3 !
  Counter is equal to 4 !
  run: [time 3]
  Counter is equal to 5 !
  Counter is equal to 6 !

很明显,$counter 变量在每次执行 run() 时并没有被重新初始化。它保留了前一次执行时的值,子程序 increment_counter() 对其进行了增加。

一种解决方案是使用全局声明的变量,使用 vars 前置声明。

  multirun1.pl
  -----------
  #!/usr/bin/perl -w

  use strict;
  use vars qw($counter);

  for (1..3){
    print "run: [time $_]\n";
    run();
  }

  sub run {

    $counter = 0;

    increment_counter();
    increment_counter();

    sub increment_counter{
      $counter++;
      print "Counter is equal to $counter !\n";
    }

  } # end of sub run

如果你运行此代码和其他提供的解决方案,则将生成预期的输出

  % ./multirun1.pl

  run: [time 1]
  Counter is equal to 1 !
  Counter is equal to 2 !
  run: [time 2]
  Counter is equal to 1 !
  Counter is equal to 2 !
  run: [time 3]
  Counter is equal to 1 !
  Counter is equal to 2 !

顺便说一下,我们之前看到的警告已经消失,问题也已经解决,因为没有在嵌套子程序中使用 my()(词法定义的)变量。

另一种方法是使用完全限定的变量。这更好,因为这样可以节省内存,但它增加了打字负担。

  multirun2.pl
  -----------
  #!/usr/bin/perl -w

  use strict;

  for (1..3){
    print "run: [time $_]\n";
    run();
  }

  sub run {

    $main::counter = 0;

    increment_counter();
    increment_counter();

    sub increment_counter{
      $main::counter++;
      print "Counter is equal to $main::counter !\n";
    }

  } # end of sub run

你还可以通过值传递变量给子程序,并在更新后将其返回。这增加了时间和内存开销,所以如果变量非常大,或者执行速度是一个问题,这可能不是一个好主意。

在开发应用程序时不要依赖变量的大小,它可能会在你不期望的情况下变得很大。例如,一个简单的 HTML 表单文本输入字段如果用户无聊并想测试你的代码有多好,可以返回几兆字节的数据。用户将 10Mb 的核心转储文件复制粘贴到表单的文本字段中,然后提交给你的脚本来处理的情况并不罕见。

  multirun3.pl
  -----------
  #!/usr/bin/perl -w

  use strict;

  for (1..3){
    print "run: [time $_]\n";
    run();
  }

  sub run {

    my $counter = 0;

    $counter = increment_counter($counter);
    $counter = increment_counter($counter);

    sub increment_counter{
      my $counter = shift;

      $counter++;
      print "Counter is equal to $counter !\n";

      return $counter;
    }

  } # end of sub run

最后,你可以使用引用来完成这项工作。下面的 increment_counter() 版本接受对 $counter 变量的引用,并在解引用后增加其值。当你使用引用时,你函数内部使用的变量实际上是函数外部的同一块内存。这种技术通常用于允许被调用的函数修改调用函数中的变量。

  multirun4.pl
  -----------
  #!/usr/bin/perl -w

  use strict;

  for (1..3){
    print "run: [time $_]\n";
    run();
  }

  sub run {

    my $counter = 0;

    increment_counter(\$counter);
    increment_counter(\$counter);

    sub increment_counter{
      my $r_counter = shift;

      $$r_counter++;
      print "Counter is equal to $$r_counter !\n";
    }

  } # end of sub run

下面是另一种更不为人知的引用使用方法。我们通过使用变量在 @_ 中的事实来修改子程序内的 $counter 的值。因此,如果你用一个参数调用一个函数,那么它们将存储在 $_[0]$_[1] 中。特别是,如果更新了 $_[0] 中的元素,那么相应的参数也会更新(或者如果它不可更新,比如用文字调用函数,例如 increment_counter(5)),则会出现错误)。

  multirun5.pl
  -----------
  #!/usr/bin/perl -w

  use strict;

  for (1..3){
    print "run: [time $_]\n";
    run();
  }

  sub run {

    my $counter = 0;

    increment_counter($counter);
    increment_counter($counter);

    sub increment_counter{
      $_[0]++;
      print "Counter is equal to $_[0] !\n";
    }

  } # end of sub run

上面给出的方法通常不推荐,因为大多数 Perl 程序员不会期望函数会改变 $counter;我们更喜欢使用 \$counter,即通过引用传递的例子。

这是一个通过将代码拆分为两个文件来完全避免问题的解决方案:第一个文件实际上只是一个包装和加载器,第二个文件包含代码的核心。

  multirun6.pl
  -----------
  #!/usr/bin/perl -w

  use strict;
  require 'multirun6-lib.pl' ;

  for (1..3){
    print "run: [time $_]\n";
    run();
  }

单独的文件

  multirun6-lib.pl
  ----------------
  use strict ;

  my $counter;
  sub run {
    $counter = 0;
    increment_counter();
    increment_counter();
  }

  sub increment_counter{
    $counter++;
    print "Counter is equal to $counter !\n";
  }

  1 ;

现在您至少有六个解决方案可供选择。

更多信息,请参阅perlref和perlsub手册页面。

本系列之前的文章

您需要了解的Perl

无需超级用户权限安装mod_perl

30分钟学会mod_perl

为什么选择mod_perl?

perldoc的鲜为人知但非常实用的选项

众所周知,一个人如果不懂得如何阅读Perl文档和在其中搜索,就不能成为一个Perl黑客,尤其是mod_perl黑客。书籍是好的,但一个触手可及且易于搜索的Perl参考资料可以节省大量时间。它总是为您提供您正在使用的perl版本的最新信息。

当然,您可以在网上使用Perl文档:[https://perldoc.perl5.cn](https://perldoc.perl5.cn)。perldoc实用程序提供对您系统上安装的文档的访问。要查找可用的Perl手册页,请执行以下操作

  % perldoc perl

要查找perl有哪些函数,请执行

  % perldoc perlfunc

要了解语法并查找特定函数的示例,您可以执行(例如,对于open()

  % perldoc -f open

注意:在perl5.005_03及更早版本中,存在一个与perldoc-q选项相关的错误。它不会调用pod2man,而是以POD格式显示部分。尽管存在这个错误,它仍然可读且非常有用。

Perl FAQ(perlfaq手册页)分为几个部分。要搜索部分以查找open,您将执行

  % perldoc -q open

这将显示所有匹配的问答部分,仍然以POD格式。

要阅读perldoc手册页,请执行

  % perldoc perldoc

参考文献

  • 在线文档:[https://perldoc.perl5.cn](https://perldoc.perl5.cn)

  • 由L.Wall、T.Christiansen和J.Orwant编写的《Programming Perl》第3版(也称为《Camel》一书,书名取自封面上的骆驼图片)。您需要参考第8章,该章讨论了嵌套子程序等内容。

  • perlrefperlsub手册页。

标签

反馈

这篇文章有什么问题吗?请通过在GitHub上打开问题或拉取请求来帮助我们[https://github.com/perladvent/perldotcom/blob/master/content/legacy/_pub_2002_05_07_mod_perl.md](https://github.com/perladvent/perldotcom/blob/master/content/legacy/_pub_2002_05_07_mod_perl.md)