【Groovy翻译系列一】Groovy编程风格指南

33 阅读11分钟

原文档链接:​​https://groovy-lang.org/style-guide.html​

Groovy是一门基于JVM的语言,堪称动态语言版Java,其各种动态语言特性填补了Java的各种空缺,让人拍案叫绝……有幸接触Groovy,遂手动翻译三篇官方文档,以便于读者从Java到Groovy的快速迁移。


一个踏上了Groovy冒险之旅的Java开发者,会先基于烂熟于心的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​​,而不是你所期望的值。

​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. Classes是一等公民

在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的调用方式,可以使用类似的字段获取方法:

resourceGroup.getResourcePrototype().getName() == SERVER_TYPE_NAME
resourceGroup.resourcePrototype.name == SERVER_TYPE_NAME

resourcePrototype.setName("something")
resourcePrototype.name = "something"

当你在Groovy中写你的beans(经常被叫做POGO (Plain Old Groovy Object))时,你不必再自行创建字段的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

你可以将命名参数与默认构造函数一起使用(首先调用构造函数,然后按照在映射中指定的顺序调用设置):

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. GStrings(插值,多行)

我们在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 = 3def 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为lists、maps、regex或ranges提供了原生的数据结构语法。确保在你的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 // exclusivedef 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等数据结构中:

​http://groovy-lang.org/gdk.html​

14. 强大的switch

Groovy的​​switch​​​比起直接只受原语和同化的C-ish语言更加强大。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. Import别名

在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的真值

所有的对象都可以被强转为boolean值:所有的可能值为​​null​​​ , ​​void​​​ , 等于0或者空则为​​false​​​ , 否则为​​true​​。

替代下面的写法:

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

你可以仅仅这样做:

if (name) {}

集合也是如此。

因此你可以在​​while()​​​ , ​​if()​​ , 三元运算符, Elvis表达式等里面使用一些简短的表达。

甚至于,通过在你的类中添加一个boolean类型的​​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断言

你可以使用​​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. Catch任何的异常

如果你不是真的很在意代码​​try​​块中的异常的类型,你可以简单地捕获它们中的任何一个并简单地省略捕获的异常的类型。所以替代下面的代码:

try {
    // ...
} catch (Exception t) {
    // something bad happens
}

使用catch ('any' 或者 'all', 或者你认为的任何异常):

try {
    // ...
} catch (any) {
    // something bad happens
}

请注意,它捕获所有异常,而不是 ​​Throwable​​​。如果你需要真正捕捉“所有东西”,你必须明确地说你想捕捉 ​​Throwable​​。

21. 对可选类型的建议

我将完成一些关于怎么用和什么时候用可选类型的内容。Groovy让你决定是否去使用强类型还是​​def​​。

我有一个相当简单的经验法则:每当你的代码被当作公有API使用时,你应该总是青睐使用强类型,这将帮助你确定更健壮的约束,以避免可能通过错误的参数类型,也能够提供更好的文档,还能帮助IDE去进行代码补全。每当代码仅供你自己使用时,像私有方法,或者当IDE可以轻易推断类型时,你就可以更自由地决定何时使用可选类型。