Kotlin系列 - 高階函數與標準庫中的經常使用函數(三)

Kotlin細節文章筆記整理更新進度:
Kotlin系列 - 基礎類型結構細節小結(一)
Kotlin系列 - 函數與類相關細節小結(二)html

1.高階函數

基本概念: 傳入或者返回函數的函數 函數引用:引用的函數名前加上 ::java

  • 有如下幾種類型:
  • 類成員方法引用:類名::成員方法名
  • 擴展函數引用:類名::擴展函數名
  • 實例函數引用:實例名::成員方法名
  • 包級別函數引用:::函數名

第一個例子:數組

打印數組中的元素(傳入包級別函數)bash

fun main(args:Array<String>) {
        args.forEach(::println) //函數引用
}

public actual inline fun println(message: Any?) {
    System.out.println(message)
}

public inline fun <T> Array<out T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}
複製代碼

forEach(action: (T) -> Unit):要求傳入了一個函數,參數爲(action: (T) -> Unit),類型爲一個參數T,返回值爲Unit
println(message: Any?):類型爲一個參數T,返回值爲Unit
咱們調用args.forEach(::println)println函數傳入給forEach閉包

第二個例子:app

過濾數組中的空字符串(傳入類成員函數)函數

fun main(args:Array<String>) {
    args.filter(String::isNotEmpty)
}

public inline fun <T> Array<out T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

public inline fun CharSequence.isNotEmpty(): Boolean = length > 0

複製代碼

這裏有個點要注意下: filter要求傳入的函數類型爲(predicate: (T) -> Boolean),可是咱們傳入的String::isNotEmpty這個方法並無參數!!!public inline fun CharSequence.isNotEmpty(): Boolean = length > 0 只有一個返回值Boolean爲何能夠呢???post

答案:由於類名::成員方法名默認就有一個參數,這個函數類型就是類名這個類型的。好比上面的String::isNotEmpty至關於isNotEmpty(String)優化

第三個例子:ui

打印數組中的元素(傳入實例函數)

fun main(args:Array<String>) {
    val t = Test()
    args.forEach(t::testName)
}
class Test{
    fun testName(name:String){
        println(name)
    }
}
public inline fun <T> Array<out T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}
複製代碼

這裏傳入的是t::testName,實例名::成員方法名就不會默認多出一個參數。若是使用Test::testName會顯示報錯信息,也驗證了咱們上面說的類名::成員方法名默認就有一個參數。

image.png

這裏總結一下:函數引用,就是將函數做爲參數變量傳入具體某個方法中,也能夠賦值給變量。注意的是,若是是類成員函數、擴展函數引用(類名:函數名),默認參數會多一個就是類自己這個參數

2. 閉包

  • 函數運行的環境
  • 持有函數運行狀態
  • 函數內部能夠定義函數/類
fun add(x: Int): (Int) -> Int {
    return fun(y: Int): Int {
        return x + y
    }
}
fun main() {
    var add2 = add(2)
    println(add2(10))
}
複製代碼

函數的定義方法能夠傳入函數,也能夠返回函數,函數內的做用域包含了函數內的子函數跟子類等。 格式 : fun 方法名(形參:函數類型) 函數類型{} 函數類型基本寫法: () -> Unit (多個參數) -> 返回類型

3. 函數複合

  • f(g(x)) 函數傳入函數
//定義兩個函數
val add5 = { i: Int -> i + 5 }

val multiplyBy2 = { i: Int -> i * 2 }

fun main() {
    println(multiplyBy2(add5(9)))
}
-----打印出來的Log
28
複製代碼

上面是基本的展現,函數中傳入函數。 下面擴展一下函數:

//定義三個函數
val add5 = { i: Int -> i + 5 }
val multiplyBy2 = { i: Int -> i * 2 }
val sum = { q: Int, w: Int -> q + w }
//關鍵點1:
infix fun <P1, P2, R> Function1<P1, P2>.andThen(function: Function1<P2, R>): Function1<P1, R> {
    return fun(p1: P1): R {
        return function.invoke(this.invoke(p1))
    }
}

// 關鍵點2:
infix fun <P1,P2,R> Function1<P2,R>.compose(function:Function1<P1,P2>):Function1<P1,R>{
    return fun (p1:P1):R{
        return this.invoke(function.invoke(p1))
    }
}
//關鍵點3:
fun <P1, P2, P3, R> Function2<P2, P3, R>.toAllSum(
    function: Function1<P1, P2>,
    function1: Function1<P2, P3>
): Function2<P1, P2, R> {
    return fun(p1: P1, p2: P2): R {
        return this.invoke(function.invoke(p1), function1.invoke(p2))
    }
}

