分享

Unmi学习Groovy之闭包

 sttx 2008-11-19

原创 Unmi 学习 Groovy 之闭包收藏

 | 旧一篇: Unmi 学习 Groovy 之正则表达式

一. 认识闭包

将代码块作为方法参数进行传递,这种机制就叫做闭包。闭包可以引用在创建闭包的范围中可见的变量。最近关于闭包的讨论也比较多,闭包能使语言更具灵动性,在动态脚本语言中较广泛的支持,如 Perl、Python、Ruby、JavaScript,还有我们的 Groovy。

有些语言能把函数作为参数传递,如 JavaScript 的回调函数,Python,甚至是 C++ 的函数指针。而 Java 在这方面又略逊一筹,需搬动一个匿名的内部类来实现类似的功能,内部类只能访问外部声明为 final 的变量。不过有呼声要在 Java SE 7 中增加闭包特性,让我们试目以待吧。

Groovy 这回大概是从 Ruby 那儿偷得闭包的语法。前面说这么多,其实你看到了就会发现,其实闭包很简单的,不信,请看:

Code   ViewCopyPrint
  1. logo = {   
  2.    println "Closure";   
  3. }   
  4. logo.call();   
  5. logo();  

用大括号括起来的,给它一个名字 logo 的那段就是一个闭包(有点像 Java 中的语句块);闭包可以通过执行它的 call() 方法调用,或者直接把它当作一个常规方法对待。闭包也可以没有名字,比如下面要讲的集合方法中用的闭包。


二. 闭包的参数

闭包可以有参数。如果是一个参数的话,该参数就直接映射到名为 it 的变量,如:

Code   ViewCopyPrint
  1. discount = { it * 0.8}; //当然你不想用 it 也行,那就指定了 { name -> name * 0.8 }   
  2. println discount(200);  //输出 160.0  

如果是多个参数的闭包,则在闭包中用 "->" 把参数列表和实现隔开,如:(当然一个参数也可以这么方式定义的)

Code   ViewCopyPrint
  1. totalPrice = {subtotal, tax, discount ->   
  2.     subtotal * discount * (1+tax);   
  3. }   
  4. println totalPrice(100,0.2,0.3); //输出为 36.00  

我看到这里,怎么越来越觉得闭包那么像 C/C++ 中的宏定义呢?

调用闭包时,如果参数数量不对会抛出 IncorrectClosureArgumentException;不过对于一个参数的闭包,可以少传但不能多传参数,不传参数时,闭包认为 it 为 null。


三. 闭包的传递

闭包在实现上是扩展自 groovy.lang.Closure 类,因为它们是类,所以可以作为参数传递给其他的方法,下面就来看一个例子:

Code   ViewCopyPrint
  1. class Handler{   
  2.     def action;           //用来保存一个闭包的实例   
  3.     def handle(object){   
  4.        action(object);    //调用闭包,并传入参数 object   
  5.     }   
  6. }   
  7.   
  8. //声明两个闭包,都是可接受一个参数   
  9. log = {object->println "Action occured: ${object}"};   
  10. save ={object->println "Object saved to the database: ${object}"};   
  11.   
  12. logHandler = new Handler(action:log);   //创建时初始化 action 为 log 闭包   
  13. saveHandler = new Handler(action:save); //创建时初始化 action 为 save 闭包   
  14.   
  15. obj = "Status changed";   
  16. logHandler.handle(obj);   //执行 action 所指向的闭包 log   
  17. saveHandler.handle(obj);  //执行 action 所指向的闭包 save  

输出为:

Action occured: Status changed
Object saved to the database: Status changed

上面这种用法可用在事件触发、回调操作或策略模式中。


四. 闭包与变量作用域

在 Groovy 中闭包可以访问创建它的上下文中定义的变量,可在闭包中修改;而且闭包内部定义的变量在周围的上下文中也是可见的。看如下代码片断:

Code   ViewCopyPrint
  1. tax = 0.2;   
  2. c1 = {   
  3.     tax += 0.1;     //闭包中能访问并修改外围的变量   
  4.     discount = 0.2;   
  5. }   
  6.   
  7. c1();   
  8. println tax;   
  9. println discount;  //闭包中的变量在外围也能访问到  

执行的结果是:

0.3
0.2


五. 闭包与集合操作

当闭包与集合整合时,它们展现了真正的威力。Groovy 中的 List、Map、String、和 Range 都有接受闭包参数的额外方法,譬如字符串也是字符的集合,也可以这么用。

