Groovy 2.0新特性之:靜態(tài)類型檢查
Groovy 2.0 剛剛發(fā)布,其中一項最大的改進就是支持靜態(tài)類型檢查。今天我們將對這個新特性進行全方位的介紹。
靜態(tài)類型檢查
Groovy 天生就是一個動態(tài)編程語言,它經(jīng)常被當作是 Java 腳本語言,或者是“更好的 Java”。很多 Java 開發(fā)者經(jīng)常將 Groovy 嵌入到 Java 程序中做為擴展語言來使用,更簡單的描述業(yè)務(wù)規(guī)則,將來為不同的客戶定制應(yīng)用等等。對這樣一個面向 Java 的用例,開發(fā)者不需要語言提供的所有動態(tài)特性,他們經(jīng)常希望 Groovy 也提供一個類似 javac 的編譯器,例如在發(fā)生一些錯誤的變量和方法名錯誤或者錯誤的類型賦值時就可以在編譯時就知道錯誤,而不是運行時才報錯。這就是為什么 Groovy 2.0 提供了靜態(tài)類型檢查功能的原因。
發(fā)現(xiàn)明顯的錯別字
靜態(tài)類型檢測器使用了 Groovy 已有強大的 AST (抽象語法樹) 轉(zhuǎn)換機制,如果你對這個機制不熟悉,你就把它當作一個可選的通過注解進行觸發(fā)的編譯器插件。這是一個可選的特性,可用可不用。要觸發(fā)靜態(tài)類型檢查,只需要在方法上使用@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 編譯器在編譯期間運行靜態(tài)類型檢查來檢查指定的方法。當我們試圖用明顯錯誤的方法來調(diào)用 someMethod() 時,編譯器將會拋出兩個編譯錯誤信息表明方法和變量為定義
檢查賦值和返回值
靜態(tài)類型檢查還能驗證返回值和變量賦值是否匹配:
- 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,因為方法已經(jīng)要求是返回 Date 類型數(shù)據(jù)。代碼中間的編譯錯誤信息也很有意思,不僅是說明了錯誤的賦值,還給出了類型推斷,因為類型檢測器知道 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 類型值后,類型檢查器將會檢查各種條件判斷分支的結(jié)構(gòu),包括 if/elese、try/catch、switch/case 等。在上面的例子中,如果 if 分支中返回字符串而不是 int,編譯器就會報錯。
自動類型轉(zhuǎn)換
靜態(tài)類型檢查器并不會對 Groovy 支持的自動類型轉(zhuǎn)換報告錯誤,例如對于返回 String, boolean 或 Class 的方法,Groovy 會自動將返回值轉(zhuǎn)成相應(yīng)的類型:
- 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
而且靜態(tài)類型檢查器在類型推斷方面也足夠聰明:
- 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. 因此當調(diào)用 ${name.toUpperCase()} 時,編譯器知道在調(diào)用 String 的 toUpperCase() 方法和下面的 trim() 方法。當對 int 數(shù)組進行迭代時,它也能理解數(shù)組的元素類型是 int.
混合動態(tài)特性和靜態(tài)類型的方法
你必須牢記于心是:靜態(tài)類型檢查限制了你可以在 Groovy 使用的方法。大部分運行時動態(tài)特性是不被允許的,因為他們無法在編譯時進行類型檢查。例如不允許在運行時通過類型的元數(shù)據(jù)類(metaclasses)來添加新方法。但當你需要使用一些例如 Groovy 的 builders 這樣的動態(tài)特性時,如果你愿意,你還是可以選擇靜態(tài)類型檢查。
@TypeChecked 注解可放在方法級別或者是類級別使用。如果你想對整個類進行類型檢查,直接在類級別上放置這個注解即可,否則就在某些方法上進行注解。你也可以使用 @TypeChecked(TypeCheckingMode.SKIP) 或者是 @TypeChecked(SKIP) 來指定整個類進行類型檢查除了某個方法。使用 @TypeChecked(SKIP) 必須靜態(tài)引入對應(yīng)的枚舉類型。下面代碼可以用來演示這個特性,其中 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 并不支持一般的類型推斷,導致今天很多地方的代碼往往是相當冗長,而且樣板結(jié)構(gòu)混亂。這掩蓋了代碼的實際用途,而且如果沒有強大的 IDE 支持的話代碼會很難寫。于是就有了 instanceof 檢查:你經(jīng)常會在 if 條件判斷語句中使用 instanceof 判斷。而在 if 語句結(jié)束后,你還是必須手工對變量進行強行類型轉(zhuǎn)換。而有了 Groovy 全新的類型檢查模式,你可以完全避免這種情況出現(xiàn):
- 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'
上述例子中,靜態(tài)類型檢查器知道 val 參數(shù)在 if 塊中是 String 類型,而在 else if 塊中是 Number 類型,無需再做任何手工類型轉(zhuǎn)換。
最低上限 Lowest Upper Bound
靜態(tài)類型檢測器比一般理解的對象類型診斷要更深入一些,請看如下代碼:
- 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]
- }
在這個例子中,我們返回了數(shù)值列表,包括 Integer 和 BigDecimal. 但靜態(tài)類型檢查器計算了一個最低的上限,實際上是一組可序列化(Serializable)和可比較(Comparable)的數(shù)值。而 Java 是不可能表示這種類型的,但如果我們使用一些交集運算,那看起來就應(yīng)該是 List<Number & Serializable & Comparable>.
不同對象類型的變量 Flow typing
雖然這可能不是一個好的方法,但有時候開發(fā)者會使用一些無類型的變量來存儲不同類型的值,例如:
- 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 當前是字符串類型,這樣調(diào)用 Groovy 為 String 增加的 toInteger() 方法就沒問題。緊接著又賦值整數(shù)給 var 變量,但現(xiàn)在如果再次調(diào)用 toUpperCase() 就會報出編譯錯誤。
還有另外一些關(guān)于 “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 本地變量先賦值了一個字符串,但是在閉包中會在一些隨機的情況下被賦值為日期類型數(shù)值。一般情況下這種只能在運行時才能報錯,因為這種錯誤是隨機發(fā)生的。因此在編譯時,編譯器是沒有機會知道 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 的實例,然后調(diào)用這個閉包方法,因此我們至少可以診斷 var 最后的類型是 A。
Groovy 編譯器的所有這些檢查都是在編譯時就完成了,但生成的字節(jié)碼還是跟一些動態(tài)代碼一樣,在行為上沒有任何改變。