当前位置: 首页 > 文档资料 > Perl 编程第三版 >

第二十九章 函数 (S-Y)

优质
小牛编辑
119浏览
2023-12-01

29.2 按照字母顺序排列的 Perl 函数.

29.2.136 s///

  • s///

替换操作符。参阅第五章里的“模式匹配操作符”。

29.2.137. scalar

  • scalar EXPR

这个伪函数可以用于 LIST 里,当在列表环境中计算会生成一个不同的结果的时候,强迫 EXPR 在
标量环境中计算。比如:

   my ($nextvar) = scalar ;

避免 在做赋值之前从标准输入把所有的行都读了进来,因为给一个列表(甚至是一个 my
列表)赋值都会产生一个列表环境。(在这里例子里如果没有 scalar,那么来自 的
第一行仍然会赋予 $nextvar,但是随后的行将会被读取并抛弃,因为我们赋值的目标列表只能接受
一个标量数值。)

当然,简单些而且没有那么混乱的方法是不使用圆括弧,这样就把标量环境改变成了列表环境:

   my $nextvar = ;

因为 print 函数是一个 LIST 操作符,所以如果你想把 @ARRAY 的长度打印出来,那么你不得不
说:

   print "Length is ", scalar(@ARRAY), "\n";

Perl 里没有与 scalar 对应的 “list”函数,因为实际上我们从来不需要强迫在列表环境里
计算。这是因为任何需要 LIST 的操作已经给他的列表参数免费提供了一个列表环境。

因为 scalar 是单目操作符,如果你不小心给 EXPR 使用了圆括弧的列表,那么这个东西的行为
就象一个标量逗号表达式一样,在空环境中计算除最后一个列表元素之外的所有其他元素,然后
返回在标量环境中计算的最后一个元素。你很少想要这样的东西。下面的一个语句:

   print uc(scalar(&foo, $bar)), $baz;

在道义上是等效于下面两个的:

   &foo;
   print(uc($bar), $baz);

参阅第二章获取关于逗号操作符的更多细节。参阅第六章的“原型”获取关于单目操作符更多的
信息。

29.2.138 seek

  • seek FILEHANDLE, OFFSET, WHENCE

这个函数为 FILEHANDLE 定位文件指针,就好象用于标准 I/O 的 fseek(3) 调用一样。文件里的
第一个位置是在偏移量 0 处,而不是 1 处。同样,偏移量指的是字节位置,而不是行数。通常,
因为行的长度是变化的,所以我们不可能不检查到该点之间的所有文件内容就能访问某一行,除非
你的所有行数都已知是特定的长度,或者你已经做了一个把行数转换成字节数的索引。(同样的
限制也适用于有着变长字符编码的字符位置:操作系统不知道什么是字符,它们只知道字节。)

FILEHANDLE 可以是一个表达式,其值给出实际的文件句柄的名字或者是一个指向任何类似文件
句柄对象的引用。该函数成功时返回真,失败时返回假。为了方便,该函数可以从各种文件位置
计算偏移量。WHENCE 的值声明你的 OFFSET 使用文件的哪个偏移量做它的开始位置:0,文件
开头;1 文件的当前位置;2,文件结尾。如果 WHENCE 的值是 1 或 2。那么 OFFSET 可以为
负值。如果你喜欢用 WHENCE 的符号值,你可以用来自 IO::Seekable 或者 POSIX 模块的
SEEK_SET,SEEK_CUR,和 SEEK_END,或者在 Perl 5.6 里的 Fcntl 模块。

如果你想为 sysread 或者 syswrite 定位文件,那么不要使用 seek;标准 I/O 缓冲技术会令
seek 对文件在系统位置上的作用变得不可预料而且也不能移植。应该用 sysseek。

因为 ANSI C 的规则和严格,在一些系统上,如果你在在读取和写出之间做切换,那么你必须做
一次搜寻。这样做的效果就好象调用标准 I/O 库的 clearerr(3) 函数。你可以用 WHENCE 为 1
(SEEK_CUR)和 OFFSET 为 0 实现这个目的而又不会移动文件位置:

   seek(TEST, 0, 1);

这个函数的一个有趣的用途是允许你跟随文件的增长,比如:

   for (;;) {
      while () {
         grok($_);         # 处理当前行
      }
      sleep 15;
      seek LOG, 0, 1;         # 重置 end-of-file 错误。
   }

最后一个 seek 在不移动指针的情况下清理文件结束错误。因你的 C 库的标准 I/O 实现的标准
程度的不同而异,你可能需要一些更象下面这样的东西:

   for (;;) {
      for ($curpos = tell FILE;   ;   $curpos = tell FILE) {
         grok($_);         # 处理当前行
      }
      sleep $for_a_while;
      seek FILE, $curpos, 0;      # 重置 end-of-file 错误。
   }

类似的策略可以用于在一个数组里记住 seek 每一行的地址。

29.2.139 seekdir

  • seekdir DIRHANDLE, POS

这个函数为下一次 readdir 对 DIRHANDLE 的调用设置当前位置。POS 必须是 telldir 返回的
值。这个函数与对应的系统库过程在可能的目录压缩问题上有相同的注意事项。该函数可能不是在
所有实现了 readdir 的系统上都实现了。而且如果 readdir 没有实现,它也肯定没实现。

29.2.140 select (输出文件句柄)

  • select FILEHANDLE
  • select

由于历史原因,Perl 里有完全互不相关的两个 select 操作符。参阅下一节获取另外一个的描述。
这个版本的 select 操作符返回当前选定的输出操作符,并且,如果你提供了 FILEHANDLE,那么
把它设置为当前缺省输出操作符。这样做有两个效果:首先,一个没有文件句柄的 write 或者
print 将缺省输出到这个 FILEHANDLE。其次,与输出相关的特殊变量将指向这个文件句柄。
比如,如果你想为多个输出文件句柄设置了相同的页顶格式,那么你可能需要做这些:

   select REPORT1;
   $^ = 'MyTop';
   select REPROT2;
   $^ = 'MyTop';

