perl语言编程 第六章 子过程 下

上一篇 / 下一篇  2008-04-18 11:49:47 / 个人分类:关于脚本语言

4.0 函数原型

Perl 可以让你定义你自己的函数,这些函数可以象 Perl 的内建函数一样调用.例如 push(@array, $item),它必须接收一个 @array 的引用,而不仅仅是 @array 中的值,这样这个数组才能够被函数改变.函数原型能够让你声明的子过程能够象很多内建函数一样获得参数,就是获得一定数目和类型的参数.我们虽然称之为函数原型,但是它们的运转更像调用环境中的自动模板,而不仅仅是 C 或 java 程序员认为的函数原型.使用这些模板,Perl 能够自动添加隐含的反斜扛或者调用 scalar,或能够使事情能变成符合模板的其他一些操作.比如,如果你定义:

sub mypush (\@@);
那么 mypush 就会象 push 一样接受参数.为了使其运转,函数的定义和调用在编译的时候必须是可见的.函数原型只影响那些不带 & 方式调用的函数.换句话说,如果你象内建函数一样调用它,它就像内建函数一样工作.如果你使用老式的方法调用子过程,那么它就象老式子过程那样工作.调用中的 & 消除所有的原型检查和相关的环境影响.

因为函数原型仅仅在编译的时候起作用,自然它对象 \&foo 这样的子过程引用和象 &{$subref} 和 $subref->() 这样的间接子过程调用的情况不起作用.同样函数原型在方法调用中也不起作用.这是因为被调用的实际函数不是在编译的时候决定的,而是依赖于它的继承,而继承在 Perl 中是动态判断的.

因为本节的重点主要是让你学会定义象内建函数一样工作的子过程,下面使一些函数原型,你可以用来模仿对应的内建函数:

声明为调用
sub mylink ($$)mylink $old, $new
sub myreverse (@)myreverse $a, $b, $c
sub myjoin ($@)myjoin ":", $a, $b, $c
sub mypop (\@)mypop @array
sub mysplice(\@$$@)mysplice @array, @array, 0, @pushme
sub mykeys (\%)mykeys %($hashref)
sub mypipe (**)mypipe READHANDLE, WRITEHANDLE
sub myindex ($$;$)myindex &getstring, "substr"
 myindex &getstring, "substr", $start
sub mysyswrite (*$;$$)mysyswrite OUTF, $buf
 mysyswrite OUTF, $buf, length($buf)-$off, $off
sub myopen (*;$@)myopen HANDLE
 myopen HANDLE, $name
 myopen HANDLE, "-|", @cmd
sub mygrep (&@)mygrep { /foo/ } $a, $b, $c
sub myrand ($)myrand 42
sub mytime ()mytime

任何带有反斜扛的原型字符(在上表左列中的圆括弧里)代表一个实际的参数(右列中有示例) 必须以以这个字符开头.例如 keys 函数的第一个参数必须以 % 开始,同样 mykeys 的第一个参数也必须以 % 开头.

分号将命令性参数和可选参数分开.(在 @ 或 % 前是多余的,因为列表本身就可以是空的) .非反斜扛函数原型字符有特殊的含义.任何不带反斜扛的 @ 或 % 会将实际参数所有剩下的参数都吃光并强制进入列表环境.(等同于语法描述中的 LIST).$ 代表的参数强迫进入标量环境.& 要求一个命名或匿名子过程的引用.

函数原型中的 * 允许子过程在该位置接受任何参数,就像内建的文件句柄那样:可以是一个名字,一个常量,标量表达式,类型团或者类型团的引用.值将可以当成一个简单的标量或者类型团(用小写字母的)的引用由子过程使用.如果你总是希望这样的参数转换成一个类型团的引用,可以使用 Symbol::qualify_to_ref,象下面这样:

useSymblo 'qualify_to_ref';subfoo (*) {my$fh = qualify_to_ref(shift,caller);
        ...
}

注意上面表中的最后三个例子会被分析器特殊对待,mygrep 被分析成一个真的列表操作符,myrand 被分析成一个真的单目操作符就象 rand 一样,同样 mytime 被分析成没有参数,就象 time 一样.

