`

Groovy 闭包 详解

阅读更多
介绍 Groovy 中的闭包是一个匿名的代码块,可以接受参数,并返回一个返回值,也可以引用和使用在它周围的,可见域中定义的变量。 在许多方面,它看起来像 java 中的匿名内部类,并且闭包的用法也确实像大多数 java 开发者使用匿名内部类的方式。但事实上,Groovy 的闭包要比 java 的匿名内部类强大,并且更加便于使用。 用函数式语言的说法,这样的匿名代码块,可以被引用为通常的匿名 lambda 表达式,或者是一个未绑定任何变量的 lambda 表达式,或者是封闭的 lambda 表达式,如果它没有包含对未绑定变量的引用的话。Groovy 并不作这些区分。 更严格的说,一个闭包是不可以被定义的。你可以定义一个代码块,使它引用本地变量或者成员属性,但是只有当它被绑定(赋予它一个含义)到一个变量时,它才成其为一个闭包。闭包是一个语义概念,就像实例一样,你不可以定义,但可以创建。更严格意义上的闭包,其所有的自由变量都将被绑定,否则只是部分闭合,也就不是一个真正的闭包。虽然 Groovy 并没有提供一个途径来定义闭合的 Lambda 函数,并且一个代码块也可能根本就不是一个闭合的 Lambda 函数(因为它可能有自由变量),但我们还是认为它们是一个闭包--就像一个语法概念一样。我们之所以称它为语法概念,这是因为代码的定义和实例的创建是一体的,这里没有什么不同。我们非常清楚的知道,这个术语的使用或多或少是错误的,但是在讨论某种语言的代码的时候,却可以简化很多事情,并且不需要深究这些差异。 闭包的正式定义语法 闭包的定义,可采用下面的方式: { [closureArguments->] statements } 这里的 [closureArguments->] 是一个可选的,逗号分隔的参数列表。statements 由 0个或多个 Groovy 语句构成。参数列表看起来有点像方法的参数列表,它们可能带类型声明,可能不带。如果指定了一个参数列表,则 符号 -> 必须出现,以分离参数列表和闭包体。statements 部分可以由 0 个,1个,或很多的 Groovy语句构成。 下面是一些有效的闭包定义方式: { item++ } { println it } { ++it } { name -> println name } { String x, int y -> println "hey ${x} the value is ${y}" } { reader -> while (true) { def line = reader.readLine() } } 闭包语义 闭包看起来像是一种方便的机制,用来定义一些东西,如内部类,但它的语义其实要比内部类提供的更强大和微妙。特别的,闭包的特性可以简要介绍如下: 1,它们拥有一个隐含的方法(在闭包的定义中从来不指定)叫 doCall()。 2,一个闭包可以被 call() 方法调用,或者通过一种特殊的无名函数 () 的语法形式来调用。这两种调用方式都会被 Groovy 转换成对闭包的 doCall() 方法的调用。 3,闭包可以接收 1....N 个参数,这些参数可以静态的声明其类型,也可以不声明类型。第一个参数是一个未声明类型的隐含变量叫 “it” , 如果没有其他显式的参数声明的话。如果调用者没有声明任何参数,则第一个参数(并且扩展为 it)将为 null。 4,开发者不需要一定用 it 作为第一个参数,如果你想用一个不同的名字的话,可以在参数列表中声明。 5,闭包总是返回一个值,要么显式地使用 return 语句,要么隐含地返回闭包体中最后一个语句的值。(也就是说,显式地 return 语句是可选 地) 6,闭包可以引用任何定义在它的封闭词法域中的变量,我们称这样的变量被绑定在闭包上。 7,即使一个闭包在它的封闭词法域外返回,那些绑定在这个闭包上的变量仍旧可以被这个闭包使用。 8,闭包在 Groovy 中是第一等公民,并且总是从类 Clousre 继承。代码可以通过一个无类型变量,或者一个类型为 Closure 的变量来 引用闭包。 9,一个闭包体只有在它被显式地调用时,才被执行,即闭包在它的定义处是不被执行的。 10,一个闭包可能被调味,因此一旦发生实例拷贝,则它的一个或多个参数会被固定为某个值。 这些特性将在下面的章节中进一步解释。 闭包是匿名的 闭包在 Groovy 中总是匿名的,不像 java 或 Groovy 的类,你不可能得到一个命名的闭包。你只能通过一个无类型变量,或类型为 Closure 的变量来引用闭包,并且把这个引用当参数传递给方法,或传递给其他闭包。 隐含方法 闭包可以被认为拥有一个隐含定义的方法,它对应于这个闭包的参数和闭包体。你不可以重载或者重定义这个方法。这个方法总是通过闭包的 call() 方法来调用,或者通过一个特殊的无名函数 () 的语法来调用。这个隐含方法的名字是 doCall() 。 闭包的参数 闭包总是含有至少一个输入参数,名字叫 it ,可以在闭包体内使用,除非定义了其他显性的参数。开发者不需要显式的声明 it 变量,就像对象中的 this 引用一样,它是隐含可用的。 如果一个闭包以 0个参数的方式被调用,则 it 的值将为 null。 开发者可以给闭包传递显式声明的参数列表,这个参数列表包含一个或多个参数名称,其间用逗号隔开。参数列表的结束由符号 “->”标识。每个参数即可以不带任何类型声明,也可以静态的指定一个类型。如果一个显式的参数表被声明,则 it 变量将不可用。 如果参数声明了类型,则这个类型将在运行期被检查。如果在闭包的调用中,有一个或多个参数的类型不匹配,则将抛出一个运行期异常。注意,这个类型检查总是发生在运行期,这里没有静态的类型检查,编译器是不会报告任何类型不匹配错误的。 Groovy 特别支持溢出参数。一个闭包可以把它的最后一个输入参数声明为 Object[] 类型,在调用时,任何多余的溢出参数将被放置在这个对象数组中。这可以被认为是对变长参数的一种支持,如: def c = { format, Object[] args -> aPrintfLikeMethod (format, args)} c ("one", "two", "three"); c ("1"); 上面例子中,两种 c 的调用都是可用的。由于上面的闭包定义了两个输入参数: fomat 和 args,并且 args 是一个Object[] 类型对象,因此对这个闭包的任何调用中,第一个参数总是绑定在 format 上,其余的参数则绑定在 args 上。在上面的第一种场景中,参数 args 将接收两个值 "two"和 "three",而参数 format 将接收 “one”;而在第二个调用情形下,参数 args 将接收 0 个元素,而 format 将接收值“1”。 闭包的返回值 闭包总是拥有一个返回值,要么是闭包体中显式的用一个或多个 return 语句来实现,要么将最后一个执行的语句的值作为返回值,前提是没有显式的指定任何 return 语句。如果最后一个执行的语句没有返回值(如调用了一个 void 类型的方法),则返回 null。 目前还没有机制,可以静态的指定一个闭包的返回值类型。 引用外部变量 闭包可以引用外部的变量,包括局部变量,方法参数和成员对象。闭包只能引用那些和闭包定义在同一个源文件中的,符合编译器词法演绎规则的变量。 一些例子有助于说得更清楚。下面的例子是有效的,并且展示了一个闭包对方法本地变量、方法参数的使用情况: public class A { private int member = 20; private String method() { return "hello"; } def publicMethod (String name_) { def localVar = member + 5; def localVar2 = "Parameter: ${name_}"; return { println "${member} ${name_} ${localVar} ${localVar2} ${method()}" } } } A sample = new A(); def closureVar = sample.publicMethod("Xavier"); closureVar(); 结果是: 20 Xavier 25 Parameter: Xavier hello 我们来看一下类A的定义,方法 publicMethod 中的闭包,访问了该方法所有可以合法访问的变量。不管是访问本地变量,方法参数,成员实例,还是函数调用,都可以。 当闭包以这种方式引用变量时,这些变量就被绑定在这个闭包上。此时,这些变量在它们定义的词法域范围内,和通常情况一样,仍旧是可用的,不仅闭包可以读取修改这些变量,其他地方的合法代码也可以读取修改这些变量。 当闭包从它的封闭域返回时,与它绑定的那些变量仍旧存活着。绑定只发生在闭包被实例化时。如果对象方法或者成员实例,在一个闭包内被使用,则指向这些对象的引用将被保存在闭包内。如果一个本地变量或一个方法参数被引用了,则编译器将会覆盖这个本地变量或方法参数的引用,使其脱离堆栈区,而存储在堆区。 保持这样的认识很重要:这样的引用方式,必须符合编译器的词法结构规定(这个例子里是类 A)。这个过程并不会通过检索调用栈而动态发生。因此下面的用法是非法的: class A { private int member = 20; private String method() { return "hello"; } def publicMethod (String name_) { def localVar = member + 5; def localVar2 = "Parameter: name_"; return { // Fails! println "${member} ${name_} ${localVar} ${localVar2} ${method()} ${bMember}" } } } class B { private int bMember = 12; def bMethod (String name_) { A aInsideB = new A(); return (aInsideB.publicMethod (name_)); } } B aB = new B(); closureVar = aB.bMethod("Xavier"); closureVar(); 这个例子和第一个例子有些相像,但我们新定义了一个类 B,在这个类中动态地创建了一个类 A 的实例,然后调用了类 A 的 publicMethod 方法。紧接着,在 publicMethod 方法中的闭包,试图引用类 B 中的一个成员,但这是不允许的,因为编译器无法静态的判定访问可见性。一些老的语言通过在运行期动态检索调用栈,而允许这种引用方式,但在 Groovy 中是不被允许的。 Groovy 支持一个特殊的 owner 变量,当一个闭包的参数隐藏(挡住)了一个同名成员时,就会有用。如: class HiddenMember { private String name; def getClosure (String name) { return { name -> println (name)} } } 在上面的代码中, println(name) 引用了方法参数 name。如果闭包想访问外围类的同名成员对象时,它可以通过 owner 变量来实现这个目的: class HiddenMember { private String name; def getClosure (String name) { return { name -> println ("Argument: ${name}, Object: ${owner.name}")} } } 闭包类型 Groovy 中的所有闭包都继承自类型 Closure。在 Groovy 编程中,每个唯一的闭包定义,都会导致创建一个唯一的类,该类继承自类型 Closure。 如果你想显式的指定参数、本地变量、成员变量中的闭包类型,则必须使用 Cloure 类型。 一个闭包的确切类型通常不会被先定义,除非你显式的指明继承自类型 Cloure 的子类。看下面的例子: def c = { println it} 上面的例子中,对闭包进行引用的变量 c 的具体类型并没有被定义,我们仅仅知道它是类型 Cloure 的某个子类。 闭包的创建和调用 当闭包的周围代码触及到它们时,闭包才被隐性的创建。例如在下面的例子中,有两个闭包被创建: class A { private int member = 20; private method() { println ("hello"); } def publicMethod (String name_) { def localVar = member + 5 def localVar2 = "Parameter: name_"; return { println "${member} ${name_} ${localVar} ${localVar2} ${method()}" } } } A anA = new A(); closureVar = anA.publicMethod("Xavier"); closureVar(); closureVar2 = anA.publicMethod("Xavier"); closureVar2(); 在上面的例子中,cloureVar 所持有的闭包对象引用,与 cloureVar2 的是不一样的。闭包通常就是以这样的方式隐性的创建--你不能通过 编程的方式来创建,如使用 new 操作符。 闭包的调用有两种方式,显性的调用方式是通过 call() 方法: closureVar.call(); 你也可以使用隐式的匿名调用: closureVar(); 如果你查看闭包的 javadoc 时,你会发现闭包的 call() 方法,在类型 Cloure 中的定义形式如下: public Object call (Object[] args); 按照这个方法声明的样子,你无需手工的将参数转换成对象数组,调用还是按照通常的方式,Groovy 会自动做这个对象数组的转换: closure ("one", "two", "three") closure.call ("one", "two", "three") 上面的两种调用方式都是合法的。但是,如果你的闭包要和 java 代码交互,那么这个 Object[] 对象就不得不手工创建了。 通过调味(curry),将闭包参数预固定为某个数值 你可以通过使用 Cloure 对象的 curry() 方法,来把一个闭包实例的一个或多个参数固定为常量【译者注:就像食物/参数入口前,先进行一下调味,然后再送入胃部处理/运算。这个技巧结合数学中的复合函数的概念,可以实现很多巧妙的用法】。这样的行为在函数编程范式中通常称为调味(curring),调味所得到的结果(译注:一个新的闭包)通常称为调味闭包(Curried Cloure)。调味闭包可以用来创建泛型闭包,即在原始定义的基础上,通过将闭包参数进行不同的绑定,从而可实现几个不同的闭包版本。 当给一个闭包实例的curry() 方法传递一个或多个参数,并调用它时,一个闭包的拷贝将被首先建立。所有的输入参数将永久性的被绑定在这个新的闭包拷贝上,传递给 curry() 的参数 1….N 将被绑定到新闭包的 1….N 参数上。然后这个新的调味闭包将被返回给调用者。 调用者对新实例(调味闭包)进行调用时,所有的传入参数将从原始闭包的第(N+1)个参数开始匹配绑定。 def c = { arg1, arg2-> println "${arg1} ${arg2}" } def d = c.curry("foo") d("bar") 在上面的例子中定义了一个原始闭包 c ,然后调用了 c.curry(“foo”),它将返回一个调味闭包,调味闭包的第一个参数arg1被永久绑定为值“foo” 。当调用这个调味闭包 d(“bar”)时,参数 “bar”被传递给原始闭包的第二个参数 arg2,最后的输出结果是 “foo bar” 请参考:用Groovy 进行函数编程 特殊案例:把闭包作为参数传递给方法 Groovy 专门提供了一个语法便利,来方便把一个闭包定义为方法的参数,以增加可读性。特别的,如果一个方法的最后一个参数,是闭包类型的话,则可以在调用这个方法时,将闭包对象放在参数括号外。如下面的例子所示: class SomeCollection { public void each (Closure c) } 然后我们可以在调用 each() 方法时,把闭包对象放在圆括号外面。 SomeCollection stuff = new SomeCollection(); stuff.each() { println it } 更传统的调用语法也是可用的。另外,由于在很多场合下,Groovy 允许省略圆括号,因此下面的两种变形语法也是合法的: SomeCollection stuff = new SomeCollection(); stuff.each { println it } // Look ma, no parens stuff.each ({ println it }) // Strictly traditional 这个规则甚至可用于方法的参数多余1个的情形,唯一的要求是闭包参数必须是最后一个参数: class SomeCollection { public void inject (x, Closure c) } stuff.inject(0) {count, item -> count + item } // Groovy stuff.inject(0, {count, item -> count + item }) // Traditional 这个规则仅适用于闭包对象在方法调用中被显式的定义(为参数)的场景,而不能通过一个闭包类型的变量来作为参数传递: class SomeCollection { public void inject (x, Closure c) } counter = {count, item -> count + item } stuff.inject(0) counter // Illegal! No Groovy for you! 如果你没有在函数调用参数中直接定义内联闭包对象,则不能使用上述语法,而必须使用更明晰的写法: class SomeCollection { public void inject (x, Closure c) } def counter = {count, item -> count + item } stuff.inject(0,counter) 闭包和匿名内部类的对比 Groovy 之所以包含闭包,是因为它可以让开发者写出更简约、更易懂的代码。Java 开发者通常用一个单方法的接口(Runable,采用Command设计模式),并结合匿名内部类来实现;而Groovy 则允许以一种更简易、直白的方式来实现。额外的,相较匿名内部类,闭包有很少量的约束,包括一些额外的功能。 绝大部分的闭包都是简短的、孤立的、碎片状的代码,并执行特定的功能任务。语法书写的流畅性要求闭包的定义简短、易读,不凌乱。例如在 java 代码中经常看到下面的类 GUI 代码: Button b = new Button ("Push Me"); b.onClick (new Action() { public void execute (Object target) { buttonClicked(); } }); 同样的代码在 Groovy 看起来是这样: Button b = new Button ("Push Me"); b.onClick { buttonClicked() } 完成同样的任务,Groovy 代码看起来更清晰,更整洁。这是 groovy 闭包的第一个准则--闭包要简练、易于书写。另外,闭包可以引用它的定义域外围的变量,而匿名内部类则有限制,更进一步,这样的变量不需要是 final 限定的。 闭包会携带和它相关的所有状态,甚至在它们引用本地变量或入口参数时,也是这样。闭包同样也符合 Groovy 的动态语言类型的优点,因此你无需声明作为参数,或作为返回值的闭包的类型(实际上,闭包可以在多层调用中携带大量的参数)。 Groovy 闭包在和采用Command 设计模式的接口的比较中,比较薄弱的就是静态类型的调用。在 java 接口中会强制指定对象的类型,及可以调用的方法集合。而在Groovy 中,所有的闭包都是 Cloure 类型,并且参数类型的检查是在运行期进行的。 闭包作为Map 的键和值 闭包作为键 你可以把闭包作为Map 的键,但必须用括号转义处理(否则可能会被看作字符串)。像下面所示: f = { println "f called" } m = [ (f): 123 ] 当以闭包作为键访问 Map 中的值时,必须使用方法 get(f),或者 m[f] 方式。“m.f”方式将被认作字符串。 println m.get(f) // 123 println m[f] // 123 println m.f // null 闭包作为值 闭包可以作为 Map 中的值存在,并且可以调用执行它,就像是调用 Map 对象的一个扩展方法一样: m = [ f: { println 'f called' } ] m.f() // f called m = new Expando( f: { println 'f called' } ) m.f() // f called 通过 use 指令来扩展groovy 你可以提供自定义的、特殊的方法来支持闭包,只要在一个 java 类中包含并实现这个方法。这些方法必须是静态的,并且至少有两个参数。第一个参数的类型必须是这个方法要操作的类型,最后一个参数必须是一个 Cloure 类型。 看下面的例子,这是一个 eachFile() 方法的变种,它忽略文件,而仅仅打印目录对象中的目录: dir = new File("/tmp") use(ClassWithEachDirMethod.class) { dir.eachDir { println it } } 注意 use() 指令,它告诉 Groovy 方法 eachFile() 是在哪个类中定义并实现的。下面就是这个方法的 java 实现以支持新的 eachFile () 功能: public class ClassWithEachDirMethod { public static void eachDir(File self, Closure closure) { File[] files = self.listFiles(); for (int i = 0; i
分享到:
评论

相关推荐

    groovy(10)-闭包委托策略1

    groovy(10)-闭包委托策略 /*闭包的三个重要变量:this,owner,delegate区别在于:this代表闭包定义处最近的对象(不包含闭包),ow

    [Groovy入门]第五讲.将流程控制语句与方法重构为闭包

    [Groovy入门]第五讲.将流程控制语句与方法重构为闭包

    apache-groovy-sdk-2.4.11

    了解 Groovy 对 Java 语法的简化变形,学习 Groovy 的核心功能,例如本地集合、内置正则表达式和闭包。编写第一个 Groovy 类,然后学习如何使用 JUnit 轻松地进行测试。借助功能完善的 Groovy 开发环境和使用技能,...

    groovy-3.0.9-API文档-中文版.zip

    赠送jar包:groovy-3.0.9.jar; 赠送原API文档:groovy-3.0.9-javadoc.jar; 赠送源代码:groovy-3.0.9-sources.jar; 赠送Maven依赖信息文件:groovy-3.0.9.pom; 包含翻译后的API文档:groovy-3.0.9-javadoc-API...

    Groovy入门经典.pdf

     Groovy提供类似于Java的语法结构,本地化支持映射和列表、方法、类,闭包和构造器等结构。由于具有动态弱类型,以及无缝访问JavaAPI等特性,Groovy语言非常适合子开发中小型规模的应用程序。  相对于Java语言,...

    groovy入门实例代码详细(包括字符串,map,闭包等等)

    这个代码包是老师上课给我们的,感觉对于初学者来说非常实用,里面包含了基本语法,包括字符串,整数,小数,数组,闭包等待,让你快速入门

    Groovy v2.4.13官方版

    使用该种语言不必编写过多的代码,同时又具有闭包和动态语言中的其他特性。  Groovy是JVM的一个替代语言(替代是指可以用 Groovy 在Java平台上进行Java 编程),使用方式基本与使用 Java代码的方式相同,该语言特别...

    apache-groovy-3.0.8.zip apache官网的groovy3.0.8版本

    apache-groovy-3.0.8.zip apache官网的groovy3.0.8版本,希望大家多多下载,apache-groovy-3.0.8.zip apache官网的groovy3.0.8版本,希望大家多多下载,apache-groovy-3.0.8.zip apache官网的groovy3.0.8版本,希望...

    [Groovy] Making Java Groovy 英文版

    Making Java Groovy is a practical handbook for developers who want to blend Groovy into their day to day work with Java It starts by introducing the key differences between Java and Groovy and how you...

    groovy入门经典,groovyeclipse 插件

    groovy入门经典,groovyeclipse 插件

    Java调用Groovy,实时动态加载数据库groovy脚本

    Java调用Groovy,实时动态加载数据库groovy脚本,java读取mongoDB的groovy脚本,加载实时运行,热部署

    apache-groovy-sdk-3.0.9.zip

    了解 Groovy 对 Java 语法的简化变形,学习 Groovy 的核心功能,例如本地集合、内置正则表达式和闭包。编写第一个 Groovy 类,然后学习如何使用 JUnit 轻松地进行测试。借助功能完善的 Groovy 开发环境和使用技能,...

    Groovy轻松入门—搭建Groovy开发环境

    Groovy轻松入门—搭建Groovy开发环境 Groovy轻松入门—搭建Groovy开发环境

    Groovy入门经典

    Groovy提供类似于Java的语法结构,本地化支持映射和列表、方法、类,闭包和构造器等结构。由于具有动态弱类型,以及无缝访问JavaAPI等特性,Groovy语言非常适合子开发中小型规模的应用程序。, 相对于Java语言,...

    groovy-2.3.6-installer

    groovy-2.3.6-installer windows安装版本

    精通 Groovy--下一代开发语言

    什么是 Groovy? Groovy 是 JVM 的一个替代语言 — 替代 是指可以用 Groovy 在 Java 平台上进行 Java 编程,使用方式基本与使用 Java 代码的方式相同。在编写新应用程序时,Groovy 代码能够与 Java 代码很好地结合,...

    Groovy.in.Action.2nd.Edition.1935182

    Groovy in Action, Second Edition is the undisputed definitive reference on the Groovy language. Written by core members of the Groovy language team, this book presents Groovy like no other can—from ...

    Groovy API docs 2.4.15 (CHM格式)

    使用该种语言不必编写过多的代码,同时又具有闭包和动态语言中的其他特性。 Groovy是JVM的一个替代语言(替代是指可以用 Groovy 在Java平台上进行 Java 编程),使用方式基本与使用 Java代码的方式相同,该语言特别...

    groovy和Java相互调用1

    Groovy 调用 Java 类groovy 调用 Java class 十分方便,只需要在类前导入该 Java 类,在 Groovy 代码中就可以无缝使用该

Global site tag (gtag.js) - Google Analytics