28. Groovy 闭包知识学习-第三篇 终篇

2023-02-23 17:41:46 浏览数 (2)

1. 介绍

本篇内容为Groovy学习笔记第28篇,继续学习闭包相关知识。前面介绍了闭包的创建与使用,以及闭包的委托等和授权策略。

今天继续深入学习闭包相关知识。

2. GStrings中的闭包

我们知道在Groovy中有两种字符串对象,一个是java.lang.String , 一个是groovy.lang.GString。关于这两个字符串的对象。我在3. Groovy 语法-字符串学习 (zinyan.com)中有详细介绍。这里就不多讲了。

这里主要讲在闭包中的GStrings对象。我们如果要创建一个GString对象,示例代码如下:

代码语言:javascript复制
def x = 1
def gs = "x = ${x}"
println gs // 输出:x = 1

而我们如果修改x的值,这个时候gs 并不会实时发生变化的。示例如下:

代码语言:javascript复制
x = 2
println gs //输出:x = 1

我们会发现gs的值并没变成x=2,因为GString只懒惰地计算值的toString表示,在GString创建时GString中的语法{x}并不表示闭包,而是x的表达式。

所以,并不是所有花括号表达的就是闭包对象,要注意区分。

在上面的例子中,GString是用一个引用x的表达式创建的。当GString被创建时,x的值是1,因此GString的值是1。当执行println时,GString被求值,并且使用toString将1转换为String。当我们将x更改为2时,我们确实更改了x的值,但它是一个不同的对象,GString仍然引用旧的对象。这也就是为什么最后还是输出:x=1的原因了。当GString引用的值发生变化时,GString才会改变它的toString表示。如果引用改变,什么也不会发生。

如果你需要一个在闭包中可以发生变化的GString,例如强制延迟计算变量,你需要使用替换语法${→x}。示例如下:

代码语言:javascript复制
def x = 1
def gs = "x = ${-> x}"
println gs  //输出:x = 1

x = 2
println gs //输出:x = 2

让我们用下面的代码来说明它说明一下,普通的GString的变化:

代码语言:javascript复制
//创建一个类
class Person {
    //定义了一个String类型的name属性
    String name
    String toString() { name }          
}
//创建对象
def zinyan = new Person(name:'zinyan.com')        
def zStuden = new Person(name:'Z同学')      
def p = zinyan                             
def gs = "Name: ${p}"
println gs       //输出:Name: zinyan.com      
p = zStuden    //将对象p 的引用指向zStuden                      
println gs    //输出:Name: zinyan.com

zinyan.name = 'zinyan'                       
println gs;  //输出:Name: zinyan

因此,如果不想依赖于突变对象或包装对象,你必须在GString中使用闭包,显式声明一个空参数列表:

代码语言:javascript复制
//创建一个类
class Person {
    //定义了一个String类型的name属性
    String name
    String toString() { name }          
}
//创建对象
def zinyan = new Person(name:'zinyan.com')        
def zStuden = new Person(name:'Z同学')      
def p = zinyan                             
def gs = "Name: ${ -> p}" //这个时候就变成了一个带闭包的GString对象了
println gs       //输出:Name: zinyan.com      
p = zStuden    //将对象p 的引用指向zStuden                      
println gs    //输出:Name: Z同学

zinyan.name = 'zinyan'                       
println gs;  //输出:Name: Z同学

总结:我们如果在GString中通过${X}来引用变量。当GString生成完毕后,后面再变化X的数据,并不会影响GString的结果。 我们如果想在GString中添加可以动态变换的,就使用闭包引用的方式${→x}

3. 强制转换

闭包可以转换为接口或单一抽象方法类。前面学习过as关键字。我们可以将闭包对象通过as关键字转换为接口对象或单一抽象方法的类对象。

示例如下:

代码语言:javascript复制
//创建一个接口对象
interface Predicate<T> {
    boolean accept(T obj)
}
//创建一个单一抽象方法类
abstract class Greeter {
    abstract String getName()
    void greet() {
        println "Hello, $name"
    }
}