fun main() {
    // 關鍵點3:
    val add5AndMulti2 = add5 andThen multiplyBy2
    // 關鍵點4:
    val add5ComposeMulti2 = add5 compose multiplyBy2
    //關鍵點5:
    val sum = sum.toAllSum(add5, multiplyBy2)

    println(add5AndMulti2(10))
    println(add5ComposeMulti2(10))
    println(sum(10,10))
}

-----打印出來的Log
30
25
35
複製代碼

上面實際上就是擴展函數,而後在函數中傳入函數跟返回函數,只有一個參數的則使用了infix中綴關鍵字。

關鍵點一、二、3都是擴展了函數類型,其中關鍵點1跟2 擴展函數類型爲傳入一個函數參數,關鍵點3擴展函數傳入兩個函數參數

舉例:關鍵點1:函數類型爲Function<P1,P2>擴展函數andThen,傳入函數類型Function1<P2, R>,返回函數類型Function1<P1, R>
第一個return:返回函數類型爲fun(p1: P1): R
第二個return:function.invoke(this.invoke(p1)),先是傳入this.invoke(p1)再將這裏返回的值傳入 function.invoke().
先調用了了andThen前的函數,再調用andThen後面的函數。
你們能夠根據這些寫法自定義多種擴展函數~~

4. run、let、with、apply、also等語法糖部分解析

  • 先以run爲例子
//方法一
public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
//方法二
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
複製代碼

方法一函數簽名:run(block: () -> R): R直接傳入代碼塊,並返回R
方法二函數簽名:T.run(block: T.() -> R): R,傳入的代碼塊block: T.() -> R,也就是調用者自己的引用,則在block中則直接可使用T中的成員變量及函數等

//使用
var sum = run { 5+3 }
println(sum)

var aList = arrayListOf("小明", "小紅", "小黑")
var aListSize = aList.run { size }
println(aListSize)
--------------------打印出來的
8
3
複製代碼

這個 contract {...}看不懂能夠暫時不用管它,kotlin中契約的一種寫法,詳情能夠看一下kotlinlang.org/docs/refere…

  • withapplyalsolet
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}
複製代碼
  • with:接受兩個參數,一個是本身自己,一個block: T.() -> R,返回return receiver.block()
    with用法:
var aList = arrayListOf("小明", "小紅", "小黑")
var l = with(aList){
  add("小黃")
  removeAt(0)
  forEach {
    print("$it、")
  }
  size
}
println(l)
---------------------打印
小紅、小黑、小黃、3
複製代碼
  • apply:方法的擴展函數,傳入block: T.() -> Unit,返回調用者自己,用法與run一致,可是最後返回的是調用者自己。
  • also:方法的擴展函數,傳入block: (T) -> Unit,這裏更前面幾個方法有點不同,block傳入了T這個調用者自己,而且函數最後返回調用者自己。 also用法:
var aList = arrayListOf("小明", "小紅", "小黑")
val  sizeFinally = aList.also {
  println(it.size)
  it.add("小黃")
  it.add("小綠")
}.size
println(sizeFinally)
---------打印
3
5
複製代碼
  • let:方法的擴張函數,傳入block: (T) -> Rlet方法返回Rlet用法:
val  sizeFinally = aList.let {
        println(it.size)
        it.add("小黃")
        it.add("小綠")
        it.size
}
  println(sizeFinally)
---------------打印
3
5
複製代碼

補充: 尾遞歸優化 tailrec

  • tailrec關鍵字添加到fun前提示編譯器尾遞歸優化。 尾遞歸:是遞歸的一種形式,遞歸中在調用完本身後沒有其餘操做的稱爲尾遞歸。
  • 尾遞歸與迭代的關係:尾遞歸能夠直接轉換成迭代(好吧,其實這個我也不是很清楚~)
//符合尾遞歸 能夠加tailrec 關鍵字優化
tailrec fun findListNode(head: ListNode?, value: Int): ListNode? {
    head ?: return null
    if (head.value == value) return head
    return findListNode(head.next, value)
}
// 不符合尾遞歸 由於最後調用完本身還跟n相乘 
fun factorial(n:Long):Long{
  return n * factorial(n-1)
}
複製代碼

上面的方法中第一種是符合尾遞歸的形式,這種咱們能夠加tailrec關鍵字,有什麼好處呢?

fun main() {
    var listNode = ListNode(0)
    var p =listNode
    for (i in 1..100000) {
        p.next = ListNode(i)
        p = p.next!!
    }
   println(findListNode(listNode,99998)?.value)
}
-----------有加了關鍵字tailrec -打印出來的Log
99998
----------沒有加關鍵字打印出來的Log
Exception in thread "main" java.lang.StackOverflowError
複製代碼

由於加了tailrec關鍵字,其實是優化成了迭代相比遞歸減低了內存空間的開銷。

相關文章
相關標籤/搜索