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

你可能感兴趣的:(编程,C++,c,C#,groovy)