也就是说,如果你使用下面的表达式:

mytime +2;

你将会得到 mytime()+2,而不是 mytime(2),这就是在没有函数原型时和使用了单目函数原型时分析的得到的不同结果.

mygrep 例子同样显示了当 & 是第一个参数的时候是如果处理的.通常一个 & 函数原型要求一个象 \&foo 或 sub{} 这样参数.当它是第一个参数时,你可以在你的匿名子过程中省略掉 sub,只在"非直接对象"的位置上传送一个简单的程序块(不带冒号).所以 & 函数原型的一个重要功能就是你可以用它生成一个新语法,只要 & 是在初始位置:

subtry (&$) {my($try, $catch) = @_;eval{ &$try };if($@) {local$_ = $@;
                &$catch;
        }
}subcatch (&) { $_[0] }

try {die"phooey";
}# 不是函数调用的结尾!catch {
        /phooey/andprint"unphooey\n";
};

它打印出 "unphooey".这里发生的事情是这样的,Perl 带两个参数调用了 try,匿名函数 {die "phooey";} 和 catch 函数的返回值,在本例中这个返回值什么都不是,只不过是它自己的参数,而整个块则是另外一个匿名函数.在 try 里,第一个函数参数是在 eval 里调用的,这样就可以捕获任何错误.如果真的出了错误,那么调用第二个函数,并且设置 $_ 变量以抛出例外.(注:没错,这里仍然有涉及 @_ 的可视性的问题没有解决.目前我们忽略那些问题.但是如果我们将来把 @_ 做成词法范围的东西,就象现在试验的线程化 Perl 版本里已经做的那样,那么那些匿名子过程就可以象闭合的行为一样.)如果你觉得这些东西听起来象胡说八道,那么你最好看看第二十九章里的 die 和 eval,然后回到第八章里看看匿名函数和闭合.另外,如果你觉得麻烦,你还可以看看 CPAN 上的 Error 模块,这个模块就是实现了一个用 try,catch,except,otherwise,和 finally 子句的灵活的结构化例外操作机制.

下面是一个 grep 操作符的重新实现(当然内建的实现更为有效):

submygrep (&@) {my$coderef =shift;my@result;foreach$_ (@_) {push(@result, $_)if&$coderef;
        }return@result;
}

一些读者希望能够看到完整的字母数字函数原型.我们有意把字母数字放在了原型之外,为的是将来我们能够很快地增加命名的,正式的参数.(可能)现在函数原型的主要目的就是让模块作者能够对模块用户作一些编译时的强制参数检查.

4.1 内联常量函数

带有 () 的函数原型表示这个函数没有任何参数,就象内建函数 time 一样.更有趣的是,编译器将这种函数当作潜在的内联函数的候选函数.当 Perl 优化和常量值替换回合后,得到结果如果是一个固定值或者是一个没有其他引用的语法作用域标量时,那么这个值就将替换对这个函数的调用.但是使用 &NAME 方式调用的函数不被"内联化",然而,只是因为它们不受其他函数原型影响.(参看第三十一章"用法模块"中的 use constant,这是一种定义这种固定值的更简单的方法).

下面的两种计算 ∏ 的函数写法都会被编译器"内联化":

sub pi () { 3.14159 }         # 不准确,但接近
   sub PI () { 4 * atan2(1, 1) }      # 和它的一样好

实际上,下面所有的函数都能被 Perl "内联化",因为 Perl 能够在编译的时候就能确定所有的值:

subFLAG_FOO ()     { 1 << 8 }subFLAG_BAR ()     { 1 << 9 }subFLAG_MASK ()    { FLAG_FOO | FLAG_BAR }subOPT_GLARCH ()   { (0x1B58 & FLAG_MASK) == 0 }subGLARCH_VAL ()   {if(OPT_GLARCH) {return23 }else{return42 }
}subN () {int(GLARCH_VAL) / 3 }BEGIN{# compiler runs this block at compile timemy$prod = 1;# persistent, private variablefor(1 .. N) { $prod *= $_ }subNFACT () { $prod }
}

