你需要了解的Perl - 第三部分

引言

本文是我们系列文章的第三篇,讨论在开始编写mod_perl程序之前应该了解的Perl基本知识。

全局变量、局部作用域和完全限定名变量

在Perl讨论中,你会听到很多关于命名空间、符号表和局部作用域的内容,但没有几个基本事实,这些都可能让你感到困惑。

符号、符号表和包;类型全局变量

有两种重要的符号类型:包全局和局部。我们稍后会讨论局部符号;现在,我们只讨论包全局符号,我们将其称为全局符号

你的代码片段(子程序名称)和全局变量的名称是符号。全局符号位于一个符号表或另一个中。代码本身和数据不在这里;符号是指向包含代码和数据内存区域的指针(间接指向)。(对C/C++程序员说明:我们在这里使用“指针”一词是一般意义上的一个数据项指向另一个数据项,而不是像C或C++中使用的特定意义。)

每个包都有一个符号表(这就是为什么全局符号实际上是包全局符号)。

你总是在一个包中工作。

就像在C语言中,你写的第一个函数必须命名为main()一样,你的第一个Perl脚本的第一个语句是在main::包中,这是默认包。除非你通过使用package语句来说明,否则你的所有符号都在main::包中。你应该知道文件和包是不相关的。你可以在单个文件中有任意数量的包;一个包可以位于一个文件中,也可以分散在多个文件中。然而,在单个文件中有一个包是常见的。要声明一个包,你写

    package mypackagename;

从下一行开始,你就在mypackagename包中,你声明的任何符号都位于该包中。当你创建一个符号(变量、子程序等)时,Perl会使用你当前工作的包的名称作为前缀来创建符号的完全限定名。

当你创建一个符号时,Perl会在当前包的符号表中为该符号创建一个符号表条目(默认为main::)。每个符号表条目被称为一个类型全局变量。每个类型全局变量可以包含有关标量、数组、哈希、子程序(代码)、文件句柄、目录句柄和格式的信息,每个都有相同的名称。所以你现在可以看到,全局变量有两个间接层:符号(这个事物的名称)指向其类型全局变量,而类型全局变量中的事物类型(标量、数组等)条目指向数据。如果我们有两个具有相同名称的标量和数组,那么它们的名称将指向同一个类型全局变量,但对于每种类型的数据,类型全局变量指向不同的地方。因此,标量的数据和数组的数据是完全独立和分开的,只是恰好具有相同的名称。

大多数情况下,我们只使用typeglob的一部分(是的,这有点浪费)。到现在为止,你知道你可以通过使用Camel书作者所说的“有趣字符”来区分它们。所以如果我们有一个名为line的标量,那么在代码中我们会称其为$line,如果我们有一个同名的数组,那么它会写成@line。两者都会指向同一个typeglob(称为*line),但由于这个“有趣字符”(也称为“装饰”),Perl不会混淆这两个。当然,我们可能会自己混淆,所以一些程序员从不为多种类型的变量使用相同的名称。

每个全局符号都在某个包的符号表中。要引用一个全局符号,我们可以写出完全限定的名称,例如$main::line。如果我们与符号所在的包相同,那么我们可以省略包名,例如$line(除非你使用了strict约定,那么你必须使用vars约定来预先声明变量)。如果我们已经将符号导入到当前包的命名空间中,我们也可以省略包名。如果我们想引用另一个包中的符号,而这个符号我们没有导入,那么我们必须使用完全限定名称,例如$otherpkg::box

大多数情况下,你不需要使用完全限定符号名称,因为你大多数时候会从包内部引用包变量。这就像C++类变量。你可以在main::包内完全工作,甚至不知道你正在使用包,也不知道符号有包名称。从某种意义上说,这很遗憾,因为你可能无法了解包,而它们非常有用。

例外情况是当你导入另一个包的变量。这会在当前包中为该变量创建一个别名,这样你就可以通过使用完全限定名称来访问它。

虽然全局变量对于共享数据很有用,并且在某些情况下是必要的,但通常更明智的做法是尽量减少它们的使用,并使用下一个讨论的词法变量

请注意,当你创建一个变量时,分配内存以存储信息的低级业务是由Perl自动处理的。解释器跟踪指针指向的内存块,并负责变量的未定义。当一个变量的所有引用都不再存在时,Perl垃圾收集器就可以回收它所占用的内存。然而,Perl几乎从不将其在进程生命周期内已经使用的内存返回给操作系统。

词法变量和符号

