Kotlin修煉指南(二):lambda表達式的精髓

lambda表達式是Kotlin函數式編程的一個重要概念,要想掌握函數式編程,就必須熟練掌握lambda表達式,並掌握它的各類寫法和實現,這些都是掌握函數式編程的基礎。編程

lambda基本形式

lambda表達式有三大特徵:微信

  1. lambda表達式存在於{}中
  2. 參數及參數類型(可省略)在->左邊
  3. 函數體在->右邊

lambda表達式返回值老是返回函數體內部最後一行表達式的值閉包

這三種形式的lambda表達式必需要可以很是熟練的掌握,這樣才能進一步的瞭解Kotlin和函數式編程。ide

無參數

無參數形式爲:函數式編程

val 函數名 = { 函數體 }函數

示例:性能

val hello = { println("hello kotlin") }

// 等價於函數
fun hello() {
    println("hello kotlin")
}複製代碼

有參數

  1. 完整表達方式:

val 函數名 : (參數1類型, 參數2類型, ...) -> 返回值類型 = { 參數1, 參數2, ... -> 函數體 }this

  1. 表達式返回值類型可自動推斷形式

val 函數名 = { 參數1:類型1, 參數2:類型2, ... -> 函數體 }spa

示例:設計

val sum: (Int, Int) -> Int = { a, b -> a + b }
// 等價於
val sum = { a: Int, b: Int -> a + b }

// 等價於函數
fun sum(a: Int, b: Int): Int {
    return a + b
}複製代碼

只有一個參數的時候,返回值中的參數形參能夠省略,引用時經過it進行引用

lambda的調用有兩種方式,一種是經過()來進行調用,另外一種是經過invoke()函數進行調用,兩種方式沒有區別。

fun main(args: Array<String>) {
    val lambda = { println("test") }
    lambda()
    lambda.invoke()
}複製代碼

在使用lambda表達式的時候,能夠用下劃線(_)表示未使用的參數,表示不處理這個參數。

匿名函數

匿名函數形式爲:

val 函數名 = fun(參數1:類型1, 參數2:類型2, ...): 返回值類型 { 函數體 }

示例:

val sum = fun(a: Int, b: Int): Int {
    return a + b
}

// 等價於函數
fun sum(a: Int, b: Int): Int {
    return a + b
}複製代碼

高階函數的演變

所謂高階函數,實際上就是數學中的複合函數的概念,f(g(x))。

引用函數

fun cal(a: Int, b: Int, f: (c: Int, d: Int) -> Int): Int {
    return f(a, b)
}

fun sum(a: Int, b: Int): Int {
    return a + b
}

fun main(args: Array<String>) {
    val result = cal(2, 3, ::sum)
    println("result = $result")
    // result = 8
}複製代碼

在cal函數中的最後一個參數是 f: (a: Int, b: Int) -> Int 表示該參數是一個函數引用,函數體內調用了最後一個參數指向的函數。隨後定義了sum函數,該函數就是cal函數的第三個參數。

::sum表示sum函數的引用,cal(2, 3, ::sum)這一句就至關於執行了sum(2, 3),因此輸出結果爲5。

函數引用能夠進一步的簡化函數的調用,相似下面這個例子:

class Test {
    fun doSomething() {
        println("test")
    }

    fun doTest(f: (Test) -> Unit) {
        f(this)
    }
}

fun main(args: Array<String>) {
    val t = Test()
    // 常規寫法 傳入函數
    t.doTest { test -> test.doSomething() }
    // 使用引用函數(Test::doSomething其實是對lambda表達式{test -> test.doSomething()}的簡化)
    t.doTest(Test::doSomething)
}複製代碼

參數lambda化

fun cal(a: Int, b: Int, f: (a: Int, b: Int) -> Int): Int {
    return f(a, b)
}

fun main(args: Array<String>) {
    val sum = { a: Int, b: Int -> a + b }
    val result = cal(2, 3, sum)
    println("result = $result")
    // result = 5
}複製代碼

利用前面寫的方式,將一個函數改寫爲lambda形式,做爲參數直接賦值給cal函數。

那麼更進一步,能夠省略這個lambda的變量,直接將lambda表達式傳入函數。

