--- title: 'Inline 內聯 & 拓展函數 & Kotlin 標準函數' disqus: kyleAlien --- Inline 內聯 & 拓展函數 & Kotlin 標準函數 === ## Overview of Content 以下參考,第一行代碼 (第三版), Kotlin 進階實戰 如有引用參考本文章請詳註出處,感謝 :smile: :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**深入 Kotlin 函數特性:Inline、擴展、標準函數全解析 | 提升程式碼效能與可讀性**](https://devtechascendancy.com/kotlin_inline_extensions_standards-func/) ::: [TOC] ## Kotlin Inline 內聯函數 內聯(`inline`)函數主要是 **透過編譯器來將我們的程式在編譯階段直接嵌入程式中**,減少呼叫函數時 Stack 出入棧的消耗,**也就是 空間換時間** (**但也不要亂用**) :::warning * **`inline` 使用注意** 在使用 inline 函數時,如果用在「遞迴」則有一些編譯器可能進入無窮編譯的情況 ::: ### Inline 基礎使用 * 這邊要配合 Kotlin Bytecode 工具一起看,接下來我們會觀察 `.class` 編譯出的程式,比對普通函數與 `inline` 在字節碼的差異 ```kotlin= fun nonInlineFunc(block : () -> Unit) { block() } inline fun inlineFunc(block : () -> Unit) { block() } fun main() { nonInlineFunc { println("No Inline function") } inlineFunc { println("Inline function") } } ``` 1. **`nonInlineFunc` 函數**:必須要調用 Function 對象來完成 lambda 呼叫(使用了 `INVOKESTATIC` 呼叫函數) > ![](https://i.imgur.com/GspOwxj.png) 呼叫 Function0#invoke 函數後就會有出入棧的消耗 > ![](https://i.imgur.com/viBfivL.png) 2. **`inlineFunc` 函數**:可以看到呼叫 **inline 函數不需要出入棧就可以呼叫**(它被直接編入呼叫的函數內),所以可以節省部分資源 > ![](https://i.imgur.com/aQ5RCWL.png) ### 禁用內聯:noinline * 當我們想要拒絕編譯器將程式當作內聯編入時,可以使用 `noinline` 關鍵字,像是我們可以使用在… 接收參數時可以使用 `noinline` 關鍵字來限制編譯器不要使用內聯函數的功能 ```kotlin= fun func_1() { println("Do something with inline.") } fun func_2() { println("Do something with inline.") } inline fun callFunction(fun1: () -> Unit, // 使用 noinline 關鍵字 noinline fun2: () -> Unit) { fun1() fun2() } fun main() { callFunction(::func_1, ::func_2) } ``` 1. 從 `.class` 檔可以看出端倪,呼叫時需要使用到 Function 關鍵字 > ![](https://i.imgur.com/3DDIpOz.png) 2. 反組譯過後的更清楚可以看到 `noninline` 標示的參數會使用 FunctionN 來呼叫 invoke > ![](https://i.imgur.com/iZziyE0.png) ### Non-local return: Inline make Lambda can return * **Kotlin 在 lambda 函數中返回是無法跳出函數** ```kotlin= fun normalFunc(block: () -> Unit) { block() } fun foo() { println("Start") normalFunc { return // Error: Not allow here } println("Done") } ``` > ![](https://i.imgur.com/ArxIUdx.png) 1. 可以使用 `@tag` 返回到指定地點 ```kotlin= fun normalFunc(block: () -> Unit) { block() } fun foo() { println("Start") normalFunc { return@normalFunc // 修正 } println("Done") // 會執行到 } ``` 2. 如果是 **`inline` 函數則可以用 return** ```kotlin= inline fun inlineFunction(block: () -> Unit) { block() } fun foo2() { println("Start") inlineFunction { // 修正 // return okay return } println("Done") // 不會執行到 Done } ``` ### 禁止 inline 返回:crossinline * **`crossinline` 關鍵字用來修飾入參**(函數的參數),一般來說 inline 函數可以局部返回,如果這時 **使用了 `crossinline` 那就禁止局部返回** :::info `crossinline` 必須配合 `inline` 一起使用 ::: ```kotlin= inline fun startFunc(crossinline lambda: () -> Unit) { println("Start") lambda() println("Done") } fun main() { startFunc { return // Error! Not allow here } } ``` > ![](https://i.imgur.com/Qd3TRBv.png) ## Kotlin 拓展函數 Kotlin 拓展函數是與 Java 很不同的特點之一,拓展函數讓 Kotlin 更接近現在語言,如果配合 Lambda 可以做到更多的事情… > 拓展函數在 `C#`, `Swift` 都有實現這種語言特性 Kotlin 拓展函數的格式如下 ```shell= <原始類>.<拓展名>() { // TODO: } ``` ### 擴展函數使用 * 依照拓展函數的格式寫,就可以建構拓展函數 ```kotlin= // 原始類 class BaseExtension { fun hello() = println("Hello") } // 拓展函數 fun BaseExtension.world() = println("Extension World") fun main() { val extension = BaseExtension() extension.hello() extension.world() } ``` > ![](https://i.imgur.com/r1TOEAA.png) :::info * 使用 Kotlin Bytecode 工具去反編譯,可以發現 **拓展函數就是一個靜態工具類**!並不會影響到原始類 > ![](https://i.imgur.com/j7CyBbF.png) ::: ### 拓展函數:注意事項 1. 拓展類如果跟原始類有相同名稱(包括參數)會使用哪個呢? 呼叫時會 **使用原始類**(原始類的優先權更高) ```kotlin= class BaseExtension { fun hello() = println("Hello") } // 拓展類(與原始相同函數名) fun BaseExtension.hello() = println("Extension Hello") fun BaseExtension.world() = println("Extension World") fun main() { val extension = BaseExtension() extension.hello() extension.world() } ``` :::warning * 可以看到反組繹出來後,它呼叫的不是靜態的 `hello` 函數,而是原始類的 `hello` 函數 > ![](https://i.imgur.com/NgLEboa.png) ::: 2. 拓展類沒有多態(繼承重寫沒有用) ```kotlin= open class BaseExtension { fun hello() = println("Hello") } fun BaseExtension.world() = println("Extension World (BaseExtension)") class NewExtension : BaseExtension() // 拓展相同名稱的函數 fun NewExtension.world() = println("Extension World (NewExtension)") fun showWorld(ex : BaseExtension) { ex.world() } fun main() { val baseEx = BaseExtension() val newEx = NewExtension() showWorld(baseEx) showWorld(newEx) } ``` > ![](https://i.imgur.com/mi1zR3c.png) :::warning * 原因是**編譯器會強制轉型為基類**(因為 `showWorld` 函數接收的就是基類),並呼叫期靜態,而不是透過對象呼叫 > ![](https://i.imgur.com/VKa9iR8.png) > ![](https://i.imgur.com/cLhb3Ll.png) ::: 3. **Java 類也可以直接呼叫拓展函數**:目標類名就是「**檔案名稱 + Kt**」,之後再加上要呼叫的函數名即可,範例如下 ```java= public class Java_Main { public static void main(String[] args) { BaseExtensionKt.world(new BaseExtension()); } } ``` ## Kotlin 拓展屬性 Kotlin 除了可以拓展函數以外,還可以拓展屬性(`property`) ### 拓展屬性使用 ```kotlin= class ExtensionField val ExtensionField.name : String get() = "Alien" fun main() { val ex = ExtensionField() println(ex.name) } ``` :::danger * 拓展屬性 **沒有 `backing field` 可以使用**!所以 **不能設定 setter**,所以只能用 val 描述! * 如果還是想用 Setter,那可以考慮這樣設計:如果你是拓展某些類型,並且該類型內「**可以儲存你自訂設定的參數**」,就可以使用 setter; 範例如下:將自己要設定的物件存入 Throwable 中的 `List<Throwable>` 列表內,並透過該 list 遍歷並判斷,取得對應的物件 ```kotlin= interface IExceptionInfo { val isHandleable : Boolean fun timeOccurred() : Long } const val SET_FIELD_FAIL = "Cannot add not Throwable type." // 拓展所有 Throwable(或其子類)的屬性(添加 `errorDetails` 屬性) var <E: Throwable> E.errorDetails: IExceptionInfo get() { // Throwable#suppressedExceptions 列表中取出相對數據 suppressedExceptions.forEach { e -> // 針對類型判斷 if (e is IExceptionInfo) { return e } } return Throwable().unHandleable { }.errorDetails } set(value) { if (value is Throwable) { val throwable = value as Throwable // 數據存入 Throwable#suppressedExceptions 列表 if (!suppressedExceptions.contains(throwable)) { // 間接使用的原先類的內部成員,來保存自己需要的數據 addSuppressed(throwable) } return } throw Exception(SET_FIELD_FAIL) } ``` ::: ## Kotlin 標準函數 - Standard.kt **Kotlin 的標準函數指的是 ++Standaed.kt (`kotlin-stdlib.jar`)++ 文件中定意義的函數**,任何 Kotlin 代碼都可以自由的調用標準函數 :::success * 以下這些標準函數使用的好,可以增強程式的可讀性! ::: ### let 函數 * 每個對象內都有一個 `let` 工具 (就類似於這個 `let` 在 Object 內),它並不是關鍵字、操作符號,它就是一個函數,**let 函數會把自身對象再次傳入,作為參數再次使用** ```kotlin= // let 原型 @kotlin.internal.InlineOnly public inline fun <T, R> T.let(block: (T) -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block(this) } ``` **let 常用來判空**,可以用在判斷對象不為空後,繼續往下操作 ```kotlin= fun useLet(s: BookShop?) { // 可以統一盼空 // 原型 : public inline fun <T, R> T.let(block: (T) -> R): R { } s?.let { shop -> shop.getDescribe() shop.workTime() } // 單一參數所以可以使用 it 取代 s?.let { it.getDescribe() it.workTime() } } ``` :::danger * **可能會想 `if` 統一判斷與 `let` 是相同的,但是並非如此 !** 若是**把變數放置全域 `var` 則 if 也無法正確判斷** 假設在 **多執行緒(線程)協作的狀況下**,有可能改動到全域變數,也就是 `if` 也無法保證該變量的安全性 (以往我們會使用鎖來解決這個問題),**而使用 `let` 則可以安全的操作** ```kotlin= var s : BookShop? = null fun main() { /** * Smart cast to 'BookShop' is impossible, * because 's' is a mutable property that could have been changed by this time */ // if(s != null) { // s.getDescribe() // s.workTime() // } s?.let { it.getDescribe() it.workTime() } } ``` > ![](https://i.imgur.com/qaXZH9x.png) ::: ### run 函數 * `run` 功能與 `with` 功能相似,**每個對象裡面都有 run 方法可以調用,而它的上下文就是調用的 `run` 的對象**,下面會使用 run 達到相同目的 ```kotlin= // run 原型 @kotlin.internal.InlineOnly public inline fun <T, R> T.run(block: T.() -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block() } ``` 可以看到 run 是必須由對象 T (泛型)來調用的 ```kotlin= fun main() { val myList = listOf<String>("Apple", "Banana", "Orange","Pear", "Grape") val strBuild = StringBuilder() val str = strBuild.run { append("Start Search...\n") for (i in myList) { append(i).append("\n") } append("Finish") toString() // 1. return String object } println("str type -> ${str.javaClass}") println("StringBuilder run function: $str") } ``` > 這邊最後使用了 toString 所以就會返回 String 對象 **--實作結果--** > ![](https://i.imgur.com/wPHbgF9.png) :::success * **run 可以解決鏈式呼叫** ! ```kotlin= fun createHelloObj() = Hello() class Hello { fun getWorldObj() = World() } class World { fun getMsg() = "Hello World, run~" }] ``` 1. 傳統 **鏈式呼叫** ```kotlin= // 鏈式呼叫 println(createHelloObj().getWorldObj().getMsg()) ``` 2. **使用 run 函數**:更清晰的可看出調用順序 ```kotlin= // 使用 run 函數 createHelloObj() .run { getWorldObj() }.run { getMsg() }.run (::println) ``` > ![](https://i.imgur.com/sDRiO9c.png) ::: :::info * **`run` & `apply` 差別** 1. **參數**:run & apply 兩者都沒有參數,lambda 內部都作用於接收者 2. **返回**:**`run` 不返回接收者**,它可返回其他對象 (如同 `let`);`apply` 則反為接收者,無法返回其他類型! ::: ### apply 函數 * `apply` 與 `run` 相似,與 run 的差異處在,**apply 只會返回該對象本身,++run 可以返回呼叫對象以外的對象++** ```kotlin= // apply 原型 @kotlin.internal.InlineOnly public inline fun <T> T.apply(block: T.() -> Unit): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } block() return this } ``` 可以看到 apply 只有一個泛型,傳遞進該類後,就只會返回該類 ```kotlin= fun main() { val myList = listOf<String>("Apple", "Banana", "Orange","Pear", "Grape") val strBuild = StringBuilder() val str = strBuild.apply { append("Start Search...\n") for (i in myList) { append(i).append("\n") } append("Finish") toString() // no Error, but still is StringBuilder type } println("str type -> ${str.javaClass}") println("StringBuilder run function: $str") } ``` **--實作結果--** > ![](https://i.imgur.com/4nUGXB2.png) :::info * **`apply` & `let` 差別** 1. **參數**:apply 沒有接收參數;let 接收原本接收者的參數 2. **返回**:apply 返回原接收者;let 可返回不同的對象 (最後一行) ::: ### with 函數 * **`with` 函數與 `run` 函數類似,是 `run` 函數的變形**;with 接收兩個參數,^1.^ 任意對象、^2.^ Lambda (閉包)函數 * **with 會提供第一個參數的上下文,讓 Lambda 內可以自由使用,並使用最後一行為返回值**,它可以在連續調用程式時看起來更簡潔 ```kotlin= // with 原型 @kotlin.internal.InlineOnly public inline fun <T, R> with(receiver: T, block: T.() -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return receiver.block() } ``` 以下遍歷一個 list,並使用 StringBuilder 串接字串 ```kotlin= import java.lang.StringBuilder fun main() { val list = listOf<String>("Apple", "Banana", "Orange","Pear", "Grape") // 一般寫法 val str = StringBuilder() // 省略 new str.append("Start Search...\n") for ( i in list) { str.append(i).append(" ") } str.append("\nFinish") var r = str.toString() println("Older Style: $r \n") // 1. Kt Standard 寫法 val result = with(StringBuilder()) { this.append("Start Search...\n") // 遞一個參數為上下文 this for(i in list) { append(i).append(" ") } append("\nFinish") // 2. 返回 } r = result.toString() println("new Style: $r \n") } ``` 1. 可以看出使用標準 with 函數讓程式更加精簡,它讓第一個參數傳入 StringBuilder 對象,第二個參數內的上下文就是 StringBuilder(this),就可以省略上下文呼叫 2. 最後一行最為返回值返回,而 append 函數返回的也是該對象 ```java= // append 函數 public StringBuilder append(String var1) { super.append(var1); return this; // 返回自身對象 } ``` **--實作結果--** > ![](https://i.imgur.com/BkzVWLS.png) ### repeat 函數 * repeat 接收一個常數 n,然後會把 Lambda 中的表達式運行你指定的次數 (前面設定的常數 n) ```kotlin= // kotlin 原型 @kotlin.internal.InlineOnly public inline fun repeat(times: Int, action: (Int) -> Unit) { contract { callsInPlace(action) } for (index in 0 until times) { action(index) } } ``` repeat 函數使用範例如下:以下用來串接字符串 ```kotlin= // 使用範例 import java.lang.StringBuilder fun main() { val str = StringBuilder() repeat(5) { str.append("Hello $it\n") // 從 0 開始 } println("use repeat function: \n${str.toString()}") } ``` **--實作結果--** > ![](https://i.imgur.com/2KYyM4P.png) ### also 函數 * also 是 kotlin 新增的函數(其語意可以理解為「順帶做某些事情」),also 原型如下 ```kotlin= @kotlin.internal.InlineOnly @SinceKotlin("1.1") public inline fun <T> T.also(block: (T) -> Unit): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } block(this) return this } ``` * also 與 apply 類似,不過**內部可以透過 it 來指定自身對象 (也就是 also 有接收者參數)**,並且返回的也是自身對象 ```kotlin= fun main() { val res = "Hello".also { println("$it World") "$it World" // 返回仍然是 Hello } println(res) } ``` > ![](https://i.imgur.com/pqdrwuG.png) :::info * also 適合針對同一原始物件,**透過副作用做事** ```kotlin= fun main() { val strList = mutableListOf<String>() "Hello".also { // Hello 的副作用 println("\"$it\" add to list") }.also { // Hello 的副作用 strList.add(it) } println(strList) } ``` > ![](https://i.imgur.com/3QGdvOJ.png) ::: ### takeIf / takeUnless 函數 * takeIf / takeUnless 函數 其實就是一個 If 判斷式的方便寫法,**再執行 predicate 判斷回復為 true 則返回接收者,false 則返回 null**;它可以讓我們省略不必要的臨時變量 1. **`takeIf` 原碼** ```kotlin= @kotlin.internal.InlineOnly @SinceKotlin("1.1") public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? { contract { callsInPlace(predicate, InvocationKind.EXACTLY_ONCE) } return if (predicate(this)) this else null } ``` :::info * 使用方式就像是 `if(...)` 的判斷 ```kotlin= fun useIfElse(msg: String) : String? { // 省略臨時變量 val predicate = msg.length > 5 return if (predicate) { msg } else null } ``` ::: `takeIf` 使用方式如下 ```kotlin= fun useTaskIf(msg: String) : String? { // 不符合條件則返回 null return msg.takeIf { it.length > 5 }?.toString() } fun main() { useTaskIf("Hello World").also { println(it) } useTaskIf("Hello").also { println(it) } } ``` > ![](https://i.imgur.com/W1XJwAY.png) 2. **`takeUnless` 原碼** ```kotlin= @kotlin.internal.InlineOnly @SinceKotlin("1.1") public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? { contract { callsInPlace(predicate, InvocationKind.EXACTLY_ONCE) } return if (!predicate(this)) this else null } ``` :::info * 使用方式就像是 `if(!...)` 的判斷 ```kotlin= fun useIfElse2(msg: String) : String? { // 省略臨時變量 val predicate = msg.length > 5 return if (!predicate) { msg } else null } ``` ::: `takeUnless` 使用方式如下 ```kotlin= fun useTaskUnless(msg: String) : String? { return msg.takeUnless { it.length > 5 }?.toString() } fun main() { useTaskUnless("Hello World").also { println(it) } useTaskUnless("Hello").also { println(it) } } ``` > ![](https://i.imgur.com/XcuBJ0c.png) ### 標準函數差異、比較 | 標準函數 | 使用 | 返回類型 | 參數 | 功能 | | -------- | -------- | -------- | - | - | | let | 透過對象 | 可返回別的對象 | ==it (本身)== | 常用於判空 | | apply | 透過對象 | ==返回本身== | 無(this 接收者) | 簡化內部上下文 | | run | 透過對象 | 可返回別的對象 | 無(this 接收者) | 簡化內部,並改變回傳類 | | with | ==直接使用== (接收一個對象) | 可返回別的對象 | 無 | 不須對象的使用 | | repeat | ==直接使用== (接收次數) | 無 | it | 重複相同代碼 | | also | 透過對象 | 返回本身 | it | 對象副作用 | | takeIf/taskUnless | 透過對象 | 返回本身 or null | it | 省略 if 判斷 & 臨時變數 | ## 更多的 Kotlin 語言相關文章 在這裡,我們提供了一系列豐富且深入的 Kotlin 語言相關文章,涵蓋了從基礎到進階的各個方面。讓我們一起來探索這些精彩內容! ### Kotlin 語言基礎 * **Kotlin 語言基礎**:想要建立堅實的 Kotlin 基礎?以下這些文章將帶你深入探索 Kotlin 的關鍵基礎和概念,幫你打造更堅固的 Kotlin 語言基礎 :::info * [**Kotlin 函數、類、屬性 | DataClass、Sealed、Object 關鍵字 | Enum、Companion、NPE**](https://devtechascendancy.com/kotlin-functions_oop_dataclass_sealed_object/) * [**深入探究 Kotlin 與 Java 泛型:擦除、取得泛型類型、型變、投影 | 協變、逆變**](https://devtechascendancy.com/explore-kotlin-java-generics_type_erasure/) * [**深入 Kotlin 函數特性:Inline、擴展、標準函數全解析 | 提升程式碼效能與可讀性**](https://devtechascendancy.com/kotlin_inline_extensions_standards-func/) * [**Kotlin DSL、操作符、中綴表達式 Infix | DSL 詳解 | DSL 設計與應用**](https://devtechascendancy.com/kotlin-dsl-operators-infix-explained/) ::: ### Kotlin 特性、特點 * **Kotlin 特性、特點**:探索 Kotlin 的獨特特性和功能,加深對 Kotlin 語言的理解,並增強對於語言特性的應用 :::warning * [**Kotlin 代理與懶加載機制:使用、lazy 深度解析**](https://devtechascendancy.com/kotlin-delegate_java-proxy_lateinit_lazy/) * [**Kotlin Lambda 編程 & Bytecode | Array & Collections 集合 | 集合函數式 API**](https://devtechascendancy.com/kotlin-lambda-bytecode-array-collections-functional/) * [**深入理解 Kotlin:智能推斷與 Contact 規則**](https://devtechascendancy.com/kotlin-smart-inference-contract-rules-guide/) ::: ### Kotlin 進階:協程、響應式、異步 * **Kotlin 進階:協程、響應式、異步**:若想深入學習 Kotlin 的進階主題,包括協程應用、Channel 使用、以及 Flow 的探索,請查看以下文章 :::danger * [**應用 Kotlin 協程:對比 Thread、創建協程、任務掛起 | Dispatcher、CoroutineContext、CoroutineScope**](https://devtechascendancy.com/applied-kotlin-coroutines-in-depth-guide/) * [**Kotlin Channel 使用介紹 | Select、Actor | 生產者消費者**](https://devtechascendancy.com/kotlin-channel_select_actor_cs/) * [**探索 Kotlin Flow:基本使用、RxJava 對比、背壓機制 | Flow 細節**](https://devtechascendancy.com/kotlin-flow-usage_compare-rx_backpressure/) ::: ## Appendix & FAQ :::info ::: ###### tags: `Kotlin`