GEP-8
摘要:静态类型检查
此 GEP 在语言中引入了一项称为静态类型检查的新功能。对于来自静态类型语言(例如 Java)的开发者来说,发现 Groovy 编译器在编译时不会报错,这通常令人不安
-
当对不同类型进行赋值时
-
当方法不存在时
-
当属性或变量不存在时
-
当返回的对象类型与方法签名不匹配时
-
…
所有这些都是静默的,因为 Groovy 语言的动态特性使得此类代码完全有效。但是,在某些情况下,开发者可能希望 Groovy 像静态类型语言一样,让编译器提示此类“错误”。为此,Groovy 必须引入静态类型检查。
原理:静态类型检查与静态编译
重要的是要区分静态类型检查和静态编译。此 GEP 的目标是提供一个选项来启用静态类型检查 (STC)。如果启用 STC,编译器将更加详细(您也会看到“grumpy”一词),但最终,生成的字节码和运行时行为将与您未启用此模式时完全相同。这与 Groovy++ 等其他编译器有很大区别,Groovy++ 将执行 STC,然后生成不同的字节码,从而产生不同的运行时语义。此 GEP 的范围仅限于静态类型检查器,因此应仅将其视为允许开发者编写静态检查代码的功能,这是一种优雅的方式,例如,利用 Groovy 语法来减少 Java 代码的冗长性,同时仍然获得强类型检查的代码。最终,IDE 可以支持 STC 模式,并向开发者提供信息。
实现细节
开发分支
从 Groovy 2.0-beta-2 开始,代码已合并到主分支。但是,如果对类型检查器进行大量开发,建议在 grumpy 分支上进行。它添加了一个名为 TypeChecked 的 AST 转换。如果设置,则 AST 转换将执行类型推断,并将类型信息存储在 AST 节点的元数据中。最终,如果发现错误,它将通过专门的 addStaticTypeError 方法将错误添加到编译器,该方法基本上与传统的 addError 方法相同,但会在消息前添加“静态类型检查”消息。这样做是为了帮助开发者确定他们看到的错误是“纯 Groovy”错误,还是 STC 模式抛出的错误。
StaticTypeCheckingTestCase 类
必须测试静态类型检查行为。由于要进行大量的检查,一个基础测试类提供了一个测试此模式的框架。静态类型检查的单元测试应覆盖此类。
做出的决定
关于本节
本节的目标是提供代码示例,这些示例演示了 STC 转换将在哪些情况下实际抱怨以及预期的错误消息,并作为将来 STC 文档的基础。本节可能不是最新的,您应该始终查看 src/test/groovy/transform/stc
目录中找到的 STC 单元测试。
功能 | 示例 | 行为 | 状态 |
---|---|---|---|
方法不存在 |
def method() { ... } methode() // typo |
抱怨未定义的方法 |
已实现 |
属性不存在 |
class A { int x } A obj = new A() a.y = 2 |
抱怨未定义的属性“y” |
已实现 |
赋值类型检查 |
int x = 2 x = 'String' |
将字符串赋值给整数是禁止的 |
已实现 |
不兼容的二元表达式 |
1 + 'string' |
检查二元表达式的参数是否兼容(此处,没有可用的“加号”方法 |
已实现 |
可能存在精度损失(1/2) |
long myLong = ... int myInt = myLong |
抱怨可能存在精度损失 |
已实现 |
可能存在精度损失(2/2) |
int myInt = 2L |
不会抱怨,因为“2”可以表示为整数 |
已实现 |
数组组件 |
String[] arr = { '1', '2', '3' } arr[2] = 200 |
无法在类型为 String[] 的数组中分配整数值 |
已实现 |
方法返回类型检查 |
String method() { 'Hello' } int x = method() // return types don't match |
确保赋值与方法返回类型兼容 |
已实现 |
显式返回类型检查 |
int method() { return 'String' // return type is not compatible } |
确保返回值与声明的返回类型兼容 |
已实现 |
隐式返回类型检查 |
int method() { 'String' // return type is not compatible } |
确保返回值与声明的返回类型兼容 |
已实现 |
隐式 toString() |
String method(String name) { StringBuilder sb = new StringBuilder() sb 'Hi ' << name << '!' } |
隐式调用 toString() |
已实现 |
基本类型推断 |
def str = 'My string' str.toUpperCase() // type of 'str' is inferred |
方法调用以及属性访问根据推断的类型进行检查 |
已实现 |
基本流程分析 |
def o ... if (o instanceof String) { o.toUpperCase() // no explicit cast required } |
当类型可以从之前的 instanceof 检查推断出来时,不需要进行强制转换 |
已实现 |
DefaultGroovyMethods 支持 |
'123'.toInteger() // toInteger() is a Groovy extension method |
方法调用可以解析到 Groovy 扩展方法 |
已实现 |
with |
class A { int x } def a = new A() a.with { x = 1 } |
方法调用可以解析到 Groovy 扩展方法 |
已实现 |
类别 |
use (MyStringCategory) { 'string'.methodInStringCategory() } |
编译器应该知道扩展方法是在类别中找到的 |
N/A(由于类别支持本质上是动态的,因此支持将受到限制) |
Groovy 列表构造函数 |
Dimension d = [100, 200] |
类型检查参数和参数数量 |
已实现 |
Groovy 映射构造函数 |
Bean myBean = [x: 100, y: 200] |
类型检查属性,并检查不正确的属性名称 |
已实现 |
闭包参数类型 |
def closure = { int x, int y -> x + y } closure(1, 2) closure('1', '2') // complains |
调用闭包时对参数进行类型检查 |
已实现 |
闭包返回类型推断 |
def closure = { int x, int y -> x + y } int sum = closure(1, 2) |
闭包返回类型可以从代码块推断出来 |
已实现 |
方法返回类型推断 |
def method(int x, int y) { x + y } int sum = method(1, 2) |
如果方法本身用 @TypeChecked 注解(或类用 @TypeChecked 注解),则可以从方法推断出返回类型 |
已实现 |
多重赋值 |
def (x, y) = [1, 2] |
在内联声明的情况下,类型检查参数 |
已实现 |
来自变量的多重赋值 |
def (x, y) = list |
在内联声明的情况下,类型检查参数 |
已实现 |
泛型 |
List<String> list = [] List<String> list = ['a', 'b', 'c'] List<String> list = [1, 2, 3] // should throw error |
泛型参数的类型检查 |
已实现 |
展开运算符 |
def list = ['a', 'b', 'c'] list*.toUpperCase() |
根据组件类型进行类型检查 |
已实现 |
闭包共享变量 |
def x = new Date() def cl = { x = 'hello' } cl() x.toUpperCase() // should throw an error because the toUpperCase() method doesn't belong to both Date and String classes |
类型检查闭包共享变量的赋值。类型检查器需要执行两遍验证,以检查对闭包共享变量的方法调用是否属于所有赋值类型的最低上界。 |
已实现 |
开放式讨论
闭包参数类型推断
在检查器的当前版本中,像
['a','b','c'].collect { it.toUpperCase() }
这样的惯用结构无法被正确识别。您必须在闭包中显式设置“it”参数的类型。这是因为闭包的预期参数类型在编译时是未知的。关于如何将此类型信息添加到源代码中,以便推断引擎能够正确处理它们,存在一些讨论。闭包参数类型推断的实现需要更改方法签名。它可能不属于类型检查器的初始版本。
统一类型
例如,在 x instanceof A || x instanceof B
的情况下,A 和 B 不相关,我们仍然可以创建一种人工的联合类型,其中包含 A 和 B 中的所有内容,以允许这些类型的方法调用。另一种选择是只允许来自 Object 的方法,这不太有趣。这种类型也可以用于多重捕获,以确保方法调用仅在多重捕获的每个异常上都存在时才有效。在当前实现(2011-10-14)中,多重捕获已经在 @TypeChecked 将进行检查的地方扩展。这意味着它实际上已经代表了一种联合类型,因为相同的代码存在于每个 catch 块中,因此如果方法在每个类型上都不可用,方法调用将失败。因此,建议的行为是将 instanceof 案例与多重捕获对齐。
参考资料和有用链接
-
GEP-8:静态类型检查(带有评论的网络存档链接)
-
Groovy 静态类型检查器:状态更新(网络存档)
邮件列表讨论
-
groovy-user:在赋值时该怎么办?关于当 STC 在赋值时检测到潜在错误(例如,在隐式数字强制转换时可能存在精度损失)时的预期行为的讨论
更新历史
- 8 (2012-02-21)
-
从 Codehaus wiki 中提取的版本
- 9 (2018-10-16)
-
大量微调