fun cal(a: Int, b: Int, f: (a: Int, b: Int) -> Int): Int {
    return f(a, b)
}

fun main(args: Array<String>) {
    val result = cal(2, 3, { a: Int, b: Int -> a + b })
    println("result = $result")
    // result = 5
}複製代碼

另外,在Kotlin中調用高階函數時,若是最後一個參數爲lambda表達式,能夠將lambda表達式寫在外面,並且若是沒有其它參數的話,小括號也是能夠省略的。

fun cal(a: Int, b: Int, f: (a: Int, b: Int) -> Int): Int {
    return f(a, b)
}

fun main(args: Array<String>) {
    val result = cal(2, 3, { a: Int, b: Int -> a + b })
    // 兩種寫法等價
    val result2 = cal(2, 3) { a: Int, b: Int -> a + b }
    println("result = $result")
    // result = 5
}複製代碼

函數變量

fun main(args: Array) {
    val sumLambda = {a: Int, b: Int -> a + b}
    var numFun: (a: Int, b: Int) -> Int
    numFun = {a: Int, b: Int -> a + b}
    numFun = sumLambda
    numFun = ::sum
    numFun(1,2)
}複製代碼

能夠看到這個變量能夠等於一個lambda表達式,也能夠等於另外一個lambda表達式變量,還能夠等於一個普通函數,可是在函數名前須要加上(::)來獲取函數引用。

lambda表達式實例

下面經過一個簡單的例子來看下一些具體的lambda表達式是怎麼寫的。

// 匿名函數
val sum = fun(a: Int, b: Int): Int {
    return a + b
}

// 具名函數
fun namedSum(a: Int, b: Int): Int {
    return a + b
}

// 高階函數
fun highSum(a: Int, b: Int, f: (Int, Int) -> Int): Int {
    return f(a, b)
}

fun main(args: Array<String>) {
    // 經過()來執行匿名函數sum
    val add = sum(1, 2)
    println(add)
    // 經過lambda表達式來完成函數highSum
    val add2 = highSum(3, 4) { a, b -> a + b }
    println(add2)
    // 經過函數引用來完成函數highSum
    val add3 = highSum(5, 6, ::namedSum)
    println(add3)

    // forEach參數接收一個函數
    args.forEach({ it: String -> println(it) })
    // 去掉返回值,自動推斷
    args.forEach({ it -> println(it) })
    // 只有一個參數的時候能夠省略it
    args.forEach({ println(it) })
    // lambda表達式在最後一個參數能夠外移
    args.forEach() { println(it) }
    // 函數若無參數能夠去掉()
    args.forEach { println(it) }
    // 引用函數
    args.forEach(::println)
}複製代碼

函數類型與實例化

相似Int、String這樣的數據類型,函數的類型表示爲:

(Type1, Type2, ...) -> Type
// 例如
(Int) -> Int

// 因此纔有了這樣的函數
fun test(a: Int, f: (Int) -> Int): Int {
    return f(a)
}複製代碼

其中(Int) -> Int的地位和Int、String的地位是等價的。

函數既然是一種類型,那麼函數也和Int、String同樣,是具備可實例化的實例的,例如Int的實例一、String的實例「xys」,那麼獲取函數的實例,主要客源經過下面三種方式:

  1. :: 雙冒號操做符表示對函數的引用
  2. lambda表達式
  3. 匿名函數
fun main(args: Array<String>) {
    // 引用函數
    println(test(1, 2, ::add))
    // 匿名函數
    val add = fun(a: Int, b: Int): Int {
        return a + b
    }
    println(test(3, 4, add))
    // lambda表達式
    println(test(5, 6, { a, b -> a + b }))// lambda做爲最後一個參數能夠提到括號外
    println(test(5, 6) { a, b -> a + b })
}

fun test(a: Int, b: Int, f: (Int, Int) -> Int): Int {
    return f(a, b)
}

fun add(a: Int, b: Int): Int {
    return a + b
}複製代碼

lambda表達式的類型

經過下面的例子,能夠了解下lambda表達式的類型,代碼以下所示。

// 無參,返回String
() -> String

// 兩個整型參數,返回字符串類型
(Int, Int) -> String 

