---
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` 呼叫函數)
> 
呼叫 Function0#invoke 函數後就會有出入棧的消耗
> 
2. **`inlineFunc` 函數**:可以看到呼叫 **inline 函數不需要出入棧就可以呼叫**(它被直接編入呼叫的函數內),所以可以節省部分資源
> 
### 禁用內聯: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 關鍵字
> 
2. 反組譯過後的更清楚可以看到 `noninline` 標示的參數會使用 FunctionN 來呼叫 invoke
> 
### 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")
}
```
> 
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
}
}
```
> 
## 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()
}
```
> 
:::info
* 使用 Kotlin Bytecode 工具去反編譯,可以發現 **拓展函數就是一個靜態工具類**!並不會影響到原始類
> 
:::
### 拓展函數:注意事項
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` 函數
> 
:::
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)
}
```
> 
:::warning
* 原因是**編譯器會強制轉型為基類**(因為 `showWorld` 函數接收的就是基類),並呼叫期靜態,而不是透過對象呼叫
> 
> 
:::
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()
}
}
```
> 
:::
### 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 對象
**--實作結果--**
> 
:::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)
```
> 
:::
:::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")
}
```
**--實作結果--**
> 
:::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; // 返回自身對象
}
```
**--實作結果--**
> 
### 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()}")
}
```
**--實作結果--**
> 
### 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)
}
```
> 
:::info
* also 適合針對同一原始物件,**透過副作用做事**
```kotlin=
fun main() {
val strList = mutableListOf<String>()
"Hello".also {
// Hello 的副作用
println("\"$it\" add to list")
}.also {
// Hello 的副作用
strList.add(it)
}
println(strList)
}
```
> 
:::
### 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)
}
}
```
> 
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)
}
}
```
> 
### 標準函數差異、比較
| 標準函數 | 使用 | 返回類型 | 參數 | 功能 |
| -------- | -------- | -------- | - | - |
| 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`