深入 Kotlin 函數特性:Inline、擴展、標準函數全解析 | 提升程式碼效能與可讀性

深入 Kotlin 函數特性:Inline、擴展、標準函數全解析 | 提升程式碼效能與可讀性

Overview of Content

本文深入探討 Kotlin 語言的強大函數特性,包括 Inline 內聯函數的基礎使用、禁用內聯、非區域返回、禁止 inline 返回等內容。

進一步介紹 Kotlin 的拓展函數和拓展屬性,包括使用方式和注意事項。此外,深入解析 Kotlin 的標準函數,包括 letrunapplywithrepeatalsotakeIf / takeUnless 函數,並比較它們的差異。閱讀本文,將有助於讀者深入理解 Kotlin 函數和拓展的豐富特性,以及標準函數的多樣運用。

以下參考,第一行代碼 (第三版), Kotlin 進階實戰,如有引用參考本文章請詳註出處,感謝 😀


Kotlin Inline 內聯函數

內聯(inline)函數主要是 透過編譯器來將我們的程式在編譯階段直接嵌入程式中,減少呼叫函數時 Stack 出入棧的消耗,也就是 空間換時間 (但也不要亂用)

inline 使用注意

在使用 inline 函數時,如果用在「遞迴」則有一些編譯器可能進入無窮編譯的情況

Inline 基礎使用

● 這邊要配合 Kotlin Bytecode 工具一起看,接下來我們會觀察 .class 編譯出的程式,比對普通函數與 inline 在字節碼的差異


fun nonInlineFunc(block : () -> Unit) {
    block()
}

inline fun inlineFunc(block : () -> Unit) {
    block()
}

fun main() {
    nonInlineFunc {
        println("No Inline function")
    }

    inlineFunc {
        println("Inline function")
    }
}

A. nonInlineFunc 函數:必須要調用 Function 對象來完成 lambda 呼叫(使用了 INVOKESTATIC 呼叫函數)

呼叫 Function0#invoke 函數後就會有出入棧的消耗

B. inlineFunc 函數:可以看到呼叫 inline 函數不需要出入棧就可以呼叫(它被直接編入呼叫的函數內),所以可以節省部分資源

禁用內聯:noinline

● 當我們想要拒絕編譯器將程式當作內聯編入時,可以使用 noinline 關鍵字,像是我們可以使用在…

接收參數時可以使用 noinline 關鍵字來限制編譯器不要使用內聯函數的功能


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)
}

A. 從 .class 檔可以看出端倪,呼叫時需要使用到 Function 關鍵字

B. 反組譯過後的更清楚可以看到 noninline 標示的參數會使用 FunctionN 來呼叫 invoke

Non-local return: Inline make Lambda can return

Kotlin 在 lambda 函數中返回是無法跳出函數


fun normalFunc(block: () -> Unit) {
    block()
}

fun foo() {
    println("Start")

    normalFunc {
        return        // Error: Not allow here
    }

    println("Done")
}

A. 可以使用 @tag 返回到指定地點


fun normalFunc(block: () -> Unit) {
    block()
}

fun foo() {
    println("Start")

    normalFunc {
        return@normalFunc    // 修正
    }

    println("Done")    // 會執行到
}

B. 如果是 inline 函數則可以用 return


inline fun inlineFunction(block: () -> Unit) {
    block()
}

fun foo2() {
    println("Start")

    inlineFunction {        // 修正
        // return okay
        return
    }

    println("Done")    // 不會執行到 Done
}

禁止 inline 返回:crossinline

crossinline 關鍵字用來修飾入參(函數的參數),一般來說 inline 函數可以局部返回,如果這時 使用了 crossinline 那就禁止局部返回

crossinline 必須配合 inline 一起使用


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 拓展函數的格式如下


<原始類>.<拓展名>() {
    // TODO:
}

擴展函數使用

● 依照拓展函數的格式寫,就可以建構拓展函數


// 原始類
class BaseExtension {
    fun hello() = println("Hello")
}

// 拓展函數
fun BaseExtension.world() = println("Extension World")

