Groovy 2.0 新特性之:静态类型检查 已翻译 100%

王振威 投递于 2012/11/27 11:50 (共 31 段, 翻译完成于 03-02)
阅读 1477
收藏 1
0
加载中

The newly released Groovy 2.0 brings key static features to the language with static type checking and static compilation, adopts JDK 7 related improvements with Project Coin syntax enhancements and the support of the new "invoke dynamic" JVM instruction, and becomes more modular than before. In this article, we’re going to look into those new features in more detail.


A "static theme" for a dynamic language

已有 1 人翻译此段
我来翻译

Static type checking

Groovy, by nature, is and will always be a dynamic language. However, Groovy is often used as a "Java scripting language", or as a "better Java" (ie. a Java with less boilerplate and more power features). A lot of Java developers actually use and embed Groovy in their Java applications as an extension language, to author more expressive business rules, to further customize the application for different customers, etc. For such Java-oriented use cases, developers don't need all the dynamic capabilities offered by the language, and they usually expect the same kind of feedback from the Groovy compiler as the one given by javac. In particular, they want to get compilation errors (rather than runtime errors) for things like typos on variable or method names, incorrect type assignments and the like. That's why Groovy 2 features static type checking support.

Spotting obvious typos

The static type checker is built using Groovy’s existing powerful AST (Abstract Syntax Tree) transformation mechanisms but for those not familiar with these mechanisms you can think of it as an optional compiler plugin triggered through an annotation. Being an optional feature, you are not forced to use it if you don’t need it. To trigger static type checking, just use the@TypeCheckedannotation on a method or on a class to turn on checking at your desired level of granularity. Let’s see that in action with a first example:

import groovy.transform.TypeChecked

void someMethod() {}

@TypeChecked
void test() {
    // compilation error:
    // cannot find matching method sommeeMethod()
    sommeeMethod()

    def name = "Marion"

    // compilation error:
    // the variable naaammme is undeclared
    println naaammme
}

We annotated thetest()method with the@TypeCheckedannotation, which instructs the Groovy compiler to run the static type checking for that particular method at compilation time. We’re trying to callsomeMethod()with some obvious typos, and to print the name variable again with another typo, and the compiler will throw two compilation errors because respectively, the method and variable are not found or declared.

已有 1 人翻译此段
我来翻译

Check your assignments and return values

The static type checker also verifies that the return types and values of your assignments are coherent:

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 = ['a', 'b', '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"
}

In this example, the compiler will complain about the fact you cannot assign aDatein anintvariable, nor can you return aStringinstead of aDatevalue specified in the method signature. The compilation error from the middle of the script is also interesting, as not only does it complain of the wrong assignment, but also because it shows type inference at play, because the type checker, of course, knows thatletters[0]is of typeString, because we’re dealing with an array ofStrings.

已有 1 人翻译此段
我来翻译

More on type inference

Since we’re mentioning type inference, let’s have a look at some other occurrences of it. We mentioned the type checker tracks the return types and values:

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
    }
}

Given a method returning a value of primitive typeint, the type checker is able to also check the values returned from different constructs likeif / elsebranches,try / catchblocks orswitch / caseblocks. Here, in our example, one branch of theif / elseblocks tries to return aStringvalue instead of a primitiveint, and the compiler complains about it.

已有 1 人翻译此段
我来翻译

Common type conversions still allowed

The static type checker, however, won’t complain for certain automatic type conversions that Groovy supports. For instance, for method signatures returningString, booleanorClass, Groovy converts return values to these types automatically:

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
The static type checker is also clever enough to do type inference:



import groovy.transform.TypeChecked

@TypeChecked
void method() {
    def name = " Guillaume "

    // 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 
    }
}

Although thenamevariable was defined withdef, the type checker understands it is of typeString. Then, when this variable is used in the interpolated string, it knows it can callString’s toUpperCase()method, or thetrim()method later one, which is a method added by the Groovy Development Kit decorating theStringclass. Last, when iterating over the elements of an array of primitiveints, it also understands that an element of that array is obviously anint.

已有 1 人翻译此段
我来翻译

Mixing dynamic features and statically typed methods

