1. 程式人生 > >Go 系列教程-9 頭等函式

Go 系列教程-9 頭等函式

 

 

什麼是頭等函式?

支援頭等函式(First Class Function)的程式語言,可以把函式賦值給變數,也可以把函式作為其它函式的引數或者返回值。Go 語言支援頭等函式的機制

本教程我們會討論頭等函式的語法和用例。

匿名函式

我們來編寫一個簡單的示例,把函式賦值給一個變數

package main

import (  
    "fmt"
)

func main() {  
    a := func() {
        fmt.Println("hello world first class function")
    }
    a()
    fmt.Printf("%T", a)
}

在 playground 上執行

在上面的程式中,我們將一個函式賦值給了變數 a(第 8 行)。這是把函式賦值給變數的語法。你如果觀察得仔細的話,會發現賦值給 a 的函式沒有名稱。由於沒有名稱,這類函式稱為匿名函式(Anonymous Function)

呼叫該函式的唯一方法就是使用變數 a。我們在下一行呼叫了它。a() 呼叫了這個函式,打印出 hello world first class function。在第 12 行,我們打印出 a 的型別。這會輸出 func()

執行該程式,會輸出:

hello world first class function
func()

要呼叫一個匿名函式,可以不用賦值給變數。通過下面的例子,我們看看這是怎麼做到的。

package main

import (  
    "fmt"
)

func main() {  
    func() {
        fmt.Println("hello world first class function")
    }()
}

在 playground 上執行

在上面的程式中,第 8 行定義了一個匿名函式,並在定義之後,我們使用 () 立即呼叫了該函式(第 10 行)。該程式會輸出:

hello world first class function

就像其它函式一樣,還可以向匿名函式傳遞引數。

package main

import (  
    "fmt"
)

func main() {  
    func(n string) {
        fmt.Println("Welcome", n)
    }("Gophers")
}

在 playground 上執行

在上面的程式中,我們向匿名函式傳遞了一個字串引數(第 10 行)。執行該程式後會輸出:

Welcome Gophers

使用者自定義的函式型別

正如我們定義自己的結構體型別一樣,我們可以定義自己的函式型別。

type add func(a int, b int) int

以上程式碼片段建立了一個新的函式型別 add,它接收兩個整型引數,並返回一個整型。現在我們來定義 add 型別的變數。

我們來編寫一個程式,定義一個 add 型別的變數。

package main

import (  
    "fmt"
)

type add func(a int, b int) int

func main() {  
    var a add = func(a int, b int) int {
        return a + b
    }
    s := a(5, 6)
    fmt.Println("Sum", s)
}

在 playground 上執行

在上面程式的第 10 行,我們定義了一個 add 型別的變數 a,並向它賦值了一個符合 add 型別簽名的函式。我們在第 13 行呼叫了該函式,並將結果賦值給 s。該程式會輸出:

Sum 11

高階函式

wiki 把高階函式(Hiher-order Function)定義為:滿足下列條件之一的函式

  • 接收一個或多個函式作為引數
  • 返回值是一個函式

針對上述兩種情況,我們看看一些簡單例項。

把函式作為引數,傳遞給其它函式

package main

import (  
    "fmt"
)

func simple(a func(a, b int) int) {  
    fmt.Println(a(60, 7))
}

func main() {  
    f := func(a, b int) int {
        return a + b
    }
    simple(f)
}

在 playground 上執行

在上面的例項中,第 7 行我們定義了一個函式 simplesimple 接收一個函式引數(該函式接收兩個 int 引數,返回一個 a 整型)。在 main 函式的第 12 行,我們建立了一個匿名函式 f,其簽名符合 simple 函式的引數。我們在下一行呼叫了 simple,並傳遞了引數 f。該程式列印輸出 67。

在其它函式中返回函式

現在我們重寫上面的程式碼,在 simple 函式中返回一個函式。

package main

import (  
    "fmt"
)

func simple() func(a, b int) int {  
    f := func(a, b int) int {
        return a + b
    }
    return f
}

func main() {  
    s := simple()
    fmt.Println(s(60, 7))
}

在 playground 上執行

在上面程式中,第 7 行的 simple 函式返回了一個函式,並接受兩個 int 引數,返回一個 int

在第 15 行,我們呼叫了 simple 函式。我們把 simple 的返回值賦值給了 s。現在 s 包含了 simple 函式返回的函式。我們呼叫了 s,並向它傳遞了兩個 int 引數(第 16 行)。該程式輸出 67。

閉包

閉包(Closure)是匿名函式的一個特例。當一個匿名函式所訪問的變數定義在函式體的外部時,就稱這樣的匿名函式為閉包。

看看一個示例就明白了。

package main

import (  
    "fmt"
)

func main() {  
    a := 5
    func() {
        fmt.Println("a =", a)
    }()
}

在 playground 上執行

在上面的程式中,匿名函式在第 10 行訪問了變數 a,而 a 存在於函式體的外部。因此這個匿名函式就是閉包。

每一個閉包都會繫結一個它自己的外圍變數(Surrounding Variable)。我們通過一個簡單示例來體會這句話的含義。

package main

import (  
    "fmt"
)