最后一个例子中,NFACT 函数也将内联化,因为它有一个空的函数原型并且函数返回的变量并没有被函数修改,而且不能被其他东西改变,因为它在一个语法作用范围里面.因此编译器在编译的时候预先计算它的值,并用这个值替换所有使用 NFACT 的地方.

如果你重新定义已经被内联化的子过程,那么你会收到一个命令性警告(你可以使用这个警告来确认一个子过程是不是已经被内联化了)因为重新定义的子过程会用先前编译产生的值代替,因此这个警告足够的确定这个子过程是否被内联化.如果你需要重新定义子过程,你可以通过删除 () 函数原型(这个更改调用方法)或者重新修改函数的写法来阻挠内联化机制来避免子过程被内联化.例如:

subnot_inlined () {return23if$$;
}

参看第十八章学习更多有关程序编译和执行阶段的知识.

4.2 谨慎使用函数原型

最好在新函数中使用函数原型,而不在旧函数中使用函数原型.Perl 中函数原型是环境模板,而不象 ANSI C 中的函数原型,因此你必须十分注意函数原型是否将你的子过程带入了一个新的环境.例如,你想写一个只有一个参数的函数,象下面这个函数:

subfunc ($) {my$n =shift;print"you gavemy$n\n";
}

这将得到一个单目操作符(象 rand 内建函数)并且改变了编译器确定函数参数的方法.使用了新的函数原型,该函数就只使用一个标量环境下的参数,而不是在列表环境下的多个参数.如果你在以数组或者列表表达式中调用这个函数,即使这个数组或列表只包含一个元素,你可能会得到完全不同的结果:

func @foo;      # 计算 @foo 元素个数
   func split /:/;      # 计算返回的域的个数
   func "a", "b", "c";   # 只传递 "a",抛弃 "b" 和 "c"
   func("a", "b", "c");   # 马上生成一个编译器错误!

你已经隐含地在参数列表前面提供了一个 scalar,这的确令人有点吃惊.如果 @foo 只包含一个元素,那么传递给函数不是这个元素,而是 1(@foo 的元素个数).并且在第二个例中,split 在标量环境中被调用,吞没你的整个 @_ 参数列表.在第三个例子中,因为 func 已经用函数原型定义为一个单目操作符,因此只有 "a" 传递给了 func;然后 func 返回值被丢弃,因为逗号操作符的存在因此继续处理下两个元素并返回 "c".最后一个例子,在编译的时候用户将得到一个语法错误.

如果你想写一个新的代码得到一个只使用一个标量参数的单目操作符,而不是任何旧的标量表达式,你可以使用下面的函数原型使它使用标量引用:

subfunc (\$) {my$nref =shift;print"you gave me $$nref\n";
}
现在,编译器可以让下面的例子中,参数以 $ 开头的通过:

func @foo;      # 编译器错误,看见了 @,但要的是 $
   func split/:/;       # 编译器错误,看见了函数,但要的是 $
   func $s;         # 这个是对的 -- 获取了真的 $ 符号
   func $a[3];       # 这个也对
   func $h{stuff}[-1];   # 这个也对
   func 2+5;       # 标量表达式也会导致编译器错误
   func ${\(2+5) };       # 对,不过它是不是比病毒还糟糕?

如果你不小心,你可能因为使用函数原型遇到很多麻烦.但如果你非常注意,你可以使用函数原型来作很多漂亮的工作.函数原型是非常强大的,当然需要谨慎使用才能得到好的结果.

5.0 子过程属性

子过程的定义和声明能够附带一些属性.如果属性列表存在,它使用空格或者冒号分割,并等同于通过 use attributes 定义的一样.请阅读三十一章的 use attributes 获得内部细节.有三个标准的子过程属性:locked, method 和 左值.

5.1 Locked 和 method 属性

# 在这个函数里只允许一个线程
   sub afunc : locked { ... }

   # 在一个特定的对象上之允许一个线程进入这个函数
   sub afunc : locked method { ... }