但请注意这样就把 REPORT2 当作当前选定的文件句柄了。这种做法可以说是反社会的做法,因为
它可能会真的把一些其他过程的 print 或者 write 语句搞坏。写的好的库过程会在退出的时候把
当前选定文件句柄设置为和进入过程时相同的那个。为了支持这个,FILEHANLDE 可以是一个
表达式,该表达式的值给出实际文件句柄的名字。因此,你可以用下面的代码保存并恢复当前
选顶的文件句柄:

   my $oldfh = select STDER; $| = 1; select $oldfh;

或者使用惯用的但有点模糊的方法:

   select((select(STDERR), $| = 1)[0])

这个例子是这样运转的:制作一个由 select(STDERR) (副作用是选定了 STDERR)的返回值和
$|=1 (它总是 1)组成的列表,但同时,又作为副作用设置了现在选定的 STDERR 的自动冲刷。
该列表的第一个元素(前面那个选定的文件句柄)现在用做外层 select 的一个参数。古怪吧?
这些足够让你知道 List 比较危险了。

你还可以使用标准的 SelectSaver? 模块在退出范围的时候自动恢复前面一个 select。

不过,虽然我们给你解释了上面的全部东西,我们还是可以指出在当今的情况下,你很少需要使用
这种形式的 select,因为你象设置的大多数特殊变量都有面向对象的封装方法帮你做这些事情。
所以,你不用直接设置 $|,而是:

   use IO::Handle;      # 糟糕的是,这可不是个*小*模块。
   STDOUT->autoflush(1);

而前面的格式化例子可以这样编码:

   use IO::Handle;
   REPORT1->format_top_name("MyTop");
   REPORT2->format_top_name("MyTop");

29.2.141 select (准备文件描述符)

  • select RBITS, WBITS, EBITS, TIMEOUT

四个参数的 select 和前面那个 select 完全无关。这个操作符用于发现你的文件描述符中那个
(如果有的话)已经准备好做输入或者输出了,或者报告一个例外条件。(这样就避免你做轮询。
)它用你声明的位掩码调用 select(2) 系统调用,你可以用 fileno 和 vec 构造这个位掩码,
象这样:

   $rin = $win = $ein = "";
   vec($rin, fileno(STDIN), 1) = 1;
   vec($win, fileno(STDIN), 1) = 1;
   $ein = $rin | $win;

如果你想在许多文件句柄上 select,你可能会写这样的子过程:

   sub fhbits {
      my @fhlist = @_;
      my $bits;
      for (@fhlist) {
         vec($bits, fileno($_), 1) = 1;
      }
      return $bits;
   }
   $rin = fhbits(qw(STDIN TTY MYSOCK));

如果你想重复使用同样的位掩码(这样做更高效),常用的惯用法是:

   ($nfound, $timeleft) = 
      select($rout=$rin, $wout=$win, $eout=$ein, $timeout);

或者阻塞住直到任意文件描述符准备好:

   $nfound = select($rout=$rin, $wout=$win, $eout=$ein, undef);

如你所见,在标量环境中调用 select 只返回 $nfound,找到的准备好的文件描述符数量。

可以用 $wout=$win 技巧是因为一个赋值语句的数值是它自身的左边,因此 $wout 先被赋值删除,
然后又被 select 删除,而 $win 不会变化。

这些参数任何一个都可以是 undef,这个时候它们被忽略。如果 TIMEOUT 不是 undef,那么就是
以秒计,而且可以是分数。(超时时间为 0 的效果就是轮询。)不是所有实现都能返回
$timeleft。如果不能返回 $timeleft,那么它总是返回等于你提供的 $timeout 的 $timeleft。

标准的 IO::Select 模块提供了 select 的更加友善的接口,主要是因为它为你做了所有你能用的
位掩码。

select 的一个应用就是实现比 sleep 分辨率更好的睡眠。要实现这个目的,声明所有位映射为
undef。因此,如果要睡眠(至少)4.75 秒钟,用:

   select undef, undef, undef, 4.75;

(在一些非 Unix 系统上,三个 undef 的形式可能不能用,你可能需要为一个有效的描述符至少
伪装一个位掩码,而那个描述符可以是从来准备不好的。)

我们不应该把缓冲的 I/O(比如 read 或 )和 select 混在一起用,除了 POSIX 允许
的以外,而且就算 POSIX 允许,也只能在真正的 POSIX 系统上使用。这时应该用 sysread。

29.2.142 semctl

  • semctl ID, SEMNUM, CMD, ARG

这个函数调用 System V IPC 函数 semctl(2)。你可能得先说 use IPC::SysV 以获取正确的
常量定义。如果 CMD 是 IPC_STAT 或者 GETALL,那么 ARG 必须是一个它可以保存返回的
semid_ds 结构或信号灯数值数组的变量。和 ioctl 和 fcntl 一样,返回值用 undef 代表
错误,“0 but true”代表零,其他情况则返回实际数值。

又见 IPC::Semaphore 模块。这个函数只有在那些支持 System V IPC 的机器上才能用。

29.2.143 semget

  • semget KEY, NSEMS, SIZE, FLAGS

这个函数调用 System V IPC 系统调用 semget(2)。在调用之前,你应该 use IPC::SysV 以
获取正确的常量定义。该函数返回信号灯 ID,或者如果有错误返回 undef。

又见 IPC::Semaphore 模块。这个函数只能在那些支持 System V IPC 的机器上用。

29.2.144 semop

  • semop KEY, OPSTRING

这个函数调用 System V IPC 系统调用 semop(2) 以执行信号灯操作,比如发信号和等待等等。
在调用之前,你应该使用 use IPC::SysV 以获取正确的常量定义。

