閉包官方文檔:Closures
這一章節介紹Groovy閉包。Groovy中的閉包是開放,匿名,且可以帶參數的代碼塊,傳回一個值并可被指派給變量。一個閉包可以是被大括号包圍的幾個變量的聲明。不太正式的閉包,在Groovy語言中可以包含無限多個定義在大括号外的變量。标準的閉包定義,提供了一系列的優點,将會在下面介紹。(這一段翻譯的太差,請略過)
1.文法 Syntax
1.1定義一個閉包 Defining a closure
一個閉包的定義遵從一下文法規則:
{ [closureParameters -> ] statements }
[closureParameters->]是一個可選擇的逗号間隔的一系列參數,statements是一個或者多個Groovy聲明。閉包的參數和方法的參數清單相似,參數可以是明确類型的也可以是不明确的。
當參數清單是指定的,->是必須的,其被當做是參數和閉包體的分隔。聲明由0,或多個Groovy聲明組成。
下面是一些有效的閉包定義類型:
{ item++ }//一個指向名為item的變量的閉包
{ -> item++ }//顯示的間隔code通過使用箭頭(->)
{ println it } //一個使用隐式參數(it)的閉包
{ it -> println it }//it是一個顯式變量,這種選法是可選的
{ name -> println name }//這種寫法較好
{ String x, int y -> //閉包接受兩個明确類型的參數
println "hey ${x} the value is ${y}"
}
{ reader -> // 一個閉包可以接受多行聲明
def line = reader.readLine()
line.trim()
}
1.2.作為對象的閉包 Closures as an object
一個閉包是groovy.lang.Closure的執行個體,可被指派給一個變量或字段,盡管看起來像一個代碼塊:
def listener = { e -> println "Clicked on $e.source" } //你可以将閉包指派給一個變量
assert listener instanceof Closure//閉包是groovy.lang.Closure的執行個體
Closure callback = { println 'Done!' }
Closure isTextFile = {//閉包的傳回值類型是可選的
File it -> it.name.endsWith('.txt')
}
1.3. 調用閉包 Calling a closure
閉包,是匿名的代碼塊,可以像方法一樣被調用。如果你定義一個閉包像這樣沒有參數:
def code = { 123 }
然後閉包内的代碼将會在調用這個閉包時執行,可以當做一個普通的方法:
assert code() == 123
可選的你也可以顯示的使用call方法調用:
assert code.call() == 123
以上這些規則,對于有參數的閉包也是适用的:
def isOdd = { int i-> i%2 == 1 } //定義一個接受int類型作為參數的閉包
assert isOdd(3) == true //可以被直接調用
assert isOdd.call(2) == false //或者使用call方法調用
def isEven = { it%2 == 0 } //使用隐式參數定義一個閉包
assert isEven(3) == false //直接調用
assert isEven.call(2) == true//使用call方法調用
與方法不同的是,閉包在調用時總會傳回一個值。下一部分将會讨論怎樣聲明一個帶參數的閉包,以及使用和隐式參數”it”
2.參數 Parameters
2.1. 普通參數 Normal parameters
閉包的參數與普通方法一樣遵從同樣的規則:
- 可選的類型 an optional type
- 一個名字 a name
- 一個可選的預設值 an optional default value
參數之艱難使用逗号隔開:
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.隐式參數 Implicit parameter
當一個閉包沒有顯示的定義一個參數清單(使用->),閉包通常使用隐式的參數,名為”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.可變參數 Varargs
閉包也可以像方法一樣可以聲明可變參數。如果閉包的最後一個參數是可變的,像下面這個類型:
def concat1 = { String... args -> args.join('') }//一個接受可變數量字元串作為第一參數的閉包
assert concat1('abc','def') == 'abcdef'
def concat2 = { String[] args -> args.join('') }//該閉包可以被調用傳入任意數量參數,而無需顯式的包裹這個數組
assert concat2('abc', 'def') == 'abcdef'
def multiConcat = { int n, String... args ->//效果同上
args.join('')*n
}
assert multiConcat(2, 'abc','def') == 'abcdefabcdef'
3.委托機制(暫時這麼翻譯)Delegation strategy
3.1.Groovy閉包 vs lambda表達式 Groovy closures vs lambda expressions
Groovy定義閉包作為groovy.lang.Clousure的執行個體對象出現。與在Java8中出現的lambda表達式不同。關于lambda請參考:Java 8 Lambda表達式探險。Delegation是Groovy閉包的關鍵概念,這在lambdas中是沒有的。閉包改變delegate或者改變delegation strategy的能力使得把Groovy設計成特定領域語言(DSL)成為了可能。
3.2.Owner,delegate,和this
了解delegate的概念,我們首先應該解釋一下this在閉包内部的含義。閉包内部通常會定義一下3種類型:
- this corresponds to the enclosing class where the closure is defined
- this 對應于閉包定義處的封閉類
- owner corresponds to the enclosing object where the closure is defined, which may be either a class or a closure
- owner 對應于閉包定義處的封閉對象(可能是一個類或者閉包)
- delegate corresponds to a third party object where methods calls or properties are resolved whenever the receiver of the message is not defined
- delegate 對應于方法調用或屬性處的第三方對象,無論消息接收者是否定義。
網上關于這方面的隻是介紹:
Groovy閉包中的this,owner和delegate
Groovy閉包深入淺出
3.2.1. this的含義 The meaning of this
在閉包中,調用getThisObject将會傳回閉包定義處所處的類。等價于使用顯示的this:
class Enclosing {
void run() {
def whatIsThisObject = { getThisObject() } //①
assert whatIsThisObject() == this //②
def whatIsThis = { this } //③
assert whatIsThis() == this //④
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { this } //⑤
}
void run() {
def inner = new Inner()
assert inner.cl() == inner //⑥
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { this } //⑦
cl()
}
assert nestedClosures() == this //⑧
}
}
注:
- 一個定義在Enclosing類中的閉包,并且傳回getThisObject
- 調用閉包将會傳回一個閉包定義處的類的Enclosing的執行個體
- 通常,你希望使用簡潔的this符号
- 傳回同一個對象
- 定義在内部類中的閉包
- 在内部類中的this将會傳回内部類,而不是頂層的那個類。
- 嵌入的閉包,比如此處cl定義在了閉包nestedClosures的大括号内部
- this對應于最近的外部類,而不是封閉的閉包!
閉包可能的調用封閉類中的方法方式:
class Person {
String name
int age
String toString() { "$name is $age years old" }
String dump() {
def cl = {
String msg = this.toString()//在閉包中使用this調用toString方法,将會調用閉包所在封閉類對象的toString方法,也就是Person的執行個體
println msg
msg
}
cl()
}
}
def p = new Person(name:'Janice', age:74)
assert p.dump() == 'Janice is 74 years old'
3.2.2.閉包中的Owner Owner of closure
閉包中的owner和閉包中的this的定義非常的像,隻不過有一點微妙的不同:它将傳回它最直接的封閉的對象,可以是一個閉包也可以是一個類的:
class Enclosing {
void run() {
def whatIsOwnerMethod = { getOwner() }//①
assert whatIsOwnerMethod() == this //②
def whatIsOwner = { owner } //③
assert whatIsOwner() == this //④
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { owner } //⑤
}
void run() {
def inner = new Inner()
assert inner.cl() == inner //⑥
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { owner }//⑦
cl()
}
assert nestedClosures() == nestedClosures //⑧
}
}
- 定義在Enclosing類内部的閉包,傳回getOwner
- 調用閉包将會傳回該閉包定義處的類的對象及Enclosing的執行個體
- 通常,使用owner是比較簡潔的
- 傳回同一個對象
- 如果閉包定義在一個内部類中
- owner将會傳回内部類,而不是頂層的類
- 被閉包包括的例子,例如cl被定義在nestedClosures的内部
- owner對應的是封閉的閉包,這是不同于this的地方
3.2.3.閉包中的Delegate Delegate of a closure
閉包的delegate可以通過使用閉包的delegate屬性或者調用getDelegate方法。這是Groovy建構為領域特定語言的一個有力的概念。closure-this和closure-owner指向的是語義上的閉包範圍,而delegate是使用者自定義的供閉包使用的對象。預設的,delegate被設定為owner:
class Enclosing {
void run() {
def cl = { getDelegate() } //①
def cl2 = { delegate } //②
assert cl() == cl2() //③
assert cl() == this //④
def enclosed = {
{ -> delegate }.call()//⑤
}
assert enclosed() == enclosed //⑥
}
}
- 獲得閉包的delegate可以通過調用getDelegate方法
- 或者使用delegate屬性
- 二者傳回同樣的對象
- 是封閉的類或這閉包
- 特别是在閉包的内部的閉包
- 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'
At this point, the behavior is not different from having a `variable defined in the lexical scope of the closure:
在這一點上,表現不同于定義在閉包括号内的變量:(這句話不好翻譯)
def target = p
def upperCasedNameUsingVar = { target.name.toUpperCase() }
assert upperCasedNameUsingVar() == 'NORMAN'
主要的不同如此:
- 上一個例子中,target是一個本地變量指向閉包内部
- delegate可以被顯式的使用,這就是說不需要字首(delegate.)下一節将會詳細讨論。
3.2.4.委托機制(暫時這麼翻譯)Delegation strategy
無論何時,在閉包中,通路一個屬性,不需要指定接收對象,這時使用的是delegation strategy:
class Person {
String name
}
def p = new Person(name:'Igor')
def cl = { name.toUpperCase() } //name不是閉包括号内的一個變量的索引
cl.delegate = p //改變閉包的delegate為Person的執行個體
assert cl() == 'IGOR'//調用成功
之是以可以這樣調用的原因是name屬性将會自然而然的被delegate的對象征用。這樣很好的解決了閉包内部屬性或者方法的調用。不需要顯示的設定(delegate.)作為接收者:調用成功是因為預設的閉包的delegation strategy使然。閉包提供了多種政策方案你可以選擇:
(屬性沒有字首時調用的政策機制—-我的了解)
- Closure.OWNER_FIRST 是預設的政策。如果一個屬性/方法存在于owner,然後他将會被owner調用。如果不是,然後delegate将會被使用
- Closure.Delegate_FIRST 使用這樣的邏輯:delegate首先使用,其次是owner
- Closure.OWNER_ONLY 隻會使用owner:delegate會被忽略
- Closure.DELEGATE_ONLY 隻用delegate:忽略owner
- Closure.TO_SELF can be used by developers who need advanced meta-programming techniques and wish to implement a custom resolution strategy: the resolution will not be made on the owner or the delegate but only on the closure class itself. It makes only sense to use this if you implement your own subclass of Closure.
使用下面的代碼來描繪一下”owner first”:
class Person {
String name
def pretty = { "My name is $name" } //定義一個執行name的閉包成員
String toString() {
pretty()
}
}
class Thing {
String name //類和Person和Thing都定義了一個name屬性
}
def p = new Person(name: 'Sarah')
def t = new Thing(name: 'Teapot')
assert p.toString() == 'My name is Sarah'//使用預設的機制,name屬性首先被owner調用
p.pretty.delegate = t //設定delegate為Thing的執行個體對象t
assert p.toString() == 'My name is Sarah'//結果沒有改變:name被閉包的owner調用
然而,改變closure的解決方案的政策改變結果是可以的:
p.pretty.resolveStrategy = Closure.DELEGATE_FIRST
assert p.toString() == 'My name is Teapot'
通過改變resolveStrategy,我們可以改變Groovy”顯式this”的指向:在這種情況下,name将會首先在delegate中找到,如果沒有發現則是在owner中尋找。name被定義在delegate中,Thing的執行個體将會被使用。
“delegate first”和”delegate only”或者”owner first”和”owner only”之間的差別可以被下面的這個其中一個delegate沒有某個屬性/方法的例子來描述:
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
}
在這個例子中,我們定義了兩個都有name屬性的類,但隻有Person具有age屬性。Person類同時聲明了一個指向age的閉包。我們改變預設的方案政策,從”owner first”到”delegate only”。由于閉包的owner是Person類,如果delegate是Person的執行個體,将會成功調用這個閉包,但是如果我們調用它,且它的delegate是Thing的執行個體,将會調用失敗,并抛出groovy.lang.MissingPropertyException。盡管這個閉包定義在Person類中,但owner沒有被使用。
關于怎樣使用以上這些特點開發DSLs的完整說明可以在a dedicated section of the manual中找到。
4.GString中的閉包 Closures int GStrings
先看一下如下代碼:
def x = 1
def gs = "x = ${x}"
assert gs == 'x = 1'
結果正如你所想象的那樣,但是如果哦我們添加如下代碼将會發生什麼:
x = 2
assert gs == 'x = 2'
你将會看到assert失敗了!原因有兩點:
- a GString only evaluates lazily the toString representation of values
- the syntax xinaGStringdoesnotrepresentaclosurebutanexpressionto x, evaluated when the GString is created.
意思是GString比較懶隻會在建立時計算。 x不是閉包,隻是一個表達式 x
在我們的例子中,GString帶有一個x的索引。當GString被建立完畢,x的值是1,是以GString被建立且值為1。我們們assert該GString則使用toString轉化成String。當我們将x的值更改為2是,我們确實改變了x的值,但是不同于對象,GString仍然指向舊的那個。
如果索引的值改變,GString隻會改變他的toString方法所代表值。如果索引發生改變,什麼都不會發生。
如果你需要一個真正的閉包在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 }//Person類的toString方法傳回name屬性
}
def sam = new Person(name:'Sam') //建立第一個Person對象名為Sam
def lucy = new Person(name:'Lucy') //另一個名為Lucy的對象
def p = sam //變量指派為sam
def gs = "Name: ${p}"//(官方文檔在這個地方犯錯誤了)
assert gs == 'Name: Sam'
p = lucy
assert gs == 'Name: Sam'
sam.name = 'Lucy'
assert gs == 'Name: Lucy'
是以,如果你希望依賴變化的對象或者封裝的對象,你應該使用顯式聲明無參閉包在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.函數式程式設計 Functional programming
像Java8中的lambda表達式–閉包,是Groovy函數式程式設計範式的核心。一些函數式程式設計關于函數的操作直接适用于Closure類,就像本部分描述的。
6.1 科裡化 Curring
對于科裡化不了解的同學,請參考Lambda演算與科裡化(Currying)百度百科:科裡化在計算機科學中,柯裡化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,并且傳回接受餘下的參數且傳回結果的新函數的技術。這個技術由 Christopher Strachey 以邏輯學家 Haskell Curry 命名的,盡管它是 Moses Schnfinkel 和 Gottlob Frege 發明的。
進入正題
在Groovy中,科裡化指的是部分程式的概念。科裡化在函數式程式設計中沒有對應的具體的概念,因為在Groovy中應用的閉包的不同的範圍規則。(這句話好難了解啊)。科裡化允許你一個一個的設定參數的值,并且傳回一個新的閉包且接收的參數少了一個。
6.1.1.左科裡化 Left currying
Left currying是先設定閉包最左邊的參數,比如下面的例子:
def nCopies = { int n, String str -> str*n }//nCopies閉包定義了兩個參數
def twice = nCopies.curry(2)//curry将會設定第一個參數為2,并建立了一個新的閉包,且接收單個參數String
assert twice('bla') == 'blabla' //是以新的閉包可以接收一個String參數
assert twice('bla') == nCopies(2, 'bla')
6.1.2.右科裡化 Right currying
與左科裡化很相似,例子如下:
def nCopies = { int n, String str -> str*n }
def blah = nCopies.rcurry('bla')
assert blah(2) == 'blabla'
assert blah(2) == nCopies(2, 'bla')
6.1.3. 基于索引的科裡化 Index based currying
為防止一個閉包超過2個參數,使用任意參數的科裡化ncurry成為了可能:
def volume = { double l, double w, double h -> l*w*h }
def fixedWidthVolume = volume.ncurry(1, 2d)
assert volume(3d, 2d, 4d) == fixedWidthVolume(3d, 4d)
def fixedWidthAndHeight = volume.ncurry(1, 2d, 4d)
assert volume(3d, 2d, 4d) == fixedWidthAndHeight(3d)
6.2. Memoization
Memoization 允許将調用閉包獲得的結果緩存起來。如果調用方法(閉包)進行計算很慢,但是又需要經常調用且使用同樣的參數。一個典型的例子就是裴波那契數的計算。一個粗糙(幼稚)的實作,看起來像這樣:
def fib
fib = { long n -> n
6.3. 組成 Composition
閉包的組成對應于函數的組成的概念,也就是說建立一個新的函數通過疊加兩個或多個函數(鍊式調用),例如:
def plus2 = { it + 2 }
def times3 = { it * 3 }
def times3plus2 = plus2
6.4.Trampoline
遞歸算法通常收到實體環境的限制:堆棧的最大值。例如,你調用一個遞歸方法太深,最終會抛出StackOverflowException。
通過使用閉包和它的trampoline能力将是一個不錯的方法。
閉包被封裝在TrampolineClosure。在調用時,一個trampolined的閉包将會調用原始的閉包并等待結果。如果調用的輸出是另一個TrampolineClosure的執行個體,這個新生成的閉包将作為結果調用trampoline()方法,這個Closure将會被調用。重複的調用并傳回trampolined 閉包執行個體将會持續直到傳回一個值而不是一個trampolined Closure。這個值将成為trampoline的最終結果。這種方式的調用連續且不會記憶體溢出。
下面是一個使用trampoline()來實作階乘的例子:
def factorial
factorial = { int n, def accu = 1G ->
if (n
6.5.方法指針 Method pointers
通常實際應用中會用到規則的方法作為閉包。例如,你可能想要使用閉包的科裡化能力,但是科裡化對于普通方法是不合适的。在Groovy中,你可以通過方法指針操作符生成一個閉包。