An important aspect to have in mind is that using the static type checking facility restricts what you are allowed to use in Groovy. Most runtime dynamic features are not allowed, as they can’t be statically type checked at compilation time. So adding a new method at runtime through the type’s metaclasses is not allowed. But when you need to use some particular dynamic feature, like Groovy’s builders, you can opt out of static type checking should you wish to.

The@TypeCheckedannotation can be put at the class level or at the method level. So if you want to have a whole class type checked, put the annotation on the class, and if you want only a few methods type checked, put the annotation on just those methods. Also, if you want to have everything type checked, except a specific method, you can annotate the latter with@TypeChecked(TypeCheckingMode.SKIP)- or@TypeChecked(SKIP)for short, if you statically import the associated enum. Let’s illustrate the situation with the following script, where thegreeting()method is type checked, whereas thegenerateMarkup()method is not:

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>")

已有 1 人翻译此段
我来翻译
Type inference and instanceof checks

Current production releases of Java don’t support general type inference; hence we find today many places where code is often quite verbose and cluttered with boilerplate constructs. This obscures the intent of the code and without the support of powerful IDEs is also harder to write. This is the case withinstanceofchecks: You often check the class of a value with instanceof inside anifcondition, and afterwards in theifblock, you must still use casts to be able to use methods of the value at hand. In plain Groovy, as well as in the new static type checking mode, you can completely get rid of those casts.


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'


In the above example, the static type checker knows that the val parameter is of typeStringinside theifblock, and of typeNumberin the else if block, without requiring any cast.

已有 1 人翻译此段
我来翻译

Lowest Upper Bound

The static type checker goes a bit further in terms of type inference in the sense that it has a more granular understanding of the type of your objects. Consider the following code:

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]
}

In this example, we return, intuitively, a list of numbers: anIntegerand aBigDecimal. But the static type checker computes what we call a "lowest upper bound", which is actually a list of numbers which are also serializable and comparable. It’s not possible to denote that type with the standard Java type notation, but if we had some kind of intersection operator like an ampersand, it could look likeList<Number & Serializable & Comparable>.

已有 1 人翻译此段
我来翻译

Flow typing

Although this is not really recommended as a good practice, sometimes developers use the same untyped variable to store values of different types. Look at this method body:

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!
}

Thevarvariable is initialized with anint. Then, aStringis assigned. The "flow typing" algorithm follows the flow of assignment and understands that the variable now holds aString, so the static type checker will be happy with thetoInteger()method added by Groovy on top ofString. Next, a number is put back in the var variable, but then, when callingtoUpperCase(), the type checker will throw a compilation error, as there’s notoUpperCase()method onInteger.

There are some special cases for the flow typing algorithm when a variable is shared with a closure which are interesting. What happens when a local variable is referenced in a closure inside a method where that variable is defined? Let’s have a look at this example:

import groovy.transform.TypeChecked

@TypeChecked test() {
    def var = "abc"
    def cl = {
        if (new Random().nextBoolean()) var = new Date()
    }
    cl()
    var.toUpperCase() // compilation error!
}
已有 1 人翻译此段
我来翻译
Thevarlocal variable is assigned aString, but then,varmight be assigned aDateif some random value is true. Typically, it’s only at runtime that we really know if the condition in the if statement of the closure is made or not. Hence, at compile-time, there’s no chance the compiler can know ifvarnow contains aStringor aDate. That’s why the compiler will actually complain about thetoUpperCase()call, as it is not able to infer that the variable contains aStringor not. This example is certainly a bit contrived, but there are some more interesting cases:



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()
}

In thetest()method above,varis assigned an instance ofA, and then an instance ofBin the closure which is call afterwards, so we can at least infer that var is of typeA.

All those checks added to the Groovy compiler are done at compile-time, but the generated bytecode is still the same dynamic code as usual - no changes in behavior at all.

Since the compiler now knows a lot more about your program in terms of types, it opens up some interesting possibilities: what about compiling that type checked code statically? The obvious advantage will be that the generated bytecode will more closely resemble the bytecode created by the javac compiler itself, making statically compiled Groovy code as fast as plain Java, among other advantages. In the next section, we’ll learn more about Groovy’s static compilation.

已有 1 人翻译此段
我来翻译
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(1)

enixyu
enixyu
mark,, 要玩玩groovy了。。。
返回顶部
顶部