Predicate filter = { it.contains 'G' } as Predicate
assert filter.accept('Groovy') == true

Greeter greeter = { 'Groovy' } as Greeter
greeter.greet()

而从Groovy 2.2.0开始,as Type表达式是可选的。你可以省略它,简单地写:

代码语言:javascript复制
Predicate filter = { it.contains 'G' }
assert filter.accept('Groovy') == true

Greeter greeter = { 'Groovy' }
greeter.greet()

3.1 任意类型的强制闭包

除了SAM类型之外,闭包还可以被强制为任何类型和特定接口。示例如下:

代码语言:javascript复制
//定义一个接口对象
interface FooBar {
    int foo()
    void bar()
}

我们可以使用as关键字强制一个闭包进入接口:

代码语言:javascript复制
def impl = { println 'ok'; 123 } as FooBar

assert impl.foo() == 123
impl.bar()

将生成一个类,其中所有方法都使用闭包实现。

4. 函数式编程

闭包,就像Java 8中的lambda表达式一样,是Groovy中函数式编程范式的核心。函数上的一些函数式编程操作可以直接在Closure类上使用。

4.1 局部套用-curray

Groovy中,套用指的是部分应用的概念。由于Groovy对闭包应用了不同的作用域规则,所以它并不符合函数式编程中curry的真正概念。在Groovy中curry将允许您设置闭包的一个参数的值,它将返回一个接受一个少参数的新闭包。

左侧套用:就是设置闭包最左边的参数,就像下面的例子:

代码语言:javascript复制
//创建一个闭包对象,传入int 和String 参数,并计算来两者相乘的结果
def nCopies = { int n, String str -> str*n }    
def twice = nCopies.curry(2)                    
println twice('zinyan') // 输出:zinyanzinyan
println twice('zinyan')==nCopies(2,'zinyan') //输出:true

例如上面的示例中,我们本来需要往闭包中传入两个参数,但是我使用curry只传入最左边的参数,而右边的参数可以在之后进行传入。

右侧套用:类似于左curry,可以设置闭包最右边的参数,使用关键方法为rcurry:

代码语言:javascript复制
def nCopies = { int n, String str -> str*n }    
def blah = nCopies.rcurry('zinyan.com')   
println blah(2)            //输出:zinyan.comzinyan.com

那么,我们如果有多个参数,例如三个入参该怎么用?那就需要使用基于索引的套用:

代码语言:javascript复制
//创建一个三个入参的闭包对象,计算三者相乘的结果
def volume = { double l, double w, double h -> 
 println "参数结果:l=$l,w=$w,h=$h"   //输出:参数结果:l=3.0,w=2.0,h=4.0
l*w*h }   
//创建一个基于索引的curry,例如下面,在索引1的地方插入参数   
def fixedWidthVolume = volume.ncurry(1, 2d)

//补充剩下的参数,并输出  
println fixedWidthVolume(3d,4d) //输出结果:24.0

//在索引1的地方开始,插入多个参数。
def fixedWidthAndHeight = volume.ncurry(1, 2d, 4d)
println  fixedWidthAndHeight(3d)        //输出:24.0

4.2 记忆化-memoize

记忆化允许对闭包调用的结果进行缓存。如果一个函数(闭包)的计算很慢,但这个函数将经常使用相同的参数被调用。

一个典型的例子是斐波那契集合。一个简单的实现可能是这样的:

代码语言:javascript复制
def fib
//创建一个闭包对象,进行斐波那契数列, 方法中使用了递归,可以看到fib对象被递归调用
fib = { long n -> n<2?n:fib(n-1) fib(n-2) }
println fib(10)  // 输出55

这是一个简单的实现,因为'fib'经常用相同的参数递归调用,导致一个指数算法:

  • 计算fib(15)需要fib(14)fib(13)的结果
  • 计算fib(14)需要fib(13)fib(12)的结果

等等,需要前面的计算结果,一步步计算才能达到最终的结果。

