閉包
本章涵蓋 Groovy 閉包。Groovy 中的閉包是一個開放的、匿名的程式碼區塊,可以接收引數、傳回值,並指定給變數。閉包可以參照在其周圍範圍中宣告的變數。與閉包的正式定義相反,Groovy 語言中的 Closure
也包含在其周圍範圍之外定義的自由變數。雖然打破了閉包的正式概念,但它提供了本章中所述的各種優點。
1. 語法
1.1. 定義閉包
封閉定義遵循此語法
{ [closureParameters -> ] statements }
其中 [closureParameters->]
是選用的逗號分隔參數清單,而陳述式是 0 個或多個 Groovy 陳述式。參數看起來類似於方法參數清單,這些參數可以是已輸入或未輸入的。
當指定參數清單時,需要 ->
字元,用於將參數從封閉主體分開。陳述式 部分包含 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 ) 的封閉 |
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 -> it.name.endsWith('.txt') (3)
}
1 | 您可以將封閉指定給變數,它是 groovy.lang.Closure 的執行個體 |
2 | 如果沒有使用 def 或 var ,請使用 groovy.lang.Closure 作為類型 |
3 | 選擇性地,您可以透過使用 groovy.lang.Closure 的一般類型來指定封閉的回傳類型 |
1.3. 呼叫封閉
封閉作為匿名程式碼區塊,可以像任何其他方法一樣呼叫。如果您定義一個不採用任何參數的封閉,如下所示
def code = { 123 }
然後,封閉內的程式碼只會在您呼叫封閉時執行,這可以使用變數來完成,就像它是一個常規方法一樣
assert code() == 123
或者,您可以明確並使用 call
方法
assert code.call() == 123
如果封閉接受參數,原則相同
def isOdd = { int i -> i%2 != 0 } (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. 一般參數
封閉函式的參數遵循與一般方法參數相同的原則
-
一個可選的類型
-
一個名稱
-
一個可選的預設值
參數之間以逗號分隔
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 | 一個封閉函式接受可變數量的字串作為第一個參數 |
2 | 它可以使用任何數量的參數來呼叫而不用將它們明確包裝成陣列 |
3 | 如果將 args 參數宣告為陣列,則可以直接使用相同的行為 |
4 | 只要最後一個參數是陣列或明確的可變參數類型 |
3. 委派策略
3.1. Groovy 封閉函式與 lambda 運算式
Groovy 將封閉函式定義為Closure 類別的實例。這使得它與Java 8 中的 lambda 運算式非常不同。委派是 Groovy 封閉函式中的關鍵概念,在 lambda 中沒有等效概念。能夠變更委派或變更封閉函式的委派策略,使得在 Groovy 中設計出漂亮的特定領域語言 (DSL) 成為可能。
3.2. 擁有者、委派和 this
要了解委派的概念,我們必須先說明封閉函式內部 this
的意義。封閉函式實際上定義了 3 個不同的東西
-
this
對應於定義封閉函式的封裝類別 -
owner
對應於定義封閉函式的封裝物件,它可以是類別或封閉函式 -
delegate
對應到一個第三方物件,當訊息的接收者未定義時,方法呼叫或屬性會被解析
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 對應到最接近的外層類別,而不是封裝閉包! |
當然,可以透過這種方式呼叫封裝類別的方法
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. 閉包的所有者
閉包的所有者與 閉包中的 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 | 但在巢狀閉包的情況下,例如這裡的 cl 定義在 nestedClosures 的範圍內 |
8 | 那麼 owner 對應到封裝閉包,因此與 this 是不同的物件! |
3.2.3. 閉包的委派
可以使用 delegate
屬性或呼叫 getDelegate
方法來存取閉包的委派。這是一個強大的概念,可以用於在 Groovy 中建立特定領域語言。雖然 this 和 owner 參考閉包的詞彙範圍,但委派是一個使用者定義的物件,閉包會使用它。預設情況下,委派設定為 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 方法來取得閉包的委派 |
2 | 或使用 delegate 屬性 |
3 | 兩者都傳回同一個物件 |
4 | 也就是封裝類別或封閉 |
5 | 特別是在巢狀封閉的情況下 |
6 | delegate 會對應到 owner |
封閉的委派可以變更為任何物件。我們透過建立兩個類別來說明這一點,它們不是彼此的子類別,但都定義了一個稱為 name
的屬性
class Person {
String name
}
class Thing {
String name
}
def p = new Person(name: 'Norman')
def t = new Thing(name: 'Teapot')
然後我們定義一個封閉,用來擷取委派上的 name
屬性
def upperCasedName = { delegate.name.toUpperCase() }
然後透過變更封閉的委派,您可以看到目標物件會變更
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.
,如下一段所述。
3.2.4. 委派策略
在封閉中,每當存取屬性而沒有明確設定接收器物件時,就會涉及委派策略
class Person {
String name
}
def p = new Person(name:'Igor')
def cl = { name.toUpperCase() } (1)
cl.delegate = p (2)
assert cl() == 'IGOR' (3)
1 | name 沒有參照封閉詞彙範圍中的變數 |
2 | 我們可以將封閉的委派變更為 Person 的執行個體 |
3 | 而且方法呼叫會成功 |
這個程式碼運作的原因是 name
屬性會在 delegate
物件上透明地解析!這是解析封閉中屬性或方法呼叫的非常強大方式。不需要設定明確的 delegate.
接收器:呼叫會進行,因為封閉的預設委派策略會這樣做。封閉實際上會定義您可以選擇的多個解析策略
-
Closure.OWNER_FIRST
是預設策略。如果屬性/方法存在於擁有者中,則會在擁有者上呼叫它。如果不存在,則會使用委派。 -
Closure.DELEGATE_FIRST
會反轉邏輯:先使用委派,然後使用擁有者 -
Closure.OWNER_ONLY
只會在擁有者上解析屬性/方法查詢:將會忽略委派。 -
Closure.DELEGATE_ONLY
只會在委派上解析屬性/方法查詢:將會忽略擁有者。 -
Closure.TO_SELF
可以由需要進階元程式設計技巧並希望實作自訂解析策略的開發人員使用:解析不會在擁有者或委派上進行,而只會在封閉類別本身上進行。只有在實作Closure
的自訂子類別時,使用這個才有意義。
讓我們用這個程式碼來說明預設的「擁有者優先」策略
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 屬性會先在擁有者上解析 |
4 | 因此,如果我們將 delegate 變更為 Thing 的執行個體 t |
5 | 結果不會改變:name 會先在封閉的 owner 中解析 |
不過,可以變更封閉的解析策略
p.pretty.resolveStrategy = Closure.DELEGATE_FIRST
assert p.toString() == 'My name is Teapot'
藉由變更 resolveStrategy
,我們修改 Groovy 解析「隱含 this」參照的方式:在這種情況下,name
會先在委派中尋找,如果找不到,則在 owner 中尋找。由於 name
定義在委派(Thing
的實例)中,因此會使用這個值。
如果委派(resp. owner)之一沒有此方法或屬性,就可以說明「委派優先」和「僅委派」或「owner 優先」和「僅 owner」之間的差異
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 (1)
cl.delegate = t
assert cl() == 42 (1)
cl.resolveStrategy = Closure.DELEGATE_ONLY
cl.delegate = p
assert cl() == 42 (2)
cl.delegate = t
try {
cl() (3)
assert false
} catch (MissingPropertyException ex) {
// "age" is not defined on the delegate
}
1 | 對於「owner 優先」,委派為何並不重要 |
2 | 對於「僅委派」,將 p 作為委派會成功 |
3 | 對於「僅委派」,將 t 作為委派會失敗 |
在此範例中,我們定義兩個類別,它們都有 name
屬性,但只有 Person
類別宣告 age
。Person
類別也宣告一個封閉,參照 age
。我們可以將預設解析策略從「owner 優先」變更為「僅委派」。由於封閉的 owner 是 Person
類別,因此我們可以檢查委派是否為 Person
的實例,呼叫封閉會成功,但如果我們呼叫它,而委派是 Thing
的實例,它會失敗,並產生 groovy.lang.MissingPropertyException
。儘管封閉定義在 Person
類別內,但並未使用 owner。
可以在手冊的專屬區段中找到關於如何使用此功能開發 DSL 的完整說明。 |
3.2.5. 存在元程式設計時的委派策略
在描述「owner 優先」委派策略時,我們談到如果它「存在」,則使用 owner 的屬性/方法,否則使用委派的屬性/方法。而「委派優先」的故事類似,但相反。我們應該更精確地使用「處理」一詞,而不是「存在」。這表示對於「owner 優先」,如果屬性/方法存在於 owner 中,或它有 propertyMissing/methodMissing 鉤子,則 owner 會處理成員存取。
我們可以在稍作修改的前一個範例中看到實際運作的狀況
class Person {
String name
int age
def fetchAge = { age }
}
class Thing {
String name
def propertyMissing(String name) { -1 }
}
def p = new Person(name:'Jessica', age:42)
def t = new Thing(name:'Printer')
def cl = p.fetchAge
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl.delegate = p
assert cl() == 42
cl.delegate = t
assert cl() == -1
在此範例中,即使我們的 Thing
類別實例(我們最後一次使用 cl
的委派)沒有 age
屬性,但它透過 propertyMissing
鉤子處理遺失的屬性,這表示 age
會是 -1
。
4. GString 中的封閉
使用下列程式碼
def x = 1
def gs = "x = ${x}"
assert gs == 'x = 1'
程式碼的行為符合預期,但如果你加入
x = 2
assert gs == 'x = 2'
你會看到 assert 失敗!有兩個原因
-
GString 僅會延遲評估值的
toString
表示法 -
GString 中的語法
${x}
不 表示閉包,而是表示$x
的表達式,在建立 GString 時評估。
在我們的範例中,GString
是使用參考 x
的表達式建立的。建立 GString
時,x
的值為 1,因此 GString
會建立一個值為 1 的值。觸發 assert 時,會評估 GString
,並使用 toString
將 1 轉換為 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 } (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 | 我們建立一個名為 Sam 的第一個 Person |
3 | 我們建立另一個名為 Lucy 的 Person |
4 | p 變數設定為 Sam |
5 | 並建立一個閉包,參考 p 的值,也就是 Sam |
6 | 因此當我們評估字串時,它會傳回 Sam |
7 | 如果我們將 p 變更為 Lucy |
8 | 字串仍評估為 Sam,因為它是建立 GString 時 p 的值 |
9 | 因此如果我們變異 Sam 以將名稱變更為 Lucy |
10 | 這次 GString 會正確地變異 |
因此如果你不想要依賴變異物件或包裝物件,你必須在 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'
6. 函式程式設計
閉包,例如 Java 8 中的 lambda 表達式 是 Groovy 中函數式程式設計範例的核心。Closure
類別上提供一些函數式程式設計運算,例如本節說明的。
6.1. 柯里化
在 Groovy 中,柯里化是指部分應用程式概念。它不對應於函數式程式設計中柯里化的實際概念,因為 Groovy 對閉包套用不同的範圍規則。Groovy 中的柯里化讓您可以設定閉包參數之一的值,它會傳回一個接受少一個參數的新閉包。
6.1.1. 左柯里化
左柯里化是指設定閉包的最左邊參數,例如以下範例
def nCopies = { int n, String str -> str*n } (1)
def twice = nCopies.curry(2) (2)
assert twice('bla') == 'blabla' (3)
assert twice('bla') == nCopies(2, 'bla') (4)
1 | nCopies 閉包定義兩個參數 |
2 | curry 會將第一個參數設定為 2 ,建立一個接受單一 String 的新閉包 (函數) |
3 | 因此,新函數只能使用 String 呼叫 |
4 | 而且等於使用兩個參數呼叫 nCopies |
6.1.2. 右柯里化
類似左柯里化,可以設定閉包的最右邊參數
def nCopies = { int n, String str -> str*n } (1)
def blah = nCopies.rcurry('bla') (2)
assert blah(2) == 'blabla' (3)
assert blah(2) == nCopies(2, 'bla') (4)
1 | nCopies 閉包定義兩個參數 |
2 | rcurry 會將最後一個參數設定為 bla ,建立一個接受單一 int 的新閉包 (函數) |
3 | 因此,新函數只能使用 int 呼叫 |
4 | 而且等於使用兩個參數呼叫 nCopies |
6.1.3. 基於索引的柯里化
如果閉包接受超過兩個參數,可以使用 ncurry
設定任意參數
def volume = { double l, double w, double h -> l*w*h } (1)
def fixedWidthVolume = volume.ncurry(1, 2d) (2)
assert volume(3d, 2d, 4d) == fixedWidthVolume(3d, 4d) (3)
def fixedWidthAndHeight = volume.ncurry(1, 2d, 4d) (4)
assert volume(3d, 2d, 4d) == fixedWidthAndHeight(3d) (5)
1 | volume 函數定義三個參數 |
2 | ncurry 會將第二個參數 (索引 = 1) 設定為 2d ,建立一個接受長度和高度的新體積函數 |
3 | 該函數等於呼叫 volume 省略寬度 |
4 | 也可以設定多個參數,從指定的索引開始 |
5 | 結果函數接受的參數數量等於原始參數數量減去 ncurry 設定的參數數量 |
6.2. 記憶化
記憶化允許快取閉包呼叫的結果。如果函數 (閉包) 執行的運算很慢,但您知道這個函數會經常使用相同的參數呼叫,記憶化很有用。一個典型的範例是斐波那契數列。一個天真的實作可能會像這樣
def fib
fib = { long n -> n<2?n:fib(n-1)+fib(n-2) }
assert fib(15) == 610 // slow!
這是一個天真的實作,因為「fib」經常使用相同的參數遞迴呼叫,導致指數演算法
-
計算
fib(15)
需要fib(14)
和fib(13)
的結果 -
計算
fib(14)
需要fib(13)
和fib(12)
的結果
由於呼叫是遞迴的,因此您可以看到我們將重複計算相同的數值,儘管它們可以快取。這個簡陋的實作可以使用 memoize
快取呼叫的結果來「修正」
fib = { long n -> n<2?n:fib(n-1)+fib(n-2) }.memoize()
assert fib(25) == 75025 // fast!
快取使用引數的實際數值運作。這表示如果您使用快取來處理非原始或封裝的原始類型,則應非常小心。 |
可以使用替代方法調整快取的行為
-
memoizeAtMost
會產生一個新的封閉,快取最多 n 個數值 -
memoizeAtLeast
會產生一個新的封閉,快取至少 n 個數值 -
memoizeBetween
會產生一個新的封閉,快取至少 n 個數值和最多 n 個數值
所有快取變體中使用的快取都是 LRU 快取。
6.3. 組合
封閉組合對應於函數組合的概念,也就是說透過組合兩個或多個函數(串連呼叫)來建立新的函數,如下例所示
def plus2 = { it + 2 }
def times3 = { it * 3 }
def times3plus2 = plus2 << times3
assert times3plus2(3) == 11
assert times3plus2(4) == plus2(times3(4))
def plus2times3 = times3 << plus2
assert plus2times3(3) == 15
assert plus2times3(5) == times3(plus2(5))
// reverse composition
assert times3plus2(3) == (times3 >> plus2)(3)
6.4. 跳板
遞迴演算法通常受到物理限制:最大堆疊高度。例如,如果您呼叫遞迴呼叫自己的方法太深,最終將收到 StackOverflowException
。
在這些情況下,一種有用的方法是使用 Closure
及其跳板功能。
封閉會封裝在 TrampolineClosure
中。在呼叫時,彈跳的 Closure
會呼叫原始 Closure
並等待其結果。如果呼叫的結果是 TrampolineClosure
的另一個執行個體(可能是呼叫 trampoline()
方法的結果),則會再次呼叫 Closure
。這種重複呼叫已傳回的彈跳 Closure
執行個體會持續進行,直到傳回的值不是彈跳的 Closure
為止。該值會成為跳板的最終結果。這樣一來,呼叫會以串列方式進行,而不是填滿堆疊。
以下是使用 trampoline()
實作階乘函數的範例
def factorial
factorial = { int n, def accu = 1G ->
if (n < 2) return accu
factorial.trampoline(n - 1, n * accu)
}
factorial = factorial.trampoline()
assert factorial(1) == 1
assert factorial(3) == 1 * 2 * 3
assert factorial(1000) // == 402387260.. plus another 2560 digits