OPSTRING 必须是一个 semop 结构的打包的数组。你可以通过说 pack("s*", $semnum, $semop,
$semflag)做每一个 semop 结构。信号灯操作的数量是由 OPSTRING 的长度隐含的。该函数在
成功的时候返回真,在失败的时候返回假。

下面的代码在等待信号灯 id 为 $semid 的信号灯 $semnum:

   $semop = pack "s*", $semnum, -1, 0;
   semop $semid, $semop or die "Semaphore trouble: $!\n";

要给信号灯发出信号,只需要把 -1 换成 1 就可以了。

参阅第十六章的“System V IPC”一节。又见 IPC::Semaphore 模块。这个函数只有支持
Systerm V IPC 的机器上可以用。

29.2.145 send

  • send SOCKET, MSG, FLAGS, TO
  • send SOCKET, MSG, FLAGS

这个函数在套接字上发送一条信息。它和同名系统调用接收相同的标志——参阅 send(2)。在
未联接的套接字上,你必须声明一个要发送的目的 TO,这样就会令 Perl 的 send 象
sendto(2) 那样运行。C 的系统调用 sendmsg(2) 目前没有在标准的 Perl 里实现。send 函数
在成功时返回发送的字节数,失败时返回 undef。

(有些非 Unix 系统错误地把套接字当作与普通文件描述符不同的东西对待,结果就是你必须总是
在套接字上 send 和 recv,而不能使用方便的标准 I/O 操作符。)

我们中至少有一个人会常犯的错误就是把 Perl 的 send 和 C 的 send 和写混淆起来:

   send SOCK, $buffer, length $buffer      # 错

这行代码会莫名其妙地失败,具体情况取决于字串长度和系统需要的 FLAG 位之间的关系。参阅
第十六章中的“消息传递”一节。

29.2.146. setpgrp

  • setpgrp PID, PGRP

这个函数为指定的 PID(对当前进程使用 PID 等于 0)设置当前进程组(PGRP)。如果在那些
没有实现 setpgrp(2) 的系统上调用 setpgrp 将会抛出一个例外。注意:有些系统上会总是忽略
你提供的参数并总是做 setpgrp(0, $$)。幸运的是,这些就是我们最常用的参数。如果省略了
参数,它们缺省是 0, 0。BSD 4.2 版本的 setpgrp 不接受任何参数,但在 BSD 4.4 里,它是
setpgid 函数的同义词。如果需要更好的移植性(从某种角度来看),直接使用 POSIX 模块里的
setpgid 函数。如果你实际上想干的事是把你的脚本作成守护进程,那么请考虑使用
POSIX::setsid() 函数。请注意 POSIX 版本的 getpgrp 并不接受参数,所以只有
setpgrp(0, 0) 是真正可以移植的。

29.2.147. setpriority

  • setpriority WHICH, WHO, PRIORITY

这个函数为 WHICH 和 WHO 里声明的一个进程,进程组,或者一个用户设置当前 PRIORITY,参阅
setpriority(2)。在那些没有实现 setpriority(2) 的机器上调用 setpriority 将抛出一个
例外。要把你的程序“nice”下四个单位(和用 nice(1) 处理你的程序一样),用:

   setpriority 0, 0, getpriority(0, 0) + 4;

一个给定的优先级的解释可能会因不同的系统而异。有些权限可能是那些非特权用户所不能使用的。

又见 CPAN 的 BSD::Resource 模块。

29.2.148. setsockopt

  • setsockopt SOCKET, LEVEL, OPTNAME, OPTVAL

这个函数设置你需要的套接字选项。出错时该函数返回 undef。LEVEL 表示你的调用瞄准的是
哪一个协议层。或者就是 SOL_SOCKET,指向在所有层之上的套接字本身。如果你不想传递参数,
那么可以把 OPTVAL 声明为 undef。在套接字上一个常用的选项是 SO_REUSEADDR,这样才能绕开
因为前一个在该端口的 TCP 联接仍然认为固执地认为它在关闭的时候,我们不能绑定特定的地址的
问题。它看起来象这样:

   use Socket;
   socket(SOCK, ...) or die "Can't make socket: $!\n";
   setsocket(SOCK, SOL_SOCKET, SO_REUSEADDR, 1)
      or warn "Can't do setdosockotp: $!\n";

参阅 setsockopt(2) 获取其他可能数值。

29.2.149 shift

  • shift ARRAY
  • shift

这个函数把数组的第一个值移出并且返回它,然后把数组长度减一并且把所有的东西都顺移。
如果在数组中不再存在元素,它返回 undef。

如果省略了 ARRAY,那么该函数在子过程和格式的词法范围里移动 @_;它在文件范围(通常是主程
序)或者在由 eval STRING,BEGIN { },CHECK { },INIT { },和 END {} 这样的构造里面的
词法范围里移动 @ARGV。

子过程通常以拷贝它们的参数到词法变量里开始,而 shift 可以用于这个目的:

   sub marine {
      my $fathoms = shift;      # 深度
      my $fishies   = shift;   # 鱼的数量
      my $o2           = shift;   # 氧气问题
      # ...
   }

shift 还可以用于在你的程序前面处理参数:

while (defined($_ = shift)) {
        /^[^-]/     && do { unshift @ARGV, $_; last };
        /^-w/       && do { $WARN = 1;         next };
        /^-r/       && do { $RECURSE = 1;      next };
        die "Unknown argument $_\n";
}

你还可以考虑使用 Getopt::Std 和 Getopt::Long 模块来处理程序参数。

又见 unshift,push,pop,和 splice。shift 和 unshift 函数在数组左边做的事情和 pop
和 push 在数组右边干的事情是一样的。

29.2.150 shmctl

  • shmctl ID, CMD, ARG

这个函数调用 System V IPC 系统调用 shmctl(2)。在调用之前,你应该 use IPC::SysV 以
获取正确的常量定义。

