Kotlin 知識梳理(10) 高階函數:Lambda 做爲形參或返回值

1、本文概要

本文是對<<Kotlin in Action>>的學習筆記,若是須要運行相應的代碼能夠訪問在線環境 try.kotlinlang.org,這部分的思惟導圖爲: java

Kotlin 知識梳理(5) - lambda 表達式和成員引用 中咱們初步認識了 lambda,這一章咱們將學到如何建立 高階函數:使用 lambda做爲 參數或者返回值 的函數。高階函數有助於簡化代碼,去除代碼重複,以及構建漂亮的抽象概念。

2、聲明高階函數

按照定義,高階函數就是 以另外一個函數做爲參數或者返回值的函數,在Kotlin中,函數能夠用lambda或者函數引用來表示。例如,標準庫中的filter函數將一個判斷式函數做爲參數,所以它就是一個高階函數。設計模式

list.filter { x > 0 }
複製代碼

2.1 函數類型

爲了聲明一個以lambda做爲實參的函數,你須要知道如何聲明 對應形參的類型。下面咱們先看一個簡單的例子,把lambda表達式保存在局部變量當中:安全

val sum = { x : Int, y : Int -> x + y }
val action = { println(42) }
複製代碼

在上面的例子中,咱們省去了類型的聲明。可是編譯器能夠推導出sumaction這兩個 變量具備函數類型,這些變量的顯示聲明爲:app

//有兩個 Int 型參數和 Int 型返回值的函數
val sum : (Int, Int) -> Int = {x, y -> x + y}
//沒有參數和返回值的函數
val action : () -> Unit = { println(42) }
複製代碼

聲明函數類型,須要 將函數參數類型放在括號中,緊接着是一個箭頭和函數的返回類型ide

(Int, String) -> Unit
複製代碼

Unit類型用於表示函數不返回任何有用的值,在聲明一個普通的函數時,Unit類型的返回值是能夠忽略的,可是一個 函數類型聲明老是須要一個顯示的返回類型,因此在這種場景下Unit是不能省略的。函數

{x, y -> x + y}中,由於它們的類型已經在函數類型的變量聲明部分指定了,不須要在lambda當中重複聲明。post

就像其它方法同樣,函數類型的返回值也能夠標記爲可空類型:學習

var canReturnNull : (Int, Int) -> Int? = { null }
複製代碼

也能夠定義一個 函數類型的可空變量,爲了明確表示 變量自己可空,而不是函數類型的返回類型可空,你須要 將整個函數類型的定義包含在括號內並在括號後添加一個問號網站

var funOrNull : ((Int, Int) -> Int)? = null
複製代碼

函數類型的參數名

能夠爲函數類型聲明中的參數指定名字:ui

//函數類型的參數如今有了名字...
fun performRequest(url : String, callback : (code : Int, content : String) -> Unit) {
    //....
}
複製代碼

調用方法爲:

>> val url = "http://kotl.in"
//可使用 API 中提供的參數名字做爲 lambda 參數的名字....
>> performRequest(url) { code, content -> / *...* / }
>> performRequest(url) { code, page -> / *...* / }
複製代碼

參數名稱不會影響類型的匹配,當你聲明一個lambda時,沒必要使用和函數類型聲明中如出一轍的參數名稱,但命名會提高代碼的可讀性而且能用於IDE的代碼補全。

2.2 調用做爲參數的函數

下面咱們討論如何實現一個高階函數,這個例子會盡可能簡單而且使用以前的lambda sum一樣的聲明,這個函數實現對於兩個整數的任意操做,而後打印出結果:

fun twoAndThree(operation: (Int, Int) -> Int) {
    val result = operation(2, 3)
    println("The result is $result")
}

fun main(args: Array<String>) {
    twoAndThree { a, b -> a + b }
    twoAndThree { a, b -> a * b }
}
複製代碼

運行結果爲:

>> The result is 5
>> The result is 6
複製代碼

