Perl语言总结(4)

2.7 环境

到现在为止,我们已经看到了一些会产生标量值的项。在我们进一步讨论项之前,我们要先讨论带环境(context)的术语。

2.7.1 标量和列表环境

你在 Perl 脚本里激活的每个操作(注:这里我们用“操作”统称操作符或项。当你开始讨论那些分析起来类似项而看起来象操作符的函数时,这两个概念间的界限就模糊了。)都是在特定的环境里进行的,并且该操作的运转可能依赖于那个环境的要求。存在两种主要的环境:标量和列表。比如,给一个标量变量,一个数组或散列的标量元素赋值,在右手边就会以标量环境计算:

$x         = funkshun();  # scalar context
$x[1]      = funkshun();  # scalar context
$x{"ray"}  = funkshun();  # scalar context

但是,如果给一个数组或者散列,或者它们的片段赋值,在右手边就会以列表环境进行计算,即便是该片段只选出了的一个元素:

@x         = funkshun();  # list context
@x[1]      = funkshun();  # list context
@x{"ray"}  = funkshun();  # list context
%x         = funkshun();  # list context

即使你用 my 或 our 修改项的定义,这些规则也不会改变:

my $x      = funkshun();  # scalar context
my @x      = funkshun();  # list context
my %x      = funkshun();  # list context
my ($x)    = funkshun();  # list context

在你正确理解标量和列表环境的区别之前你都会觉得很痛苦,因为有些操作符(比如我们上面虚构的 funkshun()函数)知道它们处于什么环境中,所以就能在列表环境中返回列表,在标量环境中返回标量。(如果这里提到的东西对于某操作成立,那么在那个操作的文档里面应该提到这一点。)用计算机行话来说,这些操作重载了它们的返回类型。不过这是一种非常简单的重载,只是以单数和复数之间的区别为基础,别的就没有了。

如果某些操作符对环境敏感,那么很显然必须有什么东西给它们提供环境。我们已经显示了赋值可以给它的右操作数提供环境,不过这个例子不难理解,因为所有操作符都给它的每个操作数提供环境。你真正感兴趣的应该是一个操作符会给它的操作数提供哪个环境。这样,你可以很容易地找出哪个提供了列表环境,因为在它们的语法描述部分都有 LIST。其他的都提供标量环境。通常,这是很直观的。(注:不过请注意,列表环境可以通过子过程调用传播,因此,观察某个语句会在标量还是列表环境里面计算并不总是很直观。程序可以在子过程里面用 wantarray 函数找出它的环境。)如果必要,你可以用伪函数 scalar 给一个 LIST 中间的参数强制一个标量环境。Perl 没有提供强制列表环境成标量环境的方法,因为在任何一个你需要列表环境的地方,都会已经通过一些控制函数提供了 LIST。

标量环境可以进一步分类成字串环境,数字环境和无所谓环境。和我们刚刚说的标量与列表环境的区别不同,操作从来不关心它们处于那种标量环境。它们只是想返回的标量值,然后让 Perl 在字串环境中把数字转换成字串,以及在数字环境中把字串转换成数字。有些标量环境不关心返回的是字串还是数字还是引用,因此就不会发生转换。这个现象会发生在你给另外一个变量赋值的时候。新的变量只能接受和旧值一样的子类型。

2.7.2 布尔环境

另外一个特殊的无所谓标量环境是布尔环境。布尔环境就是那些要对一个表达式进行计算,看看它是真还是假的地方。当我们在本书中说到“真”或“假”的时候,我们指的是 Perl 用的技术定义:如果一个标量不是空字串 "" 或者数字 0 (或者它的等效字串,"0" )那么就是真。一个引用总是真,因为它代表一个地址,而地址从不可能是 0。一个未定义值(常称做 undef)总是假,因为它看起来象 "" 或者 0——取决于你把它当作字串还是数字。(列表值没有布尔值,因为列表值从来不会产生标量环境!)

因为布尔环境是一个无所谓环境,它从不会导致任何标量转换的发生,当然,标量环境本身施加在任何参与的操作数上。并且对于许多相关的操作数,它们在标量环境里产生的标量代表一个合理的布尔值。也就是说,许多在列表环境里会产生一个列表的操作符可以在布尔环境里用于真/假测试。比如,在一个由 unlink 操作符提供的列表环境里,一个数组名产生一列值:

unlink @files; # 删除所有文件,忽略错误。

但是,如果你在一个条件里(也就是说,在布尔环境里)使用数组,数组就会知道它正处于一个标量环境并且返回数组里的元素个数,只要数组里面还有元素,通常就是真。因此,如果你想获取每个没有正确删除的文件的警告,你可能就会这样写一个循环:

while (@files) {
      my $file = shift @files;
      unlink $file or warn "Can't delete $file: $!/n";
   }

这里的 @files 是在由 while 语句提供的布尔环境里计算的,因此 Perl 就计算数组本身,看看它是“真数组”还是 “假数组”。只要里面还有文件名,它就是真数组,不过一旦最后一个文件被移出,它就变成假数组。请注意我们早先说过的依然有效。虽然数组包含(和可以产生)一列数值,我们在标量环境里并不计算列表值。我们只是告诉数组这里是标量,然后问它觉得自己是什么。

不要试图在这里用 defined @files。那样没用,因为 defined 函数是询问一个标量是否为 undef,而一个数组不是标量。简单的布尔测试就够用了。

2.7.3 空(void)环境

另外一个特殊的标量环境是空环境(void context)。这个环境不仅不在乎返回值的类型,它甚至连返回值都不想要。从函数如何运行的角度看,这和普通标量环境没有区别。但是如果你打开了警告,如果你在一个不需要值的地方,比如说在一个不返回值的语句里,使用了一个没有副作用的表达式,Perl 的编译器就会警告你,比如,如果你用一个字串当作语句:

"Camel Lot";

你会收到这样的警告:

Useless use of a constant in void context in myprog line 123;

2.7.4 代换环境

我们早先说过双引号文本做反斜杠代换和变量代换,不过代换文本(通常称做“双引号文本”)不仅仅适用于双引号字串。其他的一些双引号类构造是:通用的反勾号操作符 qx,模式匹配操作符 m//,替换操作符 s///,和引起正则表达式操作符,qr//。替换操作符在处理模式匹配之前在它的左边做代换动作,然后每次匹配左边时做右边的代换工作。

代换环境只发生在引起里,或者象引起那样的地方,也许我们把它当作与标量及列表环境一样的概念来讲并不恰当。(不过也许是对的。)

2.8 列表值和数组

既然我们谈到环境,那我们可以谈谈列表文本和它们在环境里的性质。你已经看到过一些列表文本。列表文本是用逗号分隔的独立数值表示的(当有优先级要求的时候用圆括弧包围)。因为使用圆括弧几乎从不会造成损失,所以列表值的语法图通常象下面这样说明:

(LIST)

我们早先说过在语法描述里的 LIST 表示某个东西给它的参数提供了列表环境,不过只有列表文本自身会部分违反这条规则,就是说只有在列表和操作符全部处于列表环境里才会提供真正的列表环境。列表文本在列表环境里的内容只是顺序声明的参数值。作为一种表达式里的特殊的项,一个列表文本只是把一系列临时值压到 Perl 的堆栈里,当操作符需要的时候再从堆栈里弹出来。

不过,在标量环境里,列表文本并不真正表现得象一个列表(LIST),因为它并没有给它的值提供列表环境。相反,它只是在标量环境里计算它的每个参数,并且返回最后一个元素的值。这是因为它实际上就是伪装的 C 逗号操作符,逗号操作符是一个两目操作符,它会丢弃左边的值并且返回右边的值。用我们前面讨论过的术语来说,逗号操作符的左边实际上提供了一个空环境。因为逗号操作符是左关联的,如果你有一系列逗号分隔的数值,那你总是得到最后一个数值,因为最后一个逗号会丢弃任何前面逗号生成的东西。因此,要比较这两种环境,列表赋值:

@stuff = ( "one", "two", "three");

给数组@stuff,赋予了整个列表的值。但是标量赋值:

$stuff = ( "one", "two", "three");

只是把值 "three" 赋予了变量 $stuff。和我们早先提到的 @files 数组一样,逗号操作符知道它是处于标量还是列表环境,并且根据相应环境选择其动作。

值得说明的一点是列表值和数组是不一样的。一个真正的数组变量还知道它的环境,处于列表环境时,它会象一个列表文本那样返回其内部列表。但是当处于标量环境时,它只返回数组长度。下面的东西给 $stuff 赋值 3:

