在对任何编程语言的讨论中,争议较大的一个问题就是类型模型。类型决定可以使用哪些种类的工具,并影响到应用程序的设计。很多开发人员将类型与生产率或可维护性联系起来(我就是其中的一个)。典型的 Java 开发人员通常都特别乐于维护 Java 语言的类型模型的地位,强调 Java 语言可采用更好的开发工具,在编译时捕捉某些种类的 bug(例如类型不兼容和拼写错误),以及性能等方面的优势。
如果您想理解一种新的编程语言,甚至一系列语言,那么通常应该从类型策略着手。在本文中,您将看到 Java 之外的一些语言中的类型模型。我首先简要介绍任何语言设计者在类型模型中必须考虑的一些决策,着重介绍静态类型和动态类型的一些不同的决策。我将展示一些不同极端的例子 —— Objective Caml 中的静态类型和 Ruby 中的动态类型。我还将谈到 Java 语言的类型限制,以及如何突破 Java 类型的限制快速编程。
|
类型策略
至少可以从三个角度来看待类型:
下面两个例子很好地阐释了其中两个角度的内涵。假设您编译下面这段 Java 代码:
|
会收到如下错误消息:
|
执行以下 Ruby 代码:
|
会收到以下错误消息:
|
这两种语言都倾向于强类型,因为当您试图使用一个它们期望之外的类型结构的对象时,它们都会抛出错误消息。Java 类型策略在编译时给出错误消息,因为它执行静态类型检查。而 Ruby 则是在运行时给出错误消息,因为 Ruby 支持动态类型。换句话说,Java 在编译时将对象绑定到类型。而 Ruby 则是在运行时每当更改对象的时候将对象绑定到类型。由于我是在 Java 代码中,而不是在 Ruby 中声明变量的,因此可以看到 Java 语言的显式类型与 Ruby 的隐式类型的工作方式不同。
在这三个角度中,静态类型与动态类型对于语言的特征有最大的影响,因此接下来我将重点解释这两种策略各自的优点。
静态类型的优点
在静态类型语言中,程序员(通过声明或根据约定)或编译器(根据结构和语法线索)将一种类型指定给一个变量,然后那个类型就不会改变。静态类型通常需要额外的成本,因为静态类型语言(例如 Java 语言)通常是显式类型的。这意味着必须声明所有的变量,然后编译代码。成本也伴随着收益:早期的错误检测。静态类型在最基层为编译器提供多得多的信息。更多信息所带来的好处就是,可以更早地捕捉到某些类型的错误,而动态类型语言只有到运行时才能检测到这些错误。如果您一直等到运行时才捕捉这些 bug,那么其中一些将进入生产环境。也许这正是动态类型语言受到最多指责的一个方面。
另一种观点则认为现代软件开发团队通常会运行自动测试,动态语言的支持者声称,即使是最简单的自动测试也可以捕捉到大多数的类型错误。而动态语言的支持者所能提供的对编译时错误检测不利的最好论据是,早期检测所带来的好处相对于成本来说是得不偿失的,因为不管是否使用动态类型,最终都要进行测试。
一种有趣的折中方法是在静态类型语言中使用隐式类型,从而减少类型的成本。开放源代码语言 Objective Caml (OCaml) 是静态类型语言 Lisp 的衍生物,它既能提供很好的性能,又不会牺牲生产率。OCaml 使用隐式类型,因此可以编写下面这样的采用静态类型的代码:
|
OCaml 返回:
|
根据表达式中的语法线索,OCaml 推断出 x
的类型。4
是 int
型,7
也是 int
型,因此 x
也必定是 int
型。隐式类型语言可以拥有 Java 语言所具有的所有类型安全性,甚至更多。不同之处在于您需要提供的信息量,以及在阅读程序时可用的信息量。很多喜欢静态类型的人更偏爱隐式类型。他们宁愿让编译器来做这种事情,而不愿意被迫重复地在代码中输入变量的类型。
隐式类型系统一个较大的优点是,不需要为函数的参数声明类型,因为编译器会从传入的值推断出参数的类型。因此同一个方法可以有多种用途。
|
并不是只有编译器才能利用静态类型所提供的附加信息。IDE 可以通过静态类型为重构提供更好的支持。几年前,一种革命性的思想改变了开发环境的工作方式。在 IDEA 和 Eclipse 中,您的代码看上去像一个文本视图,但是开发环境实际上正在编辑 Abstract Syntax Tree (AST)。因此,当需要重新命名一个方法或者类的时候,开发环境很容易通过在 AST 中精确定位找到方法或类被引用的每个地方。如今,如果没有通过静态类型简化的优秀的重构,我们很难想像用 Java 语言编程。在我探索 Ruby 的时候,相对于其他任何工具或特性,我更怀念 IDEA。
静态类型还有其他一些优点,在这里我不会详细描述。静态类型可以提供更好的安全性,而且显然还可以提高代码的可读性。静态类型还可以提供更多的信息,使得编译器更容易进行优化,从而提高性能。但是静态类型赢得开发人员青睐的最大原因是更容易检测错误,而且有更多可用的工具。
动态类型的优点
Ruby 专家 Dave Thomas 将动态类型称作duck typing(见参考资料),这有两层意思。第一层意思是说,这种语言不真正实现类型 —— 它利用鸭子理论 解决这个问题。第二层意思是说,如果什么东西走起来像鸭子,叫起来也像鸭子,那么它很可能就是一只鸭子。在编程语言的上下文中,duck typing 意味着如果一个对象对于某种类型的方法有反应,那么事实上就可以把它看作那种类型。这样的特性可以导致一些有趣的优化。
大多数偏爱动态类型的开发人员除了强调早期错误检测会带来不必要的成本外,还提到动态类型语言具有很好的可表达性和生产率。很简单,您通常可以用更少的关键词表达更多的思想。作为一名新的 Ruby 拥护者,我深信动态语言更能提高生产率,虽然我不能比常见的静态语言的支持者拿出更多具体的证据来。但是,从我开始编写更多的 Ruby 代码起,我就感觉到自己的生产率有了明显的提高。诚然,我仍然会看到静态类型的优点,尤其是在工具集方面,但是我逐渐认识到了静态类型的缺点。
当我开始用 Ruby 编写代码时,我受到的最大改变是产生和使用元编程结构的能力。如果您从头开始一直关注跨越边界 系列统的话,您就知道元编程,或者说编写用于编写程序的程序,是 Ruby on Rails 的一大推动力量,更一般地说,是特定于领域的语言的一大推动力量。用 Ruby 编程时,我通常会编写更大的构建块,或者用更大的块进行构建。我发现,与使用 Java 编程相比,我可以用更多类型的可重用块扩展我的程序。就像在 Java 编程中,您可以用新的类来扩展程序。还可以添加方法和数据到已有的类中,因为类是开放式的。您可以使用 mix-in(后面 运行时绑定 会讲到)来添加核心功能到已有的类中。还可以在任何时候根据需要改变一个对象的定义。我还是一名 Ruby 编程新手,这些功能用到的不多,但是当我真正开始使用它们时,结果令人大吃一惊。
例如,为了添加一个拦截器,只需重新命名一个方法,并为原有的方法创建一个新的实现。为了拦截 new
,可以编写以下代码:
|
您不需要 AspectJ 库、字节码增强或一大堆的库。您可以直接编写所需的拦截器。
动态类型在原始代码行方面也可以节省精力。由于动态语言几乎都是类型推断式的,所以您不需要花多大力气来表达基本思想。变量无需声明即可直接使用。您也不必表达参数类型的所有可能排列,只需输入一组名称。您的代码可以更加具有多态性 —— 任何对一种类型的方法有反应的对象都可以看作这种类型 —— 所以通常可以比其他语言更精简地表达思想。代码中的耦合也可以变得更松散。当您想改变某个东西的类型时,这种变化所波及的范围很有限,所以不需要在更多的地方作出相应的更改。
|
生产率提高的最后一个原因是减少了编译环节。很多动态类型语言是解释性的,所以在编写程序后可以立即看到变化。即使没有惯用的调试器,在 Ruby 中探索库和应用程序代码的行为也更为容易,因为您可以打开一个解释器,通常可以直接在调试会话中打开(我在 本系列的上一篇文章 中就展示了这一点),然后随意探索。
但是……
然而,编译不只是支持静态类型。静态类型的支持者还认为可以获得更好的性能。很多静态语言,例如 Java 代码、C 和 C++,都被称作系统语言,因为它们是构建操作系统、设备驱动程序和其他高性能系统代码的最常用的语言。这又经常导致动态语言的支持者指责静态语言总是太低级,用它们来编写应用程序生产率很低 —— 但那是一种很狭隘的观点。OCaml 语言是一种很高级的语言,支持面向对象程序设计、函数式程序设计(如 Lisp 或 Erlang)或传统的结构化程序设计。其类型模型是静态的,很多人说它的性能甚至比 C++ 的性能还好(参见 参考资料)。使用 OCaml 时,静态类型导致的开销很小,因为这种语言是类型推断式的。虽然付出了这一点成本,但可以得到非常好的性能,编译时类型检查,以及一个非常高级的语言。即使是 duck typing 最顽固的支持者也不得不承认那些优点。
|
Java 语言中的类型限制
Java 开发人员充分利用静态类型。他们有最好的开发工具,这些工具带有代码完成和重构等功能,这些都倾向于静态类型。现在开始利用测试优先开发的很多 Java 程序员获得了更大的稳定性,因为编译器可以捕捉与类型相关的 bug。新的类型特性,例如泛型,增强了类型模型,并为编译器提供更多的信息。但 Java 开发人员常常对动态类型的优点一无所知。
运行时绑定
动态类型的灵活性比您想像的更重要。在某些方面,Java 开发人员试图通过使用更多的 XML(这样可以推迟到运行时进行绑定)和字符串(这样可以表示很多不同的类型)来突破静态类型的限制。Ruby 中的配置通常采用 Ruby 代码的形式,而 Java 编程中的配置通常采用 XML 的形式。考虑 Spring 框架(参见 参考资料):为了配置一个一般的 Spring bean,您使用 XML。您必须提供一个有效的 Java 类名,并为每个变量设置属性。例如,持久引擎(如 Hibernate)需要一个会话工厂(参见 参考资料)。用 Java 语法配置一个数据访问对象很轻松:
|
问题是,这行代码是在编译时绑定的,这就太静态了。为了测试,您常常需要用其他东西,例如一个模拟的数据访问对象来替换会话工厂或数据访问对象。所以,您不必像前面那样硬编码这个例子,而是使用一个 Spring 之类的框架,以 XML 来配置项目,如下所示(摘自名为 petclinic 的 Spring Framework 例子):
|
Spring 框架是目前 Java 社区中最重要、最有影响力的框架之一,因为它使您可以延迟绑定,并使系统主要元素之间的耦合性更为松散。而且,您不需要关心继承就可以去耦。在 Java 编程中,尤其是在编写越来越多的 POJO(plain old Java object)的时候,使用继承时必须特别小心,因为在 Java 语言中只有一次这样的机会。
在动态语言,例如 Ruby 中,解决方案就截然不同。首先,我倾向于使用一个 mix-in 来实现持久性。所有关联只在 mix-in 中发现一次。可以把一个 mix-in 想像成一个接口,其背后有一个实现。换句话说,通过 mix-in,可以添加多个功能到同一个对象中,而不必使用多重继承。实际上,Active Record 通过继承一个公共基类来解决这个问题,这个公共基类混合了多种功能:
|
在 Ruby 中,您不必关心继承,因为使用开放的类(允许动态添加功能)和模块(允许混入其他功能),您可以随意添加更多的功能到对象中。那么紧密耦合呢?如果您想按 Java 的方式实现该类,那么可以看到:
|
initialize()
方法中的代码看上去像一开始的属于禁忌的 Java 版本,因为它在编译时将数据访问对象绑定到会话工厂。但这是一种动态类型语言,所以不必把自己关在一个小天地里。为了测试,总可以动态地改变类的定义。您可以在之后打开已有的类:
|
|
结束语
从某种意义上讲,作为某种编程语言的用户,您就是那种语言的类型策略的奴隶。而作为一名 Java 程序员,您应该尽量用一种拥护类型的方式编写 Java 代码。最大限度地利用类型,并依靠社区来通过框架获得更好的元编程支持,而不是自己进行元编程,这些都是发挥自身优势的好方法。有很多 Java 框架都支持用于持久性(Hibernate 和 JDO)、事务(Spring 和 EJB)、模型-视图-控制器(WebFlow 和 RIFE)以及编程模型(AspectJ)的元编程。
但是有时候需要放弃您所选择的语言的类型,不管您是在编写需要附加描述以获得更好可读性的代码,还是试图延迟类型绑定,都可以这样。Java 语言非常强大,您可以利用很多现成的项目:
您还有一个选择。通过理解其他语言中的类型策略,可以识别不适合 Java 策略的问题。当需要访问 Java 平台 而不是 Java 语言 时,可以使用其他语言的 JVM 实现(参见 参考资料)。
在本系列的下一篇文章中,您将看到 Ruby on Rails 中的测试。您可以看到 Rails 与 Java 语言共有的一些思想,有些方面 Rails 占优,而在某些方面 Java 又具有明显优势。到那时,请继续关注 “跨越边界” 的后续文章。