風格指南

一位踏上 Groovy 冒險的 Java 開發人員心中總會掛念著 Java,並會逐漸學習 Groovy,一次學習一個功能,進而提高生產力並撰寫更多慣用的 Groovy 程式碼。本文件的目的是指導此類開發人員,教授一些常見的 Groovy 語法風格、新的運算子,以及新的功能,例如閉包等。本指南並不完整,僅作為快速入門和進一步指南部分的基礎,如果您有興趣為本文件做出貢獻並加以改進,歡迎您這麼做。

1. 沒有分號

當我們來自 C / C++ / C# / Java 背景時,我們已經習慣於分號,因此我們會將它們放在任何地方。更糟糕的是,Groovy 支援 99% 的 Java 語法,有時,將一些 Java 程式碼貼到您的 Groovy 程式中非常容易,以至於您最終會在任何地方看到大量分號。但是…​Groovy 中的分號是選用的,您可以省略它們,而且移除它們更符合慣例。

2. 回傳關鍵字為選用

在 Groovy 中,方法主體中評估的最後一個表達式可以傳回,而不需要 return 關鍵字。特別是對於簡短的方法和閉包,省略它以簡潔起見會更好

String toString() { return "a server" }
String toString() { "a server" }

但有時候,當你使用變數時,這看起來並不好,而且在兩列中看到它兩次

def props() {
    def m1 = [a: 1, b: 2]
    m2 = m1.findAll { k, v -> v % 2 == 0 }
    m2.c = 3
    m2
}

在這種情況下,在最後一個表達式之前放置換行符號或明確使用 return 可能會產生更好的可讀性。

對我來說,有時使用 return 關鍵字,有時不使用,這通常是品味問題。但通常,在閉包內部,我們更常省略它,例如。因此,即使關鍵字是可選的,如果你認為它會阻礙程式碼的可讀性,這絕不表示一定要不使用它。

不過,請注意。當使用以 def 關鍵字定義的方法而不是具體的具體類型時,你可能會驚訝地看到有時會傳回最後一個表達式。因此,通常較喜歡使用具體的回傳類型,例如 void 或類型。在上面的範例中,想像我們忘記將 m2 放為要傳回的最後一個陳述式,最後一個表達式將會是 m2.c = 3,這會傳回…​ 3,而不是你預期的映射。

if/elsetry/catch 的陳述式也可以傳回值,因為在這些陳述式中評估了「最後一個表達式」

def foo(n) {
    if(n == 1) {
        "Roshan"
    } else {
        "Dawrani"
    }
}

assert foo(1) == "Roshan"
assert foo(2) == "Dawrani"

3. Def 和類型

當我們討論 def 和類型時,我經常看到開發人員同時使用 def 和類型。但這裡的 def 是多餘的。因此,請做出選擇,使用 def 或類型。

因此,不要寫

def String name = "Guillaume"

而是

String name = "Guillaume"

在 Groovy 中使用 def 時,實際的類型持有者是 Object(因此,你可以將任何物件指定給使用 def 定義的變數,如果宣告的方法傳回 def,則傳回任何類型的物件)。

在定義具有未輸入參數的方法時,你可以使用 def,但不需要,因此我們傾向於省略它們。因此,不要

void doSomething(def param1, def param2) { }

而是

void doSomething(param1, param2) { }

但正如我們在本文檔的最後一節中提到的,通常最好輸入你的方法參數,以便幫助記錄你的程式碼,並幫助 IDE 進行程式碼完成,或利用 Groovy 的靜態類型檢查或靜態編譯功能。

def 多餘且應避免的另一個地方是在定義建構函數時

class MyClass {
    def MyClass() {}
}

相反,只需移除 def

class MyClass {
    MyClass() {}
}

4. 預設為 public

預設情況下,Groovy 將類別和方法視為 public。因此,您不必在每個公開的地方都使用 public 修飾詞。只有在非公開的情況下,您才應該放置可見度修飾詞。

因此,取代

public class Server {
    public String toString() { return "a server" }
}

偏好更簡潔的

class Server {
    String toString() { "a server" }
}

您可能會好奇「封裝範圍」可見度,以及 Groovy 允許省略「public」的事實表示此範圍預設情況下不受支援,但實際上有一個特殊的 Groovy 註解允許您使用該可見度

class Server {
    @PackageScope Cluster cluster
}

5. 省略括號

Groovy 允許您省略頂層表達式的括號,例如使用 println 指令

println "Hello"
method a, b

相較於

