风格指南

踏上 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/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. 默认公开

默认情况下,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.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,则不会抛出 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 可以轻松推断类型,那么您就可以更加自由地决定何时进行类型化或不进行类型化。