Kotlin學習(十二): 函數、高級函數和Lambda表達式

Kotlin

高階函數,又稱算子(運算符)或泛函,包含多于一個箭頭的函數,高階函數是至少滿足下列一個條件的函數:1.接受一個或多個函數作為輸入,2.輸出一個函數。
在無類型Lambda 演算,所有函數都是高階的;在有類型Lambda 演算(大多數函數式編程語言都從中演化而來)中,高階函數一般是那些函數型別包含多于一個箭頭的函數。在函數式編程中,返回另一個函數的高階函數被稱為Curry化的函數。
在很多函數式編程語言中能找到的 map 函數是高階函數的一個例子。它接受一個函數 f 作為參數,并返回接受一個列表并應用 f 到它的每個元素的一個函數。

函數(Functions)

Kotlin使用函數用fun表示

fun double(x: Int): Int {

}

使用:

// 一般調用
val result = double(2)
// 使用.調用
Sample().foo() // 創建Sample類的實例,調用foo方法

參數(Parameters)和默認參數(Default Arguments)

參數的定義與變量的定義一樣,使用name: type,該類型稱為Pascal表達式,每個參數必須有顯示類型(手動設置類型),默認參數是后面加個=號。

fun read(b: Array<Byte>, off: Int = 0, len: Int = b.size()) {
      ...
}

重寫方法的時候是可以把默認參數給替換掉的。

命名參數

可以在調用函數時使用命名的函數參數。當一個函數有大量的參數或默認參數時這非常方便。
如:

fun reformat(str: String, normalizeCase: Boolean = true, upperCaseFirstLetter: Boolean = true, divideByCamelHumps: Boolean = false, wordSeparator: Char = ' ') {
    println("str: $str, normalizeCase: Boolean = $normalizeCase, upperCaseFirstLetter: Boolean = $upperCaseFirstLetter, divideByCamelHumps: Boolean = $divideByCamelHumps, wordSeparator: Char = $wordSeparator")
}

調用默認參數的時候,是這樣寫的:

reformat(str)

如果把最后一個參數不設置默認參數,那在調用的時候

reformat(str, true, true, false, '_')

這樣子就會很麻煩,每次都要把那些默認參數給寫上,在這里就可以使用命名參數了:

reformat(str, wordSeparator = '_') // 與reformat(str, true, true, false, '_')是一樣的

注意,在Java中是不能使用命名參數的

中綴符號(Infix notation)

中綴表達式是操作符以中綴形式處于操作數的中間(例:3 + 4),先來看一下Kotlin中的中綴函數:

mapOf()方法中的to就是個中綴函數:

val map: Map<Int, Int> = mapOf(1 to 1, 2 to 2)

Range里面的downTo也是個中綴函數:

(10 downTo 1).forEach { print(it) } // 10987654321

使用中綴符號infix可以調用函數,但必須符合一些條件:

  • 必須是成員方法或者擴展函數
  • 函數只有一個參數
  • 使用infix關鍵字表示

下面來寫個中綴函數:

// 定義擴展函數
infix fun Int.iInfix(x: Int): Int  = this + x

fun main(args: Array<String>) {
    // 用中綴符號表示的擴展函數使用
    println("2 iInfix 1:${2 iInfix 1}") // 打?。? iInfix 1:3
    // 與下面是相同的
    println("2.iInfix(1):${2.iInfix(1)}") // 打?。?.iInfix(1):3
}

我們來看看編譯的代碼:


返回Unit的函數(Unit-returning functions)

如果一個函數不返回值,即Java中的void,默認的返回類型就是Unit,默認不顯示:

fun printHello(name: String?): Unit { // Unit可以不顯示
    if (name != null)
        println("Hello ${name}")
    else
        println("Hi there!")
    // return Unit和 return是可選的
}

單表達式函數(Single-Expression functions)

如果一個函數只有一個并且是表達式函數體并且是返回類型自動推斷的話,這樣的函數叫做當單表達式函數,這個在前面也有說過:

fun double(x: Int): Int = x * 2
fun double(x: Int) = x * 2 // 這兩個是一樣的

可變參數(Variable number of arguments (Varargs))

在Java中使用可變參數是這樣寫的:

private void getStr(String... params) {
      ...
}

而在Kotlin中使用可變參數(通常是最后一個參數)的話,是用vararg關鍵字修飾的:

fun <T> asList(vararg ts: T): List<T> {
    val result = ArrayList<T>()
    for (t in ts) // 在這里ts的類型是數組
        result.add(t)
    return result
}

使用的時候與Java一樣:

val list = asList(1, 2, 3)

當我們調用vararg函數,不僅可以接收可以一個接一個傳遞參數,例如asList(1, 2, 3),也可以將一個數組傳遞進去,在數組變量前面加spread操作符,就是*號:

val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4) // 表示(-1, 0, 1, 2, 3, 4)

函數使用范圍(Function Scope)

在前面說過Top-level,函數使用在與類同一級聲明的,不需要再重新創建一個類來持有一個函數,稱為Top-level函數。Kotlin中的函數除了Top-level函數外,還有局部函數、成員函數和前面說過的擴展函數。

局部函數

Kotlin的局部函數是指一個函數在另一個函數中:

fun dfs(graph: Graph) {
    fun dfs(current: Vertex, visited: Set<Vertex>) {
        if (!visited.add(current)) return
        for (v in current.neighbors)
            dfs(v, visited)
    }

    dfs(graph.vertices[0], HashSet())
}

局部函數可以訪問外部函數的局部變量(即閉包),所以在上面的例子,visited可以是局部變量:

fun dfs(graph: Graph) {
    val visited = HashSet<Vertex>()
    fun dfs(current: Vertex) {
        if (!visited.add(current)) return
        for (v in current.neighbors)
            dfs(v)
    }

    dfs(graph.vertices[0])
}

成員函數(Member Functions)

成員函數是定義在一個類或對象里:

class Sample() {
    fun foo() { print("Foo") }
}

調用:

Sample().foo()

尾遞歸函數(Tail recursive functions)

Kotlin支持稱為尾遞歸的函數式編程風格,允許使用循環寫入的一些算法而不是使用遞歸函數寫入,同時沒有堆棧溢出的風險。
函數用tailrec修飾符標記并滿足所需的形式時,編譯器優化遞歸,快速和高效循環。
下面用個遞歸函數來獲取余弦的不動點(一個數學常數0.7390851332151607)在Java中使用遞歸是這樣寫的:

private double findFixPoint(double x = 1.0) {
    if (x == Math.cos(x)) {
        return x;
    } else {
        findFixPoint(Math.cos(x));
    }
}

改成Kotlin代碼的話是這樣的:

private fun findFixPoint(): Double {
    var x = 1.0
    while (true) {
        val y = Math.cos(x)
        if (x == y) return y
        x = y
    }
}

改成尾遞歸的話:


tailrec

來看看編譯后是什么樣的:


要符合tailrec的條件的話,函數必須將其自身調用作為它執行的最后一個操作。如果在遞歸調用后有更多代碼時,不能使用尾遞歸,并且不能用在 try/catch/finally 塊中。目前尾部遞歸只在 JVM 后端中支持。

高級函數與Lambdas表達式

高級函數是將函數作為參數或返回一個函數,稱為高階函數。如lock()函數:

fun <T> lock(lock: Lock, body: () -> T): T {
   lock.lock()
   try {
       return body()
   }
   finally {
       lock.unlock()
   }
}

lock函數的參數body是函數類型()->T,該body函數是一個沒有參數,返回類型為T的函數。
調用的時候可以使用函數引用(::):

fun toBeSynchronized() = sharedResource.operation()  
  
val result = lock(lock, ::toBeSynchronized)  

傳遞Lambdas調用:

val result = lock(lock, { sharedResource.operation() })

在Kotlin中,若函數最后一個參數為函數類型,調用時,該參數可以放到函數的外面:

lock (lock) {
       sharedResource.operation()
}

另一個例子是map()

fun <T, R> List<T>.map(transform: (T) -> R): List<R> {
    val result = arrayListOf<R>()
    for (item in this)
        result.add(transform(item))
    return result
}

it:單個參數的隱式名稱

若函數參數對應的函數只有一個參數,在使用時,可以省略參數定義(連同->),直接使用it代替參數:

val doubled = ints.map { it -> it * 2 }
ints.filter { it > 0 } // it表示 '(it: Int) -> Boolean'

這種方式可以寫成LINQ-style代碼:

strings.filter { it.length == 5 }
.sortBy { it }
.map { it.toUpperCase() }

函數引用(Function References)

