擴展函數
高階函數
內聯函數
在上篇文章 偷師 - Kotlin 委託 裏提到了 ViewBindingDelegate
庫,經過 kotlin
委託的方式簡化了在 Android
項目中 ViewBinding
使用。原本是不想再寫 ViewBindingDelegate
分析的,可是項目中用到的 kotlin
知識點確實也有些是須要重點記錄一下的。java
直接來看 vbpd-full/../ActivityViewBindings.kt
文件中的 viewBinding
方法:web
...
@JvmName("inflateViewBindingActivity")
public inline fun <reified T : ViewBinding> ComponentActivity.viewBinding(
createMethod: CreateMethod = CreateMethod.BIND
) = viewBinding(T::class.java, createMethod)
...
複製代碼
能夠看到這是一個擴展函數。第一個知識點來了!markdown
擴展函數定義:不改變原有類的狀況下,擴展新的功能
。ide
首先肯定一點擴展函數針對的是類,爲類提供新的功能。那是怎麼實現的呢,看下面的示例:函數
我定義了一個 String
的擴展函數用以輸出它的長度:post
private fun String.printLength() {
}
複製代碼
轉換成 java
代碼看下性能
private final void printLength(String $this$printLength) {
}
複製代碼
這樣就很好的理解擴展函數的本質了:擴展函數的本質就是一個普通的函數,它不會對原有類作任何修改,不同的地方在於它默認以類對象做爲函數的參數
。學習
在擴展函數內部你能夠經過 this
關鍵字訪問傳過來的在點符號前的對象,也就是上面示例中的 $this$printLength
參數。而 this
也能夠省略。優化
private fun String.printLength() {
Log.e("length", "$length")
}
複製代碼
轉換爲 java
代碼以下:this
private final void printLength(String $this$printLength) {
Log.e("length", String.valueOf($this$printLength.length()));
}
複製代碼
val name = "張三"
name.printLength()
複製代碼
輸出爲:2。
這也就是爲何在 ViewBindingDelegate
項目中會忽然出現 activity
變量的緣由:
@JvmName("viewBindingActivity")
public fun <T : ViewBinding> ComponentActivity.viewBinding(
viewBindingClass: Class<T>,
rootViewProvider: (ComponentActivity) -> View
): ViewBindingProperty<ComponentActivity, T> {
return viewBinding { activity -> ViewBindingCache.getBind(viewBindingClass).bind(rootViewProvider(activity)) }
}
複製代碼
擴展函數和普通函數的區別:
被擴展類型.函數名()
this
(可省略) 關鍵字訪問被擴展的目標類對象。仍是上面的代碼:
@JvmName("inflateViewBindingActivity")
public inline fun <reified T : ViewBinding> ComponentActivity.viewBinding(
createMethod: CreateMethod = CreateMethod.BIND
) = viewBinding(T::class.java, createMethod)
複製代碼
發現兩個不太認識的關鍵字:inline
、reified
。在介紹它們以前應該先理解兩個概念:
高階函數
:能夠將函數用做參數或返回值的函數。內聯函數
:使用 inline
修飾的函數。能夠消除使用高階函數時所帶來的資源消耗。先看一個正常的函數,兩數相加:
private fun addTwoNumbers(firstNumber: Int, secondNumber: Int): Int {
return firstNumber + secondNumber
}
複製代碼
很簡單,沒有什麼好說的。可是發現一個問題,若是計算兩數相減、相乘、相除就須要再定義三個函數,可是並不想這麼作,怎麼辦呢。這種狀況下高階函數就能夠派上用場了,新函數以下:
private fun calculateTwoNumber(
firstNumber: Int,
secondNumber: Int,
calculate: (Int, Int) -> Int
): Int {
return calculate(firstNumber, secondNumber)
}
複製代碼
函數 calculateTwoNumber
接受一個函數參數 calculate
,calculate
函數接受兩個 Int
型參數並返回 Int
型結果。calculateTwoNumber
就是一個高階函數。
轉成 java
來看下:
private final int calculateTwoNumber(int firstNumber, int secondNumber, Function2 calculate) {
return ((Number)calculate.invoke(firstNumber, secondNumber)).intValue();
}
複製代碼
發現 calculate
的參數類型是 Function2
,既然有了 Function2
那是否是還有 Function四、五、六、七、八、9
呢。這個能夠有,事實上有 Function0~22
共 23
個接口類型。
public interface Function<out R>
public interface Function0<out R> : Function<R> {
public operator fun invoke(): R
}
public interface Function1<in P1, out R> : Function<R> {
public operator fun invoke(p1: P1): R
}
/** 接收兩個參數的 Function */
public interface Function2<in P1, in P2, out R> : Function<R> {
/** 執行 invoke 函數 經過參數 P一、P2 獲得並返回結果 R */
public operator fun invoke(p1: P1, p2: P2): R
}
...
public interface Function10<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, out R> : Function<R> {
public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10): R
}
...
複製代碼
它們的區別就是傳參數量的區別。
那如今如今清楚了所謂的高階函數其實編譯成 java
就是 具備 Function 參數類型或返回值爲 Function 類型的函數
。
kotlin
準備了兩種方式能夠得到 Function
對象:
lambda
表達式;lambda
表達式語法以下:
{參數聲明 -> 函數體}
使用 lambda
注意如下幾點:
{a: Int, b: Int -> a + b}
等價於
{a, b -> a + b}
複製代碼
lambda
表達式能夠放在圓括號以外。calculateTwoNumber(1, 2, {a: Int, b: Int -> a + b})
等價於
calculateTwoNumber(1, 2) { a: Int, b: Int -> a + b }
複製代碼
lambda
表達式是調用時惟一的參數,那麼圓括號能夠徹底省略。run { println("...") }
複製代碼
lambda
表達式只有一個參數時能夠不用聲明惟一的參數並忽略 ->
。val ints = listOf<Int>()
ints.filter { it > 0 }
複製代碼
lambda
表達式默認返回最後一個表達是的值,也能夠經過 return
顯示指定返回值。ints.filter {
val shouldFilter = it > 0
shouldFilter
}
ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter
}
複製代碼
lambda
表達式中不可直接使用 return
,要退出 lambda
須要用到標籤。可是若是傳給的函數是內聯(內聯函數在下文講解)的,能夠直接使用 return
。fun ordinaryFunction(block: () -> Unit) {
println("hi!")
}
fun foo() {
ordinaryFunction {
return // 錯誤:不能使 `foo` 在此處返回
return@ordinaryFunction // 正確
}
}
fun main() {
foo()
}
複製代碼
lambda
表達式語法缺乏指定函數的返回類型的能力。在大多數狀況下返回類型能夠自動推斷出來。可是若是確實須要顯式指定,那就須要用到 匿名函數
了。
匿名函數和常規函數的區別在於匿名函數沒有函數名。其餘和常規函數如出一轍。
fun(x: Int, y: Int): Int {
return x + y
}
複製代碼
若是函數返回類型能夠推導出來那麼返回類型也能夠省略。
如今已經清楚了高階函數的定義,那咱們來用高階函數來計算 0~10
的和:
var result = 0
for (i in 0..10) {
result = calculateTwoNumber(result, i) { a: Int, b: Int -> a + b }
}
Log.e("highfun", "$result") // 55
複製代碼
完美!結果明顯是正確的。那再看一下編譯後的代碼:
int result = 0;
int i = 0;
for(byte var4 = 10; i <= var4; ++i) {
result = this.calculateTwoNumber(result, i, (Function2)null.INSTANCE);
}
Log.e("highfun", String.valueOf(result));
複製代碼
能夠發現每次循環都會建立 Function
實例,這樣在大量循環狀況下會產生大量對象,影響內存,這明顯得優化。優化方式有兩種:
優化一:將 lambda
放到循環外定義。
val addCalculate = { a: Int, b: Int -> a + b }
var result = 0
for (i in 0..10) {
result = calculateTwoNumber(result, i, addCalculate)
}
複製代碼
優化二:使用 inline
修飾高階函數爲內聯函數。
private inline fun calculateTwoNumber(
firstNumber: Int,
secondNumber: Int,
calculate: (Int, Int) -> Int
): Int {
return calculate(firstNumber, secondNumber)
}
複製代碼
使用 inline
修飾高階函數後查看編碼以後的代碼:
for(byte var4 = 10; i <= var4; ++i) {
int $i$f$calculateTwoNumber = false;
int var9 = false;
result += i;
}
複製代碼
能夠發現 lambda
表達式的函數體被添加到了表達式被調用的地方,從而避免了建立 Function
對象。
注意:
內聯雖然會提高性能,但同時也會致使生成的代碼增長,因此應避免內聯過大的函數
。
在上文中提到過傳遞給 inline
內聯函數的 lambda
表達式中可使用 return
返回。那咱們來看下面的例子:
private fun callFunction() {
inlined {
Log.e("inline", "2")
return
}
}
private inline fun inlined(body: () -> Unit) {
Log.e("inline", "1")
body()
Log.e("inline", "3")
}
輸出 ------
E/inline: 1
E/inline: 2
複製代碼
能夠看到代碼中有三條日誌打印信息,可是輸出中只打印了兩條。這裏 return
在輸出最後一條日誌信息時直接結束了函數。因此使用 inline
內聯函數時應該避免直接使用 return
,而改用 return@標籤
的方式。修改下代碼:
private fun callFunction() {
inlined {
Log.e("inline", "2")
return@inlined
}
}
輸出 ------
E/inline: 1
E/inline: 2
E/inline: 3
複製代碼
kotlin
中也提供了兩個修飾符來幫助限制在 lambda
中直接使用 return
:
noinline
crossinline
noinline
若是但願只內聯一部分傳給內聯函數的 lambda
表達式參數,那麼能夠用 noinline
修飾符標記不但願內聯的函數參數:
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { …… }
複製代碼
能夠內聯的 lambda
表達式只能在內聯函數內部調用或者做爲可內聯的參數傳遞,可是 noinline
的能夠以任何咱們喜歡的方式操做:賦值給變量
、傳遞給其餘高階函數
等等。
並且使用 noinline
修飾的函數參數,在爲其傳遞 lambda
表達式時不能直接使用 return
否則會報錯,須要使用 return@標籤
。修改上面的示例:
private inline fun inlined(noinline body: () -> Unit) {
Log.e("inline", "1")
body()
Log.e("inline", "3")
}
輸出 ------
E/inline: 1
E/inline: 2
E/inline: 3
複製代碼
可是使用 noinline
也會出現一個問題,咱們看一下編譯後的 java
代碼:
private final void callFunction() {
Function0 body$iv = (Function0)null.INSTANCE;
int $i$f$inlined = false;
Log.e("inline", "1");
body$iv.invoke();
Log.e("inline", "3");
}
複製代碼
看起來跟沒有使用 inline
修飾的高階函數調用是如出一轍的。並且你會看到警告信息:
Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types
意思就是若是一個內聯函數沒有可內聯的函數參數而且沒有具體化的類型參數,那麼這樣的函數極可能並沒有益處(若是你確認須要內聯,則能夠用@Suppress("NOTHING_TO_INLINE")
註解關掉該警告)。
那有沒有便可以函數內聯還能夠保證 lanmbda
傳參裏沒有直接使用 return
呢。
crossinline
crossinline
和 noinline
均可以限制 lambda
傳參不可直接使用 return
,區別在於 crossinline
修飾的函數參數仍然是內聯的。修改上面的示例:
private inline fun inlined(crossinline body: () -> Unit) {
Log.e("inline", "1")
body()
Log.e("inline", "3")
}
複製代碼
查看編譯後的 java
代碼:
private final void callFunction() {
int $i$f$inlined = false;
Log.e("inline", "1");
int var3 = false;
Log.e("inline", "2");
Log.e("inline", "3");
}
複製代碼
inline
內聯函數還提供了另外一個有意思的能力:reified
。
reified
主要簡化了訪問類型參數的能力,看以下代碼:
private inline fun <T: Activity> inlined(clazz: Class<T>) {
body()
Log.e("inline", "${clazz.name}")
}
調用:
inlined(MainActivity::class.java)
複製代碼
其實沒什麼問你題,就是看起來不是很優雅(裝X),那怎麼辦呢。使用 reified
改造一下:
private inline fun <reified T: Activity> inlined() {
body()
Log.e("inline", "${T::class.java.name}")
}
調用:
inlined<MainActivity>()
複製代碼
查看編譯後的 java
代碼其實沒什麼差異,就是簡便輕巧!
本節主要介紹了 kotlin
中的高階函數和內聯函數。高階函數能夠將函數用做參數或返回值,可是使用高階函數會有必定的性能損耗,可使用 inline
修飾爲內聯函數以免性能損耗,而且爲了不代碼量會增長,因此應避免內聯過大的函數。另外使用 noinline
、crossinline
修飾符能夠限制 lambda
傳參中直接使用 return
關鍵字以免影響函數正常執行。內聯函數還提供了 reified
簡化在函數中使用類型參數。
歡迎留言一塊兒交流學習!