词法变量的符号(即使用my关键字声明的变量)是唯一不在符号表中存在的符号。正因为如此,它们无法从声明它们的块外部访问。与词法变量相关联的没有typeglob,词法变量只能引用标量、数组或哈希。

如果你需要从包外部访问数据,你可以从子程序中返回它,或者你可以创建一个指向它的全局变量(即带有包前缀的变量),然后返回它。引用必须是全局的,这样你就可以通过完全限定名称来引用它。但就像在C中一样,尽量避免使用全局变量。使用OO方法通常通过提供方法来解决此问题,这些方法可以在包内部按词法作用域传递,并通过引用设置和获取所需值。

“词汇变量”这个说法有些误导,因为我们实际上在谈论“词汇符号”。数据也可以通过全局符号进行引用,在这种情况下,当词汇符号超出作用域时,数据仍然可以通过全局符号访问。这是完全合法的,不能与将自动C变量的指针返回到函数中的可怕错误相提并论——当指针被解引用时,将发生段错误。(注:对于C/C++程序员:在C或C++中将函数返回到自动变量的指针是一个灾难;Perl的等价物,从函数中返回到词汇变量的引用是正常且有用的。)

O'Reilly开源大会 -- 7月22日至26日,加州圣地亚哥。

从研究前沿到企业核心

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

  • my()use vars

    使用use vars(),你正在符号表中添加一个条目,并且你正在告诉编译器你将不使用显式的包名来引用该条目。

    使用my(),不会在符号表中添加条目。编译器在编译时确定哪些my()变量(即词汇变量)是相同的,一旦到达执行时间,就不能在符号表中查找这些变量。

  • my()local()

    local() 创建一个临时性的基于包的标量、数组、哈希或全局变量——也就是说,当定义的作用域在运行时结束时,将恢复先前的值(如果有的话)。对这些变量的引用也是全局的……只是值发生变化。(顺便说一句:这就是变量自杀的原因。:)

    my() 创建一个基于词汇的、非包的标量、数组或哈希——当定义的作用域在编译时结束时,变量将不再可访问。任何运行时对这种变量的引用都会在每个作用域退出时变成唯一的匿名变量。

use()、require()、do()、%INC 和 @INC 解释

@INC 数组

@INC 是一个特殊的Perl变量,相当于shell中的PATH变量。而PATH包含一个要搜索可执行文件的目录列表,@INC包含一个Perl模块和库可以从中加载的目录列表。

当你使用use()require()do()一个文件名或模块时,Perl从@INC变量获取一个目录列表,并搜索请求加载的文件。如果你要加载的文件不在所列的目录中,你必须告诉Perl在哪里可以找到该文件。你可以提供一个相对于@INC中某个目录的路径,或者提供文件的完整路径。

%INC 哈希

%INC 是另一个特殊的Perl变量,用于缓存通过use()require()do()语句成功加载和编译的文件和模块的名称。在尝试使用use()或require()加载文件或模块之前,Perl会检查它是否已在%INC哈希中。如果它在其中,那么加载和编译将根本不会执行。否则,文件将被加载到内存中,并尝试编译它。do()无条件地加载——不在%INC哈希中进行查找。

如果文件成功加载和编译,则会向%INC添加一个新的键值对。键是文件或模块的名称,正如它被传递给刚刚提到的三个函数之一时。如果它在除"."以外的任何@INC目录中找到,则值是该文件系统中的完整路径。

以下示例将使您更容易理解逻辑。

首先,让我们看看在我的系统上@INC的内容是什么

  % perl -e 'print join "\n", @INC'
  /usr/lib/perl5/5.00503/i386-linux
  /usr/lib/perl5/5.00503
  /usr/lib/perl5/site_perl/5.005/i386-linux
  /usr/lib/perl5/site_perl/5.005
  .

请注意,.(当前目录)是列表中的最后一个目录。

现在让我们加载模块strict.pm并查看%INC的内容

  % perl -e 'use strict; print map {"$_ => $INC{$_}\n"} keys %INC'

  strict.pm => /usr/lib/perl5/5.00503/strict.pm

由于strict.pm/usr/lib/perl5/5.00503/目录中找到,并且/usr/lib/perl5/5.00503/@INC的一部分,因此%INC包括作为strict.pm键的值的完整路径。

现在让我们在/tmp/test.pm中创建最简单的模块

  test.pm
  -------
  1;

它什么也不做,但在加载时返回一个true值。现在让我们以不同的方式加载它

  % cd /tmp
  % perl -e 'use test; print map {"$_ => $INC{$_}\n"} keys %INC'

  test.pm => test.pm