如果 CMD 是 IPC_STAT,那么 ARG 必须是一个将要保存返回的 shmid_ds 结构的变量。跟
ioctl 和 fcntl 一样,该函数错误时返回 undef,“0 but true”表示零,其他情况下返回
实际返回值。

该函数只能在那些支持 System V IPC 的机器上用。

29.2.151 shmget

  • shmget KEY, SIZE, FLAGS

这个函数调用 System V IPC 系统调用 shmget(2)。该函数返回共享内存段的 ID,如果有错误
则返回 undef。在调用之前,先 use SysV?::IPC。

该函数只能在那些支持 System V IPC 的机器上用。

29.2.152 shmread

  • shmread ID, VAR, POS, SIZE

这个函数从共享内存段 ID 的位置 POS 处开始读取 SIZE 大小的数据(方法是附着在该内存段
上,拷贝出数据,然后与该内存段分离。)。VAR 必须是一个将保存读取出的数据的变量。如果
成功,该函数返回真,如果失败返回假。

该函数只能在那些支持 System V IPC 的机器上用。

29.2.153 shmwrite

  • shmwrite ID, STRING, POS, SIZE

这个函数向共享内存段 ID 的位置 POS 处开始写入 SIZE 大小的数据(方法是附着在该内存段
上,拷贝入数据,然后与该内存段分离。)。如果 STRING 太长,那么只写入 SIZE 字节;如果
STRING 太短,那么在后面补空直到 SIZE 字节。如果成功,该函数返回真,如果有错误,返回假。

该函数只能在那些支持 System V IPC 的机器上用。(你可能都读烦了——我们已经写烦了。)

29.2.154 shutdown

  • shutdown SOCKET, HOW

这个函数以 HOW 声明的方式关闭一个套接字联接。如果 HOW 为 0,那么不再允许进一步的接收。
如果 HOW 为 1,那么不再允许进一步的发送。如果 HOW 为 2,那么任何事情都不允许。

   shutdown(SOCK, 0);   # 不许再读
   shutdown(SOCK, 1);   # 不许再写
   shutdown(SOCK, 2);   # 不许再 I/O

如果你想告诉对端你完成数据写出了,但还没有完成数据读取,或者反过来,在这些情况下它都
非常有用。而且它还是一种更执着的关闭方式,因为同时还关闭任何这些文件描述符在派生出的
进程中的的拷贝。

让我们想象有一个服务器想读取它的客户端的请求,直到文件结尾,然后发送一个回答。如果
客户端调用 close,那么该套接字现在将不能用于 I/O,因此不会有回答能送回来。因此,客户端
应该使用 shutdown 以半关闭这次联接:

   print SERVER "my request\n";   # 发送一些数据
   shutdown(SERVER, 1);         # 发送完毕,没有更多要发的东西了
   $answer = ;         # 但你还可以读

(如果你找到这里是为了找到关闭你的系统的办法,那么你就要执行一个外部的程序干这件事。
参阅 system。)

29.2.155. sin

  • sin EXPR
  • sin

抱歉,这个操作符什么罪都没犯(译注:英文“sin”也有“罪恶”的含义)。它只是返回 EXPR
(用弧度表示)的正弦。

如果需要正弦的逆操作,你可以使用 Math::Trig 或者 POSIX 模块的 asin 函数,或者用下面的
关系:

   sub asin { atan2($_[0], sqrt(1 - $_[0] * $_[0])) }

29.2.156 sleep

  • sleep EXPR
  • sleep

这个函数令脚本睡眠 EXPR 秒,如果没有 EXPR 则是永久睡眠,并且返回睡眠的秒数。 你可以
通过给该进程发送一个 SIGALRM 的方法来中断睡眠。在一些老式系统里,它可能比你要求的描述
整整少睡一秒,具体情况取决于它是如何计算秒的。大多数现代的系统都是睡足秒数。不过,在
这些系统上它们很有可能睡眠的时间要长一些,因为在一台繁忙的多任务系统上,你的系统可能
无法马上得到调度。如果可能,select (等待文件描述符)调用可以给你更好的分辨率。你还
可以用 syscall 调用一些 Unix 系统支持的 getitimer(2) 和 setitimer(2) 过程。你不应该
混合 alarm 和 sleep 调用,因为 sleep 通常是用 alarm 实现的。

又见 POSIXE 模块的 sigpause 函数。

29.2.157 socket

  • socket SOCKET, DOMAIN, TYPE, PROTOCOL

这个函数打开一个指定类型的套接字,并且把它附着在 SOCKET 文件句柄上。DOMAIN,TYPE,和
PROTOCOL 都是和 socket(2) 一样的声明。如果没有定义 SOCKET,那么 Perl 将自动激活它。
在使用这个函数之前,你的程序应该包含下面这行:

   use Socket;

它给你正确的常量。该函数成功时返回真,参阅在第十六章里的“套接字”节里的例子。

在那些支持对文件的 exec 时关闭(close-on-exec)的系统上,该标记将为新打开的文件描述符
设置,就象 $^F 判定的那样。参阅第二十八章里的 $^F($SYSTEM_FD_MAX)。

29.2.158 socketpair

  • socketpair SOCKET1, SOCKET2, DOMAIN, TYPE, PROTOCOL

这个函数在声明的域中创建一个指定类型的匿名套接字对。DOMAIN,TYPE,和 PROTOCOL 都和
socketpair(2) 里声明的一样。如果两个套接字参数都没有声明,那么它们自动激活。该函数成功
时返回真,失败时返回假。在那些没有实现 socketpair(2) 的系统上,调用这个函数会抛出一个
例外。