涉及到的集合操作有 each、collect、inject、find、findAll、every、any。下面以例子来帮助理解这些方法的使用。

Code   ViewCopyPrint
  1. [1,2,3].each {print it+1}; //List 的 each 方法,输出 234   
  2.   
  3. ["Name":"Unmi","Skill":"Java"].each{   
  4.     print "${it.key}: ${it.value} " //Map 的 each 方法,输出 Name: Unmi Skill: Java   
  5. };    
  6.   
  7. "Groovy".each {print it.toUpperCase()};  //String 的 each 方法,输出 GROOVY   
  8.   
  9. (1..3).each {print it+1};    //Range 的 each 方法,输出 234  


注意:上面的闭包传给方法,如果是在 GroovyShell 中逐行敲入代码时,起始花括号"{" 必须与调用方法在同一行上,比如说写成下面的方式就有问题:

Code   ViewCopyPrint
  1. [1,2,3].each    
  2. {   
  3.     print it+1  
  4. }  

然而如果你想要在单独一行上指定起始花括号,可以使用下语法(调用方法后加个圆括号):

Code   ViewCopyPrint
  1. [1,2,3].each (   
  2.   {   
  3.       print it+1  
  4.   }   
  5. )  

要是写在 .groovy 文件中或是在 GroovyConsole 中不受这个限制,但是为规范和不致换个环境又出错,还是应该让起起始花括号"{" 与调用方法在同一行。

其实怎么去理解这种要求呢?主要是两点:

其一是为什么我们通常不写这个圆括呢?那是 Groovy 允许方法调用时省略圆括号

还有就是在 GroovyShell 下,如果输完方法名,如 each,然后马上回车,就报错

ERROR groovy.lang.MissingPropertyException: Exception evaluating property 'each' for java.util.ArrayList, Reason: groovy
.lang.MissingPropertyException: No such property: each for class: java.lang.Integer
at groovysh_evaluate.run (groovysh_evaluate:1)
...

只有方法名后面加个圆括号,它才知道是参数列表开始,直至匹配的右圆括号结束,或是加了花括,它也知道是一个闭包的开始,直到匹配的花括号结束。



1) collect() 方法利用指定的闭包转换集合的元素

Code   ViewCopyPrint
  1. println ([1,2,3].collect{it*2});  //输出 [2,4,6]   

2) inject() 方法,可将前一次的迭代的结果传递给下一次迭代,请看例子:

Code   ViewCopyPrint
  1. [1,2,3].inject 0, {prevItem,item ->   
  2.     println "Previous: ${prevItem} - Current: ${item}"  
  3.     return item; //把当前值返回作为下次迭代时的注入值   
  4. }  

输出为:

Previous: 0 - Current: 1
Previous: 1 - Current: 2
Previous: 2 - Current: 3

在当前迭代中能知道上一次所迭代的值。inject 接受两个参数,第一次迭代中使用的注入值,和将要使用的闭包。这个闭包也必须定义两个参数,分别为上次迭代的注入值和当前的元素。注意在闭包中必须返回下次迭代中注入的值。否则,就会假设为 null。

为了现好的理解发这个注入语法,我们将上述例子分开来写成如下:

Code   ViewCopyPrint
  1. list = [1,2,3]   
  2. closure = {prevItem,item ->   
  3.     println "Previous: ${prevItem} - Current: ${item}"  
  4.     return item; //把当前值返回作为下次迭代时的注入值   
  5. }   
  6. list.inject(0,closure) //同时给 inject 方法调用参数框上圆括号(可选的)  

3) find() 方法找到符合闭包中所定义条件的第一次出现的元素,找不到则返回 null

Code   ViewCopyPrint
  1. println([2,5,7,9].find { it>5 }); //输出为 7  

4) findAll() 方法是返回所有符合闭包中所定义条件的元素所组成的集合

Code   ViewCopyPrint
  1. println([2,5,7,9].findAll { it>5 }); //输出为 [7, 9]  

5) every() 方法检查集合中是否每一个元素都符合闭包中指定的条件,是则返回 true,否则为 false

Code   ViewCopyPrint
  1. println([2,5,7,9].every { it>1 }); //输出为 true,如果闭中写成 it>3 则输出 false   

6) any() 方法则检查集合中是否有一个元素符合闭包中指定的条件,有则返回 true,否则为 false

Code   ViewCopyPrint
  1. println([2,5,7,9].any { it>8 }); //输出为 true  


参考:1. 《Java 脚本编程语言、框架与模式》第 4 章

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多