由于文件是在相对于.(当前目录)的位置找到的,所以相对路径被插入为值。如果我们通过将/tmp添加到末尾来修改@INC

  % cd /tmp
  % perl -e 'BEGIN{push @INC, "/tmp"} use test; \
  print map {"$_ => $INC{$_}\n"} keys %INC'

  test.pm => test.pm

这里我们仍然得到相对路径,因为模块首先相对于"."找到。目录/tmp被放置在列表中的.之后。如果我们从不同的目录执行相同的代码,那么"."目录就不会匹配,

  % cd /
  % perl -e 'BEGIN{push @INC, "/tmp"} use test; \
  print map {"$_ => $INC{$_}\n"} keys %INC'

  test.pm => /tmp/test.pm

所以我们得到完整路径。我们还可以使用unshift()将路径前置,这样它就会在"."之前用于匹配,因此我们也会得到完整路径

  % cd /tmp
  % perl -e 'BEGIN{unshift @INC, "/tmp"} use test; \
  print map {"$_ => $INC{$_}\n"} keys %INC'

  test.pm => /tmp/test.pm

以下代码

  BEGIN{unshift @INC, "/tmp"}

可以被替换为更优雅的

  use lib "/tmp";

这与我们的BEGIN块几乎相同,并且是推荐的方法。

这些修改@INC的方法可能会很耗时,因为如果您想将脚本在文件系统中移动,那么您必须修改路径。这可能很痛苦,例如,当您将脚本从开发环境移动到生产服务器时。

有一个名为FindBin的模块可以解决这个问题,但在mod_perl下它不起作用,因为它是一个模块,并且像任何模块一样,它只加载一次。因此,第一个使用它的脚本将具有所有正确的设置,但如果它们在第一个脚本所在的目录之外,则其余的脚本将不会有这些设置。

为了完整性,我将介绍这个模块。

如果您使用此模块,则不需要编写硬编码的路径。以下代码片段为您做了所有工作(文件是/tmp/load.pl

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

  use FindBin ();
  use lib "$FindBin::Bin";
  use test;
  print "test.pm => $INC{'test.pm'}\n";

在上面的示例中,$FindBin::Bin等于/tmp。如果我们把脚本移动到其他地方……例如上面的代码中的/tmp/x,那么在代码中$FindBin::Bin等于/home/x

  % /tmp/load.pl

  test.pm => /tmp/test.pm

这就像use lib一样,只是不需要硬编码的路径。

您可以使用这个方法使其在mod_perl下工作。

  do 'FindBin.pm';
  unshift @INC, "$FindBin::Bin";
  require test;
  #maybe test::import( ... ) here if need to import stuff

这有一些开销,因为它将在每次请求时从磁盘加载并重新编译FindBin模块。因此,这可能不值得。

模块、库和程序文件

在继续之前,让我们定义一下我们所说的模块程序文件

  • 模块
  • 程序文件

require()

本系列之前的文章

您需要了解的Perl - 第2部分

您需要了解的Perl

无需超级用户权限安装mod_perl

30分钟掌握mod_perl

为什么使用mod_perl?

require()读取包含Perl代码的文件并编译它。在尝试加载文件之前,它会在%INC中查找参数,以查看它是否已经被加载。如果已经加载,则require()将不执行任何操作而返回。否则,将尝试加载和编译文件。

require() 需要找到它要加载的文件。如果参数是文件的完整路径,那么它将尝试读取它。例如

  require "/home/httpd/perl/mylibs.pl";

如果路径是相对的,那么 require() 将尝试在 @INC 中列出的所有目录中搜索文件。例如

  require "mylibs.pl";

如果 @INC 列出的目录中存在多个同名文件,则将使用第一个。

文件必须返回 TRUE 作为最后的语句以指示任何初始化代码执行成功。由于你永远不知道文件在未来会经历什么变化,因此你无法确定最后一条语句始终会返回 TRUE。这就是为什么建议在文件末尾放置 ``1;” 的原因。

尽管你应该为大多数文件使用实际文件名,但如果该文件是模块,则可以使用以下约定

  require My::Module;

这等于

  require "My/Module.pm";

如果 require() 无法加载文件,无论是找不到文件还是代码编译失败,或者它没有返回 TRUE,程序就会 die()。为了防止这种情况,可以将 require() 语句包含在 eval() 异常处理块中,如下例所示

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

  eval { require "/file/that/does/not/exists"};
  if ($@) {
    print "Failed to load, because : $@"
  }
  print "\nHello\n";

当我们执行程序时

  % ./require.pl

  Failed to load, because : Can't locate /file/that/does/not/exists in
  @INC (@INC contains: /usr/lib/perl5/5.00503/i386-linux
  /usr/lib/perl5/5.00503 /usr/lib/perl5/site_perl/5.005/i386-linux
  /usr/lib/perl5/site_perl/5.005 .) at require.pl line 3.

  Hello

我们看到程序没有 die(),因为打印了 Hello。这个 技巧 在你想检查用户是否安装了某些模块时很有用。如果她没有安装,那么这不是关键,因为程序可以在没有此模块的情况下以降低功能运行。

如果我们删除 eval() 部分并再次尝试

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

  require "/file/that/does/not/exists";
  print "\nHello\n";

  % ./require1.pl

  Can't locate /file/that/does/not/exists in @INC (@INC contains:
  /usr/lib/perl5/5.00503/i386-linux /usr/lib/perl5/5.00503
  /usr/lib/perl5/site_perl/5.005/i386-linux
  /usr/lib/perl5/site_perl/5.005 .) at require1.pl line 3.

