本文翻譯自Groovy,主要介紹了Groovy語言的閉包。
.1. 語法
.1.1. 定義閉包
閉包按照以下語法定義:
{ [closureParameters -> ] statements }
closureParameters
是一個可選,由“,”分割的參數列表,statements包含0或者更多數量不等的Groovy表達式。閉包的參數列表看起來和方法的很像,而且參數可以是無類型的。
當參數列表指定了參數,我們需要->
用於分隔參數列表和表達式。statements部分可以包含0,1或者更多的Groovy的表達式。
下面是一些關於定義閉包的例子:
{ item++ } 1
{ -> item++ } 2
{ println it } 3
{ it -> println it } 4
{ name -> println name } 5
{ String x, int y 6->
println "hey ${x} the value is ${y}"
}
{ reader 7->
def line = reader.readLine()
line.trim()
}
1.閉包引用一個item
變量
2.增加一個箭頭(->)可以從代碼塊有效的分隔參數
3.it是閉包的默認的參數,當沒有傳入參數時,it爲null
4.當只有一個參數時,一般默認使用it表示參數
5.當然也可以顯示的指定其他的名字
6.閉包接收兩個指定類型的參數
7.閉包可以包含多個表達式
.1.2. 閉包實例
閉包是groovy.lang.Closure類的實例,他可以賦給一個變量或者一個屬性作爲其他的變量。儘管他是一個代碼塊:
def listener = { e -> println "Clicked on $e.source" } 1
assert listener instanceof Closure
Closure callback = { println 'Done!' } 2
Closure<Boolean> isTextFile = {File it -> 3
it.name.endsWith('.txt')
}
1.閉包是groovy.lang.Closure類的實例,可以被賦給一個變量
2.如果不使用def,我們可以把閉包賦值給一個groovy.lang.Closure類型的變量
3.可選的,我們可以指定閉包的返回類型(Boolean)。
.1.3 調用閉包
閉包作爲一個匿名的代碼塊可以像其它方法那樣被調用。可以像下面這樣定義一個沒有輸入參數的閉包:
def code = {1,2,3}
閉包內部的代碼只有在被調用時纔會執行,他可以被當作一個變量:
ssert code() == 123
或者,你也可以顯式的使用call
方法:
assert code.call() == 123
如果閉包接收參數,原理也是一樣的:
def isOdd = { int i-> i%2 == 1 } 1
assert isOdd(3) == true 2
assert isOdd.call(2) == false 3
def isEven = { it%2 == 0 } 4
assert isEven(3) == false 5
assert isEven.call(2) == true 6
1.定義一個接收int
類型參數的閉包
2.閉包被直接調用
3.或者使用call
方法調用
4.使用默認的it
,同樣能夠實現相同的功能
5.使用arg
直接調用
6.或者是使用call
方法
與方法不同,閉包在被調用時總是返回一個結果。下一節將介紹如何聲明閉包的參數,什麼時候使用它們,以及什麼是it
默認參數
.2. 參數
..2.1. 普通參數
閉包的參數遵循與普通方法參數一樣的規則:
一個 可選的類型
一個name
一個可選的默認值
參數用逗號隔開:
def closureWithOneArg = { str -> str.toUpperCase() }
assert closureWithOneArg('groovy') == 'GROOVY'
def closureWithOneArgAndExplicitType = { String str -> str.toUpperCase() }
assert closureWithOneArgAndExplicitType('groovy') == 'GROOVY'
def closureWithTwoArgs = { a,b -> a+b }
assert closureWithTwoArgs(1,2) == 3
def closureWithTwoArgsAndExplicitTypes = { int a, int b -> a+b }
assert closureWithTwoArgsAndExplicitTypes(1,2) == 3
def closureWithTwoArgsAndOptionalTypes = { a, int b -> a+b }
assert closureWithTwoArgsAndOptionalTypes(1,2) == 3
def closureWithTwoArgAndDefaultValue = { int a, int b=2 -> a+b }
assert closureWithTwoArgAndDefaultValue(1) == 3
.2.2 隱式參數
當閉包沒有顯示的定義參數列表時(->
),閉包總會默認定義一個隱式的參數(it
),如下:
def greeting = { "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
下面是完整的寫法:
def greeting = { it -> "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
如果你想聲明一個不接受參數的閉包,限制調用時不帶參數,那麼你必須顯式聲明一個空參數列表:
def magicNumber = { -> 42 }
// this call will fail because the closure doesn't accept any argument
magicNumber(11)
.2.3 可變參數
閉包可以像其它方法那樣聲明一個可變的參數列表。當參數列表的最後一個參數是長度可變的(或者是一個數組),那麼這個閉包就能接收數量不定的參數:
def concat1 = { String... args -> args.join('') } 1
assert concat1('abc','def') == 'abcdef' 2
def concat2 = { String[] args -> args.join('') } 3
assert concat2('abc', 'def') == 'abcdef'
def multiConcat = { int n, String... args -> 4
args.join('')*n
}
assert multiConcat(2, 'abc','def') == 'abcdefabcdef'
1.閉包可以接收數量可變的String參數
2.可以在調用時使用任意數量的參數,而且不用被包裝成一個數組
3.使用數組也能達到同樣的效果
4.只要最後一個參數是一個數組或者可變長度的的參數類型
.3. 委託用法 (Delegation strategy)
.3.1. Groovy閉包 VS lambda表達式
Groovy定義閉包(作爲Closure類的實例),與Java8的lambda有很大的不同。
Delegation 在Groovy閉包中是一個關鍵的概念(lambda中沒有與之對應的概念)。閉包中改變委託或者改變委託策略的能力使得我們可以在Groovy中設計美麗的領域特定語言(DSLs)
.3.2 this,owner ,delegate
要理解delegate的概念,我們首先要說明this在閉包內的含義。閉包實際上定義了三個截然不同的變量(當然it
也是):
this
對應定義閉包的類,不論閉包被嵌套了多少層閉包,都指向此類owner
對應定義閉包的對象,他可能是閉包或者類。當只有一層閉包時,等同於this
delegate
對應於調用方法的第三方對象,默認等同於owner(可變)
.3.2.1. this
在一個閉包內調用getThisObject
方法將返回定義此閉包的類,與使用this的效果一樣:
class Enclosing {
void run() {
def whatIsThisObject = { getThisObject() } 1
assert whatIsThisObject() == this 2
def whatIsThis = { this } 3
assert whatIsThis() == this 4
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { this } 5
}
void run() {
def inner = new Inner()
assert inner.cl() == inner 6
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { this } 7
cl()
}
assert nestedClosures() == this 8
}
}
1.一個閉包被定義Enclosing
類,返回getThisObject
2調用閉包將會返回一個定義此閉包的Enclosing
類的實例
3.通常,你只會想使用this
4.他返回完全相同的類
5.如果閉包被定義在一個內部類
6.閉包內的this將返回次內部類,不是頂級的那個
7.嵌套的閉包,就像cl
被定義在nestedClosures
閉包內
8.this
對應於最近的外部類,而不是閉包
當然也可以在閉包內通過this調用類的方法:
class Person {
String name
int age
String toString() { "$name is $age years old" }
String dump() {
def cl = {
String msg = this.toString() 1
println msg
msg
}
cl()
}
}
def p = new Person(name:'Janice', age:74)
assert p.dump() == 'Janice is 74 years old'
1.閉包通過this
調用toString()方法,實際上調用封閉對象的toString()方法,也就是Person實例。
.3.2.2. owner
閉包內的owner
和在閉包內定義的this
非常相似,除了一點微妙的差別:
他將返回定義此閉包的對象,不管他是類或者閉包:
class Enclosing {
void run() {
def whatIsOwnerMethod = { getOwner() } 1
assert whatIsOwnerMethod() == this 2
def whatIsOwner = { owner } 3
assert whatIsOwner() == this 4
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { owner } 5
}
void run() {
def inner = new Inner()
assert inner.cl() == inner 6
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { owner } 7
cl()
}
assert nestedClosures() == nestedClosures 8
}
}
1.一個閉包被定義在Enclosing
類,返回getOwner
2.調用閉包將返回定義此閉包的Enclosing
類的實例
3.通常,你只會想使用owner
4.他將返回相同的對象
5.如果閉包被定義在一個內部類
6.閉包內的owner
將返回次內部類,而不是最頂級的類
7.如果是被嵌套在閉包內,就像被嵌套在閉包nestedClosures
內部的cl
8.owner
對應閉包,這就是owner
和this
的不同
.3.2.3. delegate
閉包的delegate可以通過使用delegate或者調用getDelegate方法訪問。
在Groovy語言中這是一個強大的概念。this
和owner
被定義在閉包的語法範圍內,而delegate
是被定義在使用閉包的對象上。默認,delegate
被設置爲:owner
:
class Enclosing {
void run() {
def cl = { getDelegate() } 1
def cl2 = { delegate } 2
assert cl() == cl2() 3
assert cl() == this 4
def enclosed = {
{ -> delegate }.call() 5
}
assert enclosed() == enclosed 6
}
}
1.通過閉包調用getDelegate
方法,我們可以得到delegate
2.或者使用delegate
3.返回一樣的對象
4.返回類
5.在嵌套的閉包內
6.delegate
和owner
作用相同
閉包的delegate
可以被改變成任何對象。讓我們來做一個實驗驗證他,首先我們聲明兩個沒有子類的類,且都定義一個name
成員變量:
class Person {
String name
}
class Thing {
String name
}
def p = new Person(name: 'Norman')
def t = new Thing(name: 'Teapot')
然後定義一個返回delegate
對應的對象的name
成員變量的閉包:
def upperCasedName = { delegate.name.toUpperCase() }
然後通過改變閉包的delegate
,我們可以發現目標對象發生了改變:
upperCasedName.delegate = p
assert upperCasedName() == 'NORMAN'
upperCasedName.delegate = t
assert upperCasedName() == 'TEAPOT'
一定意義上說,在閉包內部使用一個自定義的變量(target)也能有同樣的效果:
def target = p
def upperCasedNameUsingVar = { target.name.toUpperCase() }
assert upperCasedNameUsingVar() == 'NORMAN'
然而,他們有很大的不同點:
在最後的例子,
target
是一個閉包內部的局部變量而
delegate
可以被顯示的使用,也就是說可以用不加前綴的方式調用delegate
,在下一節將就此做詳細的說明
.3.2.4. Delegation 策略( strategy)
在閉包內,每當我們沒有顯示指定對象的去訪問一個屬性,那麼delegate 策略是involved:
String name
}
def p = new Person(name:'Igor')
def cl = { name.toUpperCase() } 1
cl.delegate = p 2
assert cl() == 'IGOR' 3
1.在閉包內,屬性name
是沒有被定義的,閉包被調用時,會報錯
2.當我們把閉包的delegate
變成一個Person
類的實例時
3.方法可以被正常的使用
這個代碼成功的原因是閉包內的name
屬性將會被顯示的指定給與delegate對應的對象(The reason this code works is that the name property will be resolved transparently on the delegate object!)!這個特性對於在閉包內部使用屬性或者調用方法是很有幫助的。不需要我們顯示的指定delegate
,recelver:調用將會成功,因爲在閉包內默認的delegate策略就是如此。閉包實際上定義了多種resolution strategies ,我們可以根據實際需要作出選擇:
Closure.OWNER_FIRST
是默認策略。如果屬性或者方法存在於owner內,那麼他可以被owner調用,如果不存在,則會嘗試在delegate類中查找Closure.DELEGATE_FIRST
顛倒了默認邏輯:delegate是第一優先級,其次纔是ownerClosure.OWNER_ONLY
將僅僅在owner查找需要的屬性或者方法:delegate會被忽略Closure.DELEGATE_ONLY
將僅僅在delegate查找需要的屬性或者方法:owner會被忽略Closure.TO_SELF
可以被用於當開發人員需要使用先進的元數據編程技術和希望實現一個自定義的選擇策略時:這個選擇將不是owner或者delegate,而僅僅是closure類自己。當我們實現了自己的Closure
子類時,他纔是有意義的。
讓我們通過下面的代碼瞭解下默認的owner first
:
class Person {
String name
def pretty = { "My name is $name" } 1
String toString() {
pretty()
}
}
class Thing {
String name 2
}
def p = new Person(name: 'Sarah')
def t = new Thing(name: 'Teapot')
assert p.toString() == 'My name is Sarah' 3
p.pretty.delegate = t 4
assert p.toString() == 'My name is Sarah' 5
1.在這個例子中,我們定義了一個閉包,在閉包內引用了一個變量name
2.在Person
和Thing
類中都定義了name
成員變量
3.使用默認的策略,name
變量首先在owner中被尋找。
4.我們改變delegate爲Thing
類的實例t
5.結果沒有改變:name
變量首先在閉包的owner
屬性所對應的對象內尋找
然而,我們可以更改閉包的選擇策略:
p.pretty.resolveStrategy = Closure.DELEGATE_FIRST
assert p.toString() == 'My name is Teapot'
通過改變resolveStrategy
,我們可以修改Groovy選擇引用時的方式:在這個例子裏,name
將首先在delegate中尋找,如果沒有發現,再在owner中尋找。因爲name
在delegate中有被定義(一個Thing類的實例
),所以name
變量將會引用delegate內的name
成員變量。
如果delegate(resp,owner)沒有這樣的一個方法或者屬性,那麼delegate first
,delegate only
,owner first
和owner only
的不同點將會被證明:
class Person {
String name
int age
def fetchAge = { age }
}
class Thing {
String name
}
def p = new Person(name:'Jessica', age:42)
def t = new Thing(name:'Printer')
def cl = p.fetchAge
cl.delegate = p
assert cl() == 42
cl.delegate = t
assert cl() == 42
cl.resolveStrategy = Closure.DELEGATE_ONLY
cl.delegate = p
assert cl() == 42
cl.delegate = t
try {
cl()
assert false
} catch (MissingPropertyException ex) {
// "age" is not defined on the delegate
}
在這個例子裏,我們定義了兩個類:Person
類有name
和age
兩個成員變量;Thing
類只有name
一個成員變量,在Person
類裏還定義了一個引用了age
變量的閉包。我們把默認的選擇策略從owner first
改變爲delegate only
。因爲閉包的owner是Person
類的實例,我們可以測試一下如果delegate是Person
類的實例,調用閉包時會成功。但是如果我們改變delegate爲Thing
類的實例時,調用閉包將會失敗,且報groovy.lang.MissingPropertyException
異常。儘管閉包是在Person
類裏被定義的,owner是沒有被使用。
在官網可以找到如何使用這個功能來開發DSLs
的詳細介紹.
.4. 在GString中使用閉包
看下面的代碼:
def x = 1
def gs = "x = ${x}"
assert gs == 'x = 1'
代碼將會按照我們所預期的那樣運行,但是如果增加下面的代碼,將會發生什麼呢:
x = 2
assert gs == 'x = 2'
你將看到運行失敗!有兩個原因:
Gstring只懶加載
toString
的值${X}
在GString中不表示閉包而僅僅是一個符號$x
,在GString被創造時就確定。
在我們的例子中,GString
被創造時包含一個表達式(引用x)。當GString
被創造時,x的值爲1,所以GString
被創造時包含值1.當assert觸發時,GString
被確定爲1通過toString
被轉變爲String
。當我們把x改變爲2時。我們確實改變了x的值,但是他們已經是不同的對象,GString
仍然引用老的哪一個x對象。
Gstring僅僅改變他的String
,如果他引用的對象的值是發生改變,nothing will change。
如果你想在GString中擁有一個真正的閉包,例如對變量實行延遲加載。你需要使用一個不一樣的語法${->x}
,如下面代碼:
def x = 1
def gs = "x = ${-> x}"
assert gs == 'x = 1'
x = 2
assert gs == 'x = 2'
讓我們看一下這變化的代碼有什麼不一樣:
class Person {
String name
String toString() { name } 1
}
def sam = new Person(name:'Sam') 2
def lucy = new Person(name:'Lucy') 3
def p = sam 4
def gs = "Name: ${p}" 5
assert gs == 'Name: Sam' 6
p = lucy 7
assert gs == 'Name: Sam' 8
sam.name = 'Lucy' 9
assert gs == 'Name: Lucy' 10
1.Person
類有一個返回name
成員變量的toString
方法
2.我們創造一個name叫Sam的Person類實例
3.我們創造一個name叫Lucy的Person類實例
4.sam變量被賦值給變量p
5.我們創造了一個閉包,在閉包內引用了p
,也就是sam
6.所以當我們運行次閉包時,他返回sam
7.如果我們把Lucy賦值給p
8.gs任然等於Sam,因爲p的價值sam在GString被創造時就已經確定
9.所以如果我們改變sam的名字爲Lucy
10.這樣GString的值被改變了。
(這段話我也不是很明白,按我的理解:GString裏的表達式在被創造時就變成了引用的變量所對應的變量,例如上邊例子中的1和下邊例子中的sam,所以上邊例子改變X沒用,但是在下邊的例子裏,改變sam的屬性,結果還是會發生變化,也就是因此。如果大家有什麼不一樣的簡介,歡迎留言。)
so,如果你不想依賴改變對象或者包裝對象,你就必須在GString中使用閉包,通過聲明一個空的參數列表:
class Person {
String name
String toString() { name }
}
def sam = new Person(name:'Sam')
def lucy = new Person(name:'Lucy')
def p = sam
// Create a GString with lazy evaluation of "p"
def gs = "Name: ${-> p}"
assert gs == 'Name: Sam'
p = lucy
assert gs == 'Name: Lucy'
.5. 閉包強制?(Closure coercion)
閉包可以被裝換爲接口或者單例-抽象方法類型。在官網有更詳細的說明。
.6. 函數式編程
就像lambda表達式在Java8中一樣,閉包是Groovy語言函數式編程範例的核心。一些函數式編程操作功能在Closure
類就可以直接使用,就像本節說明的那樣。
.6.1 柯里化 Currying
.6.2 Memoization
我們可以用Memoization技術來代替函數中太多的遞歸調用。Memoization是一種可以緩存之前運算結果的技術,這樣我們就不需要從新計算那些已經計算過的結果。
.6.3 Composition
.6.4 Trampoline
.6.5 方法指針
把常規方法當做閉包是一個很常用的操作。例如,你可能想要使用閉包的柯里化能力,但是這在平常的方法中是不被允許的。在Groovy中,你可以通過方法指針操作把任何的方法變成閉包。
錯誤是難免的,如果讀者發現了,還望留言指出!有什麼不甚理解的地方也歡迎留言討論