1. 程式人生 > >Groovy語言規範中文版之閉包

Groovy語言規範中文版之閉包

閉包官方文件: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 對應於方法呼叫或屬性處的第三方物件,無論訊息接收者是否定義。

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 //⑧                    
    }
}

注:

  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()//在閉包中使用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 //⑧           
    }
}
  1. 定義在Enclosing類內部的閉包,返回getOwner
  2. 呼叫閉包將會返回該閉包定義處的類的物件及Enclosing的例項
  3. 通常,使用owner是比較簡潔的
  4. 返回同一個物件
  5. 如果閉包定義在一個內部類中
  6. owner將會返回內部類,而不是頂層的類
  7. 被閉包包括的例子,例如cl被定義在nestedClosures的內部
  8. 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 //⑥                       
    }
}
  1. 獲得閉包的delegate可以通過呼叫getDelegate方法
  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'

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沒有被使用。

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 xinaGStringdoesnotrepresentaclosurebutanexpressiontox, 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中,你可以通過方法指標操作符生成一個閉包。

翻譯倉促,錯誤之處還請指出!由於近期較忙,關於閉包的學習總結先告一段落~