在最后一个例子中,程序只是 die() 了,这在大多数情况下是你想要的。

有关更多信息,请参阅 perlfunc 手册页。

use()

use() 与 require() 类似,它加载和编译包含 Perl 代码的文件,但它仅与模块一起工作。传递要加载的模块的唯一方法是通过其模块名,而不是其文件名。如果模块位于 MyCode.pm 中,则正确使用 use() 它的方法是

  use MyCode

而不是

  use "MyCode.pm"

use() 将传递的参数转换为文件名,将 :: 替换为操作系统的路径分隔符(通常是 /),并在末尾附加 .pm。因此,My::Module 变为 My/Module.pm

use() 等价于

 BEGIN { require Module; Module->import(LIST); }

内部它调用 require() 来执行加载和编译任务。当 require() 完成其任务时,除非第二个参数是空,否则将调用 import()。以下对等

  use MyModule;
  BEGIN {require MyModule; MyModule->import; }

  use MyModule qw(foo bar);
  BEGIN {require MyModule; MyModule->import("foo","bar"); }

  use MyModule ();
  BEGIN {require MyModule; }

第一对导出默认标记。如果模块将 @EXPORT 设置为要默认导出的标记列表,则会发生这种情况。模块的首页通常描述了哪些标记默认导出。

第二对仅导出传递的标记。

第三对描述了调用者不想导入任何符号的情况。

import() 不是一个内置函数,它只是对 ``MyModule” 包的普通静态方法调用来告诉模块将特性列表导入当前包。有关更多信息,请参阅 Exporter 手册页。

当你编写自己的模块时,请始终记住,使用 @EXPORT_OK 而不是 @EXPORT 更好,因为前者除非被请求,否则不会导出符号。导出会污染模块用户的命名空间。此外,避免使用简短或常见的符号名称以减少名称冲突的风险。

如果函数和变量没有导出,你仍然可以使用它们的全名来访问它们,例如 $My::Module::bar$My::Module::foo()。按照惯例,你可以在名称前面使用下划线来非正式地表明它们是 内部 的,而不是用于公共用途。

有一个相应的 ``no” 命令可以取消由 use 导入的符号,即它调用 Module->unimport(LIST) 而不是 import()

do()

虽然do()的行为几乎与require()相同,但它无条件地重新加载文件。它不会检查%INC以查看文件是否已经被加载。

如果do()无法读取文件,则返回undef并将$!设置为报告错误。如果do()可以读取文件但不能编译它,则返回undef并在$@中放入错误信息。如果文件成功编译,则do()返回最后一个表达式评估的值。

参考

  • 关于Perl如何处理变量和命名空间以及use vars()my()之间差异的文章,作者为Mark-Jason Dominus - http://www.plover.com/~mjd/perl/FAQs/Namespaces.html
  • 有关Perl数据类型的深入解释,请参阅Sriram Srinivasan所著的《Advanced Perl Programming》一书中的第3章和第6章。

    当然,还有L.Wall、T. Christiansen和J.Orwant合著的《Programming Perl》(也称为“Camel”书,以书封上的骆驼图片命名)。请查看第10章、第11章和第21章。

  • Exporterperlvarperlmodperlmodlib手册页。

标签

反馈

这篇文章有什么问题吗?请在GitHub上打开问题或pull request来帮助我们。