在Perl中,子程序的引用常用来做回调函数(callback)、闭包(closure),特别是匿名子程序。

回调函数(callback)

关于什么是回调函数,见一文搞懂:词法作用域、动态作用域、回调函数、闭包

File::Find模块的find函数为例,它用来搜索给定目录下的文件,然后对每个搜索到的文件执行一些操作(通过定义子程序),这些操作对应的函数要传递给find函数,它们就是回调函数。就像unix下的find命令一样,找到文件,然后print、ls、exec CMD操作一样,这几个操作就是find命令的回调函数。

use File::Find;

sub cmd {
print "$File::Find::name\n";
}; find(\&cmd,qw(/perlapp /tmp/pyapp));

其中$File::Find::name代表的是find搜索到的从起始路径(/perlapp /tmp/pyapp)开始的全路径名,此外,find每搜索到一个文件,就会赋值给默认变量$_。它代表的是文件的basename,和$File::Find::name全路径不一样。例如:

  起始路径     $File::Find::name       $_
-------------------------------------------
/perlapp /perlapp/1.pl 1.pl
. ./a.log a.log
perlapp perlapp/2.pl 2.pl

回到回调函数的问题上。上面的示例中,定义好了一个名为cmd的子程序,但一直都没有主动地去执行这个子程序,而是将它的引用放进find函数中,由find函数每次去调用它。这就像是unix的find命令的"-print"选项一样,其中"-print"选项对应的函数就是一个回调函数。

上面的子程序只调用了一次,没必要花脑细胞去设计它的名称,完全可以将其设计为匿名子程序,放进find函数中。

use File::Find;

find(
sub {
print "$File::Find::name\n";
},
qw(/perlapp /tmp/pyapp)
);

Perl闭包(closure)简单介绍

关于闭包的详细内容,见一文搞懂:词法作用域、动态作用域、回调函数、闭包

从perl语言的角度来简单描述下闭包:子程序1中返回另一个子程序2,这个子程序2访问子程序1中的变量x,当子程序1执行结束,外界无法再访问x,但子程序2因为还引用着变量x所指向的数据对象,使得子程序2在子程序1结束后可以继续访问这个数据对象。

所以,子程序1中的变量x必须是词法变量,否则子程序1执行完后,变量x可能仍可以被外界访问、修改,如果这样,闭包和普通函数就没有意义了。

一个简单的闭包结构:

sub sub1 {
my $var1=N;
$sub2 =sub {
do something about $var1
}
return $sub2 # 返回一个闭包
} $my_closure = sub1(); # 将闭包函数存储到子程序引用变量

主要目的是为了让子程序sub1内部嵌套的子程序$sub2可以访问属于子程序sub1但不属于子程序$sub2的变量,这样一来,只要把sub1返回的闭包赋值给$my_closure,就可以让这个闭包函数一直引用$var1变量对应的数值对象,但是sub1执行完毕后,外界就无法再通过$var1去访问这个数据对象(因为是词法变量)。也就是说,sub1执行完后,$var1指向的数据对象只有闭包$my_closure可以访问。

一个典型的perl闭包:

sub how_many {       # 定义函数
my $count=2; # 词法变量$count
return sub {print ++$count,"\n"}; # 返回一个匿名函数,这是一个匿名闭包
} $ref=how_many(); # 将闭包赋值给变量$ref how_many()->(); # (1)调用匿名闭包:输出3
how_many()->(); # (2)调用匿名闭包:输出3
$ref->(); # (3)调用命名闭包:输出3
$ref->(); # (4)再次调用命名闭包:输出4

上面将闭包赋值给$ref,通过$ref去调用这个闭包,则即使how_many中的$count在how_many()执行完就消失了(因为是个词法变量,外界无法访问),但$ref指向的闭包函数仍然在引用这个变量,所以多次调用$ref会不断修改$count的值,所以上面(3)和(4)先输出3,然后输出改变后的4。而上面(1)和(2)的输出都是3,因为两个how_many()函数返回的是独立的匿名闭包,在语句执行完后数据对象3就消失了。

Perl语言有自己的特殊性,特别是它支持只执行一次的语句块(即用大括号{}包围),这使得Perl要创建一个闭包并不一定需要函数嵌套,只需将一个函数放进语句块即可:

my $closure;
{
my $count=1; # 随语句块消失的词法变量
$closure = sub {print ++$count,"\n"}; # 闭包函数
} $closure->(); # 调用一次闭包函数,输出2
$closure->(); # 再调用一次闭包函数,输出3

在上面的代码中,$count引用数在赋值时为1,在sub中使用并赋值给$closure时引用数为2,当退出代码块的时候,count引用数减为1,由于这是个词法变量,退出代码块后外界就无法通过$count来访问了,但是闭包$closure却一直可以继续访问。

闭包的形式其实多种多样。通俗意义上来说,只要一个子程序1可以访问另一个子程序2中的变量,且子程序1不会随子程序2执行结束就丢失变量,就属于闭包。当然,对于Perl来说,可能子程序2并非是必要的,正如上面的例子。