由于调用是递归的,可以看到我们在上面将一次又一次地计算相同的值,这个简单的实现可以通过使用memoize缓存调用的结果来提高效率:

代码语言:javascript复制
def fib
//创建一个闭包对象,进行斐波那契数列,使用了memoize进行记忆缓存
fib = { long n -> n<2?n:fib(n-1) fib(n-2) }.memoize()

println fib(25)  // 输出:75025

PS:缓存使用实参的实际值工作。这意味着,如果对除原语类型或方框原语类型以外的其他类型使用记忆,则应该非常小心。

缓存的行为可以使用其他方法来调整:

  • memoizeAtMost:将生成一个新的闭包,它最多缓存n个值
  • memoizeAtLeast:将生成一个新的闭包,它至少缓存n个值
  • memoizeBetween:将生成一个新的闭包,它至少缓存n个值,最多缓存n个值

所有memoize变体中使用的缓存都是LRU缓存。

4.3 组合

闭包组合对应于函数组合的概念,也就是说,通过组合两个或多个函数(链接调用)来创建一个新函数,如下例所示:

代码语言:javascript复制
def plus2  = { it   2 } // 创建一个闭包对象, it是默认传参的替代词
def times3 = { it * 3 }

// 两个闭包对象的结果进行 移位运算(https://zinyan.com/?p=395)
def times3plus2 = plus2 << times3 

println times3plus2(4) //输出:14
println times3plus2(4) == plus2(times3(4)) //输出true

// 两个闭包对象的结果进行 移位运算(https://zinyan.com/?p=395)
// 移位运算符 左右两边的顺序是会影响结果的
def plus2times3 = times3 << plus2
println plus2times3(5) //输出:21
println plus2times3(5) == times3(plus2(5))  //输出:true
// 反向合成
println times3plus2(3) == (times3 >> plus2)(3) //输出:true

有关组合的内容,通过示例进行理解了。

4.4 弹性变换-trampoline

递归算法通常受到一个物理限制:最大堆栈高度。例如,如果我们调用了一个递归调用自身过深的方法,您最终将收到一个StackOverflowException

例如我们上面介绍的斐波拉契数计算。我们填写1024,就会由于超过堆栈触发

StackOverflowException

在这种情况下,一种有用的方法是使用Closure及其弹性变换功能。

Closure被包裹在TrampolineClosure中。在调用时,TrampolineClosure将调用等待其结果的原始闭包。如果调用的结果是TrampolineClosure的另一个实例(可能是调用trampoline()方法的结果),则闭包将再次被调用。这种对返回的TrampolineClosure实例的重复调用将持续下去,直到返回一个非TrampolineClosure的值。这个值将成为最终结果。这样,调用是串行进行的,而不是填充堆栈。

下面是一个使用trampoline()实现阶乘函数的例子:

代码语言:javascript复制
def factorial
factorial = { int n, def accu = 1G ->
    if (n < 2) return accu
    //使用trampoline 进行弹性控制
    factorial.trampoline(n - 1, n * accu)
}
factorial = factorial.trampoline()

println factorial(1) //输出:1
println factorial(3)  // 输出:6
println factorial(1000)  //输出:402387260077093773543702433923003985719374864210714632543799910429938512....

4.5 方法指针

能够使用常规方法作为闭包通常是实用的。例如,可能希望使用闭包的curry功能,但普通方法不具备这些功能。在Groovy中,可以使用方法指针操作符从任何方法获得闭包。

方法指针关键字为:.&操作符,它可以用于在变量中存储对方法的引用。可以通过我的这篇文章了解方法指针运算符8. Groovy 运算符-条件运算符,对象运算符学习 (zinyan.com)

5. 小结

到这里,Groovy中关于闭包的相关基础知识就介绍完毕了。下一篇将开始学习和分享Groovy语义相关内容。

以上内容可以通过Groovy官方文档:Groovy Language Documentation (groovy-lang.org) 进行学习了解。

如果觉得总结的还比较清晰,希望能够给我点个赞鼓励一下。谢谢。

0 人点赞