fun main() {
    val extension = BaseExtension()

    extension.hello()
    extension.world()
}

● 使用 Kotlin Bytecode 工具去反編譯,可以發現 拓展函數就是一個靜態工具類!並不會影響到原始類

拓展函數:注意事項

A. 拓展類如果跟原始類有相同名稱(包括參數)會使用哪個呢? 呼叫時會 使用原始類(原始類的優先權更高)


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()
}

● 可以看到反組繹出來後,它呼叫的不是靜態的 hello 函數,而是原始類的 hello 函數

B. 拓展類沒有多態(繼承重寫沒有用)


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)
}

● 原因是編譯器會強制轉型為基類(因為 showWorld 函數接收的就是基類),並呼叫期靜態,而不是透過對象呼叫

C. Java 類也可以直接呼叫拓展函數:目標類名就是「檔案名稱 + Kt」,之後再加上要呼叫的函數名即可,範例如下


public class Java_Main {
    public static void main(String[] args) {
        BaseExtensionKt.world(new BaseExtension());
    }
}

Kotlin 拓展屬性

Kotlin 除了可以拓展函數以外,還可以拓展屬性(property

拓展屬性使用


class ExtensionField

val ExtensionField.name : String
    get() = "Alien"

fun main() {
    val ex = ExtensionField()
    println(ex.name)
}

● 拓展屬性 沒有 backing field 可以使用!所以 不能設定 setter,所以只能用 val 描述!

● 如果還是想用 Setter,那可以考慮這樣設計:如果你是拓展某些類型,並且該類型內「可以儲存你自訂設定的參數」,就可以使用 setter;

範例如下:將自己要設定的物件存入 Throwable 中的 List<Throwable> 列表內,並透過該 list 遍歷並判斷,取得對應的物件


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 代碼都可以自由的調用標準函數

● 以下這些標準函數使用的好,可以增強程式的可讀性!以及程式的物理內聚程度

let 函數

● 每個對象內都有一個 let 工具 (就類似於這個 let 在 Object 內),它並不是關鍵字、操作符號,它就是一個函數,let 函數會把自身對象再次傳入,作為參數再次使用


// 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 常用來判空,可以用在判斷對象不為空後,繼續往下操作


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()
    }
}

可能會想 if 統一判斷與 let 是相同的,但是並非如此 !

若是把變數放置全域 var 則 if 也無法正確判斷

假設在 多執行緒(線程)協作的狀況下,有可能改動到全域變數,也就是 if 也無法保證該變量的安全性 (以往我們會使用鎖來解決這個問題),而使用 let 則可以安全的操作


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()
    }
}

Kotlin 標準函數run 函數

run 功能與 with 功能相似,每個對象裡面都有 run 方法可以調用,而它的上下文就是調用的 run 的對象,下面會使用 run 達到相同目的


// 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 (泛型)來調用的


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 對象

--實作結果--

run 可以解決鏈式呼叫 !


fun createHelloObj() = Hello()


class Hello {
    fun getWorldObj() = World()
}

class World {
    fun getMsg() = "Hello World, run~"
}]

A. 傳統 鏈式呼叫


// 鏈式呼叫

println(createHelloObj().getWorldObj().getMsg())

B. 使用 run 函數:更清晰的可看出調用順序


// 使用 run 函數 

createHelloObj()
    .run {
        getWorldObj()
    }.run {
        getMsg()
    }.run (::println)

run & apply 差別

A. 參數:run & apply 兩者都沒有參數,lambda 內部都作用於接收者

B. 返回run 不返回接收者,它可返回其他對象 (如同 let);apply 則反為接收者,無法返回其他類型!

Kotlin 標準函數apply 函數

applyrun 相似,與 run 的差異處在,apply 只會返回該對象本身,run 可以返回呼叫對象以外的對象


// apply 原型
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

可以看到 apply 只有一個泛型,傳遞進該類後,就只會返回該類


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")
}

--實作結果--

apply & let 差別

A. 參數:apply 沒有接收參數;let 接收原本接收者的參數

