我的新博客地址:http://cuipengfei.me/blog/2013/10/13/scala-trait/
我在Coursera上跟了一门叫做Functional Programming Principles in Scala的课程,是由Scala的作者Martin Odersky讲授的。其中第三周的作业中使用到了Scala的trait这个语言特性。
我以前熟知的语言都没有类似的特性(Ruby的mixin和Scala的trait很像,但是Ruby我不熟),所以这周的博客就分析一下这个语言特性是如何实现的。
trait
在讲trait的实现机制之前,先看一个使用trait的例子。 假设我们有以下几个类:
1 |
|
植物家族有玫瑰和杂草。
动物家族有狗和毒蛇。
仔细观察可以发现,玫瑰和狗有一个共同的行为,它们都可以取悦人类,这个行为是用完全一样的代码实现的。
如何把Rose和Dog中的重复代码消除掉呢?有一种潜在的解决方案: 把makePeopleHappy提取到一个类中去,让植物和动物都继承自它。
这么做虽然消除了重复代码但有两个明显的缺点:
- 植物和动物继承自同一个类,不太合理
- 杂草和毒蛇也具有了取悦于人的能力,也不太合理
这时我们就可以使用trait,它没有上面提到的两个缺点。
1 |
|
我们定义一个trait,把makePeopleHappy置于其中,让Rose和Dog都with这个trait。然后就可以写这样的代码来调用它们了:
1 |
|
这样我们就解决了重复代码的问题,而且没有触及已存在的继承关系。
现在看看trait的实现机制吧,我们开始反编译!
1 |
|
真相大白了,PeoplePleaser被编译成了一个接口加一个抽象类。Rose和Dog实现这个接口,并通过调用抽象类中的静态方法来实现了makePeopleHappy。
很有趣的一点是Rose和Dog在调用静态方法时都把this传了进去,为什么呢?我们把原来的代码改成这样来看:
1 |
|
我们给makePeopleHappy加上一段额外的信息。 现在再次反编译。
1 |
|
现在就清楚了,抽象类中的静态方法可能会依赖于各个实例不同的状态,所以需要把this传递进去。 这样我们才能够给makePeopleHappy加上一段额外的信息。
弱引用是什么?
要搞清楚什么是弱引用,我们需要先知道强引用是什么。强引用并不是什么深奥的概念,其实我们平时所使用的.Net引用就是强引用。例如:
Cat kitty = new Cat();
变量kitty就是一个强引用,它指向了堆中的一个Cat对象实例。我们都知道,CLR的垃圾回收机制会标记所有被强引用到的对象,而那些剩下的未被标记的对象则会被垃圾回收。换句话说,如果一个对象一直被某个强引用所指向,那么它是不会被垃圾回收的。
从这一点来看,弱引用就完全不一样了——即使某个对象被弱引用所指向,该对象仍然会被垃圾回收。也就是说,弱引用不会影响对象的生命周期。
System.WeakReference类是.net为我们提供的一个弱引用的实现,可以这么用:
WeakReference weakReference = new WeakReference(new Cat());
Cat strongReference = weakReference.Target as Cat;
if (strongReference != null)
{
// Cat对象实例尚未被垃圾回收,可以通过strongReference进行访问
}
else
{
// Cat对象实例已被垃圾回收
}
如果在上例的第一行代码之后第二行代码之前,CLR发生了一次垃圾回收,那么可以基本断定那个Cat对象实例已经不存在了,此时weakReference.Target是null。
WeakReference类型还有一个构造函数的重载为:
Public WeakReference(Object target, bool trackResurrection)
其中bool类型的参数trackResurrection指定了这个WeakReference实例是一个长弱引用还是一个短弱引用。对于短弱引用,当它所指向的对象被垃圾回收机制标记为“不可达”状态(即将被回收)时,该弱引用的Target属性即为null。而对于长弱引用,当它所指向对象的析构函数被调用之后,它的Target属性仍然是有效的。
弱引用的内部实现
弱引用看起来很神奇,似乎是凌驾于正常的垃圾回收机制之上的,它究竟是如何实现的呢?其实WeakReference类型在内部封装了一个名为GCHandle的struct类型,正是这个GCHandle使弱引用成为可能。
CLR中的每个AppDomain都拥有一个GC句柄表。这个表的每一项记录有两个信息,一个是指向堆中某个对象的指针,另一个是这个表项的类型。总共有4种表项类型,其中Weak和WeakTrackResurrection两种类型和我们今天所讨论的弱引用相关。GCHandle这个类提供了一些操纵GC句柄表的方法。我们可以使用它的Alloc方法向GC句柄表中添加一个指定类型的表项。当垃圾回收开始后,垃圾回收器找到所有可达对象(简单的说,就是有用的对象)。然后遍历GC句柄表中每个Weak类型的表项,如果发现某表项所指的对象不属于可达对象,则会把该表项的对象指针设置为null。紧接着,垃圾回收器会找出所有不可达对象中定义了析构函数的对象,并把他们放到一个被称为freachable的队列中(freachable中的对象会等待一个CLR中特定的线程来调用他们的终结函数)。由于这些freachable中的对象现在又被freachable队列所引用,所以它们又成为可达对象了。此时,垃圾回收器会遍历GC句柄表中所有WeakTrackResurrection类型的表项,和刚才一样,如果某表项所指的对象不属于可达对象,则会把该表项的对象指针设置为null。此处需注意,对于那些一开始被判定为不可达且定义了析构函数的对象来说,它们在GC句柄表中所对于的表项指针仍然不是null。这就是Weak和WeakTrackResurrection两种类型的区别。
WeakReference就是通过表示了某个GC句柄表表项的GCHandle对象来完成跟踪对象生命周期的功能的。你也一定可以看出短弱引用利用了Weak类型的GC句柄表项,而长弱引用则利用了WeakTrackResurrection类型的表项。
WeakReference的一些注意事项
首先,WeakReference自身也实现了析构函数。也就是说,它即使不再被使用了,也不会被立即回收,而是会在内存里赖着多活一会(可能会经历不止一次的垃圾回收)。
另外,如上一节所说,WeakReference会向GC句柄表添加一个表项。而每次垃圾回收,GC句柄表都会被遍历一遍。可想而知,如果系统中存在大量的WeakReference,那么GC句柄表很可能也会非常庞大,导致垃圾回收的效率降低。
WeakReference经常会和缓存联系起来,但是它并不适和用来实现一个大型的缓存机制。这是为什么呢?一方面如前文所述,WeakReference自身实现了析构函数,也有可能导致垃圾回收的效率降低,因此应该避免在内存中创建大量的WeakReference对象实例。另一方面,我们知道一个对象如果没有被任何强引用所指向,而仅仅被弱引用所指向,那么它很有可能活不过一次垃圾回收。所以通过这样的方式所实现出来的缓存机制势必有着非常短促的缓存策略,而这种策略在大部分情况下都不会是你期望得到的。
WeakReference的三个使用场景
对象缓存
试想这样一个场景,我有一个内存受限的程序,在这个程序里经常会使用一个占用很多内存的位图对象,所幸生成这个位图对象并不复杂。所以我每次要使用那个位图对象的时候都会重新生成它,使用完毕之后就将其丢弃(不保留它的引用)。
这种方式完全能够满足我的需求,但是还能不能再优化呢?分析一下我们就可以发现,当我需要使用位图对象的时候,我上次使用的那个位图对象虽然被我丢弃了,但可能仍然没有被垃圾回收,仍然存在内存中。此时如果我能直接使用这个位图对象,就可以节省出因重建位图对象而浪费的内存和CPU资源。
改进措施很简单——使用完位图对象后,不是直接丢弃,而是用一个弱引用指向它。待下次访问位图对象时,就可以先通过弱引用判断位图对象是否还在内存中,如果还在则直接使用,否则重新创建。
辅助调试
有时,对于某种类型,我们需要知道当前程序中存在有多少对象实例,以及存在的都是哪些实例,以便于我们进行一些性能分析。这时,我们就可以使用到弱引用了。例如下面的代码:
public class A
{
private static List<WeakReference> _instances = new List<WeakReference>(); public A()
{
_instances.Add(new WeakReference(this));
} public static int GetInstanceCount()
{
GC.Collect();
return _instances.Count(x => x.Target != null);
}
}
GetInstanceCount方法可以得到内存中A类型的实例个数。另外,还可以通过instances集合来检查内存中的A类型实例都有哪些。在调试内存泄露问题的时候,这些信息都可以派上用场。
相比于各种性能分析Profiler工具,这种方法更加轻巧便捷。
弱事件
.Net中的事件有时会引起内存泄露问题。例如,A注册了B的某个事件,此时B就会暗中保留A的一个强引用,导致A无法被内存回收,直到B被回收或A反注册了B的事件。例如,我有一个对象注册了主窗口的Loaded事件,只要我不反注册该事件,那么主窗口会一直引用该对象,直到主窗口被关闭,该对象才会被回收。所以,每当我们注册某个对象的事件时,都有可能在不经意间埋下内存泄露的隐患。
解决这个问题的根本方法是,在必要的时候进行事件的反注册。但是,在某些情况下,我们可能很难判定这个“必要的时候”。另外,当我们作为类库的提供者时,我们也很难保证类库的使用者都记得要反注册事件。因此,另一个解决方案就是使用弱事件。
弱事件的实现原理很简单,就是对事件进行一层封装。不让事件发布者直接引用监听者,而是让他们保留一个监听者的弱引用。当事件触发时,发布者会先检查监听者是否还存在于内存中,如果存在才通知它。如此一来,监听者的生命周期就不会依赖于发布者了。
软件工程之系统建模篇【开卷有益】
开篇简述
博客自从大学毕业就开通了,到现在还没发布什么博文,以前不喜欢写博客,但是后来发现写文章其实也是自我提升一个方式,现在的工作不是很忙,趁此机会,写一些文章。此软件工程系统建模系列,以自己在工作中开发OA的系统为参考,结合UML语言来讲述办公自动化系统建模过程,篇幅大概20篇左右。本文作为开篇,主要简述相关的概念和这个系列的索引,由于本人技术和表述能力有限,错误之处在所难免,通过本系列,将能够学习到软件开发的各种模型设计,不求完美,但求有用。
UML概述
UML译为统一建模语言,由面向对象方法领域三位著名学者提出,并结合优秀的软件方法和思想演变而成,1997年被国际对象组织(OMG)接受,目前被公认为最好的分析和设计面向对象软件的标准建模语言。
UML组成
UML由模型元素,扩展机制、图及视图等部分构成,由模型元素或扩展机制构成图,由图构成视图。UML定义了9种不同的图,9种图分为两类:一类是静态图,包括用例图、类图、对象图、组件图和配置图,另一类是动态图,包括序列图、协作图、状态图和活动图。它包括5种不同的视图:用例视图、设计视图、过程视图、实现视图和配置视图。
UML功能
UML作为一种建模语言,则用于系统开发人员之间,开发人员与用户之间的交流。主要功能包括:为软件系统的产出建立可视化模型,规约软件系统的产出,构造软件系统的产出,为软件系统的产出建立文档。
何谓OA
OA是office Automation办公自动化的缩写,办公自动化是应用计算机技术、通信技术、系统科学和行为科学等先进的科学技术,不断地使人们的部分办公业务借助与各种办公设备,并由这些办公设备与办公人员构成的服务与某种目标的人机信息系统。OA有如下作用:
能极大地提高行政机构的工作效率或企事业单位的市场竞争能力
能提高管理者的决策水平
能节省运营成本
能降低劳动强度
在开发办公自动化系统的实例之前,应该选择和定义一个合适的系统结构,典型的系统结构分3层:表示服务层、商业服务层和数据库服务层,为使办公自动化系统的实例在未来可以扩展,可将3层结构进一步细化为6个逻辑层,每个逻辑层分别提供不同的服务
表示服务层:提供用户接口技术
商业上下文服务层:提供数据编辑
商业规则服务层:实现商业规则
数据转化服务层:将商业层的请求转化为数据兼容的语言
数据访问服务层:实行某些数据库API接口
数据库服务层:保存实体数据
系列导航
前篇:
9、软件工程之系统建模篇【设计用户接口原型】
10、软件工程之系统建模篇【设计动态模型】
11、软件工程之系统建模篇【设计数据模型】