【Kotlin回顧】5.高階函式與Lambda

語言: CN / TW / HK

theme: cyanosis


開啟掘金成長之旅!這是我參與「掘金日新計劃 · 12 月更文挑戰」的第5天,點選檢視活動詳情


Kotlin中函式都是頭等的,這意味著它可以儲存在變數與資料結構中、作為引數傳遞給其他高階函式以及從其他高階函式返回。可以向操作任何其他非函式值一樣操作函式。

為促成這點,作為一門靜態型別程式語言的Kotlin使用一系列函式型別來表示函式並提供了一組專門的語言結構,例如Lambda表示式

這段話來自Kotlin文件。從沒有接觸過Kotlin的話這段話的意思很難理解,上面有三個關鍵詞【高階函式】、【函式型別】、【Lambda表示式】先分析這三個關鍵詞然後再對上面這段話進行理解。

1.函式型別

函式型別就是函式的型別, 變數有型別IntString等,那函式的型別到底是指什麼?

fun lastElement(str: String): Char { return str[str.length - 1] }

上面的程式碼是之前用過的,意思是獲取字串的最後一個字元,其中引數型別是String,返回值型別是Char,將其抽出來就是【String -> Char】這就代表了函式的型別,一句話概括就是:將函式的【引數型別】和【返回值型別】抽象出來就得到了函式的【函式型別】。【(String) -> Char】的意思就是引數型別是【String】,返回值型別是【Char】的函式型別。這個比較好理解。類似的還有Kotlin中繼承自BaseAdapter的幾個方法

``` //函式型別:() -> Int override fun getCount(): Int {

}

//函式型別:(Int) -> Any override fun getItem(position: Int): Any {

}

//函式型別:(Int) -> Long override fun getItemId(position: Int): Long {

} ```

2.高階函式

理解餓了函式型別再來看下高階函式。

高階函式是將函式用作引數或返回值的函式, 這是高階函式的定義。

  • 函式用作引數的高階函式寫法

``` fun main() { val result = answer(20) { 10 } println("result:$result") //輸出結果:result:30 }

/* * 高階函式 * 函式型別:(Int, add方法) -> Int / fun answer(num: Int, add: () -> Int): Int { return num + add() } ```

上面的程式碼用的是高階函式中的函式用作引數的寫法,定義一個answer方法,新增一個引數num和函式add,因為add方法返回值是一個Int型別因此可以跟num直接相加並返回結果,程式碼沒有實際意義就是個例子

這裡可能會產生一個疑問:為什麼result的呼叫方式是成立的?

將上面的程式碼轉換成Java的寫法就清楚了

``` public final class HighFunctionKt { public static final void main() { int result = answer(20, (Function0)null.INSTANCE); String var1 = "result:" + result; boolean var2 = false; System.out.println(var1); }

// $FF: synthetic method public static void main(String[] var0) { main(); }

public static final int answer(int num, @NotNull Function0 add) { Intrinsics.checkNotNullParameter(add, "add"); return num + ((Number)add.invoke()).intValue(); } } ```

可以看到add方法被轉換成了Function0,並且num與add方法的返回值進行了相加,那麼這裡有一個新的疑問invoke是什麼?invokeFunction0的一個方法,作用就是呼叫函式,在Functions.kt類中,Function的數量達到Function21,從Function0Function21的區別就是傳引數量的不同。

public interface Function0<out R> : Function<R> { /** Invokes the function. */ public operator fun invoke(): R }

再來看一個稍微複雜的高階函式例子,answer函式中又添加了一個引數,同時函式引數也支援傳參

``` fun main() { val add = answer(10, 20) { num1, num2 -> num1 + num2 } val minus = answer(10, 20) { num1, num2 -> num1 - num2 } println("add:$add") //輸出結果:result:30 println("minus:$minus") //輸出結果:result:30 }

fun answer(num1: Int, num2: Int, result: (Int, Int) -> Int): Int { return result(num1, num2) } ```

answer方法做了改動,傳了兩個引數,函式型別的引數也傳入了兩個引數,這樣定義的作用是更靈活

  • 函式用作返回值的高階函式寫法

``` fun main() { println(answer(10).invoke()) //輸出結果:輸入的數:10 }

fun answer(num: Int): () -> String { return { "輸入的數:${num}" } } ```

這裡的invoke就是一個呼叫函式的功能。編譯成Java程式碼如下所示:

``` public final class HighFunctionKt { public static final void main() { Object var0 = answer(10).invoke(); boolean var1 = false; System.out.println(var0); }

// $FF: synthetic method
public static void main(String[] var0) {
    main();
}

@NotNull
public static final Function0 answer(final int num) {
    return (Function0)(new Function0() {
        // $FF: synthetic method
        // $FF: bridge method
        public Object invoke() {
            return this.invoke();
        }

        @NotNull
        public final String invoke() {
            return "輸入的數:" + num;
        }
    });
}

} ```

  • 應用場景舉例

Android開發過程中RecycleView是常用的一個元件,但是它本身不支援點選事件,現在,假設我們在Adapter中有兩個點選事件,新增和刪除,常用的寫法會先定義一個介面,對該介面定義一個變數,然後定義一個方法,程式碼如下:

``` private lateinit var mOnItemAddClickListener: OnItemAddClickListener private lateinit var mOnItemDeleteClickListener: OnItemDeleteClickListener

interface OnItemAddClickListener { fun onItemAddClick(position: Int) }

interface OnItemDeleteClickListener { fun onItemDeleteClick(position: Int) }

fun setOnItemAddClickListener(onItemAddClickListener: OnItemAddClickListener) { mOnItemAddClickListener = onItemAddClickListener }

fun setOnItemDeleteClickListener(onItemDeleteClickListener: OnItemDeleteClickListener) { mOnItemDeleteClickListener = onItemDeleteClickListener }

holder.ivAdd.setOnClickListener { mOnItemAddClickListener.onItemAddClick(position) }

holder.ivDelete.setOnClickListener { mOnItemDeleteClickListener.onItemDeleteClick(position) }

adapter.setOnItemAddClickListener(object :DemoAdapter.OnItemAddClickListener{ override fun onItemAddClick(position: Int) { TODO("Not yet implemented") } }) adapter.setOnItemDeleteClickListener(object :DemoAdapter.OnItemDeleteClickListener{ override fun onItemDeleteClick(position: Int) { TODO("Not yet implemented") } }) ```

用高階函式對其進行優化後的程式碼如下:

``` private lateinit var mOnItemAddClickListener: (Int) -> Unit private lateinit var mOnItemDeleteClickListener: (Int) -> Unit

fun setOnItemAddClickListener(listener: (Int) -> Unit) { mOnItemAddClickListener = listener }

fun setOnItemDeleteClickListener(listener: (Int) -> Unit) { mOnItemDeleteClickListener = listener }

holder.ivAdd.setOnClickListener { mOnItemAddClickListener.invoke(position) }

holder.ivDelete.setOnClickListener { mOnItemDeleteClickListener.invoke(position) }

adapter.setOnItemAddClickListener {

} adapter.setOnItemDeleteClickListener {

} ```

這兩種寫法的程式碼進行對比可以發現高階函式的實現方式中沒有定義介面,同時它程式碼量顯著減少,程式碼也變得更加簡潔。

3.系統標準高階函式

系統的標準高階函式來自Standard.kt,裡面的方法也是比較常用的

  • run

public inline fun <R> run(block: () -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block() }

block是函式式引數的名稱,傳入的函式型別是()-> RR是泛型

這段程式碼的意思就是呼叫傳入的函式並返回結果,return block()就是傳入函式的呼叫。

怎麼用?or有什麼用?

``` fun main() { run { println(add(1, 2)) } }

fun add(num1: Int, num2: Int): Int { return num1 + num2 } ```

作用就是構建Lambda更方便

  • T.run

public inline fun <T, R> T.run(block: T.() -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block() }

這個run更上面那個的區別在於它有一個接收者,有了這個接收者就可以使用上下文了,例如:

val num = 10 num.run { println("result:${this + 1}") } //輸出結果:result:11

還有一種情況,例如要從某個物件中取出它的一些屬性的值也可以通過T.run,同時由於可以將this省略因此程式碼就可以這麼寫:

``` class Person(val name:String, var age:Int)

val person = Person("張三", 19) person.run{ println("name:$name") println("age:$age") } ```

再舉個例子,TextView利用T.run修改屬性並賦值

holder.tvText.run { text = "自定義文字" setTextColor(context.getColor(R.color.color_000000)) textSize = 20F }

  • whith

public inline fun <T, R> with(receiver: T, block: T.() -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return receiver.block() }

