Groovy 2.0 新特性之:靜態類型檢查

jopen 12年前發布 | 28K 次閱讀 Groovy Java開發

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
}

我們使用了 @TypeCheckedtest() 方法進行注解,這讓 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, booleanClass 的方法,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]
}

在這個例子中,我們返回了數值列表,包括 IntegerBigDecimal. 但靜態類型檢查器計算了一個最低的上限,實際上是一組可序列化(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。

Groovy 編譯器的所有這些檢查都是在編譯時就完成了,但生成的字節碼還是跟一些動態代碼一樣,在行為上沒有任何改變。

英文原文,OSCHINA原創翻譯

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!