这个函数的通常用法是在 fork 之前使用。生成的进程中有一个关闭 SOCKET1,而另外一个关闭
SOCKET2。你可以双向使用这些套接字,而不象 pipe 函数创建的文件句柄那样是单向的。有些
系统用 socketpair 的方式定义 pipe,这时候调用 pipe(Rdr, Wtr) 相当于:

   use Socket;
   socketpair(Rdr, Wtr, AF_UNIX, SOCK_STREAM, PF_UNSPEC);
   shutdown(Rdr, 1);      # 不允许读者写
   shutdown(Wtr, 0);      # 不允许写者读

在那些支持对文件的 exec 时关闭(close-on-exec)的系统上,该标记将为新打开的文件描述符
设置,就象 $^F 判定的那样。参阅第二十八章里的 $^F($SYSTEM_FD_MAX)。又见在第十六章里
的“双向通讯”一节尾部的例子。

29.2.159 sort

  • sort USERSUB LIST
  • sort BLOCK LIST
  • sort LIST

这个函数对 LIST 进行排序并返回排好序的列表值。缺省时,它以标准字串比较顺序排序(未定义
数值排在已定义空字串前面,而空字串又在其他任何东西前面)。如果 use locale 用法起作用,
那么 sort LIST 根据当前的区域集的数值对 LIST 排序。

如果给出了 USERSUB,那么它就是一个返回小于,等于,或者大于 0 的整数的子过程名字,具体
返回什么取决于列表中的元素应该如何排序。(很便利的 和 cmp 操作符可以用于执行三向
数字和字串比较。)如果给出了 USERSUB,但该函数未定义,那么 sort 抛出一个例外。

为了提高效率,绕开了通常用于子过程的调用代码,这样就有了下面的结果:这个子过程不能是
递归子过程(你也不能用一个循环控制操作符退出该块或者过程),并且将要接受比较的两个元素
不是通过 @_ 传递进子过程的,而是通过临时设置 sort 编译所在的包的全局变量 $a 和 $b(
参阅后面的例子)。变量 $a 和 $b 是真实值的别名,所以不要在子过程中修改它们。

子过程要求的动作是比较。如果它返回的结果是不一致的(比如,有时候说 $x[1] 小于 $x[2],
而有时候说的正相反),那么结果就不会良好。(这也是你不能修改 $a 和 $b 的另外一个原因。)

USERSUB 可以是标量变量名字(未代换),这时,它的值要么是引用实际子过程的符号引用,要么
是硬引用。(符号名更好些,即使用了 use strict 'refs' 用法也如此。)在 USERSUB 的
位置,你可以提供一个 BLOCK 用做一个匿名内联排序子过程。

要做一次普通的数字排序,你可以说:

   sub numerically { $a  $b }
   @sortedbynumber = sort numerically 53, 29,11,32, 7;

要以降序排序,你可以简单地在 sort 后面应用 reverse,或者你可以在排序过程里把 $a 和 $b
反过来:

sub numerically { $a  $b }
@sortedbynumber = sort numerically 53,29,11,32,7;
   @descending = reverse sort numerically 53,29,11,32,7;

   sub reverse_numerically { $b  $a }
   @descending = sort reverse_numerically 53,29,11,32,7;

要对字串进行大小写不敏感的排序,在比较之前用 lc 处理 $a 和 $b:

    @unsorted = qw/sparrow Ostrich LARK catbird blueJAY/;
    @sorted = sort { lc($a) cmp lc($b) } @unsorted;

(在 Unicode 里,用 lc 做大小写规范化要比用 uc 好,因为有些语言里抬头体和大写是不一样
的。不过它对普通的 ASCII 排序没有什么影响,并且如果你想让 Unicode 能正确排序,那么你的
规范化过程可能要比 lc 更别致一些。)

对散列按照数值排序是 sort 函数的常用法之一。比如,如果 %sales_amount 散列记录部门销售
情况,那么在排序过程里做一次散列查找就可以让我们将散列键字根据它们的数值排序:

# 从销售额最高的部门到最低的部门

    sub bysales { $sales_amount{$b}  $sales_amount{$a} }

    for $dept (sort bysales keys %sale_amount) {
        print "$dept => $sales_amount{$dept}\n";
    }

你可以通过使用 || 或者 or 操作符级连多个比较的方法进行额外层次的排序。这种方法相当漂亮,
因为比较操作符通常在相等的时候返回 0,这样就令它们能落到下一个比较。下面,散列键字首先
根据它们相关的销售额排序,然后在根据键字本身进行排序(以处理有两个或多个部门销售额
相同的情况):

   sub by_sales_then_dept {
      $sales_amount{$b}  $sales_amount{$a}
         ||
      $a cmp $b
   }

   for $dept (sort by_sales_then_dept keys %sale_amount) {
      print "$dept => $sales_smount{$dept}\n";
   }

假设 @recs 是一个散列引用的数组,而这里每个散列包含象 FIRSTNAME,LASTNAME,AGE,
HEIGHT,和 SALARY 这样的域。下面的过程把那些记录中的人们按照下面的顺序排列:先是财富,
然后是身高,然后是年龄(越小越靠前),最后是名字的字母顺序:

sub prospects {
    $b->{SALARY}    $a->{SALARY}
        ||
    $b->{HEIGHT}    $a->{HEIGHT}
        ||
    $a->{AGE}       $b->{AGE}
        ||
    $a->{LASTNAME}  cmp  $b->{LASTNAME}
        ||
    $a->{FIRSTNAME} cmp  $b->{FIRSTNAME}
}

@sorted = sort prospects @recs;

任何可以从 $a 和 $b 中得到的有用信息都可以在一个排序过程中比较的基础来用。比如,如果
多行文本要根据特定域来排序,那么可以在排序过程中使用 split 以获取该域:

   @sorted_lines = sort {
      @a_fields = split /:/, $a;      # 冒号分隔的域
      @b_fields = split /:/, $b;
         
      $a_fields[3]  $b_fields[3]   # 对第四个域进行能够数字排序,然后
         ||
      $a_fields[0] cmp $b_fields[0]   # 对第一个域进行能够字串排序,然后
         ||
      $b_fields[2]  $a_fields[2]   # 对第而个域进行行能够数字反向排序
      ...            # 等等
   } @lines;

