给函数增加缓存功能的最直接的方式,就是给函数加个私有的散列。本例中可以使用数组代替散列,因为fib()的参数永远是非负整数。但一般情况下需要使用散列,如下所示:
# 计算给定的第n月中兔子的对数
{ my %cache;
sub fib {
my ($month) = @_;
unless (exists $cache{$month}) {
if ($month < 2) { $cache{$month} = 1 }
else {
$cache{$month} = fib($month-1) + fib($month-2);
}
}
return $cache{$month};
}
}
这里,fib接收的参数没有发生变化。但跟以前不同的是,它没有立即进行递归的斐波那契计算,而是先检查缓存。缓存就是散列%cache。函数需要计算斐波那契数fib($month)时,将计算结果保存在$cache{$month}中。以后再调用fib(),就会先检查缓存散列中是否存在要计算的值。exists $cache{$month}的目的即是如此。如果缓存中不存在指定的值,就说明还没有使用当前$month调用过该函数。于是unless块中的代码执行通常的计算,必要时就执行递归调用。但是,函数计算出结果之后,并不是立即返回,而是将结果保存到缓存散列中的适当位置。例如,当$onth < 2时,执行$cache{$month} = 1来保存缓存。
函数末尾的return $cache{$month}返回缓存中的值,这个值可能是函数刚刚计算出的,也可能是早就有的。
这样修改之后,fib函数就很快了,在第 1 章 递归和回调函数中见到的过度递归的问题也解决了。问题的起因就是重复计算,而缓存正好能避免重复计算的发生。函数遇到需要重复计算的情形,可以立即通过缓存返回结果。
为什么%cache要放在fib之外?为什么%cache和fib外边有对大括号?
如果在fib内部定义%cache,比如:
sub fib {
my %cache;
...
}
那么缓存就无法正常工作,因为每次调用fib都会生成新的%cache变量,函数返回时,这个变量就被销毁了。在函数之外定义%cache,Perl就知道%cache应当只有一个实例,在程序编译之后建立,在程序完全退出时销毁。这样%cache才能在多次调用fib之间保持住它的值。像%cache这样在所有函数之外定义的变量称为静态变量,因为它的值只有在被特意改变时才会变动,也因为C语言中类似的功能是通过关键字static(静态)定义的。
%cache定义为my,所以它在词法范围内有效。默认情况下它的作用域会一直有效到文件末尾。那样的话,定义在fib之后的所有函数都能访问并修改缓存。这与要求不符——我们希望缓存只能被fib使用。将%cache和fib都放在单独的代码块中就可以实现这一点。%cache的作用域有效到代码块末尾,而这个代码块中只有fib。