例如,下面的代码段就不属于闭包:

$y=3;
sub mysub1 {
$x=shift;
$x+$y;
} $nested_ref=\&mysub1; sub mysub2 {
$x=1;
$z=shift;
return $nested_ref->($z);
} print mysub2(2);

为mysub2中返回的$nested_ref是一个子程序mysub1的一个实例,但mysub1中使用的$y来自于全局变量,而非mysub2,且mysub2执行完后,$y也不会消失,对于闭包来说这看上去没什么必要。

Perl闭包应用

例如,通过File::Find模块的find函数,计算出给定目录下的文件数量:

use File::Find;
my $callback;
{
my $count = 0;
$callback = sub { print ++$count, ": $File::Find::name\n" };
}
find($callback, '.'); # 返回数量和文件名
find($callback, '.'); # 再次执行,数量将在上一个find的基础上递增

Perl的语法强大,可以一次性返回多个闭包:

use File::Find;
sub sub1 {
my $total_size = 0;
return(sub { $total_size += -s if -f }, sub { return $total_size });
}
my ($count_em, $get_results) = sub1( );
find($count_em, '/bin');
find($count_em, '/boot');
my $total_size = &$get_results( );
print "total size of /bin and /boot: $total_size\n";

上面两个闭包,因为同时引用同一个对象,所以闭包$count_em修改的词法变量,$get_results也可以访问。

或者:

{
my $count=10;
sub one_count{ ++$count; }
sub get_count{ $count; }
} one_count();
one_count();
print get_count();

由于代码块中的子程序有名称,所以这两个子程序在代码块结束后仍然有效(代码块结束后变量无效是因为加了my修饰符)。

但是,如果将调用语句放在代码块前面呢?

one_count();  # 1
one_count(); # 2
print get_count(); # 输出:2 {
my $count=10;
sub one_count{ ++$count; }
sub get_count{ $count; }
}

上面输出2,也就是说$count=10的赋值10行为失效。这是因为词法变量的声明和初始化(初始化为undef)是在编译期间完成的,而赋值操作是在执行到它的时候执行的。所以,编译完成后执行到one_count()这条语句时,将调用已编译好的子程序one_count,但这时还没有执行到语句块,所以$count的赋值还没有执行。

可以将上面的语句块加入到BEGIN块中:

one_count();  # 11
one_count(); # 12
print get_count(); # 输出:12 BEGIN{
my $count=10;
sub one_count{ ++$count; }
sub get_count{ $count; }
}

state修饰符替代简单的闭包

前面闭包的作用已经非常明显,就是为了让词法变量不能被外部访问,但却让子程序持续访问它。

perl 5.10提供了一个state修饰符,它和my完全一样,都是词法变量,唯一的区别在于state修饰符使得变量持久化,但对于外界来说却不可访问(因为是词法变量),而且state修饰的变量只会初始化赋值一次。

注意:

  • state修饰符不仅仅只能用于子程序中,在任何语句块中都可以使用,例如find、grep、map中的语句块。
  • 只要没有东西在引用state变量,它就会被回收。
  • 目前state只能修饰标量,修饰数组、hash时将会报错。但可以修饰数组、hash的引用变量,因为引用就是个标量

例如,将state修饰的变量从外层子程序移到内层自层序中。下面两个子程序等价:

use 5.010;   # for state
sub how_many1 {
my $count=2;
return sub {print ++$count,"\n"};
} sub how_many2 {
return sub {state $count=2;print ++$count,"\n"};
} $ref=how_many2(); # 将闭包赋值给变量$ref
$ref->(); # (1)调用命名闭包:输出3
$ref->(); # (2)再次调用命名闭包:输出4

需要注意的是,虽然state $count=2,但同一个闭包多次执行时不会重新赋值为2,而是在初始化时赋值一次。

而且,将子程序调用语句放在子程序定义语句前面是可以如期运行的(前面分析过一般的闭包不会如期运行):

$ref=how_many2();   # 将闭包赋值给变量$ref
$ref->(); # (1)调用命名闭包:输出3
$ref->(); # (2)再次调用命名闭包:输出4 sub how_many2 {
return sub {state $count=2;print ++$count,"\n"};
}

这是因为state $count=2是子程序中的一部分,无论在哪里调用到它,都会执行这一句赋值语句。

再例如,state用于while循环的语句块内部,使得每次迭代过程中都持续访问这个变量,而不会每次迭代都初始化:

#!/usr/bin/perl
use v5.10; # for state while($i<10){
state $count;
$count += $i;
say $count; # 输出:0 1 3 6 10 15 21 28 36 45
$i++;
}
say $count; # 输出空

