风格指南
踏上 Groovy 冒险之旅的 Java 开发人员,心中总会有 Java,他们会逐渐学习 Groovy,一次学习一项功能,从而提高生产力,并编写更多符合 Groovy 风格的代码。本文档的目的是指导这样的开发人员,教授一些常见的 Groovy 语法风格、新运算符以及闭包等新功能。本指南并非完整版,仅作为快速入门和进一步指南部分的基础,如果您想为本文档做出贡献并对其进行增强,可以参考此指南。
1. 不用分号
从 C/C++/C#/Java 背景而来,我们习惯了使用分号,在所有地方都使用它们。更糟糕的是,Groovy 支持 99% 的 Java 语法,有时将 Java 代码粘贴到 Groovy 程序中非常容易,以至于最终在所有地方都布满了分号。但是… 在 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. 默认公开
默认情况下,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
vs
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. 类作为一等公民
.class
后缀在 Groovy 中不需要,有点像 Java 的 instanceof
。
例如
connection.doPost(BASE_URI + "/modify.hqu", params, ResourcesResponse.class)
使用下面将介绍的 GStrings,并使用一等公民
connection.doPost("${BASE_URI}/modify.hqu", params, ResourcesResponse)
7. Getter 和 Setter
在 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(普通 Groovy 对象),您不必自己创建字段和 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
您可以使用带默认构造函数的命名参数(首先调用构造函数,然后按映射中指定的顺序调用 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()
vs
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
}
vs
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. GStrings (插值,多行)
我们经常在 Java 中使用字符串和变量连接,并使用许多双引号的打开/关闭、加号以及用于换行的 \n
字符。使用插值字符串(称为 GStrings),这样的字符串看起来更好,而且输入起来也更轻松。
throw new Exception("Unable to convert resource: " + resource)
vs
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 字符串,要么创建 GStrings。
对于多行字符串,您可以使用三引号:即对于 GStrings 使用三个双引号,对于普通字符串使用三个单引号。
如果您需要编写正则表达式模式,您应该使用“斜线”字符串符号
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 类语言强大得多。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 真值
所有对象都可以“强制转换为”布尔值:所有为 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
,则不会抛出 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 真值,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 可以轻松推断类型,那么您就可以更加自由地决定何时进行类型化或不进行类型化。