with的使用方式與T.run類似,with在返回值上帶了一個接收者,看下面程式碼

with(holder.tvText) { text = "自定義文字" setTextColor(context.getColor(R.color.color_000000)) textSize = 20F }

  • T.apply

public inline fun <T> T.apply(block: T.() -> Unit): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } block() return this }

這個函式的意思就是接收者傳入了什麼返回值就是什麼,使用案例:

``` val person = Person("張三", 19) person.apply { println("name:$name") //輸出結果:張三 println("age:$age") //輸出結果:19 }.age = 10 //把張三的年齡修改我10歲

println("name:${person.name}") //輸出結果:張三 println("age:${person.age}") //輸出結果:10 ```

有什麼用?

Android中會遇到根據狀態修改Button樣式然後還要響應點選事件的情況,常用寫法就不在這裡講了,這裡用T.apply實現:

button.apply { text = "提交" setTextColor(context.getColor(R.color.color_000000)) background = context.getDrawable(R.drawable.shape_solid_4dp_4e6cf5) }.setOnClickListener { //點選事件 }

這行程式碼是不是很簡潔。

還有webview的使用

webView.apply { settings.javaScriptEnabled = true settings.useWideViewPort = true }.loadUrl("http://www.baidu.com/")

T.apply很靈活,具體問題具體分析就好。

  • T.also

public inline fun <T> T.also(block: (T) -> Unit): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } block(this) return this }

T.also的意思就是用傳參呼叫指定函式並返回這個引數

button.also { it.text }

這裡的button在呼叫also之後在它的裡面只能用it也必須用it,這個it指的是button本身,而T.apply中是this指的是Button本身,並且這個this是可以被省略的。使用過程中的區別不大。

  • T.let

public inline fun <T, R> T.let(block: (T) -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block(this) }

T.let的意思就是使用傳參呼叫指定函式然後返回結果,程式碼如下

``` val person = Person("張三", 19) val str = "Hello Kotlin"

val result = person.let { it.name == "張三" } println("result:$result") //輸出結果:result:true

println("length:${str.let { it.length }}") //輸出結果:length:12 ```

在Person類中有一個name = 張三的例項,經過let判斷後返回true;獲取str字串的長度得到最終結果12。

  • T.takeIf

public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? { contract { callsInPlace(predicate, InvocationKind.EXACTLY_ONCE) } return if (predicate(this)) this else null }

T.takeIf的意思就是如果符合條件則返回傳入的值,否則返回null

``` val person = Person("張三", 19)

val result = person.takeIf { it.name == "李四" } println("result:${result?.name}") //輸出結果:result:null

val result = person.takeIf { it.name == "張三" } //條件成立返回person物件 ```

  • T.takeUnless

public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? { contract { callsInPlace(predicate, InvocationKind.EXACTLY_ONCE) } return if (!predicate(this)) this else null }

T.takeUnlessT.takeIf正好相反,如果滿足條件則返回null,不滿足則返回正確的謂詞

``` val person = Person("張三", 19)

val result = person.takeUnless { it.name == "李四" } println("result:${result?.name}") //輸出結果:result:張三

val result = person.takeUnless { it.name == "張三" } println("result:${result?.name}") //輸出結果:result:null ```

  • repeat

``` public inline fun repeat(times: Int, action: (Int) -> Unit) { contract { callsInPlace(action) }

for (index in 0 until times) {
    action(index)
}

} ```

action函式就是一個重複執行的函式,從0開始

``` repeat(2) { println("執行第:${it}次") }

//輸出結果: //執行第:0次 //執行第:1次 ```

4.Lambda表示式

Lambda表示式在Java中已經用的比較多了,通過它可以簡化程式碼和提高開發效率,所以我們可以把Lambda表示式理解為函式的簡寫

例如view的點選事件在Kotlin中呼叫時就是這樣的:

``` fun setOnClickListener(l: ((View!) -> Unit)?){

}

//呼叫時可以這麼寫: button.setOnClickListener{

} ```

開頭提出了一個結論:在Kotlin中函式是頭等的,為什麼這麼說呢?

  • Kotlin的函式可以獨立於類之外,這就是頂層函式
  • Kotlin的函式可以作為引數也可以作為函式,它被稱為高階函式和Lambda
  • Kotlin的函式可以向變數一樣,這叫做函式引用