// 傳入了一個lambda表達式和一個整型,返回Int
(()->Unit, Int) -> Int複製代碼

開發者能夠經過相似上面的形式來表達lambda表達式的類型,不過和Int、String同樣,lambda表達式也有本身的類,即Function類。

Kotlin封裝了Function0到Function22,一共23個Function類型,分別表示參數個數從0到22。

lambda表達式的return

除非使用標籤指定了返回點,不然return從最近的使用fun關鍵字聲明的函數返回。

fun main(args: Array<String>) {
    var sum: (Int) -> Unit = tag@{
        print("Test return $it")
        return@tag
    }
    sum(3)
}複製代碼

SAM轉換

SAM = Single Abstract Method,即惟一抽象方法

SAM轉換是爲了在Kotlin代碼中調用Java代碼所提供的一個語法糖,即爲Java的單一方法的接口,提供lambda形式的實現,例如Android中最多見的view.setOnClickListener:

// SAM convert in KT
view.setOnClickListener{ 
    view -> doSomething
}

// Java接口
public interface OnClickListener { 
     void onClick(View v); 
}複製代碼

SAM轉換是專門爲Java提供的語法糖,用於將lambda表達式轉換成相應的匿名類的實例。在Kotlin中實現相同的功能,只須要使用函數參數便可。

帶接收者的lambda表達式

lambda表達式實際上有兩種形式,一種是前面介紹的基本形式,還有一種是帶接收者的形式,兩種lambda表達式以下所示。

普通lambda表達式:{ () -> R }

即函數無入參,返回值爲R類型。

帶接收者的lambda表達式:{ T.() -> R }

即申明一個T類型的接收者對象,且無入參,返回值爲R類型。

Kotlin中的拓展函數,實際上就是使用的帶接收者的lambda表達式,

帶接收者的lambda與普通的lambda的區別主要在於this的指向區別,T.() -> R裏的this表明的是T的自身實例,而() -> R裏,this表明的是外部類的實例。

使用typealias給重複申明的lambda表達式設置別名

fun fun1(f: (Int) -> Unit) {
    f(1)
}

fun fun2(f: (Int) -> Unit) {
    f(2)
}

// 使用typealias
typealias intFun = (Int) -> Unit

fun fun3(f: intFun) {
    f(3)
}

fun fun4(f: intFun) {
    f(4)
}

fun main(args: Array<String>) {
    fun1 { println(it) }
    fun2 { println(it) }
    fun3 { println(it) }
    fun4 { println(it) }
}複製代碼

閉包

若是一個函數內部申明或者返回了一個函數,那麼這個函數被稱之爲閉包。

函數內部的變量能夠被函數內部申明的函數所訪問、修改,這就讓閉包能夠攜帶狀態(全部的中間值都會被放入內存中)。

開發者能夠經過閉包讓函數具備狀態,從而能夠封裝函數的狀態,讓函數具備面向對象的特性。

爲何須要閉包

在瞭解閉包以前,須要先了解下變量的做用域,在kotlin中,變量的做用域只有兩種,即全局變量和局部變量。

  • 全局變量,函數內部和函數外部都可以直接訪問。
  • 局部變量,只有函數內部能夠訪問。

那麼如何在函數外部訪問函數內部的局部變量呢,這就須要經過閉包來進行訪問,閉包的設計就是爲了能讓開發者讀取某個函數內部的變量。

因此閉包就是可以讀取其它函數的局部變量的函數。

閉包讓函數攜帶狀態

fun test(): () -> Int {
    var a = 1
    println(a)
    return fun(): Int {
        a++
        println(a)
        return a
    }
}

fun main(args: Array<String>) {
    val t = test()
    t()
    t()
}

// output
1
2
3複製代碼

變量t的類型其實是一個匿名函數,因此在調用t函數執行的時候,實際上執行的是返回的匿名函數,同時,因爲閉包能夠攜帶外包的變量值,因此a的狀態值被傳遞了下來。

閉包能夠訪問函數體以外的變量,這被稱之爲變量捕獲。閉包會將捕獲的變量保存在一個特殊的內存區域,從而實現閉包攜帶狀態的功能。

kotlin實現接口回調

單方法回調

