转自:http://hi.baidu.com/chenfalei/blog/item/f33ac0133500ac21dd540186.html
编程语言的最终梦想:静态类型安全
常听人说“强类型”。但个人对强类型都有不同的理解。
有的认为C++就是强类型,有的认为C也是强类型。因为它们都有类型检查。
可见,如果没有一个明确的定义,谈“强类型”不免是自说自话。
那么,可以给“强类型”下一个什么样的定义呢?
最原始的定义是这样: 静态类型系统将检查所有的错误。只要通过了静态类型检查,程序将不会有bug.
但是,明显这是不现实的,因为有些bug是纯粹逻辑上的。比如说:
print "hell world"
少打了一个o, 是bug, 但除非把自然语言识别加进来,计算机对此无能为力。
于是,我们把强类型定义为:
静态类型系统检查所有类型匹配的错误,只要通过了类型检查,就保证不会有把苹果当成汽油的事发生。
至于类型匹配错误,我们把它定义成:
当你试图把一个变量当作类型A来处理时,却意外发现它不是类型A的。它可能是类型B的,也可能什么都不是。
再谨慎一点,我们可以排除掉使用downcast的情况。如果你非要
(汽油)new 苹果(), 自己找死,我们也管不着了。
这可以说是所有支持类型的语言所追求的最高境界。想想吧,一旦你的程序通过编译,不管它有几百万行,你都可以自信地说:我的程序里面最多只有逻辑错误了。
多好啊!!!
那么,有什么语言达到了这个要求呢?C吗?C++吗? Java吗?
不幸的是,它们都不是。
先说C,
union大家都熟悉吧?你把两种不同的类型混杂于一块内存空间。然后用一个变量来标识它的真正类型。
但是,如果你的程序一旦错误地把类型A当成类型B, 编译器不会警告你的。
还有,char* p = "hello world";
这句话,也不是类型安全的。你如果做p[0]='x'; 编译器不会抱错,因为p的类型是char*, 而对char* 做下标操作是完全合法的。
C还有好多其它不严格的地方。
C++呢?上面提到的两个C的问题它同样有。
还有一些不那么明显的类型漏洞。
1。 placement new. 看这个代码:
X* px = new X();
new (px) Y();
px->m();
这里,编译器不会报错,但px->m()的结果却是未知的。
有人解释说:这是因为px指向的对象已经不存在了。但是,我们不是研究它为什么失败,你可以有一千个理由解释你的程序为什么崩溃,但是,事实很简单:它崩溃了。因为你想把一个不是X的东东当成X来使用。
2。delete p;
很惊讶是吗?
X* px = new X();
delete px;
px->m();
简简单单地就绕过了编译器,得到了一个类型匹配错误。你也可以较它悬挂指针错误,但是,根据我们对类型匹配错误的定义,我们想把px当成X*, 但实际上它并不是我们期望的类型,所以,它也是类型错误的一种。否则,不免对其他的类型漏洞不够公平,
你这个px->m()并不必例子一里的px->m()安全一丁点。 为什么我是类型错误,你就不是?
再看Java, 相比于C/C++, Java在类型安全上有了长足的进步。上面提到的问题,在Java里全都不存在了。Java彻底地扔掉了union, 扔掉了指针,这些设计在效率上可能值的探讨。但是,一个明显的事实是,它的类型系统更安全了。
Java采用了垃圾收集机制。这对很多习惯于又程序管理内存的C/C++程序员来说是有争议的。但是,如果不考虑效率等问题,只从类型安全的角度去看,它免除了delete带来的类型安全漏洞,朝真正意义上的静态类型安全又近了一步。
不过,Java也不是完整的静态类型安全。
缺乏泛形的支持,只是程序员要频繁地做类似(苹果)obj;这样的downcast. 大大地加大了程序的隐患。好在java也认识到这一点,各种不同的努力都在试图往java中加入generics. 不过,这不是本文要讨论的目的,而且,毕竟,我们的前提是不考虑downcast.
那么,不用downcast, java程序就是静态类型安全的吗?
请看这段代码:
String[] sa = new String[100];
sa[0] = "hello world";
Object[] oa = sa;
oa[0] = new Integer(1);
System.out.println(sa[0]);
通!火药桶爆炸了!
究其原因,是在于Object[] oa = sa;这一句。
根据类型理论里的协变原理,只有只读的Object[] 才能是String[]的父类型。但Java里并没有只读数组这么个类型,悲剧就这样发生了。
同样的隐患也存在于一些泛形的语言之中。如果语言想提供协变的支持,如,想让MyTemplate< Object> 是MyTemplate<String>的父类型,那么,Object类型的引用在MyTemplate的定义中也不能是随意的。它必须符合 协变的规定,只处于协变的位置上。
还有一个静态类型系统无能为力的地方,如:
String s[] = new String[2];
s[2] = "hello world";
砰!悲剧再次发生了。.
它符合我们对类析匹配错误的定义:你试图把s[2]当作String来处理,但实际上它不是。
这个问题存在于几乎各种支持数组的语言。
因为数组的的长度可以是任意的,很难设计一个类型系统来静态保证数组的边界不会被越过。
Pascal试图这样做,它通过在类型上附加数组的长度来帮助静态类型系统工作。
但是,一个规定了大小的数组虽然可以保证类型安全,可因为不同大小的数组不能互相转换,大大牺牲了程序的灵活性。
Java的解决方案很现实:既然这样做不好做,那就算了吧。我在运行时加入边界检查,虽然不能静态地保证数组的边界 安全,至少可以保证不出大漏子。 其实,理论上说,Java的做法是通过改变数组的动态语义,把一个未定义的类型匹配错误转换成了一个相对安全的定义好的对象状态错误。
是啊,哪有十全十美的事情呢?毕竟,程序作为一个状态机,总是会有非法状态的。
其实,也还是有另一种方案的。
ML, 这个编程语言界的老前辈。几乎是带类型的functional language的开山祖师了。
它的方案是什么?
简单, 不能再简单了,那就是:干脆不要数组!
听起来吓人,但是,这是和functional language的方针一致的,因为数组是一个要求副作用的数据结构,而functional是要摒弃副作用,所以,functional弃用数组也是很 自然的。(具有讽刺意味的是,ML的一些扩展还是把数组加了进来,因为数组是实现象hash table之类的数据结构的必经之路。)
说了这么多,基本上把可能的类型系统漏洞都从阴沟里翻了出来, 晒晒太阳。
C#怎么样? 我只是对C#惊鸿一瞥,当时看看,现在许久不用,都忘掉了。不过,这个数组越界的问题,相信仍然存在。
如果我们现实一点,把数组越界这个不大可能根本解决的问题抛开,那么,唯一让我对C#担心的,就是这个数组的协变问题了