調用做爲參數的函數operation和調用普通函數的語法是同樣:把括號放在函數名後,並把參數放在括號內。下面,讓咱們實現一個標準的庫函數:filter函數。它會過濾掉字符串中不屬於a..z範圍內的字母。

fun String.filter(predicate: (Char) -> Boolean): String {
    val sb = StringBuilder()
    for (index in 0 until length) {
        val element = get(index)
        if (predicate(element)) sb.append(element)
    }
    return sb.toString()
}
複製代碼

filter函數以一個判斷式做爲參數,判斷式的類型是一個函數,以字符串做爲參數並返回boolean類型的值。

fun main(args: Array<String>) {
    println("ab1c".filter { it in 'a'..'z' })
}
複製代碼

運行結果:

>> abc
複製代碼

2.3 在 Java 中使用函數

背後的原理

背後的原理是:

  • 函數類型被聲明爲普通的接口:一個函數類型的變量是FunctionN接口的一個實現。Kotlin標準庫定義了一系列的接口:Function0<R>表示沒有參數的函數,Function1<P1, R>表示一個參數的函數。
  • 一個函數類型的變量就是實現了對應的Function接口的實現類的實例,每一個接口定義了一個invoke方法,實現類的invoke方法包含了lambda函數體,調用這個方法就會執行函數。

Java中能夠很簡單地調用使用了函數類型的Kotlin函數,Java 8lambda會被自動轉換爲函數類型的值:

//Kotlin 聲明
fun processTheAnswer(f : (Int) -> Int) {
    println(f(42))
}
複製代碼
//Java
processTheAnswer(number -> number + 1)
複製代碼

在舊版的Java中,能夠傳遞一個實現了函數接口中的invoke方法的匿名內部類的實例:

>> processTheAnswer(
    new Function1<Integer, Integer>() {
        @override
        public Integer invoke(Integer number) {
            System.out.println(number);
            return number + 1;
        }
    }
)
複製代碼

Java中能夠很容易地使用Kotlin標準庫中以lambda做爲參數的擴展函數,可是必需要 顯示地傳遞一個接收者做爲第一個參數

List<String> strings = new ArrayList();
strings.add("42");
CollectionsKt.forEach(strings, s -> {
    System.out.println(s);
    retrun Unit.INSTANCE;
});
複製代碼

Java中,函數或者lambda能夠返回Unit。但由於在KotlinUnit類型是有一個值的,因此須要顯示地返回它。一個返回voidlambda不能做爲返回Unit的函數類型的實參,就像以前的例子中的(String) -> Unit

2.4 函數類型的參數默認值和 null 值

2.4.1 函數類型的參數默認值

joinToString函數爲例,咱們除了能夠定義前綴、後綴和分隔符之外,還能夠經過最後一個 函數類型的參數 指定如何將集合當中的每一個元素轉換成爲String,這是一個泛型函數:它有一個類型參數T表示集合中的元素的類型,Lambda transform將接收這個類型的參數,下面咱們來看一下如何爲它指定一個lambda做爲默認值:

fun <T> Collection<T>.joinToString(
        separator: String = ", ",
        prefix: String = "",
        postfix: String = "",
        //爲函數類型的參數提供默認值。
        transform: (T) -> String = { it.toString() }
): String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        //調用傳入的函數。
        result.append(transform(element))
    }

    result.append(postfix)
    return result.toString()
}

fun main(args: Array<String>) {
    val letters = listOf("Alpha", "Beta")
    println(letters.joinToString())
    println(letters.joinToString { it.toLowerCase() })
    println(letters.joinToString(separator = "! ", postfix = "! ",
           transform = { it.toUpperCase() }))
}
複製代碼

運行結果爲:

>> Alpha, Beta
>> alpha, beta
>> ALPHA! BETA! 
複製代碼

2.4.2 聲明一個參數可爲空的函數類型

當聲明一個參數爲可空的函數類型時,不能直接調用做爲參數傳遞進來的函數:Kotlin會由於檢測到潛在的空指針而致使編譯失敗,在這種狀況下有兩種處理方式:

  • 顯示地檢查 null 顯示地檢查null是一種比較容易理解的方法:
fun foo(callback : (() _ Unit)?) {
    if (callback != null) {
        callback()
    }
}
複製代碼
  • 經過安全調用語法調用 除此以外,由於函數類型是一個包含invoke方法的接口的具體實現,做爲一個普通方法,invoke能夠經過安全調用語法調用:
callback?.invoke() ?: /* 默認實現 */
複製代碼

2.5 返回函數的函數

從函數中返回另外一個函數適用於下面的場景:程序中的一段邏輯可能會由於程序的狀態或者其餘條件而產生變化,好比說下面的例子,運輸費用的計算依賴於選擇的運輸方式:

//聲明一個枚舉類型。
enum class Delivery { STANDARD, EXPIRED }

class Order(val itemCount : Int)

//返回的函數類型爲:形參爲 Order 類,返回類型爲 Double。
fun getShippingCalculator(delivery : Delivery) : (Order) -> Double {
    if (delivery == Delivery.EXPIRED) {
        return { order -> 6 + 2.1 * order.itemCount }
    }
    return { order -> 1.2 * order.itemCount }
}

fun main(args: Array<String>) {
	val calculator = getShippingCalculator(Delivery.EXPIRED)
    println("cost ${calculator(Order(3))}")
}
複製代碼

在上面的例子中,getShippingCalculator返回了一個函數,這個函數以Order做爲參數並返回一個Double類型的值,要返回一個函數,須要寫一個return表達式,跟上一個lambda、一個成員引用,或者其餘的函數類型的表達式。

下面,咱們來看一個過濾器的例子:

data class Person(val firstName : String, val phoneNumber : String?) class ContactListFilter {
    var prefix : String = ""
    var onlyWithPhoneNumber : Boolean = false fun getPredicate() : (Person) -> Boolean {
        val startWithPrefix = { p : Person ->
            p.firstName.startsWith(prefix)
        }
        if (!onlyWithPhoneNumber) {
            return startWithPrefix
        }
        return { startWithPrefix(it) && it.phoneNumber != null }
    }
}

fun main(args: Array<String>) {
	val contacts = listOf(Person("Dmitry", "123-4567"),
                         Person("Svelana", null))
    val contactListFilters = ContactListFilter()
    contactListFilters.prefix = "S"
    contactListFilters.onlyWithPhoneNumber = false println(contacts.filter(contactListFilters.getPredicate())) } 複製代碼

運行結果爲:

>> [Person(firstName=Svelana, phoneNumber=null)]
複製代碼

2.6 經過 lambda 去除重複代碼

咱們來看一個分析網站的例子,SiteVisit類用來保存每次訪問的路徑、持續時間和用戶的操做系統。

data class SiteVisit( val path: String, val duration: Double, val os: OS ) enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID }

val log2 = listOf(
    SiteVisit("/", 34.0, OS.WINDOWS),
    SiteVisit("/", 22.0, OS.MAC),
    SiteVisit("/login", 12.0, OS.WINDOWS),
    SiteVisit("/signup", 8.0, OS.IOS),
    SiteVisit("/", 16.3, OS.ANDROID)
)
複製代碼

接下來,咱們經過擴展函數的方式,定義一個方法用於統計 符合特定條件 的操做系統用戶的平均使用時長。

fun List<SiteVisit>.averageDuration(predicate : (SiteVisit) -> Boolean) = 
    filter(predicate).map(SiteVisit::duration).average()
複製代碼

運行下面的代碼:

fun main(args: Array<String>) {
    println(log2.averageDuration {it.os in setOf(OS.WINDOWS, OS.ANDROID) }) } 複製代碼

對於一些廣爲人知的設計模式可使用函數類型和lambda表達式進行簡化,好比策略模式。沒有lambda表達式的狀況下,你須要聲明一個接口,併爲每一種可能的策略提供實現類。使用函數類型,能夠用一個通用的函數類型來描述策略,而後傳遞不一樣的lambda表達式做爲不一樣的策略。


更多文章,歡迎訪問個人 Android 知識梳理系列:

相關文章
相關標籤/搜索