Kotlin 之高階函式與Lambda表示式與閉包

fb0122發表於2018-10-29

圖片來自必應

在kotlin中,函式和物件一樣,都是“一等公民”,這也就表示在kotlin中,函式可以做變數能做的事情,如可以儲存在變數與資料結構中、或是作為引數傳遞給其他高階函式並且也可以作為高階函式的返回值、也可以像其他任何非函式值一樣呼叫函式

那麼,當函式作為一等公民之後,會給我們的程式設計帶來什麼樣的變化呢?

當函式作為一等公民之後,我們就能夠使用一種新的程式設計思想——函數語言程式設計。函數語言程式設計是結構化程式設計的一種,我們在Java中始終使用到的思想是物件導向程式設計,我們將一切看成是一個一個的物件去處理問題,而函數語言程式設計是將一個一個的函式巢狀起來而得到最後的結果。

高階函式

高階函式就是將函式作為引數或返回值的函式

高階函式的定義應該很好理解,這裡我們引用kotlin官方文件的一個例子:

    fun <T, R> Collection<T>.fold(initial: R, combine: (acc: R, nextElement: T) -> R): R{
        var accumulator: R = initial
        for(element: T in this){
            accumulator = combine(accumulator, element)
        }
        return accumulator
    }
複製程式碼

在上面的例項程式碼中,將一個函式型別 (R, T) -> R作為引數傳遞給Collection的擴充套件函式fold。因此我們可以將該fold函式稱為高階函式。

在kotlin中,所有函式型別都可用一個圓括號括起來的引數型別列表與一個返回型別表示,如:(A, B) -> C。表示函式分別接受型別為A和B的兩個引數並且返回一個型別為C的返回值。

Lambda表示式

Lambda表示式的意義用一句話來說明就是:Lambda表示式就是一個匿名函式。

Lambda表示式的完整語法可以用如下形式表示:

val sum = {x: Int, y: Int -> x + y }
複製程式碼

在kotlin中,有一個約定:如果函式的最後一個引數接收函式,那麼作為相應的引數傳入的Lambda表示式可以放在括號外面(尾隨閉包),如下:

val product = items.fold(1){acc, e -> acc * e}
複製程式碼
  • it:單個引數的隱式名稱

一個Lambda表示式只有一個引數是很常見的,如果編譯器能夠自己識別出引數的型別,那麼這個引數的生命可以在呼叫時忽略, 如:

IntArray.filter{ it  > 0}
複製程式碼

上述程式碼的filter時IntArray的一個擴充套件方法, 上述程式碼可以獲取到一個IntArray中值大於0 的子Array。我們來看一下它的原始碼中的函式宣告:

/**
 * Returns a list containing only elements matching the given [predicate].
 */
public inline fun IntArray.filter(predicate: (Int) -> Boolean): List<Int> {
    return filterTo(ArrayList<Int>(), predicate)
}

public inline fun <C : MutableCollection<in Int>> IntArray.filterTo(destination: C, predicate: (Int) -> Boolean): C {
    for (element in this) if (predicate(element)) destination.add(element)
    return destination
    }
複製程式碼

可以看到,filterTo方法是通過predicate(element)方法,將符合條件的元素新增到一個新的array並且返回。而filter方法只有一個引數perdicate:(Int) -> Boolean ,因此在之前的呼叫中,我們可以省略不寫並且用 it > 0 表示。

  • lamda表示式的返回值

lamda表示式的返回值如果沒有明確的用return說明,則會返回最後一個值。如下:

ints.filter{
    val shouldFilter = it > 0
    shouldFilter			//將返回shouldFilter, 等價於 return shouldFilter
}
複製程式碼

閉包

閉包是在Javascript中經常用到的一個特性,可以用閉包來完成很多高階特性,也是在函數語言程式設計中經常會用到的一個特性。

對於閉包的概念,先來看兩段程式碼:

//第一段程式碼
var count = 2
fun readCount(){
    print(count)   // count為全域性變數, 因此列印出:2
}

//第二段程式碼
fun countWrapper(){
    var count = 2
}
print(count)	//error: 因為count為區域性變數,作用域為countWrapper內部,因此在外部無法讀取
複製程式碼

通過上述程式碼可以看到:如果想要在外部訪問某函式內部生命的區域性變數,如果直接訪問是無法訪問的。那麼應該如何去訪問呢?—— 需要用到閉包。如果使用到閉包的特性,我們可以將上面第二段程式碼修改稱為:

fun countWrapper(){
    var count = 2
    return {
        print(count)
    }
}
var result = countWrapper()
result()  //輸出:2
複製程式碼

可以看到,我們在countWrapper中返回了一個lambda表示式,並列印count。然後我們初始化了該函式的例項result,直接呼叫result()就能夠訪問到count的值。那麼我個人理解的閉包的概念就是:

閉包:如果一個外部變數由於被Lambda表示式或者匿名內部類函式呼叫,而導致其生命週期長於原本的作用域,則可以稱為閉包。

在kotlin中,lambda表示式或者匿名函式可以訪問其閉包(即在外部作用域中的值),並且可以修改閉包中捕獲的變數(在Java8中,lambda表示式是無法對其外部作用域中的值作出修改的):

var sum = 0
ints.filter { it > 0 }.forEach {
    sum += it
}
print(sum)
複製程式碼

可以看出,當使用閉包的時候,存在一個私有作用域,可以訪問其外部作用域,可以在將其暴露給外部呼叫的時候按需修改,並且外部無法直接讀取該私有作用域。這樣就可以將功能模組拆分為不同的函式,並且函式中的私有作用域存在相互呼叫的可能,並且相互獨立不影響,可以很好的實現函數語言程式設計。

相關文章