println("Hello")
method(a, b)

當閉包是方法呼叫的最後一個參數時,例如使用 Groovy 的 each{} 疊代機制時,您可以將閉包放在閉合括號之外,甚至省略括號

list.each( { println it } )
list.each(){ println it }
list.each  { println it }

始終偏好第三種形式,因為它更自然,因為一對空的括號只不過是無用的語法噪音!

在某些情況下需要括號,例如進行巢狀方法呼叫或呼叫沒有參數的方法時。

def foo(n) { n }
def bar() { 1 }

println foo 1 // won't work
def m = bar   // won't work

6. 類別作為一等公民

在 Groovy 中不需要 .class 字尾,有點像 Java 的 instanceof

例如

connection.doPost(BASE_URI + "/modify.hqu", params, ResourcesResponse.class)

使用我們將在下面介紹的 GString,並使用一等公民

connection.doPost("${BASE_URI}/modify.hqu", params, ResourcesResponse)

7. 取得器和設定器

在 Groovy 中,取得器和設定器形成我們稱之為「屬性」的東西,並提供存取和設定此類屬性的捷徑表示法。因此,取代呼叫取得器/設定器的 Java 方式,您可以使用類似欄位的存取表示法

resourceGroup.getResourcePrototype().getName() == SERVER_TYPE_NAME
resourceGroup.resourcePrototype.name == SERVER_TYPE_NAME

resourcePrototype.setName("something")
resourcePrototype.name = "something"

在 Groovy 中撰寫 bean(通常稱為 POGO(純粹的 Groovy 物件))時,您不必自己建立欄位和取得器/設定器,而是讓 Groovy 編譯器為您執行此操作。

因此,取代

class Person {
    private String name
    String getName() { return name }
    void setName(String name) { this.name = name }
}

您可以簡單地撰寫

class Person {
    String name
}

如您所見,沒有可見度修飾詞的獨立「欄位」實際上會讓 Groovy 編譯器為您產生一個私有欄位以及一個取得器和設定器。

當然,在 Java 中使用此類 POGO 時,取得器和設定器確實存在,並且可以像往常一樣使用。

雖然編譯器會建立通常的取得器/設定器邏輯,但如果您希望在這些取得器/設定器中執行任何其他或不同的操作,您仍然可以自由地提供它們,而編譯器將使用您的邏輯,而不是預設產生的邏輯。

8. 使用命名參數和預設建構函式初始化 bean

對於像這樣的 bean

class Server {
    String name
    Cluster cluster
}

與其在後續陳述中設定每個設定值,如下所示

def server = new Server()
server.name = "Obelix"
server.cluster = aCluster

您可以使用帶預設建構函式的命名參數(首先呼叫建構函式,然後按順序呼叫設定值,順序為在映射中指定的順序)

def server = new Server(name: "Obelix", cluster: aCluster)

9. 使用 with()tap() 對同一個 bean 進行重複操作

在建立新執行個體時,帶預設建構函式的命名參數很有趣,但如果您要更新已給您的執行個體,您是否必須重複「server」前綴?不用,感謝 Groovy 在任何類型的所有物件上新增的 with()tap() 方法

server.name = application.name
server.status = status
server.sessionCount = 3
server.start()
server.stop()

相較於

server.with {
    name = application.name
    status = status
    sessionCount = 3
    start()
    stop()
}

與 Groovy 中的任何閉包一樣,最後一個陳述會被視為傳回值。在上述範例中,這是 stop() 的結果。要將其用作建構函式,僅傳回輸入物件,還有 tap()

def person = new Person().with {
    name = "Ada Lovelace"
    it // Note the explicit mention of it as the return value
}

相較於

def person = new Person().tap {
    name = "Ada Lovelace"
}

注意:您也可以使用 with(true) 代替 tap(),並使用 with(false) 代替 with()

10. 等於和 ==

Java 的 == 實際上是 Groovy 的 is() 方法,而 Groovy 的 == 是聰明的 equals()

要比較物件的參考,您應使用 a.is(b),而不是 ==

但要執行一般的 equals() 比較,您應偏好 Groovy 的 ==,因為它也會避免 NullPointerException,而與左邊或右邊是否為 null 無關。

與其

status != null && status.equals(ControlConstants.STATUS_COMPLETED)

執行

status == ControlConstants.STATUS_COMPLETED

11. GString(內插、多行)

我們經常在 Java 中使用字串和變數串接,其中有許多開啟 / 關閉雙引號、加上號和 \n 字元以換行。透過內插字串(稱為 GString),此類字串看起來更好,且輸入起來較不費力