不过,因为 sort 使用给 $a 和 $b 的不同的数值对多次运行排序过程,所以前面的例子将会比对
每一行都做多余的重新分裂。

为了避免发生象为了比较数据域导致的多次的行分裂带来的开销,我们可以在排序之前对每个值进行
一次操作,然后把生成的信息保存起来。下面,我们创建了一个匿名数组以捕获每一行以及该行的
分裂结果:

   @temp = map { [$_, split /:/] } @lines;

然后,我们对数组引用排序:

   @temp = sort {
      @a_fields = @$a[1..$#$a];
      @b_fields = @$b[1..$#$b];

      $a_fields[3]  $b_fields[3]   # 对第四个域进行能够数字排序,然后
         ||
      $a_fields[0] cmp $b_fields[0]   # 对第一个域进行能够字串排序,然后
         ||
      $b_fields[2]  $a_fields[2]   # 对第而个域进行行能够数字反向排序
      ...               # 等等
   } @temp;

在这个数组引用排完序之后,我们就可以从这个匿名数组里检索原始行了:

   @sorted_lines = map {$_->[0] } @temp;

概而括之,这个 map-sort-map 技巧,就是我们通常称之为 Schwartzian 变换的东西,可以用
一个语句实现:

   @sorted_lines = map { $_->[0] }
         sort {
            @a_fields = @$a[1..$#$a];
            @b_fields = @$b[1..$#$b];

            $a_fields[3]  $b_fields[3]
               ||
            $a_fields[0]  $b_fields[0]
               ||
            $b_fields[2]  $b_fields[2]
            ...
         }
         map { [$_, split /:/]} @lines;

不要把 $a 和 $b 定义成词法变量(用 my)。它们都是包全局变量(如果它们可以免于
use strict 对普通全局变量的限制)。不过你的确需要保证你的排序过程是在同一个包里的,
或者用调用者的包名字修饰 $a 和 $b。

我们已经说过,在 Perl 5.6 里你可以用标准的参数传递方法(以及不一样的是,用 XS 子过程做
排序子过程)写排序子过程,前提是你用一个 ($$) 的原型声明了这个排序子过程。并且如果你是
这么用的,那么实际上你是可以把 $a 和 $b 声明为词法变量的:

   sub numerically ($) {
      my ($a, $b) = @_;
      $a  $b;
   }

将来,当完整的原型都实现了以后,你就可以只用说:

 
   sub numerically ($a, $b) { $a  $b}

然后我们或多或少就能回到开始的地方。

29.2.160 splice

  • splice ARRAY, OFFSET, LENGTH, LIST
  • splice ARRAY, OFFSET, LENGTH
  • splice ARRAY, OFFSET
  • splice ARRAY

这个函数从一个 ARRAY 中删除 OFFSET 和 LENGTH 指明的元素,并且,如果给出了LIST,则用
LIST 的元素替换它。如果 OFFSET 是负数,那么该函数从数组的后面向前数,但如果该值会伸到
数组开头的前面,那么就会抛出一个例外。在列表环境中,splice 返回从该数组中删除的元素。
在标量环境中,它返回最后删除的元素,而如果没有的话返回 undef。如果新元素的数量不等于
旧元素的数量,那么该数组根据需要伸缩,并且元素的位置根据衔接后的情况进行改变。如果省略
了 LENGTH,那么该函数从数组里删除从 OFFSET 开始的所有东西。如果省略了 OFFSET,那么该
数组在读取的时候清空。下面的等式成立(假设 $[ 为 0):

直接方法splice 等效
push(@a, $x, $y)splice(@a, @a, 0, $x, $y)
pop(@a)splice(@a, -1)
shift(@a)splice(@a, 0, 1)
unshift(@a, $x, $y)splice(@a, 0, 0, $x, $y)
$a[$x] = $ysplice(@a, $x, 1, $y)
(@a, @a = ())splice(@a)

splice 函数还可以方便地用于切开传递给子过程的参数列表。比如,假设列表长度在列表之前传递:

   sub list_eq {            # 比较两个列表值
      my @a = splice(@_, 0, shift);
      my @b = splice(@_, 0, shift);
      return 0 unless @a == @b;   #  长度相同?
      while(@a) {
         return 0 if pop(@a) ne pop(@b);
      }
      return 1;
   }
   if (list_eq($len, @foo[1..$len], scalar(@bar), @bar)) { ... }

不过,拿数组引用来干这事更清晰一些。

29.2.161 spit

  • split /PATTERN/, EXPR, LIMIT
  • split /PATTERN/, EXPR
  • split /PATTERN/
  • split

这个函数扫描字串中 EXPR 给出的分隔符,并且把该字串劈成一个子字串列表,在列表环境中返回
生成的列表值,或者在标量环境中返回子字串的数量。(注:标量环境同时还令 split 把它的
结果写到 @_,不过这个用法现在废弃了。)分隔符是用重复的模式匹配进行判断的,用的是
PATTERN 里给出的正则表达式,因此分隔符可以是任意大小,并且不一定在每次匹配都是一样的
字串。(分隔符不象平常那样返回;我们在本节稍后讨论例外情况。)如果 PATTERN 完全不能匹配
该字串,那么 split 把原始字串当作子字串返回。如果它匹配了一次,那么你就得到两个子字串,
以此类推。你可以在 PATTERN 里使用正则表达式修饰词,比如 /PATTERN/i,/PATTERN/x,等等。
如果你以模式 /^/ 进行分裂,那么就假设是 //m 修饰词。

如果声明了 LIMIT 并且是正的,该函数分裂成不超过那么多的域(当然如果它用光了分隔符,那么
是可以分裂成比较少的子字串的)。如果 LIMIT 是负数,那就把它当作声明了任意大的 LIMIT。
如果省略了 LIMIT 或者是零,那么将从结果中删除结尾的空域(那些潜在的 pop 用户应该好好
记住)。如果省略了 EXPR,那么该函数就分裂 $_ 字串。如果还省略了 PATTERN 或者它是一个
文本空格,“ ”,那么该函数对空格进行操作,/\s+/,但是忽任何开头的空格。

可以分裂任意长度的字串:

   @chars  = split //,    $word;
   @fields  = split /:/,   $line;
   @words = split " ",   $paragraph;
   @lines = split /^/,   $buffer;

一个可以匹配空串或者其他的一些比空串长的字串的模式(比如,一个由任意一个字符加上 * 或者
? 修饰的模式)将把 EXPR 的值分裂成独立的字符,只要它匹配字符之间的空串;非空匹配会象
通常的情况那样忽略匹配过的分隔符字符。(换句话来说,一个模式不会在一个点匹配多过一次,
即使它和一个零宽匹配也如此。)比如:

   print join ':', split / */, 'hi there';

生成输出“h:i:t:h:e:r:e”。空白消失了是因为它作为分隔符一部分匹配。举一个小例子,空模式 // 简单
地分裂成独立的字符,而空格并不消失。(对于正常模式匹配而言,// 模式会在上一次成功匹配处重复,
但是 split 的模式免受此过。)

LIMIT 参数只分裂字串的一部分:

   ($login, $passwd, $remainder) = split /:/, $_, 3;

我们鼓励你把你的字串分裂成这样的列表名字,这样你的代码就有了自文档的特性。(可以用于
出错检查,请注意如果字串里比三个域少,那么 $remainder 将会是未定义。)当给一个列表赋值
的时候,如果省略了 LIMIT,那么 Perl 提供一个 LIMIT,其数值比列表中的变量数量大一,以此
避免不必要的工作。对于上面的分裂,LIMIT 缺省时是 4,而 $remainder 将只收到第三个域,而
不是所有剩下的域。在时间要求很严格的应用里,避免分裂成比我们需要的更多的域是一个好习惯。
(强大的语言的问题就是,它给你强大的功能的是以花费在时间上的愚蠢为代价的。)

我们早先说过分隔符不会被返回,但是如果 PATTERN 包含圆括弧,那么每一对圆括弧匹配的子字串
都会包括在结果列表中,分散在那些平常返回的域之中。下面是一个简单的例子:

   split /([-,])/, "1-10,20";

生成列表:

   (1, '-', 10, ',', 20)

如果有更多圆括弧,那么为每个圆括弧对返回一个域,即使有些圆括弧对没有匹配也如此,这种情况
下,为那些位置返回未定义数值。因此,如果你说:

   split /(-)|(,)/, "1-10,20";

那么结果是:

   (1, '-', undef, 10, undef, 20);

/PATTERN 参数的位置可以放这么一个表达式,该声明在运行时生成不同的模式。和普通模式一样,
如果想只做一次运行时编译,那么用 /$varable/o。

有一个特殊的情况,如果该表达式是一个空格(“ ”),那么该函数会象没有参数的 split 那样
在空格上把字串分裂开。因此 split(" ") 可以用于模拟 awk 的缺省行为。相反,split(/ /)
将给你和前导空格一样多的空的初始化域。(除了这个特殊的例子以外,如果你提供的是一个字串
而不是一个正则表达式,那么它还是会被解释成一个正则表达式。)你可以用这个属性把开头和
结尾的空白删除,并且把中间的空白都压缩成一个空白:

   $string = join(' ', split(' ', $string));

下面的例子把一个 RFC 822 消息头分裂成一个包含 $head{Date},$head{Subject},等等的
散列。它使用了给一个散列赋予一个配对列表的技巧,理由是域和分隔符交错。它利用圆括弧把
每个分隔符的一部分当作返回列表值的一部分返回。因为 split 模式保证把返回的东西利用包含
圆括弧的好处按照配对的形式返回,所以散列赋值就可以保证收到一个包含键字/数值对的列表,
这里每个键字就是一个头域的名字。(糟糕的是,这个技巧会丢失有着相同域的多个行的信息,
比如 Received-By 行。啊,哦...)

   $header =~ s/\n\s+/ /g;      # 融合连续行
   %head = ('FRONTSTUFF', split /^(\S*?):\s*/m, $header);

下面的例子处理整个 Unix passwd(5) 文件。你可以忽略 chomp,这个时候 $shell 的结尾将有
换行符。

   open PASSWD, '/etc/passwd';
   while () {
      chomp;      # 删除结尾的换行符
      ($login, $passwd, $uid, $gid, $gcos, $home, $shell) = 
         split /:/;
      ...
   }

下面是一个如何处理每个输入文件里的每一行中的每个词,创建一个单词频率散列的例子:

   while () {
      foreach $word (split) {
         $count{$word}++;
      }
   }

split 的逆操作由 join 执行(只不过 join 只能在所有域之间用同样的分隔符连接)。要用固定
位置的域分解字串,请使用 unpack。

29.2.162. sprintf

  • sprintf FORMAT, LIST

这个函数返回一个格式化字串,格式化习惯是 C 的库函数 sprintf 的是 printf 习惯。参阅你的
系统的 sprintf(3) 或 printf (3) 获取一些通用原则的解释。FORMAT 包含一个带有嵌入的域
指示符的文本,LIST 里的元素就是逐一替换到这些域中去的。

Perl 做自己的 sprintf 格式化——它模拟 C 函数 sprintf,但是它没有用 C 的 sprintf。
(注:除了浮点数以外,并且就算是浮点数也只允许标准的修饰词。)结果是,任何你本地的
sprintf(3) 函数的扩展都不能在 Perl 里使用。

Perl 的 sprintf 允许全局使用的已知转化在 表29-4 中列出。

表29-4。sprintf 的格式

含义
%%一个百分号
%c一个带有给定数字的字符
%s一个字串
%d一个有符号整数,十进制
%u一个无符号整数,十进制
%o一个无符号整数,八进制
%x一个无符号整数,十六进制
%e一个浮点数,科学记数法表示
%f一个浮点数,用固定的小数点表示
%g一个浮点数,以 %e 或 %f 表示

另外,Perl 允许下列广泛支持的转换:

含义
%x类似 %x,但使用大写字符
%E类似 %e,但使用大写的“E”
%G类似 %g,但是带一个大写的“E”(如果正确的话)
%b一个无符号整数,二进制
%p一个指针(输出十六进制的 Perl 值的地址)
%n特殊:把到目前为止输出的字符数放到参数列表中的下一个变量里

最后,为了向下兼容(我们的意思就是“向下”),Perl 允许下列不必要的但广泛支持的转换:

含义
%i%d 的同义词
%D%ld 的同义词
%U%lu 的同义词
%O%lo 的同义词
%F%f 的同义词

Perl 允许下列众所周知的标志出现在 % 和转换字符之间:

域 | 含义|

space用空格前缀正数
+用加号前缀正数
-在域内左对齐
-用零而不是空格进行右对齐
#给非零八进制前缀“0”,给非零十六进制前缀“0x”
number最小域宽度
.number“精度”:浮点数的小数点后面的位数字串最大长度。整数最小长度

l | | 把整数解释成 C 类型的 long 或者 unsigned long|

h把整数解释成 C 类型的 short 或者 unsigned short(如果没有提供标志,那么把整数解释成 C 类型 int 或者 unsigned)

还有两个 Perl 相关的标志

含义
V把整数解释成 Perl 标准的整数类型
v把字串解释成一个整数向量,输出成用点分隔的数字,或者是用任意参数列表里前面带 * 的字串分隔

如果你的 Perl 理解“四倍数”(64位整数),不管是该平台本机支持还是因为你指明 Perl 带着
该功能编译,那么字符 d u o x X b i D U O 打印64位整数,并且它们前面可以选择前缀 ll,
L,或则 q。比如,%lld %16LX %qo。

如果 Perl 理解“long double”(要求该平台支持 long double),那么你可以在 e f g E F
G 标志前面增加可选的 ll 或者 L。比如,%llf %Lg。

在标志里可以出现数字的位置,都可以用一个星号(“*”)代替,这时候 Perl 使用参数列表里的
下一个项作为给出的数字(也就是说,当作域宽度或者精度)。如果通过“*”获取的域宽度是负数,
那么它和“-”标志有一样的效果:左对齐。

v 标志可以用于显示任意字串里的序数值:

   sprintf "version is v%vd\n", $^V;      # Perl 的版本
   sprintf "address is %vd\n", %addr;   # IPv4 地址
   sprintf "address is %*vX\n", ":", $addr;   # IPv6 地址
   sprintf "bits are %*vb\n", " ", $bits;   # 随机的位串

29.2.163 sqrt

  • sqrt EXPR
  • sqrt

这个函数返回 EXPR 的平方根。如果需要其他的根,比如立方根,你可以使用 ** 操作符求那个
数字的分数幂。不要试图在着两种方法里使用负数,因为它有一些稍微有些复杂的问题(并且抛出
一个例外)。但是有一个模块可以处理这些事情:

   use Main::Complex;
   print sqrt(-2);      # 打印出 1.4142135623731i

29.2.164. srand

  • srand EXPR
  • srand

这个函数为 rand 操作符设置随机数种子。如果省略了 EXPR,那么它使用一个内核提供的半随机的
数值(如果内核支持 /dev/urandom 设备)或者是一个基于当前时间和进程号以及一些其他东西的
数值。通常我们完全没有必要调用 srand,因为如果你没有明确调用它,那么它也会在第一次调用
rand 操作符时隐含调用。不过,在早于 Perl 5.004 的版本里不是这样的,所以如果你的脚本
需要在老 Perl 版本上运行,那么你就应该调用 srand。

那些经常被调用的程序(比如 CGI 脚本),如果只是简单地用 time ^ $$ 做种子的话,那么很
容易惨遭下面的数学性质的攻击,那就是:有三分之一的机会 a^b == (a+1)^(b+1)。所以不要
这么干。应该用下面的代码:

   srand( time() ^ ($ + ($ << 15)) );

如果用于加密目的,那么你需要用比缺省的种子生成更随机的算法。有些系统上有 /dev/random
设备就比较合适,否则,拿一个或多个会迅速改变操作系统状态的程序的输出,压缩以后进行
校验和计算是常用的方法。比如:

   srand (time ^ $ ^ unpack "%32L*", `ps wwaxl | gzip`);

如果你特别关心这些问题,那么请参阅 CPAN 上的 Math::TrulyRandom 模块。

不要在你的程序里多次调用 srand,除非你知道你在干什么并且知道为什么这么做。这个函数的
目的是给 rand 函数种子,这样 rand 就可以在你每次运行你的程序的时候生成不同的序列。只
需要在你的程序开始做一次,否则你就不能从 rand 中获得随机的数值!

29.2.165. stat

  • stat FILEHANDLE
  • stat EXPR
  • stat

在标量环境里,这个函数返回一个布尔值,该值表示调用是否成功。在列表环境里,它返回一个
13 个元素的列表,给出一个文件的统计信息,该文件要么是通过 FILEHANDLE 打开,要么是用
EXPR 命名。它的典型应用如下:

   ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size,
      $atime, $mtime, $ctime, $blksize, $blocks)
         = stat $filename;

不是在所有文件系统类型上都支持这些域;不支持的域返回 0。表 29-5 列出了各个域的含义。

表 29-5。stat 返回的域