风格指南
Java 开发者在开始 Groovy 冒险时,总是会想到 Java,并会逐步学习 Groovy,一次学习一个特性,从而提高生产力并编写更地道的 Groovy 代码。本文档的目的是指导此类开发者,教授一些常见的 Groovy 语法风格、新运算符以及闭包等新特性。本指南并不完整,仅作为快速入门和进一步指南章节的基础,如果您想为文档做贡献并对其进行增强。
1. 无分号
从 C / C++ / C# / Java 背景转过来的开发者,我们非常习惯分号,以至于我们把它们放在任何地方。更糟糕的是,Groovy 支持 99% 的 Java 语法,有时,将一些 Java 代码粘贴到 Groovy 程序中是如此容易,以至于你最终到处都是分号。但是……Groovy 中的分号是可选的,你可以省略它们,而且省略它们更符合习惯。
2. return 关键字可选
在 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
,而不是你期望的 map。
像 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 时,才应该添加可见性修饰符。
所以,与其这样写
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)
使用我们将在下面介绍的 GStrings,并使用一等公民
connection.doPost("${BASE_URI}/modify.hqu", params, ResourcesResponse)
7. Getters 和 Setters
在 Groovy 中,getter 和 setter 构成了我们所说的“属性”,并提供了访问和设置这些属性的快捷方式。因此,您可以使用类似字段的访问方式,而不是 Java 中调用 getter/setter 的方式。
resourceGroup.getResourcePrototype().getName() == SERVER_TYPE_NAME
resourceGroup.resourcePrototype.name == SERVER_TYPE_NAME
resourcePrototype.setName("something")
resourcePrototype.name = "something"
当您用 Groovy 编写您的 bean 时,通常称为 POGO(Plain Old Groovy Objects),您无需亲自创建字段和 getter/setter,而是让 Groovy 编译器为您完成。
所以,与其这样写
class Person {
private String name
String getName() { return name }
void setName(String name) { this.name = name }
}
你只需写
class Person {
String name
}
正如您所看到的,一个没有修饰符可见性的独立“字段”实际上会使 Groovy 编译器为您生成一个私有字段以及一个 getter 和 setter。
当然,当从 Java 使用这样的 POGO 时,getter 和 setter 确实存在,并且可以像往常一样使用。
虽然编译器会创建常规的 getter/setter 逻辑,但如果您希望在这些 getter/setter 中执行任何额外或不同的操作,您可以自由地提供它们,编译器将使用您的逻辑,而不是默认生成的逻辑。
8. 使用命名参数和默认构造函数初始化 bean
使用一个像这样的 bean
class Server {
String name
Cluster cluster
}
而不是在后续语句中设置每个 setter,如下所示
def server = new Server()
server.name = "Obelix"
server.cluster = aCluster
你可以使用带有默认构造函数的命名参数(首先调用构造函数,然后按照它们在 map 中指定的顺序调用 setter)。
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. Equals 和 ==
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
。使用插值字符串(称为 GStrings),这样的字符串看起来更好,输入起来也不那么痛苦。
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 被强制转换为 String 时,它会评估闭包并获取返回值的 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 字符串也可以创建 GString。
对于多行字符串,您可以将引号加倍:即,对于 GString 使用三重双引号,对于普通 String 使用三重单引号。
如果你需要编写正则表达式模式,你应该使用“斜杠”字符串表示法
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{}
。这些方法为编程语言添加了函数式风味,并有助于更轻松地处理复杂的算法。由于语言的动态特性,许多新方法通过修饰应用于各种类型。您可以在 String、Files、Streams、Collections 等许多类型上找到许多非常有用的方法。
14. switch 的强大功能
Groovy 的 switch
比 C 语言中的 switch
强大得多,后者通常只接受基本类型和类似类型。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()
方法的类型也可以判断一个值是否与某个 case 对应。
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 Truth
所有对象都可以被“强制转换”为布尔值:所有 null
、void
、等于零或空的值都评估为 false
,否则评估为 true
。
所以不要这样写
if (name != null && name.length > 0) {}
你只需这样做
if (name) {}
集合等也是如此。
因此,你可以在 while()
、if()
、三元运算符、Elvis 运算符(见下文)等中使用一些快捷方式。
甚至可以通过在您的类中添加一个布尔型的 asBoolean()
方法来定制 Groovy Truth!
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
要检查您的参数、返回值等,您可以使用 assert
语句。
与 Java 的 assert
不同,Groovy 的 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
}
请注意,它捕获的是所有 Exception,而不是 Throwable。如果您确实需要捕获“所有”,则必须明确说明要捕获 Throwable。 |
21. 可选类型建议
最后,我将就何时以及如何使用可选类型发表一些看法。Groovy 允许您决定是使用显式强类型,还是使用 def
。
我有一个相当简单的经验法则:无论你正在编写的代码是否会作为公共 API 被其他人使用,你都应该始终倾向于使用强类型,这有助于使契约更强大,避免可能的参数类型错误,提供更好的文档,也有助于 IDE 进行代码补全。当代码仅供你自己使用时,例如私有方法,或者 IDE 可以轻松推断类型时,你就可以更自由地决定是否进行类型化。