只有在子过程或者方法要被多个线程调用的时候,设置 locked 属性才有意义.当设置一个不是方法的子过程的时候,Perl 确保在进入子过程之前获得一个锁.当设置一个方法子过程时(具有 method 属性的子过程),Perl 确保在执行之前锁住它的第一个参数(所属的对象).

method 属性能够被它自己使用:

sub afunc : method { ... }

现在它只是用来标记子过程,使之不产生 "Ambiguous call resolved as CORE::%s" 警告.(我们以后可以给它更多的含义).

属性系统是用户可扩展的,Perl 可以让你创建自己的属性名.这些新的属性必须是简单的标记名字(除了 "_" 字符之外没有任何标点符号).它们后边可以有一个参数列表用来检查它的花括弧是否匹配正确.

下面是一些正确的语法的例子(即使这些属性是未知的):

sub fnord (&\%) : switch(10, foo(7,3)) : expensive;
   sub plugh () : Ugly('\(") :Bad;
   sub xyzzy : _5x5 { ... }

下面是一些不正确语法的例子:

sub fnord : Switch(10, foo());   # ()-字串不平衡
   sub snoid : Ugly ('(');      # ()-字串不平衡
   sub xyzzy : 5x5;      # "5x5" 不是合法的标识符
   sub plugh : Y2::north;      # "Y2::north"不是简单标识符
   sub snurt : foo + bar;      # "+" 不是一个冒号或空格

属性列表作为一个常量字符串列表传递进子过程相关的代码.它的正确工作方法是高度试验性的.查阅 attributes(3) 获得属性列表的详细信息和操作方法.

5.3 左值属性

除非你定义子过程返回一个 左值,否则你你不能从子过程中返回一个可以修改的标量值:

my$val;subcanmod : 左值 {
        $val;
}subnomod {
        $val;
}

canmod() = 5;# 给 $val 赋值为 5nomod()  = 5;# 错误
如果你正传递参数到一个有 左值 属性的子过程,你一般会使用圆括弧来防止歧义:

canmod $x = 5;      # 先给 $x 赋值 5!
   canmod 42 = 5;      # 无法改变常量,编译时错误
   canmod($x)= 5;      # 这个是对的
   canmod(42)= 5;      # 这个也对

如果你想使用省略的写法,你可以在子过程只使用一个参数的情况下省略圆括弧.使用 ($) 函数原型定义一个函数可以使该函数被解释为一个具有命名的单目操作符优先级的操作符.因为命名单目操作符优先级高于赋值,所以你不再需要圆括弧(需不需要圆括弧只是一个代码风格的问题).

当一个子过程允许空参数时(使用 () 函数原型),你可以使用下面的方法而不会引起歧义:

canmod = 5;

因为没有哪个合法项以 = 开头,因此它能正确工作.同样,具有左值属性的方法调用在不传送任何参数时也能省略圆括弧:

$obj->canmod = 5;

我们保证在未来的 Perl 版本中不改变上面的两种方法.当你希望在方法调用中封装对象属性时,它们是非常简便的方法(因此它们可以象方法调用一样被继承但又象变量一样访问).

左值子过程和子过程的赋值表达式右边部分可以通过使用标量替换子过程的方法,来确定是标量环境还是列表环境.例如:

data(2,3) = get_data(3,4);

上边两个子过程都在标量环境中调用,而在:

(data(2,3)) = get_data(3,4);
和:

   (dat(3), data(3) = get_data(3,4);

中,所有的子过程在列表环境中被调用.

在当前的实现中不允许从左值子过程直接返回数组和散列结构.不过你总是可以返回一个引用来解决这个问题.


TAG:

 

评分:0

我来说两句

显示全部

:loveliness: :handshake :victory: :funk: :time: :kiss: :call: :hug: :lol :'( :Q :L ;P :$ :P :o :@ :D :( :)

日历

« 2008-07-26  
  12345
6789101112
13141516171819
20212223242526
2728293031  

数据统计

  • 访问量: 3280
  • 日志数: 555
  • 建立时间: 2008-01-07
  • 更新时间: 2008-06-24

RSS订阅

Open Toolbar