Perl回调函数和闭包的更多相关文章

  1. Go基础系列:函数(2)——回调函数和闭包

    回调函数和闭包 当函数具备以下两种特性的时候,就可以称之为高阶函数(high order functions): 函数可以作为另一个函数的参数(典型用法是回调函数) 函数可以返回另一个函数,即让另一个 ...

  2. javascript回调函数,闭包作用域,call,apply函数解决this的作用域问题

    在JavaScrip中,function是内置的类对象,也就是说它是一种类型的对象,可以和其它String.Array.Number.Object类的对象一样用于内置对象的管理.因为function实 ...

  3. php 回调函数结合闭包(匿名函数)的使用示例

    <?php /** * php 回调函数结合闭包(匿名函数)的使用 */ function callback( $callback ){ $variable = 'program'; $ret1 ...

  4. 前端(十三)—— JavaScript高级:回调函数、闭包、循环绑定、面向对象、定时器

    回调函数.闭包.循环绑定.面向对象.定时器 一.函数高级 1.函数回调 // 回调函数 function callback(data) {} // 逻辑函数 function func(callbac ...

  5. perl 回调函数

    在计算机程序设计中,回调函数,或简称回调(Callback),是指通过函数参数传递到其它代码的,某一块可执行代码的引用.这一设计允许了底层代码调用在高层定义的子程序. 没啥不好理解的呀,就是向函数的参 ...

  6. golang中匿名函数的应用-回调函数-闭包

    package main import ( "fmt" "strconv" ) type funcType func(int, int) int // 自定义函 ...

  7. javascript 函数初探 (四)--- 回调函数

    回调函数 既然函数与任何被赋值给变量的数据是相同的,那么她当然可以像其他数据那样被定义.删除.拷贝,以及当成参数传递给其它函数. 我们定义一个函数,这个函数有两个函数类型的参数,然后他会分别执行这两个 ...

  8. PHP中的回调函数和匿名函数

    html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,bi ...

  9. 理解和使用 JavaScript 中的回调函数

    理解和使用 JavaScript 中的回调函数 标签: 回调函数指针js 2014-11-25 01:20 11506人阅读 评论(4) 收藏 举报  分类: JavaScript(4)    目录( ...

随机推荐

  1. 软件光栅器实现(二、VS和PS的运作,法线贴图,切空间的计算)

    二.软件光栅器的VS和PS的输入.输出和运作,实现法线贴图效果的版本.转载请注明出处. 这里介绍的VS和PS是实现法线映射的版本,本文仅介绍实现思路,并给出代码供参考.切空间计算.光照模型等相关公式不 ...

  2. Android Studio 3.1.2 修改字体(font)大小(size) 及老版本修改主题、字体、颜色 参照地址

    Android Studio 3.1.2  修改字体(font)大小(size) 步骤:File-Settings-Editor-Color Scheme-Color Scheme Font-Size ...

  3. 计算机网络三:域名、IP地址和TCP/IP协议

    一.域名        域名(Domain Name),简称域名.网域,是由一串用点分隔的字符型标志名字组成的Internet上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的电子方位(有时 ...

  4. Windows下安装BeautifulSoup4显示'You are trying to run the Python 2 version of Beautiful Soup under Python 3.(`python setup.py install`) or by running 2to3 (`2to3 -w bs4`).'

    按照网上教程,将cmd的目录定位到解压缩文件夹地址,然后 >>python setup.py install ( Window下不能直接解压tar.giz文件,可以使用7z解压软件提取解压 ...

  5. Django积木块三——静态文件和上传文件

    静态文件和上传的文件 # 静态文件 STATIC_URL = '/static/' STATICFILES_DIRS = ( os.path.join(BASE_DIR, 'static'), ) # ...

  6. VMware安装xp虚拟机

    VMware安装xp虚拟机 1.用到的软件: 2.安装VMware:  接受 选择自定义 要等上一小会. 输入密钥:百度一个就可以了. 安装成功: 禁用VMware网卡: 3.安装xp系统: 创建新的 ...

  7. PMP:11.项目采购管理

    项目采购管理包括从项目团队外部采购或获取所需产品.服务或成果的各个过程.  项目采购管理包括编制和管理协议所需的管理和控制过程,例如,合同.订购单.协议备忘录 (MOA),或服务水平协议 (SLA). ...

  8. Azure Sphere–“Object reference not set to an instance of an object” 解决办法

    在开发Azure Sphere应用时,如果出现项目无法编译,出现“Object reference not set to an instance of an object”时,必须从下面两个方面进行检 ...

  9. 项目Alpha冲刺(团队6/10)

    项目Alpha冲刺(团队6/10) 团队名称: 云打印 作业要求: 项目Alpha冲刺(团队) 作业目标: 完成项目Alpha版本 团队队员 队员学号 队员姓名 个人博客地址 备注 221600412 ...

  10. 项目文件与 SVN 资源库同步提示错误 Attempted to lock an already-locked dir

    问题描述 之前为了图方便,在eclipse中直接把三个jsp文件复制到了eclipse中我新建的一个文件夹中,把svn版本号信息也带过来了,然后我又删除了这三个jsp文件,接着先把这三个jsp复制到桌 ...