Overview of Content
本文深入探討 Kotlin 語言的強大函數特性,包括 Inline 內聯函數的基礎使用、禁用內聯、非區域返回、禁止 inline 返回等內容。
進一步介紹 Kotlin 的拓展函數和拓展屬性,包括使用方式和注意事項。此外,深入解析 Kotlin 的標準函數,包括 let
、run
、apply
、with
、repeat
、also
、takeIf
/ 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 函數
● apply
與 run
相似,與 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 null | it | 省略 if 判斷 & 臨時變數 |
更多的 Kotlin 語言相關文章
在這裡,我們提供了一系列豐富且深入的 Kotlin 語言相關文章,涵蓋了從基礎到進階的各個方面。讓我們一起來探索這些精彩內容!
Kotlin 語言基礎
● Kotlin 語言基礎:想要建立堅實的 Kotlin 基礎?以下這些文章將帶你深入探索 Kotlin 的關鍵基礎和概念,幫你打造更堅固的 Kotlin 語言基礎
Kotlin 特性、特點
● Kotlin 特性、特點:探索 Kotlin 的獨特特性和功能,加深對 Kotlin 語言的理解,並增強對於語言特性的應用
Kotlin 進階:協程、響應式、異步
● Kotlin 進階:協程、響應式、異步:若想深入學習 Kotlin 的進階主題,包括協程應用、Channel 使用、以及 Flow 的探索,請查看以下文章