1. 程式人生 > >Go語言入門(6)閉包

Go語言入門(6)閉包

一 函數語言程式設計概論

在過去近十年時間裡,面向物件程式設計大行其道,以至於在大學的教育裡,老師也只會教給我們兩種程式設計模型,面向過程和麵向物件。孰不知,在面向物件思想產生之前,函數語言程式設計已經有了數十年的歷史。就讓我們回顧這個古老又現代的程式設計模型,看看究竟是什麼魔力將這個概念在21世紀的今天再次拉入我們的視野。

隨著硬體效能的提升以及編譯技術和虛擬機器技術的改進,一些曾被效能問題所限制的動態語言開始受到關注,Python、Ruby 和 Lua 等語言都開始在應用中嶄露頭角。動態語言因其方便快捷的開發方式成為很多人喜愛的程式語言,伴隨動態語言的流行,函數語言程式設計也再次進入了我們的視野。

究竟什麼是函數語言程式設計呢?

在維基百科中,對函數語言程式設計有很詳細的介紹。Wiki上對Functional Programming的定義:

In computer science, functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids state and mutable data.

簡單地翻譯一下,也就是說函數語言程式設計是一種程式設計模型,他將計算機運算看做是數學中函式的計算,並且避免了狀態以及變數的概念。

二 閉包

在函式程式設計中經常用到閉包,閉包是什麼?它是怎麼產生的及用來解決什麼問題呢?先給出閉包的字面定義:閉包是由函式及其相關引用環境組合而成的實體(即:閉包=函式+引用環境)。這個從字面上很難理解,特別對於一直使用命令式語言進行程式設計的程式設計師們。

閉包只是在形式和表現上像函式,但實際上不是函式。函式是一些可執行的程式碼,這些程式碼在函式被定義後就確定了,不會在執行時發生變化,所以一個函式只有一個例項。閉包在執行時可以有多個例項,不同的引用環境和相同的函式組合可以產生不同的例項。所謂引用環境是指在程式執行中的某個點所有處於活躍狀態的約束所組成的集合。其中的約束是指一個變數的名字和其所代表的物件之間的聯絡。那麼為什麼要把引用環境與函式組合起來呢?這主要是因為在支援巢狀作用域的語言中,有時不能簡單直接地確定函式的引用環境。這樣的語言一般具有這樣的特性:

函式是一等公民(First-class value),即函式可以作為另一個函式的返回值或引數,還可以作為一個變數的值。
函式可以巢狀定義,即在一個函式內部可以定義另一個函式。

在面向物件程式設計中,我們把物件傳來傳去,那在函數語言程式設計中,要做的是把函式傳來傳去,說成術語,把他叫做高階函式。在數學和電腦科學中,高階函式是至少滿足下列一個條件的函式:

  1. 接受一個或多個函式作為輸入
  2. 輸出一個函式

在函數語言程式設計中,函式是基本單位,是第一型,他幾乎被用作一切,包括最簡單的計算,甚至連變數都被計算所取代。

閉包小結:

函式只是一段可執行程式碼,編譯後就“固化”了,每個函式在記憶體中只有一份例項,得到函式的入口點便可以執行函數了。在函數語言程式設計語言中,函式是一等公民(First class value):第一類物件,我們不需要像命令式語言中那樣藉助函式指標,委託操作函式,函式可以作為另一個函式的引數或返回值,可以賦給一個變數。函式可以巢狀定義,即在一個函式內部可以定義另一個函式,有了巢狀函式這種結構,便會產生閉包問題。如:

package main
 
import (
    "fmt"
)
 
func adder() func(int) int {
    sum := 0
    innerfunc := func(x int) int {
        sum += x
        return sum
    }
    return innerfunc
}
 
func main() {
    pos, neg := adder(), adder()
    for i := 0; i < 10; i++ {
        fmt.Println(pos(i), neg(-2*i))
    }
 
}

在這段程式中,函式innerfunc是函式adder的內嵌函式,並且是adder函式的返回值。我們注意到一個問題:內嵌函式innerfunc中引用到外層函式中的區域性變數sum,Go會這麼處理這個問題呢?先讓我們來看看這段程式碼的執行結果:

0 0  
1 -2  
3 -6  
6 -12   
10 -20  
15 -30  
21 -42  
28 -56  
36 -72  
45 -90  

注意: Go不能在函式內部顯式巢狀定義函式,但是可以定義一個匿名函式。如上面所示,我們定義了一個匿名函式物件,然後將其賦值給innerfunc,最後將其作為返回值返回。

當用不同的引數呼叫adder函式得到(pos(i),neg(i))函式時,得到的結果是隔離的,也就是說每次呼叫adder返回的函式都將生成並儲存一個新的區域性變數sum。其實這裡adder函式返回的就是閉包。
這個就是Go中的閉包,一個函式和與其相關的引用環境組合而成的實體。一句關於閉包的名言: 物件是附有行為的資料,而閉包是附有資料的行為。

三 閉包使用

閉包經常用於回撥函式,當IO操作(例如從網路獲取資料、檔案讀寫)完成的時候,會對獲取的資料進行某些操作,這些操作可以交給函式物件處理。

除此之外,在一些公共的操作中經常會包含一些差異性的特殊操作,而這些差異性的操作可以用函式來進行封裝。看下面的例子:

package main
 
import (
    "errors"
    "fmt"
)
 
