风格指南

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/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 时,才应该添加可见性修饰符。

所以,与其这样写

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.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 Truth

所有对象都可以被“强制转换”为布尔值:所有 nullvoid、等于零或空的值都评估为 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 可以轻松推断类型时,你就可以更自由地决定是否进行类型化。