throw new Exception("Unable to convert resource: " + resource)

相較於

throw new Exception("Unable to convert resource: ${resource}")

在花括號內,您可以輸入任何類型的表達式,不只是變數。對於簡單變數或 variable.property,您甚至可以省略花括號

throw new Exception("Unable to convert resource: $resource")

您甚至可以使用 ${-> resource } 的閉包表示法,以延遲評估這些表達式。當 GString 將強制轉換為字串時,它會評估閉包並取得傳回值的 toString() 表示法。

範例

int i = 3

def s1 = "i's value is: ${i}"
def s2 = "i's value is: ${-> i}"

i++

assert s1 == "i's value is: 3" // eagerly evaluated, takes the value on creation
assert s2 == "i's value is: 4" // lazily evaluated, takes the new value into account

當字串及其連接式在 Java 中很長時

throw new PluginException("Failed to execute command list-applications:" +
    " The group with name " +
    parameterMap.groupname[0] +
    " is not compatible group of type " +
    SERVER_TYPE_NAME)

你可以使用 \ 延續字元(這不是多行字串)

throw new PluginException("Failed to execute command list-applications: \
The group with name ${parameterMap.groupname[0]} \
is not compatible group of type ${SERVER_TYPE_NAME}")

或使用三重引號的多行字串

throw new PluginException("""Failed to execute command list-applications:
    The group with name ${parameterMap.groupname[0]}
    is not compatible group of type ${SERVER_TYPE_NAME)}""")

你也可以透過對該字串呼叫 .stripIndent() 來移除出現在多行字串左側的縮排。

另外請注意 Groovy 中單引號和雙引號之間的差異:單引號總是建立 Java 字串,不會內插變數,而雙引號則會在存在內插變數時建立 Java 字串或 G 字串。

對於多行字串,你可以將引號加倍:例如,G 字串使用三重雙引號,而純粹的字串使用三重單引號。

如果你需要撰寫正規表示式模式,你應該使用「斜線」字串表示法

assert "foooo/baaaaar" ==~ /fo+\/ba+r/

「斜線」表示法的優點是你不需要對反斜線進行雙重跳脫,這讓使用正規表示式變得更簡單。

最後但並非最不重要的一點,在你需要字串常數時優先使用單引號字串,在你明確依賴字串內插時使用雙引號字串。

12. 資料結構的原生語法

Groovy 為資料結構(例如清單、對應、正規表示式或值範圍)提供原生語法結構。請務必在你的 Groovy 程式中利用它們。

以下是這些原生結構的一些範例

def list = [1, 4, 6, 9]

// by default, keys are Strings, no need to quote them
// you can wrap keys with () like [(variableStateAcronym): stateName] to insert a variable or object as a key.
def map = [CA: 'California', MI: 'Michigan']

// ranges can be inclusive and exclusive
def range = 10..20 // inclusive
assert range.size() == 11
// use brackets if you need to call a method on a range definition
assert (10..<20).size() == 10 // exclusive

def pattern = ~/fo*/

// equivalent to add()
list << 5

// call contains()
assert 4 in list
assert 5 in list
assert 15 in range

// subscript notation
assert list[1] == 4

// add a new key value pair
map << [WA: 'Washington']
// subscript notation
assert map['CA'] == 'California'
// property notation
assert map.WA == 'Washington'

// matches() strings against patterns
assert 'foo' ==~ pattern

13. Groovy 開發套件

繼續討論資料結構,當你需要反覆運算集合時,Groovy 提供各種其他方法,裝飾 Java 的核心資料結構,例如 each{}find{}findAll{}every{}collect{}inject{}。這些方法為程式語言增添了函數式風格,並有助於更輕鬆地處理複雜演算法。由於語言的動態特性,許多新方法透過裝飾套用於各種類型。你可以在字串、檔案、串流、集合等更多項目上找到許多非常有用的方法

14. switch 的威力

Groovy 的 switch 比通常只接受基本類型和同化的 C 類語言強大許多。Groovy 的 switch 接受幾乎任何類型的類型。

def x = 1.23
def result = ""
switch (x) {
    case "foo": result = "found foo"
    // lets fall through
    case "bar": result += "bar"
    case [4, 5, 6, 'inList']:
        result = "list"
        break
    case 12..30:
        result = "range"
        break
    case Integer:
        result = "integer"
        break
    case Number:
        result = "number"
        break
    case { it > 3 }:
        result = "number > 3"
        break
    default: result = "default"
}
assert result == "number"