class Test {
    private var callBack: ((str: String) -> Unit)? = null
    fun setCallback(myCallBack: ((str: String) -> Unit)) {
        this.callBack = myCallBack
    }
}複製代碼

使用函數替代了接口的實現。

回調寫法的演進

Java思想的kotlin寫法

interface ICallback {
    fun onSuccess(msg: String)

    fun onFail(msg: String)
}

class TestCallback {

    var myCallback: ICallback? = null

    fun setCallback(callback: ICallback) {
        myCallback = callback
    }

    fun init() {
        myCallback?.onSuccess("success message")
    }
}

fun main(args: Array<String>) {
    val testCallback = TestCallback()
    testCallback.setCallback(object : ICallback {
        override fun onSuccess(msg: String) {
            println("success $msg")
        }

        override fun onFail(msg: String) {
            println("fail $msg")
        }
    })
    testCallback.init()
}複製代碼

使用lambda表達式替代匿名內部類實現。

class TestCallback {

    var mySuccessCallback: (String) -> Unit? = {}
    var myFailCallback: (String) -> Unit? = {}

    fun setCallback(successCallback: (String) -> Unit, failCallback: (String) -> Unit) {
        mySuccessCallback = successCallback
        myFailCallback = failCallback
    }

    fun init() {
        mySuccessCallback("success message")
        myFailCallback("fail message")
    }
}

fun main(args: Array<String>) {
    val testCallback = TestCallback()
    testCallback.setCallback({ println("success $it") }, { println("fail $it") })
    testCallback.init()
}複製代碼

這樣去掉了接口和匿名內部類。

高階函數的使用場景

高階函數的一個重要使用場景就是集合的操做,裏面下面這個例子,分別使用Java和Kotlin實現了「找最大值」的方法。

data class Test(val name: String, val age: Int)

fun main(args: Array<String>) {
    // Java寫法
    val testList = listOf(Test("xys", 18), Test("qwe", 12), Test("rty", 10), Test("zxc", 2))
    findMax(testList)
    // Kotlin寫法
    println(testList.maxBy { it.age })
    println(testList.maxBy(Test::age))
}

fun findMax(test: List<Test>) {
    var max = 0
    var currentMax: Test? = null
    for (t in test) {
        if (t.age > max) {
            max = t.age
            currentMax = t
        }
    }
    println(currentMax)
}複製代碼

函數式集合操做

fliter & map

filter用於數據的篩選,相似的還有filterIndexed,即帶Index的過濾器、filterNot,即過濾全部不知足條件的數據。

map用於對數據進行變換,表明了一種一對一的變換關係,它能夠對集合中的數據作一次變換,相似的還有mapIndexed()。

fun main(args: Array<String>) {
    val test = listOf(1, 3, 5, 7, 9)

    // filter函數遍歷集合並選出應用給定lambda後會返回true的那些元素
    println("大於5的數 ${test.filter { it > 5 }}")
    // map函數對集合中的每個元素應用給定的函數並把結果收集到一個新集合
    println("平方操做 ${test.map { it * it }}")

    val testList = listOf(Test("xys", 18), Test("qwe", 12), Test("rty", 10), Test("zxc", 2))
    // 將一個列表轉換爲另外一個列表
    println("只展現name ${testList.map { it.name }}")
    // filter與map鏈式操做
    println("展現age大於10的name ${testList.filter { it.age > 10 }.map { it.name }}")
}

data class Test(val name: String, val age: Int)複製代碼

all & any & count & find

fun main(args: Array<String>) {
    val test = listOf(1, 3, 5, 7, 9)

    // all判斷是否所有符合lambda表達式的條件
    println("是否所有符合>10 ${test.all { it > 10 }}")
    // any判斷是否存在有符合lambda表達式的條件的數據
    println("是否存在>8 ${test.any { it > 8 }}")
    // count獲取符合lambda表達式條件的數據個數
    println("大於5的個數 ${test.count { it > 5 }}")
    // find獲取符合lambda表達式條件的第一個數據
    println("第一個大於5 ${test.find { it > 5 }}")
    println("最後一個大於5 ${test.findLast { it > 5 }}")
}複製代碼

groupBy & partition & flatMap

flatMap()表明了一個一對多的關係,能夠將每一個元素變換爲一個新的集合,再將其平鋪成一個集合。

