風格指南
一位踏上 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
/else
、try
/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.List
和 java.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 真相
所有物件都可以「強制轉換」為布林值:所有 null
、void
、等於零或為空的值會評估為 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 可以輕鬆推斷類型時,您就可以更自由地決定何時輸入或不輸入。