函數可以作為參數使用,當把一個函數當作一個值傳遞的時候,可以使用::操作符,將函數引用:

fun isOdd(x: Int) = x % 2 != 0

val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd)) // 輸出 [1, 3]

::isOdd是函數類型(Int) -> Boolean的一個值。

當上下文可以推導出函數的類型時,::用于重載函數:

fun isOdd(x: Int) = x % 2 != 0
fun isOdd(s: String) = s == "brillig" || s == "slithy" || s == "tove"

val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd)) // 引用到 isOdd(x: Int)

也可以直接指定類型:

val predicate: (String) -> Boolean = ::isOdd   // 引用到 isOdd(x: String)

如果有一個函數有兩個函數參數,返回的類型也是一個函數類型

fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
    return { x -> f(g(x)) }
}

那么在引用的時候,會是怎么樣的呢?

fun isOdd(x: Int) = x % 2 != 0 // (Int) -> Boolean

fun length(s: String) = s.length // (String) -> Int

fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
    return { x -> f(g(x)) }
}

val oddLength = compose(::isOdd, ::length) // oddLength的類型是(String) -> Boolean

下劃線表示未使用的變量(1.1版開始)

如果Lambda中有參數未使用,可以使用下劃線代替參數名:

    val map = mapOf(1 to 1, 2 to 2, 3 to 3)
    for ((key, value) in map) {
        print("$value!") // 打印1!2!3!
    }
    for ((_, value2) in map) {
        print("$value2!") // 打印1!2!3!
    }

我們來看看編譯后的區別

decompiled

可以看出,加了改成下劃線后,不會去獲取var2.getKey()).intValue()的值。

Lambda表達式和匿名函數(Lambda Expressions and Anonymous Functions)

“Lambda 表達式”(lambda expression)是一個匿名函數,Lambda表達式基于數學中的λ演算得名,直接對應于其中的lambda抽象(lambda abstraction),是一個匿名函數,即沒有函數名的函數。Lambda表達式可以表示閉包(注意和數學傳統意義上的不同)?!獊碜园俣劝倏?/p>

max(strings, { a, b -> a.length < b.length })

可以看出max是一個高階函數,第二個參數是一個函數類型,等同于下面的函數:

fun compare(a: String, b: String): Boolean = a.length < b.length

函數類型

對于函數接受另一個函數作為參數,我們必須為該參數指定函數類型。
如上面的max的定義:

fun <T> max(collection: Collection<T>, less: (T, T) -> Boolean): T? {
    var max: T? = null
    for (it in collection)
        if (max == null || less(max, it))
            max = it
    return max
}

可以看出第二個參數less的類型是(T, T) -> Boolean,即以TT為參數,返回值為Boolean類型的函數。
如果要定義每個參數的變量名,可以這樣寫:

val compare: (x: T, y: T) -> Int = ...

語法

一個Lambda表達式通常使用{ }包圍,參數是定義在()內,可以添加類型注解,實體部分跟在“->”后面;如果Lambda的推斷返回類型不是Unit,那么Lambda主體中的最后一個(或單個)表達式將被視為返回值。
一個最普通的Lambda表達:

val sum: (Int, Int) -> Int = { x, y -> x + y }  

使用return標簽時,可以隱式返回最后一個表達式的值:

// 下面兩個是等效的
ints.filter {
    val shouldFilter = it > 0 
    shouldFilter
}

ints.filter {
    val shouldFilter = it > 0 
    return@filter shouldFilter
}

如果函數接受另一個函數作為最后一個參數,那么lambda表達式參數可以在()參數列表外部傳遞。

// 下面兩個是等效的
lock(lock, { sharedResource.operation() })
lock (lock) {
    sharedResource.operation()
}

匿名函數(Anonymous Functions)

Lambda表示在定義時,可以明確定義返回值類型;但是在大部分情況下,沒有必要明確定義的,因為返回值類型都可以自動推斷出。
如果需要明確定義返回值類型,可以使用匿名函數代替:

fun(x: Int, y: Int): Int = x + y

匿名函數除了省略了函數名稱,其他跟一般函數的定義基本類似,函數體可以是一個表達式或一個代碼塊:

fun(x: Int, y: Int): Int {
    return x + y
}

若參數類型可以通過上下文推斷出來,也可以省略:

ints.filter(fun(item) = item > 0)

