3.8 对象方法里的缓存

对象方法,它经常不理解地把缓存的值保存在独立的散列里。考虑一个投资银行写的程序里的Investor对象。该对象表现了银行的一个客户:

package Investor;# Compute total amount currently invested
sub total {my $self = shift;# ... complex computation performed here ...return $total;
}

如果$total不会改变,就可以缓存它,用对象的本身作为缓存散列的键:

# Compute total amount currently invested
{ my %cache;sub total {my $self = shift;return $cache{$self} if exists $cache{$self};# ... complex computation performed here ...return $cache{$self} = $total;}
}

然而,这个技术有一个严重的问题。当用一个对象作为一个散列键时,Perl把它转换成一个字符串。典型的散列键将看上去像Investor=HASH(0x80ef8dc)。十六进制数字是对象的数据实际存放的地址。本质上,这个键对任何两个对象都不一样,即冒着错误的缓存命中的风险,恢复一个对象的总数,却想着它属于另一个不同的对象。在Perl里,这些散列键确实对系统中任何给定的时刻存在的所有对象都不同,但对已回收的对象没有保证。如果一个对象被销毁了而一个新的对象被创建了,那个新的对象恰好存在于旧的对象先前占有的内存地址,那么混淆如下:

# here 90,000 is returned from the cache
$old_total = $old_object->total();
undef $old_object;
$new_object = Investor->new();
$new_total = $new_object->total();

这里要求新的投资者的投资总数。它应该是0,因为投资者是新的。然而,->{total}方法恰好查看缓存,以刚被销毁的$old_object用过的同样的散列键;这个方法看到90000存在那里,并错误地返回它。这个问题可以用一个DESTROY方法解决,它从缓存里删除一个对象的数据,或者在程序里对每个对象关联一个唯一的不再复用的ID数字,并用这个ID数字作为散列键,但是有个更直接的解决方法。

在一个面向对象编程上下文,缓存散列的技术是独特的,因为有个更自然的地方存放缓存的数据:就像数字存在于它的对象自身。一个缓存的总数变为另一个属性,后者可以或不被每个独立的对象携带:

# Compute total amount currently invested
sub total {my $self = shift;return $self->{cached_total} if exists $self->{cached_total};# ... complex computation performed here ...return $self->{cached_total} = $total;
}

这里的逻辑和之前的完全一样,唯一的不同是:这个方法为每个对象把总数保存在对象自身,而不是在一个辅助的散列。这避免了辅助散列带来的散列键冲突的问题。

这个技术的另一个优点是,用来保存缓存的总数的存储空间在对象销毁时会自动回收。用辅助的散列,每个缓存的值会一直存在,甚至在其所属的对象都被销毁以后。

最后,把缓存的信息分别存放在每个对象里在对象到期时带来了更灵活的控制。在例子里,total计算某个投资者已经投资的总数。缓存这个总数是合适的,因为投资者不会太频繁地新投入钱。但是永久缓存它可能也不合适。在这个例子里,无论何时一个投资者投资更多的钱,都需要以某种方式告知total函数,缓存的总数不再正确了,必须要丢掉再重新计算。这就称为缓存值的到期(expiring)。

用辅助散列的技术,要是不在缓存散列的作用域里增加一个特殊需求的方法,就无法做到这一点,如下:

# Compute total amount currently invested
{ my %cache;sub total {my $self = shift;return $cache{$self} if exists $cache{$self};# ... complex computation performed here ...return $cache{$self} = $total;}sub expire_total {my $self = shift;delete $cache{$self};}
}
sub invest {my ($self, $amount, ...) = @_;$self->expire_total;...
}

用面向对象技术,则不必增加特别的方法,因为每个方法都可以在它需要的时候直接使缓存的总数到期:

# Compute total amount currently invested
sub total {my $self = shift;return $self->{cached_total} if exists $self->{cached_total};# ... complex computation performed here ...return $self->{cached_total} = $total;
}
sub invest {my ($self, $amount, ...) = @_;delete $self->{cached_total};...
}

对于对象方法,经常需要把每个计算过的值缓存在与它有联系的对象中,而不是一个独立的散列。但之前介绍的momoize函数没有这么做,但是不难建立一个这么做的:

### Code Library: memoize-method
sub memoize_method {my ($method, $key) = @_;return sub {my $self = shift;return $self->{$key} if exists $self->{$key};return $self->{$key} = $method->($self, @_);};
}

$method是一个指向真实方法的引用。$key是缓存的值,将被保存于每个对象的位置的名字。这里返回的存根函数适合用做一个方法。当存根被执行时,它取回其行为被调用的对象,就像别的方法一样,然后它在对象里查找成员数据$key,看是否有值缓存在那里。如果有,存根返回缓存的值;如果没有,它调用真实的方法,把结果缓存入对象,并返回新缓存的结果。
为使用这个,可以写成如下这样:

*Investor::total = memoize_method(\&Investor::total, 'cached_total');
$investor_bob->total;

这把存根安装入符号表以替代原始的方法。或者,可以使用:

$memoized_total = memoize_method(\&Investor::total, 'cached_total');
$investor_bob->$memoized_total;

这两者不完全一样。在前一种情况,所有对->total的调用将使用方法的带记忆的版本,包括来自继承方法的子类的调用。在后一种情况,只有当使用->memoized(...)以明确地要求时,才得到方法的带记忆的版本。

《高阶Perl》——3.8 对象方法里的缓存相关推荐

  1. 《高阶Perl》——导读

    前 言 在编程圈子里有一句著名的俗语,一个优秀的Fortran程序员可以用任何语言写Fortran程序.然而,让人悲哀的是,不管他们是否愿意,Fortran程序员用任何语言写Fortran程序.类似地 ...

  2. 陶敏修《高阶Perl》访谈录(来自The Perl Review)

    陶敏修<高阶Perl>访谈录 简介:陶明修(Mark Jason Dominus),资深Perl专家,Perl模块开发者,也是Perl.com/PerlMonth/ThePerlJourn ...

  3. 高阶系统怎么用matlab降阶,一种基于非线性规划的高阶系统最优降阶方法

    一种基于非线性规划的高阶系统最优降阶方法 [技术领域] [0001] 本发明涉及一种基于非线性规划的高阶系统最优降阶方法,它是一种线性时不变 系统的最优降阶方法,是针对单输入单输出系统给出的一种使得降 ...

  4. 理解Vue中的methods对象方法里的this指向,并解读源码

    Vue3的版本 "version": "3.2.20" 阅读区域 618 - 645 解决疑惑 1. methods 对象的 this指向 2. methods ...

  5. 《高阶Perl》——3.4 记忆术

    3.4 记忆术 给函数添加缓存技术代码不是非常困难的.且已经看到,需要的改变对任何函数几乎一样.那么,为什么不让计算机做这些呢?若告诉Perl想要使一个函数具有缓存行为.Perl应该能自动地执行所需的 ...

  6. 《高阶Perl》——第3章 缓存与记忆术

    第3章 缓存与记忆术 在1.8节看到了一个普通的递归函数有时执行得非常糟糕.解决许多这些性能问题的一个简单和普遍的方法,和非递归环境里出现的一样,就是缓存(caching). 考虑一个程序把图像从一种 ...

  7. C++高阶 RAII机制(以对象管理资源)

    文章目录 1.什么是RAII 2.RAII的原理 3.实例: 1.什么是RAII RAII(Resource Acquisition Is Initialization)是由c++之父Bjarne S ...

  8. 高阶系统怎么用matlab降阶,高阶系统模型一种降阶方法.pdf

    高阶系统模型一种降阶方法.pdf . . 5 卷 1 V o l 5 , N o l 第 第 期 用 应 基础与工程科学学报 1 9 7 s a r e 9 9 月 R N F B I C IE E ...

  9. 箭头函数的this指向谁_高阶函数

    NodeJS 系列文章,本篇是第一篇,首先,预计将后续高频使用逻辑串一遍,依次是高阶函数,promise以及事件机制.本篇主要是高阶函数. call.bind.apply call.apply 都是改 ...

最新文章

  1. JQuery变量数字相加的研究
  2. db2 如何导出insert语句_MySQL 执行脚本/导入/导出数据
  3. Android之项目推荐使用的第三方库
  4. 连续投影算法_中航国画荣获“2020 IAV国际视听嘉年华” 大屏幕投影显示行业三大奖项!...
  5. Android Studio-------添加按钮
  6. 两平面平行但不重合的条件是_____「初一数学」平行线的判定与性质的综合应用...
  7. java 返回js,Javascript返回上一页
  8. 前端开发的壁垒在哪儿?
  9. 我的家庭私有云计划-6
  10. Deteming the User Intent of Web Search Engine
  11. linux常用分区大小,Linux基本知识点总结——硬盘分区及LVM
  12. 如何用计算机算回归方程,简单线性回归方程与在线计算器_三贝计算网_23bei.com...
  13. 测斜原始数据处理工具
  14. 几何公差学习笔记(二)-- 公差带的定义、标注和解释
  15. 淘宝API item_search_similar - 搜索相似的商品
  16. 解决 SharePoint 2013 管理中心登录问题。
  17. 腾讯青浦二期数据中心为啥得到了USGBC的青眼?
  18. 8代cpu装服务器系统,8代cpu不支持win7吗
  19. 静态IP和动态IP有什么区别?
  20. javaScript中console.log()的用法

热门文章

  1. MySQL性能调优与架构设计——第11章 常用存储引擎优化
  2. ReflectedSchemas应该定期清理否则会占用大量C盘空间
  3. C# Object.Equals方法深入研究
  4. 软考信息系统项目管理师_项目风险管理---软考高级之信息系统项目管理师019
  5. OAuth2.0_授权服务配置_密码模式及其他模式_Spring Security OAuth2.0认证授权---springcloud工作笔记145
  6. 快速开发框架工作笔记002---项目开发中整理_整合好的_Netty高并发处理快速开发框架_Netty快速开发框架
  7. MyCat分布式数据库集群架构工作笔记0009---Mycat主--从复制配置上
  8. 持续集成部署Jenkins工作笔记0005---应用服务器设置账号密码说明
  9. 行业术语001--关键绩效指标KPI_NLP_ACL
  10. Web前端工作笔记002---json数据查询的方法_json查询大全,JsonSQL数据查询,jfunk数据查询