Perl 中的模块有多种风格,并且有几种不同的东西使它们成为一个模块。
定义
如果满足以下条件,则某些东西可以称为 模块:
约定
还有一些公认的约定:
- 模块通常应该只包含一个
package,
- 模块名称应为camel case,且不应包含下划线
_(例如:Data::Dumper、WWW::Mechanize::Firefox)
- 完全小写的模块不是模块,它们是pragmas。
通常一个模块要么包含一组函数 (subs),要么是 object oriented。让我们先看看集合。
作为函数集合的模块
捆绑了一堆相关功能的典型模块使用一种方法将这些功能导出到代码的命名空间中。一个典型的例子是List::Util。有几种方法可以导出东西。最常见的是Exporter。
当您从模块中获取一个函数并将其放入您的代码中时,这称为导入它。如果您想多次使用该功能,这很有用,因为它可以保持名称简短。导入的时候可以直接调用它的名字。
use List::Util 'max';
print max(1, 2, 3);
不导入时,需要使用全名。
use List::Util (); # there's an empty list to say you don't want to import anything
print List::Util::max(1, 2, 3); # now it's explicit
之所以有效,是因为 Perl 将一个对 List::Util::max 后面的函数的引用安装到您的名称空间中,名称为 max。如果不这样做,则需要使用全名。它有点像 Windows 桌面上的快捷方式。
您的模块不必提供导出/导入功能。你可以把它当作一个东西的集合,并用它们的全名来称呼它们。
作为包集合的模块
虽然每个.pm 文件都称为一个模块,但人们通常也将作为一个分布的整个集合称为一个模块。想到DBI 之类的东西,其中包含很多.pm 文件,这些文件都是模块,但人们仍然只谈论DBI 模块。
面向对象的模块
并非每个模块都需要包含独立功能。一个模块(现在我们更多地讨论上面的那个)也可以包含一个class。在这种情况下,它通常不会导出任何函数。实际上,我们不再调用subs 函数,而是调用方法。 package 名称成为类的名称,您创建名为 objects 的类的 instances,然后在这些对象上调用方法,最终成为你的包裹。
加载模块
在 Perl 中加载模块有两种主要方式。您可以在编译时和运行时进行。 perl1 编译器(是的,有一个编译器,尽管它是解释性语言)加载文件,编译它们,然后切换到运行时运行编译后的代码。当它遇到要加载的新文件时,它会切换回编译时间,编译新代码,等等。
编译时间
要在编译时加载一个模块,你use它。
use Data::Dumper;
use List::Util qw( min max );
use JSON ();
这等价于以下内容。
BEGIN {
require Data::Dumper;
Data::Dumper->import;
require List::Util;
List::Util->import('min', 'max');
require JSON;
# no import here
}
BEGIN block 在编译期间被调用。链接文档中的示例有助于理解这些来回切换的概念。
use 语句通常位于程序的顶部。您首先执行 pragmas(use strict 和 use warnings 应该始终是您在 shebang 之后的第一件事),然后是 use 语句。应该使用它们,以便您的程序在启动期间加载所需的一切。这样在运行时,它会更快。对于运行时间较长或启动时间无关紧要的事物,例如在 Plack 上运行的 Web 应用程序,这就是您想要的。
运行时间
如果您想在运行时加载某些内容,请使用require。它不会为您导入任何内容。它还会暂时切换到新文件的编译时间,然后又回到它离开的运行时间。这使得有条件地加载模块成为可能,这在 CGI 上下文中尤其有用,其中在运行期间解析新文件所花费的额外时间超过了为程序的每次调用加载所有内容的成本,尽管它可能不是需要。
require Data::Dumper;
if ($foo) {
require List::Util;
return List::Util::max( 1, 2, 3, $foo );
}
也可以将字符串或变量传递给require,这样你不仅可以有条件地加载东西,还可以动态地加载。
my $format = 'CSV'; # or JSON or XML or whatever
require "My::Parser::$format";
这是相当先进的,但有它的用例。
此外,require 普通 Perl 文件也可以在运行时以 .pl 结尾。这通常在遗留代码中完成(我称之为意大利面条)。不要在新代码中这样做。也不要在旧代码中这样做。这是不好的做法。
在哪里加载什么
一般来说,您应该始终use 或require 在任何给定模块中依赖的每个模块。永远不要依赖你的代码的其他一些downstream 部分为你加载东西的事实。模块是为了encapsulate 功能,所以它们至少应该能够独立一点。如果你想在以后重用你的某个模块,而你忘记包含一个依赖,那你会很伤心。
它还使您的代码更容易阅读,因为顶部明确说明的依赖项和导入有助于维护人员(或future you)了解您的代码是关于什么的,它做了什么以及它是如何做的。
没有两次加载相同的东西
Perl 会为您解决这些问题。当它在编译时解析代码时,它会跟踪它已加载的内容。将这些东西放入超级全局variable %INC,这是已加载名称的哈希值,以及它们来自何处。
$ perl -e 'use Data::Dumper; print Dumper \%INC'
$VAR1 = {
'Carp.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/site_perl/5.20.1/Carp.pm',
'warnings.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/5.20.1/warnings.pm',
'strict.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/5.20.1/strict.pm',
'constant.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/site_perl/5.20.1/constant.pm',
'XSLoader.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/site_perl/5.20.1/x86_64-linux/XSLoader.pm',
'overloading.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/5.20.1/overloading.pm',
'bytes.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/5.20.1/bytes.pm',
'warnings/register.pm' => '/home/julien/perl5/perlbrew/perls/perl-5.20.1/lib/5.20.1/warnings/register.pm',
'Exporter.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/site_perl/5.20.1/Exporter.pm',
'Data/Dumper.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/5.20.1/x86_64-linux/Data/Dumper.pm',
'overload.pm' => '/home/foo/perl5/perlbrew/perls/perl-5.20.1/lib/5.20.1/overload.pm'
};
每次调用 use 和 require 都会在该哈希中添加一个新条目,除非它已经存在。在这种情况下,Perl 不会再次加载它。如果您 used 模块,它仍然会为您导入名称。这样可以确保没有循环依赖。
关于遗留代码要记住的另一件重要事情是,如果您使用require 普通.pl 文件,则需要获取正确的路径。因为%INC中的key不是模块名,而是你传递的字符串,所以执行以下操作会导致同一个文件被加载两次。
perl -MData::Dumper -e 'require "scratch.pl"; require "./scratch.pl"; print Dumper \%INC'
$VAR1 = {
'./scratch.pl' => './scratch.pl',
'scratch.pl' => 'scratch.pl',
# ...
};
模块从哪里加载
就像%INC 一样,还有一个超级全局variable @INC,它包含Perl 在其中查找模块的路径。您可以使用lib pragma 或通过环境变量@ 向其中添加内容987654387@ 等等。
use lib `lib`;
use My::Module; # this is in lib/My/Module.pm
命名空间
您在模块中使用的包在 Perl 中定义命名空间。默认情况下,当您创建没有 package 的 Perl 脚本时,您位于包 main 中。
#!/usr/bin/env perl
use strict;
use warnings;
sub foo { ... }
our $bar;
sub foo 将在主 .pl 文件中以 foo 的形式提供,但也可以在其他任何地方以 main::foo 的形式提供。简写为::foo。包变量$bar 也是如此。真的是$main::bar 或只是$::bar。谨慎使用它。您不希望脚本中的内容泄漏到模块中。这是一个非常糟糕的做法,以后会回来咬你。
在您的模块中,事物位于声明它们的包的命名空间中。这样,您可以从外部访问它们(除非它们是词法范围与my,您应该做大多数事情)。这基本上没问题,但你不应该弄乱其他代码的内部结构。除非您想破坏内容,否则请改用定义的接口。
当您将某些内容导入命名空间时,它只是如上所述的快捷方式。这可能很有用,但您也不想污染您的命名空间。如果你将很多东西从一个模块导入到另一个模块,这些东西也会在那个模块中可用。
package Foo;
use List::Util 'max';
sub foo { return max(1, 2, 3) }
package main; # this is how you switch back
use Foo;
print Foo::max(3, 4, 5); # this will work
因为您通常不希望这种情况发生,所以您应该仔细选择要导入命名空间的内容。另一方面,您可能不在乎,这也可以。
将事情私有化
Perl 不理解私有或公共的概念。当您知道命名空间如何工作时,您几乎可以了解所有非词法的内容。甚至有一些方法可以使用词汇,但它们涉及一些神秘的黑魔法,我不会深入探讨。
但是,有一个关于如何将事物标记为私有的约定。每当一个函数或变量以下划线开头时,它都应该被认为是私有的。像Data::Printer 这样的现代工具在显示数据时会考虑到这一点。
package Foo;
# this is considered part of the public interface
sub foo {
_bar();
}
# this is considered private
sub _bar {
...
}
最好开始做这样的事情,并远离 CPAN 上的模块内部。像这样命名的东西不被认为是稳定的,它们不是 API 的一部分,并且可以随时更改。
结论
这是对这里涉及的一些概念的非常广泛的概述。一旦您使用了几次,其中大部分将很快成为您的第二天性。我记得在我作为开发人员的培训期间,我花了大约一年的时间来解决这个问题,尤其是导出。
当您开始一个新模块时,perldoc 页面perlnewmod 非常有帮助。您应该阅读并确保您理解其中的内容。
1:注意到 perl 中的小 p 了吗?我这里说的是程序,不是the name of the language,是Perl。