更普遍地說,具有 isCase() 方法的類型也可以決定值是否與情況相符

15. 匯入別名

在 Java 中,當使用兩個名稱相同但來自不同套件的類別時,例如 java.util.Listjava.awt.List,您可以匯入一個類別,但必須對另一個類別使用完全限定的名稱。

此外,有時在您的程式碼中,多次使用長類別名稱會增加冗長性並降低程式碼的清晰度。

為了改善這種情況,Groovy 提供了匯入別名

import java.util.List as UtilList
import java.awt.List as AwtList
import javax.swing.WindowConstants as WC

UtilList list1 = [WC.EXIT_ON_CLOSE]
assert list1.size() instanceof Integer
def list2 = new AwtList()
assert list2.size() instanceof java.awt.Dimension

在靜態匯入方法時,您也可以使用別名

import static java.lang.Math.abs as mabs
assert mabs(-4) == 4

16. Groovy 真相

所有物件都可以「強制轉換」為布林值:所有 nullvoid、等於零或為空的值會評估為 false,如果不是,則評估為 true

因此,您不必撰寫

if (name != null && name.length > 0) {}

您只需執行

if (name) {}

集合等也一樣。

因此,您可以在 while()if()、三元運算子、Elvis 運算子(見下文)等中使用一些捷徑。

甚至可以透過為您的類別新增布林 asBoolean() 方法來自訂 Groovy 真相!

17. 安全圖形導覽

Groovy 支援 . 運算子的變體,以安全導覽物件圖形。

在 Java 中,當您對圖形中深處的節點感興趣,並且需要檢查 null 時,您通常會寫出複雜的 if 或巢狀 if 陳述式,如下所示

if (order != null) {
    if (order.getCustomer() != null) {
        if (order.getCustomer().getAddress() != null) {
            System.out.println(order.getCustomer().getAddress());
        }
    }
}

使用 ?. 安全解除參考運算子,您可以使用以下方式簡化此類程式碼

println order?.customer?.address

在整個呼叫鏈中會檢查 Null,如果任何元素為 null,則不會擲回 NullPointerException,如果某個元素為 null,則結果值將為 null。

18. 宣告

若要檢查您的參數、回傳值等,可以使用 assert 陳述式。

與 Java 的 assert 相反,`assert` 不需要啟用才能運作,因此總是會檢查 `assert`。

def check(String name) {
    // name non-null and non-empty according to Groovy Truth
    assert name
    // safe navigation + Groovy Truth to check
    assert name?.size() > 3
}

您也會注意到 Groovy 的「Power Assert」陳述提供的良好輸出,其中包含各種子表達式斷言值的圖形檢視。

19. Elvis 運算子用於預設值

Elvis 運算子是一個特殊的簡寫三元運算子,對於預設值來說很方便。

我們常常必須撰寫像這樣的程式碼

def result = name != null ? name : "Unknown"

感謝 Groovy Truth,null 檢查可以簡化為僅「name」。

更進一步,由於您無論如何都會傳回「name」,因此不必在這個三元表達式中重複「name」兩次,我們可以使用 Elvis 運算子移除問號和冒號之間的內容,以便上述內容變成

def result = name ?: "Unknown"

20. 捕捉任何例外

如果您真的不在乎在 try 區塊中引發的例外類型,您可以簡單地捕捉任何例外,並省略捕捉到的例外類型。因此,不要像這樣捕捉例外

try {
    // ...
} catch (Exception t) {
    // something bad happens
}

然後捕捉任何東西(「any」或「all」,或任何讓您認為是任何東西的東西)

try {
    // ...
} catch (any) {
    // something bad happens
}
請注意,它會捕捉所有例外,而不是 `Throwable`。如果您真的需要捕捉「所有內容」,您必須明確表示您要捕捉 `Throwable`。

21. 選擇性輸入建議

我將以一些關於何時以及如何使用選擇性輸入的說明作為結束。Groovy 讓您可以決定是否使用明確的強型別,或何時使用 def

我有一個相當簡單的經驗法則:每當您編寫的程式碼將由其他人作為公開 API 使用時,您應該始終優先使用強型別,它有助於加強合約,避免可能的傳遞引數類型錯誤,提供更好的文件,並幫助 IDE 完成程式碼。每當程式碼僅供您使用,例如私有方法,或當 IDE 可以輕鬆推斷類型時,您就可以更自由地決定何時輸入或不輸入。