匿名函數的返回類型跟一般函數一樣:對應只有一行執行代碼的函數,編譯器可以自動推斷出來返回類型,可以省略;對應多方代碼塊的函數,需要顯示定義返回值類型(為Unit可以省略)。

Lambdas與匿名函數的區別

  • 匿名函數作為參數,一般定義在()中;而Lambda表達式可以定義到調用函數()外。
  • 另外區別在非局部返回(non-local returns)行為上:非標簽注解的return(返回對應的最內層的函數(即fun),在匿名函數中,退出該匿名函數;而在Lambda表達中,退出包含該表達式的函數。

下面舉個例子來區分


    fun lambdaReturn() {
        val list = asList(1, 2, 3, 4)
        loge("test", list.toString())
        val lambdaList = list.map {
            it * 2
            return
        }
        loge("test", lambdaList.toString())
    }

    fun anonymousReturn() {
        val list = asList(1, 2, 3, 4)
        loge("test", list.toString())
        val lambdaList = list.map(fun(it: Int): Int {
            return it * 2
        })
        loge("test", lambdaList.toString())
    }

    fun <T> asList(vararg ts: T): List<T> {
        val result = ArrayList<T>()
        for (t in ts) // 可變參數ts是數組
            result.add(t)
        return result
    }

在調用lambdaReturn()函數的時候會打?。?/p>

lambdaReturn

而在調用anonymousReturn()函數的時候會打印:

anonymousReturn

這里也就證明了,在Lambdas表達式中return會返回到外層的函數中,而在匿名函數中return會返回的匿名函數的外層函數中。

閉包(Closures)

閉包是指可以包含自由(未綁定到特定對象)變量的代碼塊;這些變量不是在這個代碼塊內或者任何全局上下文中定義的,而是在定義代碼塊的環境中定義(局部變量)。“閉包” 一詞來源于以下兩者的結合:要執行的代碼塊(由于自由變量被包含在代碼塊中,這些自由變量以及它們引用的對象沒有被釋放)和為自由變量提供綁定的計算環境(作用域)。
——來自百度百科

Lambda表達式及匿名函數(以及局部函數,對象表達式)可以訪問包含它的外部范圍定義的變量(Java中只能是常量,在Kotlin中可以是變量):

var sum = 0
ints.filter {
    it > 0
}.forEach {
    sum += it
}
print(sum)

事實上函數、Lambda、if語句、for循環、when語句等都是閉包,但通常情況下,我們所說的閉包是 Lambda 表達式。
閉包可以在定義的時候直接執行閉包操作,這種閉包一般用在初始化操作上:

{ x: Int, y: Int, z: String ->
    println("${x + y}_ $z")
}(4, 5, "test")

像我們寫構造函數的時候,主構造函數不包含任何代碼,初始化代碼必須寫在init代碼塊中,而init的代碼塊就是閉包:

init

屬性里面的setter也是閉包:

setter

在build.gradle里面都是閉包:

build.gradle

帶接收者的函數字面值(Function Literals with Receiver)

Kotlin提供了使用指定的接收者對象調用文本函數的功能,這就是文本擴展函數。在文本函數中,可以調用接收者對象上的方法。類似于擴展函數,可以調用方法內的接收者對象的成員

val sum : Int.(other: Int) -> Int = { it + 1 }

sum的類型是Int.(one: Int) -> Int,傳入一個Int類型的值,返回Int類型,在閉包里面返回值。

調用的時候與擴展函數一樣:

val a = 1.sum(2) // a的值為3

如果用個匿名函數的話,可以直接指定函數文本的接收者類型:

val sums = fun Int.(other: Int): Int {
    println(this)
    println(this + other)
    return this + other
}

調用

10.sums(10) 

打印結果


上面兩個的sumsums的區別:

sum
sums

Lambda表達式:

class HTML {
    fun body(one: Int) {
        println("body$one")
    }
}

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()  // 創建接收器對象
    html.init()        // 將接收器對象傳遞給lambda
    // 等同于init(html)
    return html
}

使用的時候,可以先聲明一個HTML.() -> Unit類型,然后調用html方法

val init: HTML.() -> Unit = {
    body(1)
}
// 因為html方法返回的是一個HTML類型,所以可以在后面直接使用body方法
html(init).body(2)  

簡化

html {      
    body()   
}.body()
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容