B. 返回:apply 返回原接收者;let 可返回不同的對象 (最後一行)

Kotlin 標準函數with 函數

with 函數與 run 函數類似,是 run 函數的變形;with 接收兩個參數,1. 任意對象、2. Lambda (閉包)函數

with 會提供第一個參數的上下文,讓 Lambda 內可以自由使用,並使用最後一行為返回值,它可以在連續調用程式時看起來更簡潔


// 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 串接字串


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")
}

A. 可以看出使用標準 with 函數讓程式更加精簡,它讓第一個參數傳入 StringBuilder 對象,第二個參數內的上下文就是 StringBuilder(this),就可以省略上下文呼叫

B. 最後一行最為返回值返回,而 append 函數返回的也是該對象


// append 函數
public StringBuilder append(String var1) {
    super.append(var1);

    return this;    // 返回自身對象
}

--實作結果--

Kotlin 標準函數repeat 函數

● repeat 接收一個常數 n,然後會把 Lambda 中的表達式運行你指定的次數 (前面設定的常數 n)


// 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 函數使用範例如下:以下用來串接字符串


// 使用範例
import java.lang.StringBuilder

fun main() {
    val str = StringBuilder()

    repeat(5) {
        str.append("Hello $it\n")   // 從 0 開始
    }

    println("use repeat function: \n${str.toString()}")
}

--實作結果--

Kotlin 標準函數also 函數

● also 是 kotlin 新增的函數(其語意可以理解為「順帶做某些事情」),also 原型如下


@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 有接收者參數),並且返回的也是自身對象


fun main() {
    val res = "Hello".also {
        println("$it World")

        "$it World"    // 返回仍然是 Hello
    }

    println(res)
}

● also 適合針對同一原始物件,透過副作用做事


fun main() {
    val strList = mutableListOf<String>()

    "Hello".also {
        // Hello 的副作用
        println("\"$it\"  add to list")
    }.also {
        // Hello 的副作用
        strList.add(it)
    }

    println(strList)
}

Kotlin 標準函數takeIf / takeUnless 函數

● takeIf / takeUnless 函數 其實就是一個 If 判斷式的方便寫法,再執行 predicate 判斷回復為 true 則返回接收者,false 則返回 null;它可以讓我們省略不必要的臨時變量

A. takeIf 原碼


@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
}

● 使用方式就像是 if(...) 的判斷


fun useIfElse(msg: String) : String? {
    // 省略臨時變量
    val predicate = msg.length > 5

    return if (predicate) {
        msg
    } else
        null
}

takeIf 使用方式如下


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)
    }
}

B. takeUnless 原碼


@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
}

● 使用方式就像是 if(!...) 的判斷


fun useIfElse2(msg: String) : String? {
    // 省略臨時變量
    val predicate = msg.length > 5

    return if (!predicate) {
        msg
    } else
        null
}

takeUnless 使用方式如下


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)
    }
}

Kotlin 標準函數差異、比較

標準函數使用返回類型參數功能
let透過對象可返回別的對象it (本身)常用於判空
apply透過對象返回本身無(this 接收者)簡化內部上下文
run透過對象可返回別的對象無(this 接收者)簡化內部,並改變回傳類
with直接使用 (接收一個對象)可返回別的對象不須對象的使用
repeat直接使用 (接收次數)it重複相同代碼
also透過對象返回本身it對象副作用
takeIf/taskUnless透過對象返回本身 or nullit省略 if 判斷 & 臨時變數

更多的 Kotlin 語言相關文章

在這裡,我們提供了一系列豐富且深入的 Kotlin 語言相關文章,涵蓋了從基礎到進階的各個方面。讓我們一起來探索這些精彩內容!

Kotlin 特性、特點

Kotlin 特性、特點:探索 Kotlin 的獨特特性和功能,加深對 Kotlin 語言的理解,並增強對於語言特性的應用

Kotlin 進階:協程、響應式、異步

Kotlin 進階:協程、響應式、異步:若想深入學習 Kotlin 的進階主題,包括協程應用、Channel 使用、以及 Flow 的探索,請查看以下文章

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響