func appendStr() func(string) string {  
    t := "Hello"
    c := func(b string) string {
        t = t + " " + b
        return t
    }
    return c
}

func main() {  
    a := appendStr()
    b := appendStr()
    fmt.Println(a("World"))
    fmt.Println(b("Everyone"))

    fmt.Println(a("Gopher"))
    fmt.Println(b("!"))
}

在 playground 上執行

在上面程式中,函式 appendStr 返回了一個閉包。這個閉包綁定了變數 t。我們來理解這是什麼意思。

在第 17 行和第 18 行宣告的變數 a 和 b 都是閉包,它們綁定了各自的 t 值。

我們首先用引數 World 呼叫了 a。現在 a 中 t 值變為了 Hello World

在第 20 行,我們又用引數 Everyone 呼叫了 b。由於 b 綁定了自己的變數 t,因此 b 中的 t 還是等於初始值 Hello。於是該函式呼叫之後,b 中的 t 變為了 Hello Everyone。程式的其他部分很簡單,不再解釋。

該程式會輸出:

Hello World  
Hello Everyone  
Hello World Gopher  
Hello Everyone !

頭等函式的實際用途

迄今為止,我們已經定義了什麼是頭等函式,也看了一些專門設計的示例,來學習它們如何工作。現在我們來編寫一些實際的程式,來展現頭等函式的實際用處。

我們會建立一個程式,基於一些條件,來過濾一個 students 切片。現在我們來逐步實現它。

首先定義一個 student 型別。

type student struct {  
    firstName string
    lastName string
    grade string
    country string
}

下一步是編寫一個 filter 函式。該函式接收一個 students 切片和一個函式作為引數,這個函式會計算一個學生是否滿足篩選條件。寫出這個函式後,你很快就會明白,我們繼續吧。

func filter(s []student, f func(student) bool) []student {  
    var r []student
    for _, v := range s {
        if f(v) == true {
            r = append(r, v)
        }
    }
    return r
}

在上面的函式中,filter 的第二個引數是一個函式。這個函式接收 student 引數,返回一個 bool 值。這個函式計算了某一學生是否滿足篩選條件。我們在第 3 行遍歷了 student 切片,將每個學生作為引數傳遞給了函式 f。如果該函式返回 true,就表示該學生通過了篩選條件,接著將該學生新增到了結果切片 r 中。你可能會很困惑這個函式的實際用途,等我們完成程式你就知道了。我添加了 main 函式,整個程式如下所示:

package main

import (  
    "fmt"
)

type student struct {  
    firstName string
    lastName  string
    grade     string
    country   string
}

func filter(s []student, f func(student) bool) []student {  
    var r []student
    for _, v := range s {
        if f(v) == true {
            r = append(r, v)
        }
    }
    return r
}

func main() {  
    s1 := student{
        firstName: "Naveen",
        lastName:  "Ramanathan",
        grade:     "A",
        country:   "India",
    }
    s2 := student{
        firstName: "Samuel",
        lastName:  "Johnson",
        grade:     "B",
        country:   "USA",
    }
    s := []student{s1, s2}
    f := filter(s, func(s student) bool {
        if s.grade == "B" {
            return true
        }
        return false
    })
    fmt.Println(f)
}

在 playground 上執行

在 main 函式中,我們首先建立了兩個學生 s1 和 s2,並將他們新增到了切片 s。現在假設我們想要查詢所有成績為 B 的學生。為了實現這樣的功能,我們傳遞了一個檢查學生成績是否為 B 的函式,如果是,該函式會返回 true。我們把這個函式作為引數傳遞給了 filter 函式(第 38 行)。上述程式會輸出:

[{Samuel Johnson B USA}]

假設我們想要查詢所有來自印度的學生。通過修改傳遞給 filter 的函式引數,就很容易地實現了。

實現它的程式碼如下所示:

c := filter(s, func(s student) bool {  
    if s.country == "India" {
        return true
    }
    return false
})
fmt.Println(c)

請將該函式新增到 main 函式,並檢查它的輸出。

我們最後再編寫一個程式,來結束這一節的討論。這個程式會對切片的每個元素執行相同的操作,並返回結果。例如,如果我們希望將切片中的所有整數乘以 5,並返回出結果,那麼通過頭等函式可以很輕鬆地實現。我們把這種對集合中的每個元素進行操作的函式稱為 map 函式。相關程式碼如下所示,它們很容易看懂。

package main

import (  
    "fmt"
)

func iMap(s []int, f func(int) int) []int {  
    var r []int
    for _, v := range s {
        r = append(r, f(v))
    }
    return r
}
func main() {  
    a := []int{5, 6, 7, 8, 9}
    r := iMap(a, func(n int) int {
        return n * 5
    })
    fmt.Println(r)
}

在 playground 上執行

該程式會輸出:

[25 30 35 40 45]

現在簡單概括一下本教程討論的內容:

  • 什麼是頭等函式?
  • 匿名函式
  • 使用者自定義的函式型別
  • 高階函式
    • 把函式作為引數,傳遞給其它函式
    • 在其它函式中返回函式
  • 閉包
  • 頭等函式的實際用途