极客学院团队出品 · 更新于 2018-11-28 11:00:43

语法风格指南

愿意使用 Groovy 的 Java 开发者往往还是会保留着 Java 的思维,通过对 Groovy 的逐渐学习,每次了解一个特性,他们的努力越来越具有成效,Groovy 代码写得也越来越娴熟。我们的文档力图继续指导开发者,教授一些常用的 Groovy 语法风格、新的操作符,以及一些新的特性,比如闭包等。这篇指南并不完整,只能作为快速入门以及今后深入的奠基石,你可能以后会为本文档贡献内容并对它作出一番改进。

1. 不用分号

拥有 C/C++/C#/Java 背景的开发者往往习惯于到处使用分号。更严重的是,Groovy 支持绝大部分的 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,而并非是你所期望的映射。

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 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 }

一般往往推荐采用第三种方法,它显得更自然一些。从语法层面上来看,内容为空的括号对是一种无用的垃圾。

然而,在有些情况下,Groovy 是不允许去除括号的。遇到顶级的表达式,自然可以忽略括号,但对于内嵌的方法调用或在赋值语句的右侧,则是不允许忽略括号的。

def foo(n) { n }

println foo 1 // 不起作用   
def m = foo 1

6. 作为一等公民存在的类

Groovy 中并不需要 .class 后缀,这有点像 Java 中的 instanceof

比如:

connection.doPost(BASE_URI + "/modify.hqu", params, ResourcesResponse.class)

使用之后介绍的 GString,应用头等公民的结果是这样的:

connection.doPost("${BASE_URI}/modify.hqu", params, ResourcesResponse)

7. Getter 与 Setter

Groovy 中的 getter 与 setter 构成了我们称之为 “属性”(property)的形式,从而为访问这种属性提供了一种快捷标记。因此,我们完全可以舍弃 Java 式的调用方法,而采用字段样式的访问标记:

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() 来处理对于同一 bean 的重复操作

在创建新实例时,带有默认构造函数的命名参数是非常有用的。但是,如果更新一个已有实例呢?难道你还必须一遍一遍重复 server 前缀?不必如此,Groovy 所提供的 with() 方法可以应用于所有类型的对象,比如像下面这样:

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()
}

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" // 急切地计算,一创建时就求值
assert s2 == "i's value is: 4" // 拖延式计算,考虑新值   

当字符串与它们的联合表达式用 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 用三重双引号,对单纯的字符串用三重单引号。

如果需要编写正则表达式模式,应该使用“斜杠式”字符串标记法:

assert "foooo/baaaaar" ==~ /fo+\/ba+r/  

这样写的好处在于不必使用双重转义反斜杠,从而更便于使用 regex。

最后要强调的是,在需要字符串常量时,尽量优先使用单引号字符串,而在显然需要字符串插值时,才使用双引号字符串。

12. 数据结构的原生语法

Groovy 为一些数据结构(如列表、映射、正则表达式以及值范围)提供了原生的语法结构,一定要利用好它们。

下面是一些原生构造:

def list = [1, 4, 6, 9]

// 默认,键是 String 类型,所以不需要用引号括起来
// 你可以用像 [(variableStateAcronym): stateName] 这样的带有 () 的结构来封装键,插入变量或对象  

def map = [CA: 'California', MI: 'Michigan']

def range = 10..20
def pattern = ~/fo*/

// 等同于 add()  
list << 5

// 调用 contains()
assert 4 in list
assert 5 in list
assert 15 in range

// 下标符号  
assert list[1] == 4

// 添加一个新的键值对   
map << [WA: 'Washington']
// 下标符号  
assert map['CA'] == 'California'
// 属性标记  
assert map.WA == 'Washington'

// 判断字符串是否与模式匹配   
assert 'foo' =~ pattern

13. Groovy 开发工具包

继续探讨数据结构,在需要对集合迭代时,Groovy 提供了多种方法,通过装饰模式强化 Java 的核心数据结构,比如:each{}find{}findAll{}every{}collect{}以及inject{}等。这些方法不仅为编程语言提供了功能性帮助,而且还能便于人们实现复杂的算法。通过装饰模式,很多新方法已经添加到不同的类型中,这要感谢语言本身的动态特性。可以在下面这个网站找到很多的有用方法,它们可以用于字符串、文件、流以及集合等:http://beta.groovy-lang.org/gdk.html

14. switch 的魔力

switch 在 Groovy 中的作用要比在 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 juList
import java.awt.List as aList

import java.awt.WindowConstants as WC

还可以静态地导入方法:

import static pkg.SomeClass.foo
foo()

16. Groovy Truth

任何对象都可以被强制转换为布尔值:任何为 nullvoid的对象,等同于 0 或空的值,都会解析为 false,凡之则为 true

所以不必这样写:

if (name != null && name.length > 0) {}

只需这样写就好了:

if (name) {}

这一原则也可以用于集合等对象。

因此,可以在诸如 while()if()、三元运算子以及 Elvis 操作符等结构中使用一些快捷形式。

甚至可以自定义 Groovy Truth 对象,只需为类加入一个 asBoolean() 布尔方法即可。

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 有所不同,Groovy 的 assert 并不需要激活,它是一直被检查的。

def check(String name) {
    // 根据 Groovy Truth,name 应为非 null 与非空
    assert name
    // 安全导航 + Groovy Truth  
    assert name?.size() > 3
}

另外要注意的是,Groovy 的 “强力断言” 语句提供的输出结果是很出色的,在生成的图表中对每个子表达式的各种值都进行了断言。

19. 用于默认值的 Elvis 操作符

Elvis 操作符是一种特殊的三元操作符,对于处理默认值来说不啻是一种快捷方式。

我们往往会像下面这样来书写:

def result = name != null ? name : "Unknown"

多亏有了 Groovy Truth,null 检查可以简化为只用 name 就可以了。

进一步来说,既然要返回 name,那么与其在这个三元表达式中重复两次名称,不如去掉问号和冒号之间的东西,使用 Elvis 操作符,可以这样来完成:

def result = name ?: "Unknown"

20. 异常捕捉

如果不关心 try 语句块中所要抛出的异常类型,可以只捕捉异常而忽略它们的类型。所以,像下面这样的语句:

try {
    // ...
} catch (Exception t) {
    // 一些糟糕的事情   
}

就可以变成下面这样捕捉任何异常(anyall 都可以,只要是能让你认为是任何东西的词儿就可以用):

try {
    // ...
} catch (any) {
    // 一些糟糕的事情  
}

它会捕捉所有异常,而并不仅是 Throwable 的异常。如果需要捕捉的是“每一个”异常,必须明确地声明要捕捉的是 Throwable 异常。

21. 额外的类型建议

最后讲讲什么时候以及如何使用可选类型。Groovy 允许自己决定是否使用显式的强类型,或何时使用 def

简单的经验法则是:如果你写的代码将被其他人用作公共 API,你就应该使用强类型,它能有助于合约的健壮性,避免可能通过的参数类型错误,形成更好的文档,有助于 IDE 自动完成代码。假如代码只是自用,比如私有方法,或 IDE 能够轻松地推断类型,那么你就可以更自由地确定何时利用类型。

上一篇: 设计模式