type Traveser func(ele interface{})
/*
    Process:封裝公共切片陣列操作
*/
func Process(array interface{}, traveser Traveser) error {
 
    if array == nil {
        return errors.New("nil pointer")
    }
    var length int //陣列的長度
    switch array.(type) {
    case []int:
        length = len(array.([]int))
    case []string:
        length = len(array.([]string))
    case []float32:
        length = len(array.([]float32))
        default:
        return errors.New("error type")
    }
    if length == 0 {
        return errors.New("len is zero.")
    }
    traveser(array)
    return nil
}
/*
    具體操作:升序排序陣列元素
*/
func SortByAscending(ele interface{}) {
    intSlice, ok := ele.([]int)
    if !ok {
        return
    }
    length := len(intSlice)
 
    for i := 0; i < length-1; i++ {
        isChange := false
        for j := 0; j < length-1-i; j++ {
 
            if intSlice[j] > intSlice[j+1] {
                isChange = true
                intSlice[j], intSlice[j+1] = intSlice[j+1], intSlice[j]
            }
        }
 
        if isChange == false {
            return
        }
 
    }
}
/*
    具體操作:降序排序陣列元素
*/
func SortByDescending(ele interface{}) {
 
    intSlice, ok := ele.([]int)
    if !ok {
        return
    }
    length := len(intSlice)
    for i := 0; i < length-1; i++ {
        isChange := false
        for j := 0; j < length-1-i; j++ {
            if intSlice[j] < intSlice[j+1] {
                isChange = true
                intSlice[j], intSlice[j+1] = intSlice[j+1], intSlice[j]
            }
        }
 
        if isChange == false {
            return
        }
 
    }
}
 
func main() {
 
    intSlice := make([]int, 0)
    intSlice = append(intSlice, 3, 1, 4, 2)
 
    Process(intSlice, SortByDescending)
    fmt.Println(intSlice) //[4 3 2 1]
    Process(intSlice, SortByAscending)
    fmt.Println(intSlice) //[1 2 3 4]
 
    stringSlice := make([]string, 0)
    stringSlice = append(stringSlice, "hello", "world", "china")
 
    /*
       具體操作:使用匿名函式封裝輸出操作
    */
    Process(stringSlice, func(elem interface{}) {
 
        if slice, ok := elem.([]string); ok {
            for index, value := range slice {
                fmt.Println("index:", index, "  value:", value)
            }
        }
    })
    floatSlice := make([]float32, 0)
    floatSlice = append(floatSlice, 1.2, 3.4, 2.4)
 
    /*
       具體操作:使用匿名函式封裝自定義操作
    */
    Process(floatSlice, func(elem interface{}) {
 
        if slice, ok := elem.([]float32); ok {
            for index, value := range slice {
                slice[index] = value * 2
            }
        }
    })
    fmt.Println(floatSlice) //[2.4 6.8 4.8]
}

輸出結果:

[4 3 2 1]   
[1 2 3 4]
index: 0   value: hello
index: 1   value: world
index: 2   value: china
[2.4 6.8 4.8]

在上面的例子中,Process函式負責對切片(陣列)資料進行操作,在操作切片(陣列)時候,首先要做一些引數檢測,例如指標是否為空、陣列長度是否大於0等。這些是操作資料的公共操作。具體針對資料可以有自己特殊的操作,包括排序(升序、降序)、輸出等。針對這些特殊的操作可以使用函式物件來進行封裝。
再看下面的例子,這個例子沒什麼實際意義,只是為了說明閉包的使用方式。

package main
 
import (
    "fmt"
)
 
type FilterFunc func(ele interface{}) interface{}
 
/*
  公共操作:對資料進行特殊操作
*/
func Data(arr interface{}, filterFunc FilterFunc) interface{} {
 
    slice := make([]int, 0)
    array, _ := arr.([]int)
 
    for _, value := range array {
 
        integer, ok := filterFunc(value).(int)
        if ok {
            slice = append(slice, integer)
        }
 
    }
    return slice
}
/*
  具體操作:奇數變偶數(這裡可以不使用介面型別,直接使用int型別)
*/
func EvenFilter(ele interface{}) interface{} {
 
    integer, ok := ele.(int)
    if ok {
        if integer%2 == 1 {
            integer = integer + 1
        }
    }
    return integer
}
/*
  具體操作:偶數變奇數(這裡可以不使用介面型別,直接使用int型別)
*/
func OddFilter(ele interface{}) interface{} {
 
    integer, ok := ele.(int)
 
    if ok {
        if integer%2 != 1 {
            integer = integer + 1
        }
    }
 
    return integer
}
 
func main() {
    sliceEven := make([]int, 0)
    sliceEven = append(sliceEven, 1, 2, 3, 4, 5)
    sliceEven = Data(sliceEven, EvenFilter).([]int)
    fmt.Println(sliceEven) //[2 2 4 4 6]
 
    sliceOdd := make([]int, 0)
    sliceOdd = append(sliceOdd, 1, 2, 3, 4, 5)
    sliceOdd = Data(sliceOdd, OddFilter).([]int)
    fmt.Println(sliceOdd) //[1 3 3 5 5]
 
}

輸出結果:

[2 2 4 4 6]  
[1 3 3 5 5]

Data作為公共函式,然後分別定義了兩個具體的特殊函式:偶數和奇數的過濾器,定義具體的操作。

四 總結

上面例子中閉包的使用有點類似於面向物件設計模式中的模版模式,在模版模式中是在父類中定義公共的行為執行序列,然後子類通過過載父類的方法來實現特定的操作,而在Go語言中我們使用閉包實現了同樣的效果。

其實理解閉包最方便的方法就是將閉包函式看成一個類 ,一個閉包函式呼叫就是例項化一個類(在Objective-c中閉包就是用類來實現的),然後就可以從類的角度看出哪些是全域性變數,哪些是區域性變數。例如在第一個例子中,posneg分別例項化了兩個閉包類,在這個閉包類中有個閉包全域性變數sum。所以這樣就很好理解返回的結果了。