groupBy()方法會返回一個Map >的Map對象,其中Key就是咱們分組的條件,value就是分組後的集合。

fun main(args: Array<String>) {
    val test = listOf("a", "ab", "b", "bc")

    // groupBy按照lambda表達式的條件重組數據並分組
    println("按首字母分組 ${test.groupBy(String::first)}")
    // partition按照條件進行分組,該條件只支持Boolean類型條件,first爲知足條件的,second爲不知足的
    test.partition { it.length > 1 }.first.forEach { print("$it、") }
    println()
    test.partition { it.length > 1 }.second.forEach { print("$it、") }
    println()
    // flatMap首先按照lambda表達式對元素進行變換,再將變換後的列表合併成一個新列表
    println(test.flatMap { it.toList() })
}複製代碼

sortedBy

sortedBy()用於根據指定的規則進行順序排序,若是要降序排序,則須要使用sortedByDescending(),

fun main(args: Array<String>) {
    val test = listOf(3, 2, 4, 6, 7, 1)
    println(test.sortedBy { it })
}複製代碼

take & slice

take()和slice()用於進行數據切片,從某個集合中返回指定條件的新集合。相似的還有takeLast()、takeIf()等。

fun main(args: Array<String>) {
    val test = listOf(3, 2, 4, 6, 7, 1)
    // 獲取前3個元素的新切片
    println(test.take(3))
    // 獲取指定index組成的新切片
    println(test.slice(IntRange(2, 4)))
}複製代碼

reduce

fun main(args: Array<String>) {
    val test = listOf("a", "ab", "b", "bc")

    // reduce函數將一個集合的全部元素經過傳入的操做函數實現數據集合的累積操做效果。
    println(test.reduce { acc, name -> "$acc$name" })
}複製代碼

做用域函數

前面文章提到的做用域函數就是高階函數的一個重要實例。

lambda表達式的其它特性

惰性序列操做

當一些集合函數進行鏈式調用的時候,每一個函數的調用結果都將保存爲一個新的臨時列表,所以,大量的鏈式操做會產生大量的中間變量,從而致使性能問題,爲了提升效率,能夠把鏈式操做改成序列(sequance)。

調用擴展函數asSequence把任意集合轉換成序列,調用toList來作反向的轉換

fun main(args: Array<String>) {
    val testList = listOf(Test("xys", 18), Test("qwe", 12), Test("rty", 10), Test("zxc", 2))

    // 函數的鏈式調用
    println("集合調用 展現age大於10的name ${
    testList.filter { it.age > 10 }
            .map { it.name }}")
    // 函數的序列操做
    println("序列操做 展現age大於10的name ${
    testList.asSequence()
            .filter { it.age > 10 }
            .map { it.name }
            .toList()}")
}

data class Test(val name: String, val age: Int)複製代碼

一個完整的序列包括兩個操做,即中間序列和末端序列,中間序列操做始終都是惰性的,末端序列操做觸發全部的惰性計算。

//中間操做            //末端操做
testList.asSequence(). filter {..}.map {..}.toList() 複製代碼

所以,經過序列的這種方式,就避免了產生大量中間集合,從而提升了性能。

延遲計算

fun main(args: Array<String>) {
    val addResult = lateAdd(2, 4)
    print(addResult())
}

fun lateAdd(a: Int, b: Int): Function0<Int> {
    fun add(): Int {
        return a + b
    }
    return ::add
}複製代碼

在lateAdd內部定義了一個局部函數,最後返回了該局部函數的引用,對結果使用()操做符拿到最終的結果,達到延遲計算的目的。

fun main(args: Array<String>) {
    val funs = mapOf("sum" to ::sum)
    val mapFun = funs["sum"]
    if (mapFun != null) {
        val result = mapFun(1, 2)
        println("sum result -> $result")
    }
}

fun sum(a: Int, b: Int): Int {
    return a + b
}複製代碼

掌握了lambda表達式,就等於戰士有了槍,多練習,多思考,才能掌握lambda表達式的精髓,爲後面掌握函數式編程,打下堅實的基礎。

歡迎關注個人微信公衆號——Android羣英傳

相關文章
相關標籤/搜索