@stuff = ("one", "two", "three"); $stuff = @stuff;

如果你希望它获取值 "three",那你可能是认为 Perl 使用逗号操作符的规则,把 @stuff 放在堆栈里的临时值都丢掉,只留下一个交给 $stuff。不过实际上不是这样。@stuff 数组从来不把它的所有值都放在堆栈里。实际上,它从来不在堆栈上放任何值。它只是在堆栈里放一个数值——数组长度,因为它知道自己处于标量环境。没有任何项或者操作符会在标量环境里把列表放入堆栈。相反,它会在堆栈里放一个标量,一个它喜欢的值,而这个值不太可能是列表的最后一个值(就是那个在列表环境里返回的值),因为最后一个值看起来不象时在标量环境里最有用的值。你的明白?(如果还不明白,你最好重新阅读本自然段,因为它很重要。)

现在回到真正的 LIST(列表环境)。直到现在我们都假设列表文本只是一个文本列表。不过,正如字串文本可能代换其他子字串一样,一个列表文本也可以代换其他子列表。任何返回值的表达式都可以在一个列表中使用。所使用的值可以是标量值或列表值,但它们都成为新列表值的一部分,因为 LIST 会做子列表的自动代换。也就是说,在计算一个 LIST 时,列表中的每个元素都在一个列表环境中计算,并且生成的列表值都被代换进 LIST,就好象每个独立的元素都是 LIST 的成员一样。因此数组在一个 LIST 中失去它们的标识(注:有些人会觉得这是个问题,但实际上不是。如果你不想失去数组的标识,那么你总是可以用一个引用代换数组。参阅第八章。)。列表:

(@stuff,@nonsense,funkshun())

包含元素 @stuff,跟着是元素 @nonsense,最后是在列表环境里调用子过程 &funkshun 时它的返回值。请注意她们的任意一个或者全部都可能被代换为一个空列表,这时就象在该点没有代换过数组或者函数调用一样。空列表本身由文本 () 代表。对于空数组,它会被代换为一个空列表因而可以忽略,把空列表代换为另一个列表没有什么作用。所以,((),(),())等于()。

这条规则的一个推论就是你可以在任意列表值结尾放一个可选的逗号。这样,以后你回过头来在最后一个元素后面增加更多元素会简单些:

@releases = (
      "alpha",
      "beta",
      "gamma",);

或者你可以完全不用逗号:另一个声明文本列表的方法是用我们早先提到过的 qw(引起字)语法。这样的构造等效于在空白的地方用单引号分隔。例如:

@froots = qw(
    apple       banana      carambola
    coconut     guava       kumquat
    mandarin    nectarine   peach
    pear        persimmon   plum);

(请注意那些圆括弧的作用和引起字符一样,不是普通的圆括弧。我们也可以很容易地使用尖括弧或者花括弧或者斜杠。但是圆括弧看起来比较漂亮。)

一个列表值也可以象一个普通数组那样使用脚标。你必须把列表放到一个圆括弧(真的)里面以避免混淆。我们经常用到从一个列表里抓取一个值,但这时候实际上是抓了列表的一个片段,所以语法是:

(LIST)[LIST]

例子:
   
   # Stat 返回列表值
   $modification_time = (stat($file))[9];

   # 语法错误
   $modification_time = stat($file)[9];   # 忘记括弧了。

   # 找一个十六进制位
   $hexdigit = ('a','b','c','d','e','f')[$digit-10];

   # 一个“反转的逗号操作符”。
   return (pop(@foo),pop(@foo))[0];

   # 把多个值当作一个片段
   ($day, $month, $year) = (localtime)[3,4,5];

2.8.1 列表赋值

只有给列表赋值的每一个元素都合法时,才能给整个列表赋值:

($a, $b, $c) = (1, 2, 3); ($map{red}, ${map{green}, $map{blue}) = (0xff0000, 0x00ff00, 0x0000ff);

你可以给一个列表里的 undef 赋值。这一招可以很有效地把一个函数的某些返回值抛弃:

($dev, $ino, undef, undef, $uid, $gid) = stat($file);

最后一个列表元素可以是一个数组或散列:

($a, $b, @rest) = split;
   my ($a, $b, %rest) = @arg_list;
实际上你可以在赋值的列表里的任何地方放一个数组或散列,只是第一个数组或散列会吸收所有剩余的数值,而且任何在它们后面的东西都会被设置为未定义值。这样可能在 local 或 my 里面比较有用,因为这些地方你可能希望数组初始化为空。

你甚至可以给空列表赋值:

() = funkshun();

这样会导致在列表环境里调用你的函数,但是把返回值丢弃。如果你在没有赋值(语句)的情况下调用了此函数,那它就会在一个空环境里被调用,而空环境是标量环境,因此可能令此函数的行为完全不同。

在标量环境里的列表赋值返回赋值表达式右边生成的元素的个数:

$x = (($a,$b)=(7,7,7));      # 把 $x 设为 3,不是 2
   $x = ( ($a, $b) = funk());   # 把 $x 设为 funk() 的返回数
   $x = ( () = funk() );      # 同样把$x 设为 funk() 的返回数
这样你在一个布尔环境里做列表赋值就很有用了,因为大多数列表函数在结束的时候返回一个空(null)列表,空列表在赋值时生成一个 0,也就是假。下面就是你可能在一个 while 语句里使用的情景:
while (($login, $password) = getpwent) {
      if (crypt($login, $password) eq $password) {
         print "$login has an insecure password!/n";
      }
   }

2.7.6 数组长度

你可以通过在标量环境里计算数组 @days 而获取数组 @days 里面的元素的个数,比如:

@days + 0; # 隐含地把 @days 处于标量环境 scalar(@days) # 明确强制 @days 处于标量环境

请注意此招只对数组有效。并不是一般性地对列表值都有效。正如我们早先提到的,一个逗号分隔的列表在标量环境里返回最后一个值,就象 C 的逗号操作符一样。但是因为你几乎从来都不需要知道 Perl 里列表的长度,所以这不是个问题。

和 @days 的标量计算有紧密联系的是 $#days。这样会返回数组里最后一个元素的脚标,或者说长度减一,因为(通常)存在第零个元素。给 $#days 赋值则修改数组长度。用这个方法缩短数组的长度会删除插入的数值。你在一个数组扩大之前预先伸展可以获得一定的效能提升。(你还可以通过给超出数组长度之外的元素赋值的方法来扩展一个数组。)你还可以通过给数组赋空列表 () 把它裁断为什么都没有。下面的两个语句是等效的:

@whatever = (); $#whatever = -1;

而且下面的表达式总是真:

scalar(@whatever) == $#whatever + 1;

截断一个数组并不回收其内存。你必须 undef(@whatever) 来把它的内存释放回你的进程的内存池里。你可能无法把它释放回你的系统的内存池,因为几乎没有那种操作系统支持这样做。

2.9 散列

如前所述,散列只是一种有趣的数组类型,在散列里你是用字串而不是数字来取出数值。散列定义键字和值之间的关联,因此散列通常被那些打字不偷懒的人称做关联数组。

在 Perl 里实际上是没有叫什么散列文本的东西的,但是如果你给一个散列赋一个普通列表的值,列表里的每一对值将被当作一对键字/数值关联:

%map = ('red', 0xff0000,'green', 0x00ff00,'blue',0x0000ff);

上面形式和下面的形式作用相同:

%map = ();      # 先清除散列
   $map{red} = 0xfff0000;
   $map{green} = 0x00ff00;
   $map{blue} = 0x0000ff;
通常在键字/数值之间使用 => 操作符会有更好的可读性。=> 操作符只是逗号的同义词,不过却有更好的视觉区分效果,并且还把任何空标识符引起在其左边(就象上面的花括弧里面的标识符),这样,它在若干种操作中就显得非常方便,包括初始化散列变量:

%map = (
      red => 0xff0000,
      green => 0x00ff00,
      blue => 0x0000ff,
           );

或者初始化任何当作记录使用的匿名散列引用:

$rec = {
      NAME => 'John Simth',
      RANK => 'Captain',
      SERNO => '951413',
   };
或者用命名的参数激活复杂的函数:
$fiels = radio_group(
         NAME => 'animals'
         VALUES   =>['camel','llama','ram','wolf'],
         DEFAULT   =>'camel',
         LINEBREAD   => 'true',
         LABELS   =>/%animal_names,
      );

不过这里我们又走的太远了。先回到散列。

你可以在一个列表环境里使用散列变量(%hash),这种情况下它把它的键字/数值对转换成列表。但是,并不意味着以某种顺序初始化的散列就应该同样的顺序恢复出来。散列在系统内部实现上是使用散列表来达到高速查找,这意味着记录存储的顺序和内部用于计算记录在散列表的里的位置的散列函数有关,而与任何其它事情无关。因此,记录恢复出来的时候看起来是随机的顺序。(当然,每一对键字/数值是以正确的顺序取出来的。)关于如何获得排序输出的例子,可以参考第二十九章的 keys 函数。

当你在标量环境里计算散列变量的数值的时候,它只有在散列包含任意键字/数值对时才返回真。如果散列里存在键字/数值对,返回的值是一个用斜线分隔的已用空间和分配的总空间的值组成的字串。这个特点可以用于检查 Perl 的(编译好的)散列算法在你的数据集里面性能是否太差。比如,你把 10,000 个东西放到一个散列里面,但是在标量环境里面计算 %HASH 得出“1/8”,意味着总共八个桶里只用了一个桶。大概是一个桶里存放了 10,000 个条目。这可是不应该发生的事情。

要算出一个散列里面的键字的数量,在标量环境里使用 keys 函数:scalar(keys(%HASH)).

你可以通过在花括弧里面声明用逗号分隔的,超过一个键字的方法仿真多维数组。列出的键字连接到一起,由 $;($SUBSCRIPT_SEPARATOR)(缺省值是 chr(28))的内容分隔。结果字串用做散列的真实键字。下面两行效果相同:

$people{ $state, $country } = $census_results;
   $people{ join $; =>$state, $county} = $census_results;
这个特性最初是为了支持 a2p(awk 到 perl 转换器)而实现的。现在,你通常会只使用第九章,数据结构,里写的一个真的(或者,更真实一些)的多维数组。旧风格依然有用的一个地方是与 DBM 文件捆绑在一起的散列(参阅第三十二章,标准模块,里的 DB_File),因为它不支持多维键字。

请不要把多维散列仿真和片段混淆起来。前者表示一个标量数值,后者则是一个列表数值:

$hash{ $x, $y, $z}      # 单个数值
   @hash{ $x, $y, $z}      # 一个三个值的片段

2.10 型团(typeglob)和文件句柄

Perl 里面有种特殊的类型叫类型团(typeglob)用以保留整个符号表记录。(符号表记录 *foo 包括 $foo, @foo, %foo,&foo 和其他几个 foo 的简单解释值。)类型团(typeglob)的类型前缀上一个 *,因为它代表所有类型。

类型团(typeglob)(或由此的引用)的一个用途是是用于传递或者存储文件句柄。如果你想保存一个文件句柄,你可以这么干:

$fh = *STDOUT;

或者作为一个真的引用,象这样:

$fh = /*STDOUT;

这也是创建一个本地文件句柄的方法,比如:

sub newopen {
      my  $path = shift;
      local *FH;      # 不是my() 或 our ()
      open(FH,$path ) or return undef;
      return *FH:      # 不是/*FH!
   }
   $fh = newopen('/etc/passwd');
参阅 open 函数获取另外一个生成新文件句柄的方法。

类型团如今的主要用途是把一个符号表取另一个符号表名字做别名。别名就是外号,如果你说:

*foo = *bar;

那所有叫“foo”的东西都是每个对应的叫“bar”的同意词。你也可以通过给类型团赋予引用实现只给某一个变量取别名:

*foo = /$bar;

这样 $foo 就是 $bar 的一个别名,而没有把 @foo 做成 @bar 的别名,或者把 %foo 做成 %bar 的别名。所有这些都只影响全局(包)变量;词法不能通过符号表记录访问。象这样给全局变量别名看起来可能有点愚蠢,不过事实是整个模块的输入/输出机制都是建筑在这个特性上的,因为没有人要求你正在当别名用的符号必须在你的名字空间里。因此:

local *Here::blue = /$There::green;

临时为 $There::green 做了一个叫 $Here::blue 的别名,但是不要给 @There:green 做一个叫 @Here::blue 的别名,或者给 %There::green 做一个 %Here::blue 的别名。幸运的是,所有这些复杂的类型团操作都隐藏在你不必关心的地方。参阅第八章的“句柄参考”和“符号表参考”,第十章的“符号表”,和第十一章,模块,看看更多的关于类型团的讨论和重点。

2.11 输入操作符

这里我们要讨论几个操作符,因为他们被当作项分析。有时候我们称它们为伪文本,因为它们在很多方面象引起的字串。(象 print 这样的输出操作符被当作列表操作符分析并将在第二十九章讨论。)

2.11.1 命令输入(反勾号)操作符

首先,我们有命令输入操作符,也叫反勾号操作符,因为它看起来象这样:

$info = `finger $user`;

一个用反勾号(技术上叫重音号)引起的字串首先进行变量替换,就象一个双引号引起的字串一样。得到的结果然后被系统当作一个命令行,而且那个命令的输出成为伪文本的值。(这是一个类似 Unix shell 的模块。)在标量环境里,返回一个包含所有输出的字串。在列表环境里,返回一列值,每行输出一个值。(你可以通过设置 $/ 来使用不同的行结束符。)

每次计算伪文本的时候,该命令都得以执行。该命令的数字状态值保存在 $?(参阅第二十八章获取 $? 的解释,也被称为 $CHILD_ERROR )。和这条命令的 csh 版本不同的是,对返回数据不做任何转换——换行符仍然是换行符。和所有 shell 不同的是,Perl 里的单引号不会隐藏命令行上的变量,使之避免代换。要给 shell 传递一个 $,你必须用反斜杠把它隐藏起来。我们上面的 finger 例子里的 $user 被 Perl 代换,而不是被 shell。(因为该命令 shell 处理,参阅第二十三章,安全,看看与安全有关的内容。)

反勾号的一般形式是 qx//(意思是“引起的执行”),但这个操作符的作用完全和普通的反勾号一样。你只要选择你的引起字符就行了。有一点和引起的伪函数类似:如果你碰巧选择了单引号做你的分隔符,那命令行就不会进行双引号代换;

$perl_info = qx(ps $$); # 这里 $$ 是 Perl 的处理对象 $perl_info = qx'ps $$'; # 这里 $$ 是 shell 的处理对象

2.11.2 行输入(尖角)操作符

最频繁使用的是行输入操作符,也叫尖角操作符或者 readline 函数(因为那是我们内部的叫法)。计算一个放在尖括弧里面的文件句柄(比如 STDIN)将导致从相关的文件句柄读取下一行。(包括新行,所以根据 Perl 的真值标准,一个新输入的行总是真,直到文件结束,这时返回一个未定义值,而未定义值习惯是为假。)通常,你会把输入值赋予一个变量,但是有一种情况会发生自动赋值的现象。当且仅当行输入操作符是一个 while 循环的唯一一个条件的时候,其值自动赋予特殊变量$_。然后就对这个赋值进行测试,看看它是否定义了。(这些东西看起来可能有点奇怪,但是你会非常频繁地使用到这个构造,所以值得花些时间学习。)因此,下面行是一样的:

while (defined($_ = <STDIN>)) {print $_; }   # 最长的方法
   while ($_ = <STDIN) { pirnt; }         # 明确使用 $_
   while (<STDIN>) { PRINT ;}         # 短形式
   for (;<STDIN>;) { print;}         # 不喜欢用while 循环
   print $_ while defined( $_ = <STDIN>);      # 长的语句修改
   print while $_ = <STDIN>;         # 明确$_
   print while <STDIN>;            # 短的语句修改

请记住这样的特殊技巧要求一个 while 循环。如果你在其他的什么地方使用这样的输入操作符,你必须明确地把结果赋给变量以保留其值:

while(<FH1>&& <fh2>) { ... }         # 错误:两个输入都丢弃
   if (<STDIN>)   { print; }         # 错误:打印$_原先的值
   if ($_=<STDIN>)   {PRINT; }         # 有小问题:没有测试是否定义
   if (defined($_=<STDIN>)) { print;}      # 最好

当你在一个 $_ 循环里隐含的给 $_ 赋值的时候,你赋值的对象是同名全局变量,而不是 while 循环里的那只局部的。你可以用下面方法保护一个现存的 $_ 的值:

while(local $_=) { print; } # 使用局部 $_

当循环完成后,恢复到原来的值。不过,$_ 仍然是一个全局变量,所以,不管有意无意,从那个循环里调用的函数仍然能够访问它。当然你也可以避免这些事情的发生,只要定义一个文本变量就行了:

while (my $line =) { print $line;} # 现在是私有的了

(这里的两个 while 循环仍然隐含地进行测试,看赋值结果是否已定义,因为 my 和 local 并不改变分析器看到的赋值。)文件句柄 STDIN,STDOUT,和 STDERR 都是预定义和预先打开的。额外的文件句柄可以用 open 或 sysopen 函数创建。参阅第二十九章里面那些函数的文档获取详细信息。

在上面的 while 循环里,我们是在一个标量环境里计算行输入操作符,所以该操作符分别返回每一行。不过,如果你在一个列表环境里使用这个操作符,则返回一个包括所有其余输入行的列表,每个列表元素一行。用这个方法你会很容易就使用一个很大的数据空间,所以一定要小心使用这个特性:

$one_line =; # 获取第一行 $all_lines =; # 获取文件其余部分。

没有哪种 while 处理和输入操作符的列表形式相关联,因为 while 循环的条件总是提供一个标量环境(就象在任何其他条件语句里一样)。

在一个尖角操作符里面使用空(null)文件句柄是一种特殊用法;它仿真典型的 Unix 的命令行过滤程序(象 sed 和 awk)的特性。当你从一个 <> 读取数据行的时候,它会魔术般的把所有你在命令行上提到的所有文件的所有数据行都交给你。如果你没有(在命令行)上声明文件,它就把标准输入交给你,这样你的程序就可以很容易地插入到一个管道或者一个进程中。

下面是其工作原理的说明:当第一次计算 <> 时,先检查 @ARGV 数组,如果它是空(null),则 $ARGV[0] 设置为 “-”,这样当你打开它的时候就是标准输入。然后 @ARGV 数组被当作一个文件名列表处理。更明确地说,下面循环:

while (<>) { ... # 处理每行的代码 }

等效于下面的类 Perl 的伪代码:

@ARGV = ('-')   unless @ARGV;      # 若为空则假设为STDIN
   while( @ARGV) {
      $ARGV = shift @ARGV;      # 每次缩短@ARGV
      if( !open(ARGV, $ARGV)) {
         warn "Can't open $ARGV: $!/n";
         next;
      }
      while (<ARGV>) {
         ...         # 处理每行的代码
      }
   }
第一段代码除了没有那么唠叨以外,实际上是一样的。它实际上也移动 @ARGV,然后把当前文件名放到全局变量 $ARGV 里面。它也在内部使用了特殊的文件句柄 ARGV——<> 只是更明确的写法 <ARGV>(也是一个特殊文件句柄)的一个同义词,(上面的伪代码不能运行,因为它把当作一个普通句柄使用。)

你可以在第一个 <> 语句之前修改 @ARGV,直到数组最后包含你真正需要的文件名列表为止。因为 Perl 在这里使用普通的 open 函数,所以如果碰到一个“-”的文件名,就会把它当作标准输入,而其他更深奥的 open 特性是是 Perl 自动提供给你的(比如打开一个名字是“gzip -dc <file.gz|”的文件)。行号($.)是连续的,就好象你打开的文件是一个大文件一样。(不过你可以重置行号,参阅第二十九章看看当到了 eof 时怎样为每个文件重置行号。)

如果你想把 @ARGV 设置为你自己的文件列表,直接用:

# 如果没有给出 args 则缺省为 README @ARGV = ("README") unless @ARGV;

如果你想给你的脚本传递开关,你可以用 Getopt::* 模块或者在开头放一个下面这样的循环:

while( @ARGV and $ARGV[0] =~ /^-/) {
      $_ = shift;
      last if /^--$/;
      if (/^-D(.*)/) {$debug = $1 }
      if (/^-v/)       { $verbose++}
      ...      # 其他开关
   }
   while(<>){
      ...      # 处理每行的代码
   }

符号 <> 将只会返回一次假。如果从这(返回假)以后你再次调用它,它就假设你正在处理另外一个 @ARGV 列表,如果你没有设置 @ARGV,它会从 STDIN 里输入。

如果尖括弧里面的字串是一个标量变量(比如,<$foo>),那么该变量包含一个间接文件句柄,不是你准备从中获取输入的文件句柄的名字就是一个这样的文件句柄的引用。比如:

$fh = /*STDIN; $line = <$fh>;

或:

open($fh, "<data.txt"); $line = <$fh>;

2.11.3 文件名聚集操作符

你可能会问:如果我们在尖角操作符里放上一些更有趣的东西,行输入操作符会变成什么呢?答案是它会变异成不同的操作符。如果在尖角操作符里面的字串不是文件句柄名或标量变量(甚至只是多了一个空格),它就会被解释成一个要 “聚集”(注:文件团和前面提到的类型团毫无关系,除了它们都把 * 字符用于通配符模式以外。当用做通配符用途时,字符 * 有“聚集”(glob)的别名。对于类型团而言,它是聚集符号表里相同名字的符号。对于文件团而言,它在一个目录里做通配符匹配,就象各种 shell 做的一样。)的文件名模式。这里的文件名模式与当前目录里的(或者作为文件团模式的一部分直接声明的目录)文件名进行匹配,并且匹配的文件名被该操作符返回。对于行输入而言,在标量环境里每次返回一个名字,而在列表环境里则是一起返回。后面一种用法更常见;你常看到这样的东西:

@files = <*.xml>;

和其他伪文本一样,首先进行一层的变量代换,不过你不能说 <$foo>,因为我们前面已经解释过,那是一种间接文件句柄。在老版本的 Perl 里,程序员可以用插入花括弧的方法来强制它解释成文件团:<${foo}>。现在,我们认为把它当作内部函数 glob($foo) 调用更为清晰,这么做也可能是在第一时间进行干预的正确方法。所以,如果你不想重载尖角操作符(你可以这么干。)你可以这么写:

@files = glob("*.xml");

不管你用 glob 函数还是老式的尖括弧形式,文件团操作符还是会象行输入操作符那样做 while 特殊处理,把结果赋予 $_。(也是在第一时间重载尖角操作符的基本原理。)比如,如果你想修改你的所有 C 源代码文件的权限,你可以说:

while (glob "*.c") { chmod 0644, $_; }

等效于:

while (<*.c>) { chmod 0644,$_; }

最初 glob 函数在老的 Perl 版本里是作为一个 shell 命令实现的(甚至在旧版的 Unix 里也一样),这意味着运行它开销相当大,而且,更糟的是它不是在所有地方运行得都一样。现在它是一个内建的函数,因此更可靠并且快多了。参阅第三十二章里的 File:Glob 模块的描述获取如何修改这个操作符的缺省特性的信息,比如如何让它把操作数(参数)里面的空白当作路径名分隔符,是否扩展发音符或花括弧,是否大小写敏感和是否对返回值排序等等。

当然,处理上面 chmod 命令的最短的和可能最易读的方法是把文件团当作一个列表操作符处理:

chmod 0644, <*.c>;

文件团只有在开始(处理)一个新列表的时候才计算它(内嵌)的操作数。所有数值必须在该操作符开始处理之前读取。这在列表环境里不算什么问题,因为你自动获取全部数值。不过,在标量环境里时,每次调用操作符都返回下一个值,或者当你的数值用光后返回一个假值。同样,假值只会返回一次。所以如果你预期从文件团里获取单个数值,好些的方法是:

($file) =; # 列表环境

上面的方法要比:

$fiole =; # 标量环境

好,因为前者返回所有匹配的文件名并重置该操作符,而后者要么返回文件名,要么返回假。

如果你准备使用变量代换功能,那么使用 glob 操作符绝对比使用老式表示法要好,因为老方法会导致与间接文件句柄的混淆。这也是为什么说项和操作符之间的边界线有些模糊的原因:

@files = <$dir/*.[ch]>; # 能用,不过应该避免这么用。 @files = glob("dir/*.[ch]"); # 把glob当函数用。 @files = glob $some_pattern; # 把glob当操作符用。

我们在最后一个例子里把圆括弧去掉是为了表明 glob 可以作为函数(一个项)使用或者是一个单目操作符用;也就是说,一个接受一个参数的前缀操作符。glob 操作符是一个命名的单目操作符的例子;是我们下一章将要谈到的操作符。稍后,我们将谈谈模式匹配操作符,它也是分析起来类似项,而作用象操作符。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章