闭包的正式定义形式

介绍

         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" , "three" )
closure.call ( "one" , "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 < files.length; i++) {
           if (files[i].isDirectory()) {
               closure.call(files[i]);
           }
       }
   }
}


       如果要传递更多的参数,则这些参数必须放在上面提到的第一个参数和最后一个参数之间。

---------------------------------------------------------

相关文章

背景:    8月29日,凌晨4点左右,某服务告警,其中一个...
https://support.smartbear.comeadyapi/docs/soapui/steps/g...
有几个选项可用于执行自定义JMeter脚本并扩展基线JMeter功能...
Scala和Java为静态语言,Groovy为动态语言Scala:函数式编程,...
出处:https://www.jianshu.com/p/ce6f8a1f66f4一、一些内部...
在运行groovy的junit方法时,报了这个错误:java.lang.Excep...