Groovy 2.0新特性之:静态类型检查
Groovy 2.0 刚刚发布,其中一项最大的改进就是支持静态类型检查。今天我们将对这个新特性进行全方位的介绍。
静态类型检查
Groovy 天生就是一个动态编程语言,它经常被当作是 Java 脚本语言,或者是“更好的 Java”。很多 Java 开发者经常将 Groovy 嵌入到 Java 程序中做为扩展语言来使用,更简单的描述业务规则,将来为不同的客户定制应用等等。对这样一个面向 Java 的用例,开发者不需要语言提供的所有动态特性,他们经常希望 Groovy 也提供一个类似 javac 的编译器,例如在发生一些错误的变量和方法名错误或者错误的类型赋值时就可以在编译时就知道错误,而不是运行时才报错。这就是为什么 Groovy 2.0 提供了静态类型检查功能的原因。
发现明显的错别字
静态类型检测器使用了 Groovy 已有强大的 AST (抽象语法树) 转换机制,如果你对这个机制不熟悉,你就把它当作一个可选的通过注解进行触发的编译器插件。这是一个可选的特性,可用可不用。要触发静态类型检查,只需要在方法上使用@TypeChecked 注解即可。让我们来看一个简单的例子:
import groovy.transform.TypeChecked void someMethod() {} @TypeChecked void test() { // compilation error: // cannot find matching method sommeeMethod() sommeeMethod() def name = "oschina" // compilation error: // the variable naaammme is undeclared println naaammme }
我们使用了 @TypeChecked 对 test() 方法进行注解,这让 Groovy 编译器在编译期间运行静态类型检查来检查指定的方法。当我们试图用明显错误的方法来调用 someMethod() 时,编译器将会抛出两个编译错误信息表明方法和变量为定义
检查赋值和返回值
静态类型检查还能验证返回值和变量赋值是否匹配:
import groovy.transform.TypeChecked @TypeChecked Date test() { // compilation error: // cannot assign value of Date // to variable of type int int object = new Date() String[] letters = ['o', 's', 'c'] // compilation error: // cannot assign value of type String // to variable of type Date Date aDateVariable = letters[0] // compilation error: // cannot return value of type String // on method returning type Date return "today" }
在这个例子中,编译器将告诉你不能将 Date 值赋值个 int 变量,你也不能返回一个 String,因为方法已经要求是返回 Date 类型数据。代码中间的编译错误信息也很有意思,不仅是说明了错误的赋值,还给出了类型推断,因为类型检测器知道 letters[0] 的类型是 String。
类型推断 type inference
因为提到了类型推断,让我们来看看其他的一些情况,我们说过类型检测器会检查返回类型和值:
import groovy.transform.TypeChecked @TypeChecked int method() { if (true) { // compilation error: // cannot return value of type String // on method returning type int 'String' } else { 42 } }
指定了方法必须返回 int 类型值后,类型检查器将会检查各种条件判断分支的结构,包括 if/elese、try/catch、switch/case 等。在上面的例子中,如果 if 分支中返回字符串而不是 int,编译器就会报错。
自动类型转换
静态类型检查器并不会对 Groovy 支持的自动类型转换报告错误,例如对于返回 String, boolean 或 Class 的方法,Groovy 会自动将返回值转成相应的类型:
import groovy.transform.TypeChecked @TypeChecked boolean booleanMethod() { "non empty strings are evaluated to true" } assert booleanMethod() == true @TypeChecked String stringMethod() { // StringBuilder converted to String calling toString() new StringBuilder() << "non empty string" } assert stringMethod() instanceof String @TypeChecked Class classMethod() { // the java.util.List class will be returned "java.util.List" } assert classMethod() == List
而且静态类型检查器在类型推断方面也足够聪明:
import groovy.transform.TypeChecked @TypeChecked void method() { def name = " oschina.net " // String type inferred (even inside GString) println "NAME = ${name.toUpperCase()}" // Groovy GDK method support // (GDK operator overloading too) println name.trim() int[] numbers = [1, 2, 3] // Element n is an int for (int n in numbers) { println } }
虽然变量 name 使用 def 进行定义,但类型检查器知道它的类型是 String. 因此当调用 ${name.toUpperCase()} 时,编译器知道在调用 String 的 toUpperCase() 方法和下面的 trim() 方法。当对 int 数组进行迭代时,它也能理解数组的元素类型是 int.
混合动态特性和静态类型的方法
你必须牢记于心是:静态类型检查限制了你可以在 Groovy 使用的方法。大部分运行时动态特性是不被允许的,因为他们无法在编译时进行类型检查。例如不允许在运行时通过类型的元数据类(metaclasses)来添加新方法。但当你需要使用一些例如 Groovy 的 builders 这样的动态特性时,如果你愿意,你还是可以选择静态类型检查。
@TypeChecked 注解可放在方法级别或者是类级别使用。如果你想对整个类进行类型检查,直接在类级别上放置这个注解即可,否则就在某些方法上进行注解。你也可以使用 @TypeChecked(TypeCheckingMode.SKIP) 或者是 @TypeChecked(SKIP) 来指定整个类进行类型检查除了某个方法。使用 @TypeChecked(SKIP) 必须静态引入对应的枚举类型。下面代码可以用来演示这个特性,其中 greeting() 方法是需要检查的,而 generateMarkup() 方法则不用:
import groovy.transform.TypeChecked import groovy.xml.MarkupBuilder // this method and its code are type checked @TypeChecked String greeting(String name) { generateMarkup(name.toUpperCase()) } // this method isn't type checked // and you can use dynamic features like the markup builder String generateMarkup(String name) { def sw =new StringWriter() new MarkupBuilder(sw).html { body { div name } } sw.toString() } assert greeting("Cédric").contains("<div>CÉDRIC</div>")
类型推断和 instanceof 检查
目前的 Java 并不支持一般的类型推断,导致今天很多地方的代码往往是相当冗长,而且样板结构混乱。这掩盖了代码的实际用途,而且如果没有强大的 IDE 支持的话代码会很难写。于是就有了 instanceof 检查:你经常会在 if 条件判断语句中使用 instanceof 判断。而在 if 语句结束后,你还是必须手工对变量进行强行类型转换。而有了 Groovy 全新的类型检查模式,你可以完全避免这种情况出现:
import groovy.transform.TypeChecked import groovy.xml.MarkupBuilder @TypeChecked String test(Object val) { if (val instanceof String) { // unlike Java: // return ((String)val).toUpperCase() val.toUpperCase() } else if (val instanceof Number) { // unlike Java: // return ((Number)val).intValue().multiply(2) val.intValue() * 2 } } assert test('abc') == 'ABC' assert test(123) == '246'
上述例子中,静态类型检查器知道 val 参数在 if 块中是 String 类型,而在 else if 块中是 Number 类型,无需再做任何手工类型转换。
最低上限 Lowest Upper Bound
静态类型检测器比一般理解的对象类型诊断要更深入一些,请看如下代码:
import groovy.transform.TypeChecked // inferred return type: // a list of numbers which are comparable and serializable @TypeChecked test() { // an integer and a BigDecimal return [1234, 3.14] }
在这个例子中,我们返回了数值列表,包括 Integer 和 BigDecimal. 但静态类型检查器计算了一个最低的上限,实际上是一组可序列化(Serializable)和可比较(Comparable)的数值。而 Java 是不可能表示这种类型的,但如果我们使用一些交集运算,那看起来就应该是 List<Number & Serializable & Comparable>.
不同对象类型的变量 Flow typing
虽然这可能不是一个好的方法,但有时候开发者会使用一些无类型的变量来存储不同类型的值,例如:
import groovy.transform.TypeChecked @TypeChecked test() { def var = 123 // inferred type is int var = "123" // assign var with a String println var.toInteger() // no problem, no need to cast var = 123 println var.toUpperCase() // error, var is int! }
上面代码中 var 变量一开始是 int 类型,后来又赋值了字符串,“flow typing”算法可以理解赋值的顺序,并指导 var 当前是字符串类型,这样调用 Groovy 为 String 增加的 toInteger() 方法就没问题。紧接着又赋值整数给 var 变量,但现在如果再次调用 toUpperCase() 就会报出编译错误。
还有另外一些关于 “flow typing” 算法的特殊情况,当某个变量在一个闭包中被共享该会是怎么样的一种情况呢?
import groovy.transform.TypeChecked @TypeChecked test() { def var = "abc" def cl = { if (new Random().nextBoolean()) var = new Date() } cl() var.toUpperCase() // compilation error! }
var 本地变量先赋值了一个字符串,但是在闭包中会在一些随机的情况下被赋值为日期类型数值。一般情况下这种只能在运行时才能报错,因为这种错误是随机发生的。因此在编译时,编译器是没有机会知道 var 变量是字符串还是日期,这就是为什么编译器无法得知错误的原因。尽管这个例子有点做作,但还有更有趣的情况:
import groovy.transform.TypeChecked class A { void foo() {} } class B extends A { void bar() {} } @TypeChecked test() { def var = new A() def cl = { var = new B() } cl() // var is at least an instance of A // so we are allowed to call method foo() var.foo() }
在 test() 方法中,var 先被赋值为 A 的实例,紧接着在闭包中被赋值为 B 的实例,然后调用这个闭包方法,因此我们至少可以诊断 var 最后的类型是 A。